All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.automation.tecan.plateop.TecanCopyPlateOp Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
package com.hfg.automation.tecan.plateop;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.hfg.automation.AutomationConfigurationException;
import com.hfg.automation.LabwareTypeGroup;
import com.hfg.automation.Plate;
import com.hfg.automation.PlateLocation;
import com.hfg.automation.VolumeCheckStringency;
import com.hfg.automation.WellRef;
import com.hfg.automation.platelayer.LayerPlate;
import com.hfg.automation.platelayer.LayerWell;
import com.hfg.automation.platelayer.PlateDataLayer;
import com.hfg.automation.plateop.VolumeMask;
import com.hfg.automation.tecan.TecanDeckPosition;
import com.hfg.automation.tecan.TecanLabwareContainer;
import com.hfg.automation.tecan.TecanPlate;
import com.hfg.automation.tecan.TecanTipConfig;
import com.hfg.automation.tecan.worklistcmd.TecanWorklistAspirateCommand;
import com.hfg.automation.tecan.worklistcmd.TecanWorklistCommand;
import com.hfg.automation.tecan.worklistcmd.TecanWorklistCommentCommand;
import com.hfg.automation.tecan.worklistcmd.TecanWorklistDispenseCommand;
import com.hfg.exception.ProgrammingException;
import com.hfg.units.Quantity;
import com.hfg.units.Unit;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.collection.OrderedMap;

//------------------------------------------------------------------------------
/**
 Tecan-specific implementation of the Copy Plate Operation.
 
@author J. Alex Taylor, hairyfatguy.com
*/ //------------------------------------------------------------------------------ // com.hfg XML/HTML Coding Library // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com // [email protected] //------------------------------------------------------------------------------ public class TecanCopyPlateOp extends TecanPerPlateLiquidTransferPlateOp { private List mAspirateSources; private List mDispenseDestinations; private Integer mNumTips; private Map> mTransferMap; // Tecan expects volumes to be specified as microliters private static final Unit MICROLITERS = Unit.valueOf("μL"); private static final Quantity ZERO_VOLUME = new Quantity(0, Unit.valueOf("μL")); private static Set sAspirateAllowedGroups = new HashSet<>(); private static Set sDispenseAllowedGroups = new HashSet<>(); static { sAspirateAllowedGroups.add(LabwareTypeGroup.Plate); sAspirateAllowedGroups.add(LabwareTypeGroup.Tube); sDispenseAllowedGroups.add(LabwareTypeGroup.Plate); sDispenseAllowedGroups.add(LabwareTypeGroup.Tube); } private enum Mode { dryRun, normal } //########################################################################## // PUBLIC METHODS //########################################################################## //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setAspirateVolumeDataLayerName(String inValue) { return (TecanCopyPlateOp) super.setAspirateVolumeDataLayerName(inValue); } //--------------------------------------------------------------------------- public TecanCopyPlateOp setAspirateSources(Collection inValues) { mAspirateSources = null; if (CollectionUtil.hasValues(inValues)) { for (TecanPlate plate : inValues) { addAspirateSource(plate); } } return this; } //--------------------------------------------------------------------------- public TecanCopyPlateOp addAspirateSource(TecanPlate inValue) { aspirateGroupCheck(inValue); if (null == mAspirateSources) { mAspirateSources = new ArrayList<>(10); } mAspirateSources.add(inValue); return this; } //--------------------------------------------------------------------------- public List getAspirateSources() { return mAspirateSources; } //--------------------------------------------------------------------------- public TecanCopyPlateOp setDispenseDestinations(Collection inValues) { if (null == mDispenseDestinations) { mDispenseDestinations = new ArrayList<>(10); } mDispenseDestinations.addAll(inValues); return this; } //--------------------------------------------------------------------------- public TecanCopyPlateOp addDispenseDestination(TecanLabwareContainer inValue) { dispenseGroupCheck(inValue); if (null == mDispenseDestinations) { mDispenseDestinations = new ArrayList<>(10); } mDispenseDestinations.add(inValue); return this; } //--------------------------------------------------------------------------- public List getDispenseDestinations() { return mDispenseDestinations; } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setTipConfig(TecanTipConfig inValue) { return (TecanCopyPlateOp) super.setTipConfig(inValue); } //--------------------------------------------------------------------------- private void aspirateGroupCheck(TecanLabwareContainer inValue) { if (! sAspirateAllowedGroups.contains(inValue.getTecanLabwareType().getGroup())) { throw new AutomationConfigurationException("Labware " + StringUtil.singleQuote(inValue.getTecanLabel()) + " is not a type allowed to be used as an aspiration source!"); } } //--------------------------------------------------------------------------- private void dispenseGroupCheck(TecanLabwareContainer inValue) { if (! sDispenseAllowedGroups.contains(inValue.getTecanLabwareType().getGroup())) { throw new AutomationConfigurationException("Labware " + StringUtil.singleQuote(inValue.getTecanLabel()) + " is not a type allowed to be used as a dispense destination!"); } } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setAspirateVolume(Quantity inValue) { return (TecanCopyPlateOp) super.setAspirateVolume(inValue); } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setDispenseVolume(Quantity inValue) { return (TecanCopyPlateOp) super.setDispenseVolume(inValue); } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setAspirateVolumeMask(VolumeMask inValue) { return (TecanCopyPlateOp) super.setAspirateVolumeMask(inValue); } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setDispenseVolumeMask(VolumeMask inValue) { return (TecanCopyPlateOp) super.setDispenseVolumeMask(inValue); } //--------------------------------------------------------------------------- public TecanCopyPlateOp setNumTips(Integer inValue) { mNumTips = inValue; return this; } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setOutputOrder(Integer inValue) { return (TecanCopyPlateOp) super.setOutputOrder(inValue); } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setVolumeCheckStringency(VolumeCheckStringency inValue) { return (TecanCopyPlateOp) super.setVolumeCheckStringency(inValue); } //--------------------------------------------------------------------------- @Override public TecanCopyPlateOp setComment(String inValue) { return (TecanCopyPlateOp) super.setComment(inValue); } //--------------------------------------------------------------------------- public void generateWorklist(OutputStream inStream) throws IOException { generateWorklist(inStream, Mode.normal); } /* //--------------------------------------------------------------------------- public VolumeMask calculatePostOpTotalVolumeMask(Plate inPlate) { VolumeMask postOpVolumeMask = null; if (getAspirateSources().contains(inPlate)) { // TODO // What will the src volumes be after the copy operation? } else if (getDispenseDestinations().contains(inPlate)) { postOpVolumeMask = getDispenseDestinationVolumeMask((TecanPlate) inPlate); if (null == postOpVolumeMask) { throw new AutomationConfigurationException(name() + " could not calculate post operation volumes!"); } } return postOpVolumeMask; } */ //--------------------------------------------------------------------------- // TODO: What to do if the requested plate is a source location instead of a destination? public VolumeMask calculatePostOpTotalVolumeMask(Plate inPlate) throws IOException { Map> transferMap = getTransferMap(); VolumeMask postOpVolumeMask = new VolumeMask(); for (Map destVolumeMap : transferMap.values()) { for (PlateLocation loc : destVolumeMap.keySet()) { if (loc.getPlate().equals(inPlate)) { postOpVolumeMask.addVolume(loc.getWellRef(), destVolumeMap.get(loc)); } } } return postOpVolumeMask; } //--------------------------------------------------------------------------- public Map> getTransferMap() { if (null == mTransferMap) { try { generateWorklist(null, Mode.dryRun); } catch (IOException e) { throw new ProgrammingException(e); } } return mTransferMap; } //--------------------------------------------------------------------------- public Quantity getDestinationVolumeFromSrcDataLayer(PlateLocation inDestinationLocation, String inDataLayerName) { Quantity volume = null; Map> dryRunTransferMap = getTransferMap(); for (PlateLocation srcPlateLocation : dryRunTransferMap.keySet()) { if (dryRunTransferMap.get(srcPlateLocation).containsKey(inDestinationLocation)) { PlateDataLayer dataLayer = ((LayerPlate) srcPlateLocation.getPlate()).getDataLayer(inDataLayerName); if (dataLayer != null) { volume = dataLayer.getData(srcPlateLocation.getWellRef()); break; } } } return volume; } //--------------------------------------------------------------------------- private void preflightChecks() { if (!CollectionUtil.hasValues(mAspirateSources)) { throw new AutomationConfigurationException("No aspiration sources has been specified!"); } if (! CollectionUtil.hasValues(mDispenseDestinations)) { throw new AutomationConfigurationException("No dispense destinations have been specified!"); } // Ensure that all sources are plates for (TecanLabwareContainer srcContainer : getAspirateSources()) { if (! srcContainer.getTecanLabwareType().getGroup().equals(LabwareTypeGroup.Plate)) { throw new AutomationConfigurationException("Source containers must currently be plates for this PlateOp! " + "Source " + srcContainer.getTecanLabel() + " is a " + srcContainer.getTecanLabwareType() + "."); } } // Ensure that all destinations are plates for (TecanLabwareContainer destContainer : getDispenseDestinations()) { if (! destContainer.getTecanLabwareType().getGroup().equals(LabwareTypeGroup.Plate)) { throw new AutomationConfigurationException("Destination containers must currently be plates for this PlateOp! " + "Dest " + destContainer.getTecanLabel() + " is a " + destContainer.getTecanLabwareType() + "."); } } // Ensure that all sources have valid Tecan labels for (TecanLabwareContainer srcContainer : getAspirateSources()) { if (null == srcContainer.getTecanLabel()) { throw new AutomationConfigurationException("Src container without a plate label (deck position)!"); } if (getDeckConfig() != null) { TecanDeckPosition deckPosition = getDeckConfig().getDeckPosition(srcContainer.getTecanLabel()); if (null == deckPosition) { throw new AutomationConfigurationException("Src container label " + StringUtil.singleQuote(srcContainer.getTecanLabel()) + " is not valid for the specified deck configuration!"); } else if (! srcContainer.getTecanLabwareType().getClass().isAssignableFrom(deckPosition.getContainerType().getClass())) { throw new AutomationConfigurationException("Src container is incompatible with deck position " + StringUtil.singleQuote(srcContainer.getTecanLabel()) + "!"); } } } // Ensure that all destinations have valid Tecan labels for (TecanLabwareContainer destContainer : getDispenseDestinations()) { if (null == destContainer.getTecanLabel()) { throw new AutomationConfigurationException("Destination container without a plate label (deck position)!"); } if (getDeckConfig() != null) { TecanDeckPosition deckPosition = getDeckConfig().getDeckPosition(destContainer.getTecanLabel()); if (null == deckPosition) { throw new AutomationConfigurationException("Destination container label " + StringUtil.singleQuote(destContainer.getTecanLabel()) + " is not valid for the specified deck configuration!"); } else if (! destContainer.getTecanLabwareType().getClass().isAssignableFrom(deckPosition.getContainerType().getClass())) { throw new AutomationConfigurationException("Destination container is incompatible with deck position " + StringUtil.singleQuote(destContainer.getTecanLabel()) + "!"); } } } // Ensure that either a constant volume or a volume mask has been specified for the operation. if (null == getDispenseVolume() && null == getDispenseVolumeMask() && null == getAspirateVolumeMask() && null == getAspirateVolumeDataLayerName() && null == getDispenseVolumeDataLayerName() && null == ((TecanPlate)getAspirateSources().get(0)).getOccupiedWells().iterator().next().getVolume()) { throw new AutomationConfigurationException("No dispense volume, VolumeMask, or volume DataLayer has been specified for the plate operation!"); } } //--------------------------------------------------------------------------- private VolumeMask getDispenseDestinationVolumeMask(TecanPlate inPlate) { VolumeMask plateDispenseVolumeMask = null; if (StringUtil.isSet(getDispenseVolumeDataLayerName())) { PlateDataLayer dispenseVolumeDataLayer = inPlate.getDataLayer(getDispenseVolumeDataLayerName()); if (dispenseVolumeDataLayer != null) { plateDispenseVolumeMask = new VolumeMask(dispenseVolumeDataLayer); } } if (null == plateDispenseVolumeMask) { // There weren't per-plate volume masks. if (getDispenseVolumeMask() != null) { // TODO: Clone before returning? plateDispenseVolumeMask = getDispenseVolumeMask(); } else if (getDispenseVolume() != null) { plateDispenseVolumeMask = new VolumeMask().addVolume(inPlate.getLayout().getWellRefs(), getDispenseVolume()); } else if (StringUtil.isSet(getDispenseVolumeDataLayerName())) { PlateDataLayer dispenseVolumeDataLayer = inPlate.getDataLayer(getDispenseVolumeDataLayerName()); if (dispenseVolumeDataLayer != null) { plateDispenseVolumeMask = new VolumeMask(dispenseVolumeDataLayer); } } else if (getAspirateVolumeMask() != null) { // TODO: Clone before returning? plateDispenseVolumeMask = getAspirateVolumeMask(); } else if (getAspirateVolume() != null) { // Assume aspirate vol = dispense vol plateDispenseVolumeMask = new VolumeMask().addVolume(inPlate.getLayout().getWellRefs(), getAspirateVolume()); } } if (null == plateDispenseVolumeMask) { // Don't have enough info throw new AutomationConfigurationException("No enough information for PlateOp " + name() + " to calculate destination volumes!"); } return plateDispenseVolumeMask; } //--------------------------------------------------------------------------- private void generateWorklist(OutputStream inStream, Mode inMode) throws IOException { preflightChecks(); PrintStream printStream = null; if (inMode.equals(Mode.normal)) { printStream = new PrintStream(inStream); if (StringUtil.isSet(getComment())) { printStream.println(new TecanWorklistCommentCommand(getComment())); } } mTransferMap = new OrderedMap<>(); int tip = 1; // TODO: How does the Tecan deal with worklists for multiple plates? int destPlateIndex = 0; for (int i = 0; i < getAspirateSources().size(); i++) { TecanLabwareContainer srcContainer = getAspirateSources().get(i); TecanPlate srcPlate = (TecanPlate) srcContainer; TecanPlate actualDestPlate = (TecanPlate) getDispenseDestinations().get(destPlateIndex); TecanPlate currentDestPlate = (inMode.equals(Mode.normal) ? actualDestPlate : actualDestPlate.clone()); int destWellIndex = 0; if (inMode.equals(Mode.normal) && getDeckConfig() != null) { TecanLabwareContainer currentContainerAtSrcPosition = getDeckConfig().getDeckPosition(srcContainer.getTecanLabel()).getContainer(); if (null == currentContainerAtSrcPosition || ! currentContainerAtSrcPosition.equals(srcContainer)) { String comment = "Place src " + (StringUtil.isSet(srcContainer.getRackId()) ? srcContainer.getRackId() + " " : "") + "at deck position " + srcContainer.getTecanLabel(); printStream.println(new TecanWorklistCommentCommand(comment).toString()); getDeckConfig().getDeckPosition(srcContainer.getTecanLabel()).setContainer(srcContainer); } TecanLabwareContainer currentContainerAtDestPosition = getDeckConfig().getDeckPosition(actualDestPlate.getTecanLabel()).getContainer(); if (null == currentContainerAtDestPosition || ! currentContainerAtDestPosition.equals(actualDestPlate)) { String comment = "Place dest " + (StringUtil.isSet(actualDestPlate.getRackId()) ? actualDestPlate.getRackId() + " " : "") + "at deck position " + actualDestPlate.getTecanLabel(); printStream.println(new TecanWorklistCommentCommand(comment).toString()); getDeckConfig().getDeckPosition(actualDestPlate.getTecanLabel()).setContainer(actualDestPlate); } } if (! srcContainer.getTecanLabwareType().getGroup().equals(LabwareTypeGroup.Plate)) { throw new AutomationConfigurationException("Source containers must currently be plates for this PlateOp!"); } // TODO: Test for a plate type match between the plate and the plate layout for (WellRef srcWellRef : srcPlate.getLayout().getWellRefs(false)) // for (LayerWell srcWell : srcPlate.getOccupiedWells()) { PlateLocation srcPlateLoc = new PlateLocation(srcPlate, srcWellRef); Map destMap = mTransferMap.get(srcPlateLoc); if (null == destMap) { destMap = new OrderedMap<>(2); mTransferMap.put(srcPlateLoc, destMap); } WellRef destWellRef; if (destWellIndex < currentDestPlate.getType().size()) { destWellRef = currentDestPlate.getLayout().getWellRefFromIndex(destWellIndex++); } else { destPlateIndex++; if (destPlateIndex == getDispenseDestinations().size()) { throw new AutomationConfigurationException("Not enough destination plates for the operation to complete!"); } actualDestPlate = (TecanPlate) getDispenseDestinations().get(destPlateIndex); currentDestPlate = (inMode.equals(Mode.normal) ? actualDestPlate : actualDestPlate.clone()); destWellIndex = 0; // Reset the well index destWellRef = currentDestPlate.getLayout().getWellRefFromIndex(destWellIndex++); } PlateLocation destPlateLoc = new PlateLocation(actualDestPlate, destWellRef); destMap.put(destPlateLoc, null); LayerWell srcWell = srcPlate.getWell(srcWellRef); if (null == srcWell) { continue; } // Honor the selection layer if present if (srcWell.getSelectionLayer() != null && ! srcWell.getSelectionLayer().isSelected()) { // This well wasn't selected. Skip it. continue; } // What is the volume to be aspirated? Quantity aspirateVolume = getAspirateVolume(); if (null == aspirateVolume && getAspirateVolumeMask() != null) { aspirateVolume = getAspirateVolumeMask().getVolume(srcWell.getRef()); } else if (StringUtil.isSet(getAspirateVolumeDataLayerName())) { PlateDataLayer aspirateVolumeDataLayer = srcPlate.getDataLayer(getAspirateVolumeDataLayerName()); if (aspirateVolumeDataLayer != null) { aspirateVolume = aspirateVolumeDataLayer.getData(srcWellRef); if (null == aspirateVolume) { aspirateVolume = ZERO_VOLUME; } } } else if (srcWell.getVolume() != null) { aspirateVolume = srcWell.getVolume(); } // What is the volume to be dispensed? Quantity dispenseVolume = getDispenseVolume(); if (null == dispenseVolume) { if (getDispenseVolumeMask() != null) { dispenseVolume = getDispenseVolumeMask().getVolume(srcWell.getRef()); } else if (StringUtil.isSet(getDispenseVolumeDataLayerName())) { PlateDataLayer dispenseVolumeDataLayer = currentDestPlate.getDataLayer(getDispenseVolumeDataLayerName()); if (dispenseVolumeDataLayer != null) { dispenseVolume = dispenseVolumeDataLayer.getData(destWellRef); if (null == aspirateVolume) { dispenseVolume = ZERO_VOLUME; } } } } /* if (CollectionUtil.hasValues(getPerPlateVolumeMasks()) && null == dispenseVolume) { continue; } */ // Deal with partially supplied information. If they specified aspiration volumes // but not dispense volumes or vice versa, assume that the two volumes are the same. if (null == aspirateVolume && dispenseVolume != null) { aspirateVolume = dispenseVolume; } else if (null == dispenseVolume && aspirateVolume != null) { dispenseVolume = aspirateVolume; } if (null == aspirateVolume) { throw new RuntimeException("No " + srcWell.getRef() + " volume or volume mask value specified for the operation!"); } // Tecan expects volumes to be specified as microliters aspirateVolume = aspirateVolume.convertTo(MICROLITERS); dispenseVolume = dispenseVolume.convertTo(MICROLITERS); if (dispenseVolume.floatValue() > aspirateVolume.floatValue()) { String msg = "The dispense volume for well " + srcWell.getRef() + " is larger than the aspirate volume!"; if (getVolumeCheckStringency().equals(VolumeCheckStringency.warning)) { addWarning(msg); } else if (getVolumeCheckStringency().equals(VolumeCheckStringency.error)) { throw new AutomationConfigurationException(msg); } } if (aspirateVolume.floatValue() > 0) { // Is the aspirate volume within capacity specs for the tip type? if (getTipConfig() != null) { Quantity tipMinCapacity = getTipConfig().getTipType().getMinCapacity(); // TODO: Should there be other ways to handle volumes less than the minimum capacity? if (aspirateVolume.lessThan(tipMinCapacity)) { String msg = "Aspiration volume for well " + srcWell + " (" + aspirateVolume + ") is less than the minimum capacity for the " + getTipConfig().getTipType().name() + " tip (" + tipMinCapacity + ")!"; if (getVolumeCheckStringency().equals(VolumeCheckStringency.warning)) { addWarning(msg); } else if (getVolumeCheckStringency().equals(VolumeCheckStringency.error)) { throw new AutomationConfigurationException(msg); } } } // Deal with the scenario where the amount to dispense exceeds the max capacity of the tip Quantity volumeRemainingToDispense = dispenseVolume; do { if (getTipConfig() != null && volumeRemainingToDispense.greaterThan(getTipConfig().getTipType().getMaxCapacity().multiplyBy(2))) { aspirateVolume = getTipConfig().getTipType().getMaxCapacity(); } else if (getTipConfig() != null && volumeRemainingToDispense.greaterThan(getTipConfig().getTipType().getMaxCapacity())) { // We don't want the remaining volume to get below the minimum capacity, so divide the remainder in half aspirateVolume = volumeRemainingToDispense.divideBy(2); } else { aspirateVolume = volumeRemainingToDispense; } dispenseVolume = aspirateVolume; if (inMode.equals(Mode.normal)) { // Aspirate TecanWorklistAspirateCommand aspirateRecord = new TecanWorklistAspirateCommand() .setRackLabel(srcContainer.getTecanLabel()) .setRackId(srcContainer.getRackId()) .setRackName(srcContainer.getTecanLabwareType().name()) .setPosition(getTecanWellPosition(srcPlate.getType(), srcWell.getRef())) .setVolume(Math.round(aspirateVolume.floatValue())) .setLiquidClass(getAspirateLiquidClass()); if (mNumTips != null) { aspirateRecord.setPosition(tip++); } printStream.println(aspirateRecord.toString()); // Dispense TecanWorklistDispenseCommand dispenseRecord = new TecanWorklistDispenseCommand() .setRackLabel(currentDestPlate.getTecanLabel()) .setRackId(currentDestPlate.getRackId()) .setRackName(currentDestPlate.getTecanLabwareType().name()) .setPosition(getTecanWellPosition(currentDestPlate.getType(), destWellRef)) .setVolume(Math.round(dispenseVolume.floatValue())) .setLiquidClass(getDispenseLiquidClass()); printStream.println(dispenseRecord.toString()); // Tip wash printStream.println(TecanWorklistCommand.WASH.toString()); if (mNumTips != null && tip > mNumTips) { tip = 1; } } // TODO: We are not currently removing the aspirate volume from the source well // Update the volume in the destination plate LayerWell well = currentDestPlate.getWell(destWellRef); if (null == well) { well = currentDestPlate.allocateWell(destWellRef); } if (srcWell.getSamples() != null) { boolean volumeAdded = false; for (Comparable sample : srcWell.getSamples()) { if (! volumeAdded) { well.addSample(sample, dispenseVolume); } else { well.addSample(sample); } } } else { well.addVolume(dispenseVolume); } destMap.put(destPlateLoc, well.getSampleVolume()); volumeRemainingToDispense = volumeRemainingToDispense.subtract(dispenseVolume); } while (volumeRemainingToDispense.floatValue() > 0); } else { destMap.put(destPlateLoc, ZERO_VOLUME); } } destPlateIndex++; } if (inMode.equals(Mode.normal)) { printStream.close(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy