com.hfg.automation.tecan.plateop.TecanRearrayPlateOp Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
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.PlateRole;
import com.hfg.automation.VolumeCheckStringency;
import com.hfg.automation.Well;
import com.hfg.automation.WellRef;
import com.hfg.automation.platelayer.LayerPlate;
import com.hfg.automation.platelayer.PlateDataLayer;
import com.hfg.automation.platelayer.PlateSampleLayer;
import com.hfg.automation.plateop.RearrayPlateOp;
import com.hfg.automation.plateop.VolumeMask;
import com.hfg.automation.tecan.TecanDeckPosition;
import com.hfg.automation.tecan.TecanLabwareContainer;
import com.hfg.automation.tecan.TecanLiquidClass;
import com.hfg.automation.tecan.TecanOpStep;
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.automation.tecan.worklistcmd.TecanWorklistUserPromptCommand;
import com.hfg.exception.ProgrammingException;
import com.hfg.units.Quantity;
import com.hfg.units.Unit;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.collection.OrderedMap;
//------------------------------------------------------------------------------
/**
Tecan-specific implementation of the Rearray 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 TecanRearrayPlateOp extends TecanPerPlateLiquidTransferPlateOp implements RearrayPlateOp
{
private List mAspirateSources;
private List mDispenseDestinations;
private Class mSampleClass;
private String mSampleLayerName;
private Integer mSourcePlateBatchSize;
private List mOpSteps;
// Optional reagent to (pre)include with the transfer
private TecanLabwareContainer mReagentContainer;
private Quantity mFixedReagentQuantity;
private String mReagentQuantityDataLayerName;
private TecanLiquidClass mReagentAspirateLiquidClass;
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, MICROLITERS);
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 TecanRearrayPlateOp setAspirateVolumeDataLayerName(String inValue)
{
return (TecanRearrayPlateOp) super.setAspirateVolumeDataLayerName(inValue);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setSampleClass(Class inValue)
{
mSampleClass = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setSampleLayerName(String inValue)
{
mSampleLayerName = inValue;
return this;
}
//---------------------------------------------------------------------------
public Class getSampleClass()
{
return mSampleClass;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setSourcePlateBatchSize(Integer inValue)
{
mSourcePlateBatchSize = inValue;
return this;
}
//---------------------------------------------------------------------------
public Integer getSourcePlateBatchSize()
{
return mSourcePlateBatchSize;
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setOutputOrder(Integer inValue)
{
return (TecanRearrayPlateOp) super.setOutputOrder(inValue);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setAspirateSources(Collection inValues)
{
mAspirateSources = null;
if (CollectionUtil.hasValues(inValues))
{
for (TecanPlate plate : inValues)
{
addAspirateSource(plate);
}
}
return this;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp addAspirateSource(TecanPlate inValue)
{
aspirateGroupCheck(inValue);
if (null == mAspirateSources)
{
mAspirateSources = new ArrayList<>(10);
}
mAspirateSources.add(inValue);
// If the sample class hasn't been specified yet and there is a single sample layer in the source plate, assume that that is the sample class
if (null == getSampleClass())
{
Collection sampleLayers = inValue.getSampleLayers();
if (CollectionUtil.hasSingleValue(sampleLayers))
{
setSampleClass(sampleLayers.iterator().next().getSampleClass());
}
}
return this;
}
//---------------------------------------------------------------------------
public List getAspirateSources()
{
return mAspirateSources;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setDispenseDestinations(Collection inValues)
{
if (null == mDispenseDestinations)
{
mDispenseDestinations = new ArrayList<>(10);
}
mDispenseDestinations.addAll(inValues);
return this;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp addDispenseDestination(TecanPlate inValue)
{
dispenseGroupCheck(inValue);
if (null == mDispenseDestinations)
{
mDispenseDestinations = new ArrayList<>(10);
}
mDispenseDestinations.add(inValue);
return this;
}
//---------------------------------------------------------------------------
public List getDispenseDestinations()
{
return mDispenseDestinations;
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setAspirateVolume(Quantity inValue)
{
return (TecanRearrayPlateOp) super.setAspirateVolume(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setDispenseVolume(Quantity inValue)
{
return (TecanRearrayPlateOp) super.setDispenseVolume(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setAspirateVolumeMask(VolumeMask inValue)
{
return (TecanRearrayPlateOp) super.setAspirateVolumeMask(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setDispenseVolumeMask(VolumeMask inValue)
{
throw new AutomationConfigurationException("A dispense volume mask should not be specified on a Re-array operation!");
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setAspirateLiquidClass(TecanLiquidClass inValue)
{
return (TecanRearrayPlateOp) super.setAspirateLiquidClass(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setLiquidClass(TecanLiquidClass inValue)
{
return (TecanRearrayPlateOp) super.setLiquidClass(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setTipConfig(TecanTipConfig inValue)
{
return (TecanRearrayPlateOp) super.setTipConfig(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setComment(String inValue)
{
return (TecanRearrayPlateOp) super.setComment(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanRearrayPlateOp setVolumeCheckStringency(VolumeCheckStringency inValue)
{
return (TecanRearrayPlateOp) super.setVolumeCheckStringency(inValue);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp includeReagentInAspirate(TecanLabwareContainer inReagentContainer, TecanLiquidClass inReagentAspirateLiquidClass)
{
return includeReagentInAspirate(inReagentContainer, (Quantity) null, inReagentAspirateLiquidClass);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp includeReagentInAspirate(TecanLabwareContainer inReagentContainer, Quantity inQuantity)
{
return includeReagentInAspirate(inReagentContainer, inQuantity, null);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp includeReagentInAspirate(TecanLabwareContainer inReagentContainer,
Quantity inQuantity,
TecanLiquidClass inReagentAspirateLiquidClass)
{
mReagentContainer = inReagentContainer;
mFixedReagentQuantity = inQuantity;
mReagentAspirateLiquidClass = inReagentAspirateLiquidClass;
return this;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp includeReagentInAspirate(TecanLabwareContainer inReagentContainer, String inReagentQuantityDataLayerName)
{
return includeReagentInAspirate(inReagentContainer, inReagentQuantityDataLayerName, null);
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp includeReagentInAspirate(TecanLabwareContainer inReagentContainer,
String inReagentQuantityDataLayerName,
TecanLiquidClass inReagentAspirateLiquidClass)
{
mReagentContainer = inReagentContainer;
mReagentQuantityDataLayerName = inReagentQuantityDataLayerName;
mReagentAspirateLiquidClass = inReagentAspirateLiquidClass;
return this;
}
//---------------------------------------------------------------------------
public TecanRearrayPlateOp setOpSteps(List inSteps)
{
mOpSteps = inSteps;
return this;
}
//---------------------------------------------------------------------------
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;
}
//---------------------------------------------------------------------------
public void generateWorklist(OutputStream inStream)
throws IOException
{
generateWorklist(inStream, Mode.normal);
}
//---------------------------------------------------------------------------
public Map> getTransferMap()
{
if (null == mTransferMap)
{
try
{
generateWorklist(null, Mode.dryRun);
}
catch (IOException e)
{
throw new ProgrammingException(e);
}
}
return mTransferMap;
}
//---------------------------------------------------------------------------
// TODO: What to do if the requested plate is a source location instead of a destination?
public VolumeMask calculatePostOpTotalVolumeMask(Plate inPlate)
throws IOException
{
generateWorklist(null, Mode.dryRun);
VolumeMask postOpVolumeMask = new VolumeMask();
for (Map destVolumeMap : mTransferMap.values())
{
for (PlateLocation loc : destVolumeMap.keySet())
{
if (loc.getPlate().equals(inPlate))
{
postOpVolumeMask.addVolume(loc.getWellRef(), destVolumeMap.get(loc));
}
}
}
return postOpVolumeMask;
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//---------------------------------------------------------------------------
private void preflightChecks()
{
if (! CollectionUtil.hasValues(getAspirateSources()))
{
throw new AutomationConfigurationException("No aspiration sources have been specified!");
}
if (! CollectionUtil.hasValues(getDispenseDestinations()))
{
throw new AutomationConfigurationException("No dispense destinations have been specified!");
}
if (getDeckConfig() != null)
{
// Make sure that the src plate labels refer to position labels defined in the deck config.
int srcSlot = 0;
for (TecanPlate srcPlate : getAspirateSources())
{
if (! StringUtil.isSet(srcPlate.getTecanLabel()))
{
// Try to auto-assign a Tecan label
List srcPositions = getDeckConfig().getDeckPositionsByRole(PlateRole.SOURCE);
if (CollectionUtil.hasValues(srcPositions))
{
srcPlate.setTecanLabel(srcPositions.get(srcSlot++).getLabel());
if (srcSlot >= srcPositions.size())
{
srcSlot = 0;
}
}
else
{
throw new AutomationConfigurationException("No Tecan label applied to plate " + srcPlate.name() + " and no deck positions defined with SOURCE role to allow auto-assignment!");
}
}
getDeckConfig().validateDeckPosition(srcPlate.getTecanLabel());
}
// Make sure that the destination plate labels refer to position labels defined in the deck config.
int destSlot = 0;
for (TecanPlate destPlate : getDispenseDestinations())
{
if (! StringUtil.isSet(destPlate.getTecanLabel()))
{
// Try to auto-assign a Tecan label
List destPositions = getDeckConfig().getDeckPositionsByRole(PlateRole.DESTINATION);
if (CollectionUtil.hasValues(destPositions))
{
destPlate.setTecanLabel(destPositions.get(destSlot++).getLabel());
if (destSlot >= destPositions.size())
{
destSlot = 0;
}
}
else
{
throw new AutomationConfigurationException("No Tecan label applied to plate " + destPlate.name() + " and no deck positions defined with DESTINATION role to allow auto-assignment!");
}
}
getDeckConfig().validateDeckPosition(destPlate.getTecanLabel());
}
// If defined, make sure that the reagent label refers to a position label defined in the deck config.
if (mReagentContainer != null)
{
getDeckConfig().validateDeckPosition(mReagentContainer.getTecanLabel());
}
if (CollectionUtil.hasValues(mOpSteps)
&& getTipConfig() != null)
{
// Is the volume within capacity specs for the tip type?
for (TecanOpStep step : mOpSteps)
{
Quantity tipMinCapacity = getTipConfig().getTipType().getMinCapacity();
// TODO: Should there be other ways to handle volumes less than the minimum capacity?
if (step.getQuantity().lessThan(tipMinCapacity))
{
String msg = "Transfer volume for step (" + step.getQuantity() + ") 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);
}
}
}
}
}
if (null == getSampleClass())
{
setSampleClass(String.class);
// throw new AutomationConfigurationException("No sample class has been specified!");
}
}
//---------------------------------------------------------------------------
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!");
}
}
//---------------------------------------------------------------------------
private String getNameForPlate(TecanPlate inPlate)
{
// What should we call the src plate in case of an exception?
String plateName = inPlate.name();
if (! StringUtil.isSet(plateName))
{
if (! StringUtil.isSet(inPlate.getBarcode()))
{
plateName = inPlate.getBarcode();
}
else
{
plateName = inPlate.getTecanLabel();
}
}
return plateName;
}
//---------------------------------------------------------------------------
public 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()));
}
String tipConfigMsg;
if (getTipConfig() != null)
{
tipConfigMsg = "Tip config: " + getTipConfig().toString();
}
else
{
tipConfigMsg = "No tip configuration specified.";
}
printStream.println(new TecanWorklistCommentCommand(tipConfigMsg).toString());
}
mTransferMap = new OrderedMap<>();
// TODO: How does the Tecan deal with worklists for multiple plates?
int destPlateIndex = 0;
TecanPlate actualDestPlate = getDispenseDestinations().get(destPlateIndex);
TecanPlate currentDestPlate = (inMode.equals(Mode.normal) ? actualDestPlate : actualDestPlate.clone());
int numTips = 1;
if (getTipConfig() != null
&& getTipConfig().getNumTips() != null)
{
numTips = getTipConfig().getNumTips();
}
int srcPlateBatchSize = 1;
if (getDeckConfig() != null)
{
if (getSourcePlateBatchSize() != null)
{
srcPlateBatchSize = getSourcePlateBatchSize();
}
else
{
List srcPositions = getDeckConfig().getDeckPositionsByRole(PlateRole.SOURCE);
if (CollectionUtil.hasValues(srcPositions))
{
srcPlateBatchSize = srcPositions.size();
}
}
}
// Set up destination plate(s)
if (inMode.equals(Mode.normal)
&& getDeckConfig() != null)
{
int initialDestPlateBatchSize = 1;
if (getDeckConfig() != null)
{
List destPositions = getDeckConfig().getDeckPositionsByRole(PlateRole.DESTINATION);
if (CollectionUtil.hasValues(destPositions))
{
initialDestPlateBatchSize = destPositions.size();
}
if (getDispenseDestinations().size() < initialDestPlateBatchSize)
{
initialDestPlateBatchSize = getDispenseDestinations().size();
}
}
StringBuilderPlus comments = new StringBuilderPlus().setDelimiter(" ");
for (int i = 0; i < initialDestPlateBatchSize; i++)
{
TecanPlate destPlate = getDispenseDestinations().get(i);
TecanLabwareContainer currentContainerAtDeckPosition = getDeckConfig().getDeckPosition(destPlate.getTecanLabel()).getContainer();
if (null == currentContainerAtDeckPosition
|| !currentContainerAtDeckPosition.equals(destPlate))
{
String comment = "Place dest " + (StringUtil.isSet(destPlate.getRackId()) ? destPlate.getRackId() : destPlate.name())
+ " at deck position " + destPlate.getTecanLabel() + ".";
comments.delimitedAppend(comment);
// Tell the deck config that this plate is now at this deck position
getDeckConfig().getDeckPosition(destPlate.getTecanLabel()).setContainer(destPlate);
}
}
printStream.println(new TecanWorklistUserPromptCommand(comments.toString()).toString());
}
// Start by looping over ea. batch of source plates
List> srcPlateBatches = CollectionUtil.chunkList(getAspirateSources(), srcPlateBatchSize);
for (List srcPlateBatch : srcPlateBatches)
{
// TODO: Test for a plate type match between the plate and the plate layout
int tip = 1;
if (inMode.equals(Mode.normal)
&& getDeckConfig() != null)
{
StringBuilderPlus comments = new StringBuilderPlus().setDelimiter(" ");
for (TecanPlate srcPlate : srcPlateBatch)
{
if (CollectionUtil.hasValues(srcPlate.getSelectedWells())) // Skip plates that don't need to be operated on
{
getDeckConfig().validateDeckPosition(srcPlate.getTecanLabel());
TecanLabwareContainer currentContainerAtSrcPosition = getDeckConfig().getDeckPosition(srcPlate.getTecanLabel()).getContainer();
if (null == currentContainerAtSrcPosition
|| !currentContainerAtSrcPosition.equals(srcPlate))
{
String comment = "Place src " + (StringUtil.isSet(srcPlate.getRackId()) ? srcPlate.getRackId() : srcPlate.name())
+ " at deck position " + srcPlate.getTecanLabel() + ".";
comments.delimitedAppend(comment);
// Tell the deck config that this plate is now at this deck position
getDeckConfig().getDeckPosition(srcPlate.getTecanLabel()).setContainer(srcPlate);
}
}
}
printStream.println(new TecanWorklistUserPromptCommand(comments.toString()).toString());
}
for (TecanPlate srcPlate : srcPlateBatch)
{
VolumeMask reagentVolumeMask = null;
if (mReagentContainer != null
&& StringUtil.isSet(mReagentQuantityDataLayerName))
{
reagentVolumeMask = new VolumeMask(srcPlate.getDataLayer(mReagentQuantityDataLayerName));
if (null == reagentVolumeMask)
{
throw new AutomationConfigurationException("Reagent data layer " + StringUtil.singleQuote(mReagentQuantityDataLayerName)
+ " wasn't found on source plate " + StringUtil.singleQuote(srcPlate.name()) + "!");
}
}
if (inMode.equals(Mode.normal)
&& getDeckConfig() != null)
{
getDeckConfig().validateDeckPosition(srcPlate.getTecanLabel());
TecanLabwareContainer currentContainerAtSrcPosition = getDeckConfig().getDeckPosition(srcPlate.getTecanLabel()).getContainer();
if (null == currentContainerAtSrcPosition
|| !currentContainerAtSrcPosition.equals(srcPlate))
{
String comment = "Place src " + (StringUtil.isSet(srcPlate.getRackId()) ? srcPlate.getRackId() + " " : "")
+ "at deck position " + srcPlate.getTecanLabel();
printStream.println(new TecanWorklistCommentCommand(comment).toString());
getDeckConfig().getDeckPosition(srcPlate.getTecanLabel()).setContainer(srcPlate);
}
TecanLabwareContainer currentContainerAtDestPosition = getDeckConfig().getDeckPosition(currentDestPlate.getTecanLabel()).getContainer();
if (null == currentContainerAtDestPosition
|| !currentContainerAtDestPosition.equals(currentDestPlate))
{
String comment = "Place dest " + (StringUtil.isSet(currentDestPlate.getRackId()) ? currentDestPlate.getRackId() + " " : "")
+ "at deck position " + currentDestPlate.getTecanLabel();
printStream.println(new TecanWorklistCommentCommand(comment).toString());
getDeckConfig().getDeckPosition(currentDestPlate.getTecanLabel()).setContainer(currentDestPlate);
}
}
for (WellRef srcWellRef : srcPlate.getLayout().getWellRefs(false))
{
PlateLocation srcPlateLoc = new PlateLocation(srcPlate, srcWellRef);
Map destMap = mTransferMap.get(srcPlateLoc);
if (null == destMap)
{
destMap = new OrderedMap<>(2);
mTransferMap.put(srcPlateLoc, destMap);
}
Well srcWell = srcPlate.getWell(srcWellRef);
if (srcWell.isSelected())
{
WellRef destWellRef = currentDestPlate.getNextAvailableSampleWellRef(getSampleClass(), mSampleLayerName);
if (null == destWellRef)
{
destPlateIndex++;
if (destPlateIndex == getDispenseDestinations().size())
{
throw new AutomationConfigurationException("Not enough destination plates for the operation to complete!");
}
actualDestPlate = getDispenseDestinations().get(destPlateIndex);
currentDestPlate = (inMode.equals(Mode.normal) ? actualDestPlate : actualDestPlate.clone());
destWellRef = currentDestPlate.getNextAvailableSampleWellRef(getSampleClass(), mSampleLayerName);
}
PlateLocation destPlateLoc = new PlateLocation(actualDestPlate, destWellRef);
// Individual steps can be specified to handle more complex operations
if (CollectionUtil.hasValues(mOpSteps))
{
//TODO: Still fragile - add more checks and handling for volumes exceeding tip capacities
for (TecanOpStep step : mOpSteps)
{
TecanWorklistCommand record;
switch (step.getWorklistCommandType())
{
case ASPIRATE:
switch (step.getPlateRole())
{
case REAGENT:
if (null == mReagentContainer)
{
throw new AutomationConfigurationException("No Reagent specified via includeReagentInAspirate() to support the reagent ASPIRATE step!");
}
record = new TecanWorklistAspirateCommand()
.setRackLabel(mReagentContainer.getTecanLabel())
.setRackId(mReagentContainer.getRackId())
.setRackName(mReagentContainer.getTecanLabwareType().name())
.setPosition(tip)
.setVolume(step.getQuantity().floatValue())
.setLiquidClass(step.getLiquidClass() != null ? step.getLiquidClass() :
mReagentAspirateLiquidClass != null ? mReagentAspirateLiquidClass : getAspirateLiquidClass());
break;
case SOURCE:
record = new TecanWorklistAspirateCommand()
.setRackLabel(srcPlate.getTecanLabel())
.setRackId(srcPlate.getRackId())
.setRackName(srcPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(srcPlate.getType(), srcWell.getRef()))
.setVolume(step.getQuantity().floatValue())
.setLiquidClass(step.getLiquidClass() != null ? step.getLiquidClass() : getAspirateLiquidClass());
break;
case DESTINATION:
record = new TecanWorklistAspirateCommand()
.setRackLabel(currentDestPlate.getTecanLabel())
.setRackId(currentDestPlate.getRackId())
.setRackName(currentDestPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(currentDestPlate.getType(), destWellRef))
.setVolume(step.getQuantity().floatValue())
.setLiquidClass(step.getLiquidClass() != null ? step.getLiquidClass() : getAspirateLiquidClass());
updateDestinationVolume(destMap, srcWell, currentDestPlate, destWellRef, step.getQuantity().invert());
break;
default:
throw new AutomationConfigurationException("PlateRole " + StringUtil.singleQuote(step.getPlateRole()) + " not currently supported as a ASPIRATE step!");
}
break;
case DISPENSE:
switch (step.getPlateRole())
{
case SOURCE:
record = new TecanWorklistDispenseCommand()
.setRackLabel(srcPlate.getTecanLabel())
.setRackId(srcPlate.getRackId())
.setRackName(srcPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(srcPlate.getType(), srcWell.getRef()))
.setVolume(step.getQuantity().floatValue())
.setLiquidClass(step.getLiquidClass() != null ? step.getLiquidClass() : getDispenseLiquidClass());
updateDestinationVolume(destMap, srcWell, currentDestPlate, destWellRef, step.getQuantity());
break;
case DESTINATION:
record = new TecanWorklistDispenseCommand()
.setRackLabel(currentDestPlate.getTecanLabel())
.setRackId(currentDestPlate.getRackId())
.setRackName(currentDestPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(currentDestPlate.getType(), destWellRef))
.setVolume(step.getQuantity().floatValue())
.setLiquidClass(step.getLiquidClass() != null ? step.getLiquidClass() : getDispenseLiquidClass());
break;
default:
throw new AutomationConfigurationException("PlateRole " + StringUtil.singleQuote(step.getPlateRole()) + " not currently supported as a DISPENSE step!");
}
break;
default:
throw new AutomationConfigurationException("Command type " + StringUtil.singleQuote(step.getWorklistCommandType()) + " not currently supported as a step!");
}
if (inMode.equals(Mode.normal))
{
printStream.println(record.toString());
}
}
if (inMode.equals(Mode.normal))
{
// Tip wash
printStream.println(TecanWorklistCommand.WASH.toString());
}
tip++;
if (tip > numTips)
{
tip = 1;
}
}
else
{
// What is the volume of sample to be aspirated?
Quantity aspirateVolume = getAspirateVolume();
if (null == aspirateVolume
&& getAspirateVolumeMask() != null)
{
aspirateVolume = getAspirateVolumeMask().getVolume(srcWell.getRef());
}
else if (StringUtil.isSet(getAspirateVolumeDataLayerName()))
{
PlateDataLayer aspirateSrcLayer = srcPlate.getDataLayer(getAspirateVolumeDataLayerName());
if (aspirateSrcLayer != null)
{
aspirateVolume = aspirateSrcLayer.getData(srcWell.getRef());
if (null == aspirateVolume)
{
// A zero-volume well is a indicator that the well should be skipped
aspirateVolume = ZERO_VOLUME;
}
}
}
// What is the volume of sample to be dispensed?
Quantity dispenseVolume = getDispenseVolume();
if (null == dispenseVolume
&& getDispenseVolumeMask() != null)
{
dispenseVolume = getDispenseVolumeMask().getVolume(destWellRef);
}
else if (srcWell.getVolume() != null
&& 0 == srcWell.getVolume().floatValue())
{
// A zero-volume well is a indicator that the well should be skipped
dispenseVolume = ZERO_VOLUME;
}
// 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;
}
if (null == aspirateVolume)
{
if (srcWell.getVolume() != null)
{
// Assume that the entire volume should be transferred
aspirateVolume = srcWell.getVolume();
}
else
{
throw new RuntimeException("No " + getNameForPlate(srcPlate) + " " + srcWell.getRef() + " volume or volume mask value specified for the operation!");
}
}
if (null == dispenseVolume)
{
dispenseVolume = aspirateVolume;
}
// 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 " + destWellRef + " is larger than the aspirate volume!";
if (getVolumeCheckStringency().equals(VolumeCheckStringency.warning))
{
addWarning(msg);
}
else if (getVolumeCheckStringency().equals(VolumeCheckStringency.error))
{
throw new AutomationConfigurationException(msg);
}
}
// Reagent to include in the aspirate with the sample transfer?
Quantity reagentDispenseVolumeToInclude = null;
if (inMode.equals(Mode.normal)
&& mReagentContainer != null)
{
Quantity reagentAspriateVol = mFixedReagentQuantity;
if (null == reagentAspriateVol)
{
// Was the reagent quantity specified on the src plate?
reagentAspriateVol = reagentVolumeMask.getVolume(srcWellRef);
}
if (null == reagentAspriateVol)
{
throw new AutomationConfigurationException("Reagent " + mReagentContainer.getTecanLabel() + " was specified without a quantity!");
}
// 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 reagentVolumeRemainingToDispense = reagentAspriateVol;
do
{
if (getTipConfig() != null
&& reagentVolumeRemainingToDispense.greaterThan(getTipConfig().getTipType().getMaxCapacity().multiplyBy(2)))
{
reagentAspriateVol = getTipConfig().getTipType().getMaxCapacity();
}
else if (getTipConfig() != null
&& reagentVolumeRemainingToDispense.greaterThan(getTipConfig().getTipType().getMaxCapacity()))
{
// We don't want the remaining volume to get below the minimum capacity, so divide the remainder in half
reagentAspriateVol = reagentVolumeRemainingToDispense.divideBy(2);
}
Quantity reagentDispenseVolume = reagentAspriateVol;
// Tecan expects volumes to be specified as microliters
reagentAspriateVol = reagentAspriateVol.convertTo(MICROLITERS);
// Reagent aspirate
TecanWorklistAspirateCommand reagentAspirateRecord = new TecanWorklistAspirateCommand()
.setRackLabel(mReagentContainer.getTecanLabel())
.setRackId(mReagentContainer.getRackId())
.setRackName(mReagentContainer.getTecanLabwareType().name())
.setPosition(tip)
.setVolume(reagentAspriateVol.floatValue())
.setLiquidClass(mReagentAspirateLiquidClass != null ? mReagentAspirateLiquidClass : getAspirateLiquidClass());
printStream.println(reagentAspirateRecord.toString());
reagentVolumeRemainingToDispense = reagentVolumeRemainingToDispense.subtract(reagentDispenseVolume);
// Do we need to dispense the reagent or can we combine the aspirate with the sample aspirate?
if (getTipConfig() != null
&& (reagentVolumeRemainingToDispense.floatValue() > 0
|| reagentAspriateVol.add(aspirateVolume).greaterThan(getTipConfig().getTipType().getMaxCapacity())))
{
// Reagent Dispense
TecanWorklistDispenseCommand reagentDispenseRecord = new TecanWorklistDispenseCommand()
.setRackLabel(currentDestPlate.getTecanLabel())
.setRackId(currentDestPlate.getRackId())
.setRackName(currentDestPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(currentDestPlate.getType(), destWellRef))
.setVolume(reagentDispenseVolume.floatValue())
.setLiquidClass(getDispenseLiquidClass());
printStream.println(reagentDispenseRecord.toString());
}
else
{ // Let the sample dispense know we have already aspirated some reagent that needs to be included
reagentDispenseVolumeToInclude = reagentDispenseVolume;
}
} while (reagentVolumeRemainingToDispense.floatValue() > 0);
}
if (dispenseVolume.floatValue() > 0)
{
// 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))
{
if (reagentDispenseVolumeToInclude != null)
{
dispenseVolume = dispenseVolume.add(reagentDispenseVolumeToInclude);
}
// Aspirate
TecanWorklistAspirateCommand aspirateRecord = new TecanWorklistAspirateCommand()
.setRackLabel(srcPlate.getTecanLabel())
.setRackId(srcPlate.getRackId())
.setRackName(srcPlate.getTecanLabwareType().name())
.setPosition(getTecanWellPosition(srcPlate.getType(), srcWell.getRef()))
.setVolume(aspirateVolume.floatValue())
.setLiquidClass(getAspirateLiquidClass());
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(dispenseVolume.floatValue())
.setLiquidClass(getDispenseLiquidClass());
printStream.println(dispenseRecord.toString());
if (srcWell.getVolume() != null)
{
srcWell.subtractVolume(aspirateVolume);
}
}
// Update the volume in the destination plate
updateDestinationVolume(destMap, srcWell, currentDestPlate, destWellRef, dispenseVolume);
/*
PlateSampleLayer sampleLayer;
if (StringUtil.isSet(mSampleLayerName))
{
sampleLayer = currentDestPlate.getSampleLayer(mSampleLayerName);
if (null == sampleLayer)
{
sampleLayer = new PlateSampleLayer(getSampleClass());
sampleLayer.setName(mSampleLayerName);
currentDestPlate.addLayer(sampleLayer);
}
}
else
{
sampleLayer = currentDestPlate.getSampleLayer(getSampleClass());
if (!CollectionUtil.hasValues(sampleLayer.getSamples(destWellRef))
&& srcWell.getSamples() != null)
{
for (Comparable sample : srcWell.getSamples())
{
sampleLayer.addSample(destWellRef, sample);
}
}
}
sampleLayer.addVolume(destWellRef, dispenseVolume);
destMap.put(destPlateLoc, currentDestPlate.getWell(destWellRef).getSampleVolume());
*/
volumeRemainingToDispense = volumeRemainingToDispense.subtract(dispenseVolume);
}
while (volumeRemainingToDispense.floatValue() > 0);
if (inMode.equals(Mode.normal))
{
// Tip wash
printStream.println(TecanWorklistCommand.WASH.toString());
}
tip++;
if (tip > numTips)
{
tip = 1;
}
}
else
{
// No sample volume to transfer but there might be reagent to transfer whose well we need to reserve
// Update the volume in the destination plate
updateDestinationVolume(destMap, srcWell, currentDestPlate, destWellRef, dispenseVolume);
/*
PlateSampleLayer sampleLayer;
if (StringUtil.isSet(mSampleLayerName))
{
sampleLayer = currentDestPlate.getSampleLayer(mSampleLayerName);
if (null == sampleLayer)
{
sampleLayer = new PlateSampleLayer(getSampleClass());
sampleLayer.setName(mSampleLayerName);
currentDestPlate.addLayer(sampleLayer);
}
}
else
{
sampleLayer = currentDestPlate.getSampleLayer(getSampleClass());
if (null == sampleLayer)
{
sampleLayer = new PlateSampleLayer(getSampleClass());
sampleLayer.setName(mSampleLayerName);
currentDestPlate.addLayer(sampleLayer);
}
}
sampleLayer.addVolume(destWellRef, dispenseVolume);
destMap.put(destPlateLoc, dispenseVolume);
*/
}
}
}
}
}
}
if (inMode.equals(Mode.normal))
{
printStream.close();
}
}
//---------------------------------------------------------------------------
// Updates the volume in the destination plate and in the destination map
private void updateDestinationVolume(Map inDestMap,
Well inSrcWell,
TecanPlate inDestPlate,
WellRef inDestWellRef,
Quantity inVolume)
{
PlateSampleLayer sampleLayer;
if (StringUtil.isSet(mSampleLayerName))
{
sampleLayer = inDestPlate.getSampleLayer(mSampleLayerName);
if (null == sampleLayer)
{
sampleLayer = new PlateSampleLayer(getSampleClass());
sampleLayer.setName(mSampleLayerName);
inDestPlate.addLayer(sampleLayer);
}
}
else
{
sampleLayer = inDestPlate.getSampleLayer(getSampleClass());
if (null == sampleLayer)
{
sampleLayer = new PlateSampleLayer(getSampleClass());
sampleLayer.setName(mSampleLayerName);
inDestPlate.addLayer(sampleLayer);
}
else if (! CollectionUtil.hasValues(sampleLayer.getSamples(inDestWellRef))
&& inSrcWell.getSamples() != null
&& inVolume.intValue() > 0) // Only transfer samples if some volume was transferred
{
for (Comparable sample : inSrcWell.getSamples())
{
sampleLayer.addSample(inDestWellRef, sample);
}
}
}
sampleLayer.addVolume(inDestWellRef, inVolume);
inDestMap.put(new PlateLocation(inDestPlate, inDestWellRef), inDestPlate.getWell(inDestWellRef).getSampleVolume());
}
}