com.hfg.automation.tecan.plateop.TecanAddReagentPlateOp 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.*;
import com.hfg.automation.*;
import com.hfg.automation.platelayer.PlateDataLayer;
import com.hfg.automation.platelayer.PlateReagentLayer;
import com.hfg.automation.plateop.AddReagentPlateOp;
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.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.TecanWorklistReagentDistributionCommand;
import com.hfg.math.Counter;
import com.hfg.math.Range;
import com.hfg.units.Quantity;
import com.hfg.units.Unit;
import com.hfg.util.CompareUtil;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.collection.OrderedMap;
//------------------------------------------------------------------------------
/**
Tecan-specific implementation of the Add Reagent 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]
//------------------------------------------------------------------------------
/*
Scenarios:
1. One aspiration per dispense
2. One aspiration per multiple dispenses
3. Multiple aspirations and dispenses per well
*/
public class TecanAddReagentPlateOp extends TecanPerPlateLiquidTransferPlateOp implements AddReagentPlateOp
{
private R mReagent;
private TecanLabwareContainer mAspirateSrc;
private List mDispenseDestinations;
private TecanTipConfig mTipConfig;
private int mMaxTipCycles = 1;
private boolean mAllowMultiDispense = false;
private Quantity mTotalVolumeTarget;
private TecanWorklistCommand mWashType = TecanWorklistCommand.WASH;
private String mDispenseVolumeSrcDataLayerName;
// Tecan expects volumes to be specified as microliters
private static final Unit MICROLITES = Unit.valueOf("μL");
private static Set sAspirateAllowedGroups = new HashSet<>();
private static Set sDispenseAllowedGroups = new HashSet<>();
static
{
sAspirateAllowedGroups.add(LabwareTypeGroup.Plate);
sAspirateAllowedGroups.add(LabwareTypeGroup.Trough);
sAspirateAllowedGroups.add(LabwareTypeGroup.Tube);
sDispenseAllowedGroups.add(LabwareTypeGroup.Plate);
sDispenseAllowedGroups.add(LabwareTypeGroup.Trough);
sDispenseAllowedGroups.add(LabwareTypeGroup.Tube);
sDispenseAllowedGroups.add(LabwareTypeGroup.Wash_and_Waste);
}
//###########################################################################
// CONSTRUCTORS
//###########################################################################
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp(R inReagent)
{
setReagent(inReagent);
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//---------------------------------------------------------------------------
public R getReagent()
{
return mReagent;
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setTipConfig(TecanTipConfig inValue)
{
mTipConfig = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanTipConfig getTipConfig()
{
return mTipConfig;
}
//---------------------------------------------------------------------------
/**
* Specifies the maximum number of aspirate/dispense cycles that a tip can be used for.
* @param inValue the maximum number of aspirate/dispense cycles that a tip can be used for
* @return this plate operation object to facilitate method chaining.
*/
public TecanAddReagentPlateOp setMaxTipCycles(int inValue)
{
if (inValue <= 0)
{
throw new AutomationConfigurationException("The max tip cycles cannot be set <= 0!");
}
mMaxTipCycles = inValue;
return this;
}
//---------------------------------------------------------------------------
public int getMaxTipCycles()
{
return mMaxTipCycles;
}
//---------------------------------------------------------------------------
/**
* Specifies whether to allow multiple dispenses for a single aspirate.
* @param inValue whether to allow multiple dispenses for a single aspirate
* @return this plate operation object to facilitate method chaining.
*/
public TecanAddReagentPlateOp setAllowMultiDispense(boolean inValue)
{
mAllowMultiDispense = inValue;
return this;
}
//---------------------------------------------------------------------------
public boolean getAllowMultiDispense()
{
return mAllowMultiDispense;
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setOutputOrder(Integer inValue)
{
return (TecanAddReagentPlateOp) super.setOutputOrder(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setVolumeCheckStringency(VolumeCheckStringency inValue)
{
return (TecanAddReagentPlateOp) super.setVolumeCheckStringency(inValue);
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setAspirateSrc(TecanLabwareContainer inValue)
{
aspirateGroupCheck(inValue);
mAspirateSrc = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanLabwareContainer getAspirateSrc()
{
return mAspirateSrc;
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setDispenseDestinations(Collection inValues)
{
if (null == mDispenseDestinations)
{
mDispenseDestinations = new ArrayList<>(10);
}
mDispenseDestinations.addAll(inValues);
return this;
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp addDispenseDestination(TecanLabwareContainer inValue)
{
dispenseGroupCheck(inValue);
if (null == mDispenseDestinations)
{
mDispenseDestinations = new ArrayList<>(10);
}
mDispenseDestinations.add(inValue);
return this;
}
//---------------------------------------------------------------------------
public List getDispenseDestinations()
{
return mDispenseDestinations;
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setDispenseVolumeSrcDataLayerName(String inSrcDataLayerName)
{
mDispenseVolumeSrcDataLayerName = inSrcDataLayerName;
return this;
}
//---------------------------------------------------------------------------
public String getDispenseVolumeSrcDataLayerName()
{
return mDispenseVolumeSrcDataLayerName;
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setDispenseVolumeDataLayerName(String inValue)
{
return (TecanAddReagentPlateOp) super.setDispenseVolumeDataLayerName(inValue);
}
//---------------------------------------------------------------------------
public VolumeMask calculatePostOpTotalVolumeMask(Plate inPlate)
{
VolumeMask opVolumeMask = getDispenseDestinationVolumeMask((TecanPlate) inPlate);
if (null == opVolumeMask)
{
throw new AutomationConfigurationException(name() + " could not calculate post operation volumes!");
}
return opVolumeMask;
}
//---------------------------------------------------------------------------
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!");
}
}
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setTotalVolumeTarget(Quantity inValue)
{
mTotalVolumeTarget = inValue;
return this;
}
//---------------------------------------------------------------------------
public Quantity getTotalVolumeTarget()
{
return mTotalVolumeTarget;
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setAspirateVolume(Quantity inValue)
{
return (TecanAddReagentPlateOp) super.setAspirateVolume(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setDispenseVolume(Quantity inValue)
{
return (TecanAddReagentPlateOp) super.setDispenseVolume(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setAspirateVolumeMask(VolumeMask inValue)
{
return (TecanAddReagentPlateOp) super.setAspirateVolumeMask(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setLiquidClass(TecanLiquidClass inValue)
{
return (TecanAddReagentPlateOp) super.setLiquidClass(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setDispenseVolumeMask(VolumeMask inValue)
{
return (TecanAddReagentPlateOp) super.setDispenseVolumeMask(inValue);
}
//---------------------------------------------------------------------------
@Override
public Quantity getDestinationVolumeFromSrcDataLayer(PlateLocation inDestinationLocation, String inDataLayerName)
{
// Doesn't really make sense for this class
return null;
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setAspirateLiquidClass(TecanLiquidClass inValue)
{
return (TecanAddReagentPlateOp) super.setAspirateLiquidClass(inValue);
}
//---------------------------------------------------------------------------
@Override
public TecanAddReagentPlateOp setComment(String inValue)
{
return (TecanAddReagentPlateOp) super.setComment(inValue);
}
/*
//---------------------------------------------------------------------------
public TecanAddReagentPlateOp setWashTypeAndFreq(TecanWorklistRecord inWashType, Integer inAfterNDispenses)
{
mWashType = inWashType;
mWashFreq = inAfterNDispenses;
return this;
}
//---------------------------------------------------------------------------
public Integer getWashFreq()
{
return mWashFreq;
}
*/
//---------------------------------------------------------------------------
public TecanWorklistCommand getWashType()
{
return mWashType;
}
//---------------------------------------------------------------------------
public void generateWorklist(OutputStream inStream)
throws IOException
{
checkConfiguration();
PrintStream printStream = new PrintStream(inStream);
if (StringUtil.isSet(getComment()))
{
printStream.println(new TecanWorklistCommentCommand(getComment()));
}
if (getDeckConfig() != null)
{
TecanLabwareContainer currentContainerAtReagentPosition = getDeckConfig().getDeckPosition(mAspirateSrc.getTecanLabel()).getContainer();
if (null == currentContainerAtReagentPosition
|| !currentContainerAtReagentPosition.equals(mAspirateSrc))
{
String comment = "Place reagent " + (StringUtil.isSet(mAspirateSrc.getRackId()) ? mAspirateSrc.getRackId() + " " : "")
+ "at deck position " + mAspirateSrc.getTecanLabel();
printStream.println(new TecanWorklistCommentCommand(comment).toString());
getDeckConfig().getDeckPosition(mAspirateSrc.getTecanLabel()).setContainer(mAspirateSrc);
}
}
int tip = 1;
// TODO: How does the Tecan deal with worklists for multiple plates?
for (int destIndex = 0; destIndex < getDispenseDestinations().size(); destIndex++)
{
TecanLabwareContainer container = getDispenseDestinations().get(destIndex);
if (! container.getTecanLabwareType().getGroup().equals(LabwareTypeGroup.Plate))
{
throw new AutomationConfigurationException("Destinations must currently be plates for this PlateOp!");
}
TecanPlate plate = (TecanPlate) container;
if (getDeckConfig() != null)
{
TecanLabwareContainer currentContainerAtDestPosition = getDeckConfig().getDeckPosition(container.getTecanLabel()).getContainer();
if (null == currentContainerAtDestPosition
|| ! currentContainerAtDestPosition.equals(container))
{
String comment = "Place dest " + (StringUtil.isSet(container.getRackId()) ? container.getRackId() + " " : "")
+ "at deck position " + container.getTecanLabel();
printStream.println(new TecanWorklistCommentCommand(comment).toString());
getDeckConfig().getDeckPosition(container.getTecanLabel()).setContainer(container);
}
}
// TODO: Test for a plate type match between the plate and the plate layout
// Special case if all the dispense volumes are the same
if (getDispenseVolume() != null
|| (getDispenseVolumeMask() != null
&& CollectionUtil.hasValues(getDispenseVolumeMask().getOccupiedWellRefs())
&& getDispenseVolumeMask().allVolumesAreIdentical())
|| (StringUtil.isSet(getDispenseVolumeDataLayerName())
&& CollectionUtil.hasValues(plate.getDataLayer(getDispenseVolumeDataLayerName()).getOccupiedWellRefs())
&& new VolumeMask(plate.getDataLayer(getDispenseVolumeDataLayerName())).allVolumesAreIdentical()))
{
// Use the Reagent Distribution (R) record
WellRange wellRange;
VolumeMask dispenseVolumeMask = null;
Quantity dispenseVolume = getDispenseVolume();
if (dispenseVolume != null)
{
wellRange = plate.getType().getWellRange();
}
else
{
dispenseVolumeMask = getDispenseVolumeMask();
if (null == dispenseVolumeMask)
{
dispenseVolumeMask = new VolumeMask(plate.getDataLayer(getDispenseVolumeDataLayerName()));
}
dispenseVolume = dispenseVolumeMask.getVolume(dispenseVolumeMask.getOccupiedWellRefs().iterator().next());
wellRange = dispenseVolumeMask.getOccupiedWellRange(plate.getLayout());
}
if (null == dispenseVolume
|| 0 == dispenseVolume.floatValue())
{
continue;
}
dispenseVolume = dispenseVolume.convertTo(MICROLITES);
// 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)))
{
dispenseVolume = 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
dispenseVolume = volumeRemainingToDispense.divideBy(2);
}
else
{
dispenseVolume = volumeRemainingToDispense;
}
int multiDispense = 1;
if (getTipConfig() != null)
{
multiDispense = getTipConfig().getTipType().getMaxCapacity().intValue() / dispenseVolume.intValue();
}
TecanWorklistReagentDistributionCommand reagentDistributionRecord = new TecanWorklistReagentDistributionCommand()
.setSourceRackLabel(getAspirateSrc().getTecanLabel())
.setSourceRackId(getAspirateSrc().getRackId())
.setSourceRackName(getAspirateSrc().getTecanLabwareType().name())
.setSourcePositionRange(new Range<>(1, getTipConfig().getNumTips()))
.setDestinationRackLabel(plate.getTecanLabel())
.setDestinationRackId(plate.getRackId())
.setDestinationRackName(plate.getTecanLabwareType().name())
.setDestinationPositionRange(plate.getType(), wellRange)
.setMultiDispense(multiDispense) // TODO: should this be adjusted based on the max tip volume or a fixed number like 12?
.setVolume(dispenseVolume)
.setLiquidClass(getAspirateLiquidClass());
// Excluded wells?
if (getDispenseVolume() != null)
{
reagentDistributionRecord.excludeWells(plate.getType(), plate.getLayout().getWellsToKeepEmpty());
}
else
{
Iterator iterator = plate.getLayout().wellRefIterator();
while (iterator.hasNext())
{
WellRef wellRef = iterator.next();
Quantity quantity = dispenseVolumeMask.getVolume(wellRef);
if (null == quantity
|| 0 == quantity.floatValue())
{
reagentDistributionRecord.excludeWell(plate.getType(), wellRef);
}
}
}
printStream.println(reagentDistributionRecord.toString());
volumeRemainingToDispense = volumeRemainingToDispense.subtract(dispenseVolume);
} while (volumeRemainingToDispense.floatValue() > 0);
}
else
{
// Use Aspirate and Dispense ops for ea. well
Map> tipPositionMap = new OrderedMap<>(getTipConfig().getNumTips());
Map tipCycleMap = new HashMap<>(10);
Iterator iterator = plate.getLayout().wellRefIterator();
while (iterator.hasNext())
{
WellRef wellRef = iterator.next();
// What is the volume of reagent to be aspirated?
Quantity aspirateVolume = getAspirateVolume();
if (null == aspirateVolume
&& getAspirateVolumeMask() != null)
{
aspirateVolume = getAspirateVolumeMask().getVolume(wellRef);
}
// What is the volume of reagent to be dispensed?
Quantity dispenseVolume = getDispenseVolume();
if (null == dispenseVolume)
{
if (getDispenseVolumeMask() != null)
{
dispenseVolume = getDispenseVolumeMask().getVolume(wellRef);
}
else if (StringUtil.isSet(getDispenseVolumeDataLayerName()))
{
PlateDataLayer dispenseVolumeLayer = plate.getDataLayer(getDispenseVolumeDataLayerName());
if (dispenseVolumeLayer != null)
{
dispenseVolume = dispenseVolumeLayer.getData(wellRef);
}
}
else
{
// Does the plate have a reagent layer defined?
PlateReagentLayer reagentLayer = plate.getReagentLayer(getReagent());
if (reagentLayer != null)
{
dispenseVolume = reagentLayer.getVolume(wellRef);
}
}
}
if (null == dispenseVolume)
{
if (StringUtil.isSet(getDispenseVolumeDataLayerName()))
{
continue;
}
else if (mTotalVolumeTarget != null)
{
Well well = plate.getWell(wellRef);
if (well != null
&& well.getVolume() != null
&& well.getVolume().floatValue() > 0)
{
aspirateVolume = mTotalVolumeTarget.subtract(well.getVolume());
}
else
{
aspirateVolume = new Quantity(0, Unit.valueOf("μL"));
}
}
}
// 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 (aspirateVolume != null
&& aspirateVolume.floatValue() > 0)
{
// Is the aspirate volume within capacity specs for the tip type?
Quantity tipMinCapacity = mTipConfig.getTipType().getMinCapacity();
// TODO: Should there be other ways to handle volumes less than the minimum capacity?
if (aspirateVolume.lessThan(tipMinCapacity))
{
String msg = getReagent() + " aspiration volume for well " + wellRef
+ " (" + aspirateVolume + ") is less than the minimum capacity for the " + mTipConfig.getTipType().name() + " tip (" + tipMinCapacity + ")!";
if (getVolumeCheckStringency().equals(VolumeCheckStringency.warning))
{
addWarning(msg);
}
else if (getVolumeCheckStringency().equals(VolumeCheckStringency.error))
{
throw new AutomationConfigurationException(msg);
}
}
// Tecan expects volumes to be specified as microliters
aspirateVolume = aspirateVolume.convertTo(MICROLITES);
dispenseVolume = dispenseVolume.convertTo(MICROLITES);
if (dispenseVolume.greaterThan(aspirateVolume))
{
String msg = "The dispense volume for well " + wellRef + " is larger than the " + getReagent() + " aspirate volume!";
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;
Counter cycleCounter = tipCycleMap.get(tip);
if (null == cycleCounter)
{
cycleCounter = new Counter();
tipCycleMap.put(tip, cycleCounter);
}
cycleCounter.increment();
tipPositionMap.computeIfAbsent(tip, value -> new ArrayList<>(50))
.add(new Transfer()
.setTipCycle(cycleCounter.intValue())
.setTipPosition(tip)
.addAspirateVolume(aspirateVolume)
.addDispense(new PlateLocation(plate, wellRef), dispenseVolume));
volumeRemainingToDispense = volumeRemainingToDispense.subtract(dispenseVolume);
// Update the volume in the destination plate
Well well = plate.getWell(wellRef);
if (null == well)
{
well = plate.allocateWell(wellRef);
}
well.addVolume(dispenseVolume);
} while (volumeRemainingToDispense.floatValue() > 0);
tip++;
if (getTipConfig().getNumTips() != null
&& tip > getTipConfig().getNumTips())
{
tip = 1;
}
}
}
// Can cycles be combined?
if (getAllowMultiDispense())
{
combineCycles(tipPositionMap);
}
// Output ops
while (CollectionUtil.hasValues(tipPositionMap))
{
for (int i = 1; i <= getTipConfig().getNumTips(); i++)
{
List tipPositionTransfers = tipPositionMap.get(i);
if (CollectionUtil.hasValues(tipPositionTransfers))
{
for (int cycle = 0; cycle < getMaxTipCycles(); cycle++)
{
if (0 == tipPositionTransfers.size())
{
break;
}
Transfer transfer = tipPositionTransfers.remove(0);
// Aspirate
TecanWorklistAspirateCommand aspirateRecord = new TecanWorklistAspirateCommand()
.setRackLabel(getAspirateSrc().getTecanLabel())
.setRackId(getAspirateSrc().getRackId())
.setRackName(getAspirateSrc().getTecanLabwareType().name())
.setVolume(Math.round(transfer.getAspirateVolume().floatValue()))
.setLiquidClass(getAspirateLiquidClass());
if (getTipConfig().getNumTips() != null)
{
aspirateRecord.setPosition(transfer.getTipPosition());
}
printStream.println(aspirateRecord.toString());
for (PlateLocation plateLocation : transfer.getDispenseMap().keySet())
{
// Dispense
TecanWorklistDispenseCommand dispenseRecord = new TecanWorklistDispenseCommand()
.setRackLabel(plate.getTecanLabel())
.setRackId(plate.getRackId())
.setRackName(plate.getTecanLabwareType().name())
.setPosition(TecanPlate.getTecanWellPosition(plate.getType(), plateLocation.getWellRef()))
.setVolume(Math.round(transfer.getDispenseMap().get(plateLocation).floatValue()))
.setLiquidClass(getDispenseLiquidClass());
printStream.println(dispenseRecord.toString());
}
}
// Tip wash
if (getWashType() != null)
{
printStream.println(getWashType().toString());
}
if (0 == tipPositionTransfers.size())
{
tipPositionMap.remove(i);
}
}
}
}
}
}
printStream.close();
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//---------------------------------------------------------------------------
private TecanAddReagentPlateOp setReagent(R inValue)
{
mReagent = inValue;
return this;
}
//---------------------------------------------------------------------------
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 wasn't a data layer to specify the dispense volumes.
// Do we have enough info to calculate one?
if (null == getDispenseVolume()
&& null == getDispenseVolumeMask()
&& null == inPlate.getReagentLayer(getReagent())
&& null == getAspirateVolume()
&& null == getAspirateVolumeMask()
&& (null == getTotalVolumeTarget()
&& inPlate.containsVolumes()))
{
throw new AutomationConfigurationException("No enough information for PlateOp " + name() + " to calculate destination volumes!");
}
plateDispenseVolumeMask = new VolumeMask();
Iterator iterator = inPlate.getLayout().wellRefIterator();
while (iterator.hasNext())
{
WellRef wellRef = iterator.next();
// What is the volume of reagent to be dispensed?
Quantity dispenseVolume = getDispenseVolume();
if (null == dispenseVolume)
{
if (getDispenseVolumeMask() != null)
{
dispenseVolume = getDispenseVolumeMask().getVolume(wellRef);
}
else
{
// Does the plate have a reagent layer defined?
PlateReagentLayer reagentLayer = inPlate.getReagentLayer(getReagent());
if (reagentLayer != null)
{
dispenseVolume = reagentLayer.getVolume(wellRef);
}
}
}
if (mTotalVolumeTarget != null)
{
// What is the volume of reagent to be aspirated? Assume that the dispense amount should be the same as the aspirate amount
Quantity aspirateVolume = getAspirateVolume();
if (null == aspirateVolume
&& getAspirateVolumeMask() != null)
{
aspirateVolume = getAspirateVolumeMask().getVolume(wellRef);
}
Well well = inPlate.getWell(wellRef);
if (well != null
&& well.getVolume() != null
&& well.getVolume().floatValue() > 0)
{
aspirateVolume = mTotalVolumeTarget.subtract(well.getVolume());
}
else
{
aspirateVolume = new Quantity(0, Unit.valueOf("μL"));
}
dispenseVolume = aspirateVolume;
}
plateDispenseVolumeMask.setVolume(wellRef, dispenseVolume);
}
}
return plateDispenseVolumeMask;
}
//---------------------------------------------------------------------------
private void checkConfiguration()
{
if (null == mTipConfig)
{
throw new AutomationConfigurationException("No tip configuration has been specified!");
}
if (null == mAspirateSrc)
{
throw new AutomationConfigurationException("No aspiration source has been specified!");
}
if (! CollectionUtil.hasValues(mDispenseDestinations))
{
throw new AutomationConfigurationException("No dispense destinations have been specified!");
}
if (! StringUtil.isSet(mAspirateSrc.getTecanLabel()))
{
throw new AutomationConfigurationException("Reagent container without a label (deck position)!");
}
if (getDeckConfig() != null)
{
TecanDeckPosition deckPosition = getDeckConfig().getDeckPosition(mAspirateSrc.getTecanLabel());
if (null == deckPosition)
{
throw new AutomationConfigurationException("Reagent container label " + StringUtil.singleQuote(mAspirateSrc.getTecanLabel()) + " is not valid for the specified deck configuration!");
}
else if (! mAspirateSrc.getTecanLabwareType().getClass().isAssignableFrom(deckPosition.getContainerType().getClass()))
{
throw new AutomationConfigurationException("Reagent container is incompatible with deck position " + StringUtil.singleQuote(mAspirateSrc.getTecanLabel()) + "!");
}
}
// Ensure that all destinations have valid labels
for (TecanLabwareContainer destContainer : getDispenseDestinations())
{
if (! StringUtil.isSet(destContainer.getTecanLabel()))
{
throw new AutomationConfigurationException("Destination container without a 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()) + "!");
}
}
}
if (StringUtil.isSet(getDispenseVolumeDataLayerName()))
{
for (TecanPlate destPlate : (List) (Object) getDispenseDestinations())
{
if (null == destPlate.getDataLayer(getDispenseVolumeDataLayerName()))
{
throw new AutomationConfigurationException("Destination container " + StringUtil.singleQuote(destPlate.getTecanLabel())
+ " doesn't have the expected data layer " + StringUtil.singleQuote(getDispenseVolumeDataLayerName()) + "!");
}
}
}
if (null == getTipConfig())
{
throw new AutomationConfigurationException("No tip configuration specified!");
}
}
//---------------------------------------------------------------------------
private void combineCycles(Map> inTipPositionMap)
{
Quantity maxVolume = getTipConfig().getTipType().getMaxCapacity().subtract(new Quantity("5 μL"));
for (List tipList : inTipPositionMap.values())
{
for (int i = 0; i < tipList.size() - 1; i++)
{
Transfer transfer1 = tipList.get(i);
for (int j = i + 1; j < tipList.size(); j++)
{
Transfer transfer2 = tipList.get(j);
// Can the two transfers be combined?
if (transfer1.getAspirateVolume().add(transfer2.getAspirateVolume()).floatValue() < maxVolume.floatValue())
{
transfer1.addAspirateVolume(transfer2.getAspirateVolume());
for (PlateLocation plateLocation : transfer2.getDispenseMap().keySet())
{
transfer1.addDispense(plateLocation, transfer2.getDispenseMap().get(plateLocation));
}
tipList.remove(j--);
}
}
}
}
}
private class Transfer implements Comparable
{
private int mTipCycle;
private int mTipPosition;
private Quantity mAspriateVolume;
private Map mDispenseMap;
//------------------------------------------------------------------------
public String toString()
{
return "Cycle: " + mTipCycle + "; Position: " + mTipPosition;
}
//------------------------------------------------------------------------
public Transfer setTipCycle(int inValue)
{
mTipCycle = inValue;
return this;
}
//------------------------------------------------------------------------
public int getTipCycle()
{
return mTipCycle;
}
//------------------------------------------------------------------------
public Transfer setTipPosition(int inValue)
{
mTipPosition = inValue;
return this;
}
//------------------------------------------------------------------------
public int getTipPosition()
{
return mTipPosition;
}
//------------------------------------------------------------------------
public Transfer addAspirateVolume(Quantity inValue)
{
mAspriateVolume = (mAspriateVolume != null ? mAspriateVolume.add(inValue) : inValue);
return this;
}
//------------------------------------------------------------------------
public Quantity getAspirateVolume()
{
return mAspriateVolume;
}
//------------------------------------------------------------------------
public Transfer addDispense(PlateLocation inLocation, Quantity inVol)
{
if (null == mDispenseMap)
{
mDispenseMap = new OrderedMap<>(5);
}
mDispenseMap.put(inLocation, inVol);
return this;
}
//------------------------------------------------------------------------
public Map getDispenseMap()
{
return mDispenseMap;
}
//------------------------------------------------------------------------
@Override
public int compareTo(Transfer inObj2)
{
int result = -1;
if (inObj2 != null)
{
result = CompareUtil.compare(getTipPosition(), inObj2.getTipPosition());
if (0 == result)
{
result = CompareUtil.compare(getDispenseMap().keySet(), inObj2.getDispenseMap().keySet());
}
if (0 == result)
{
result = CompareUtil.compare(getTipCycle(), inObj2.getTipCycle());
}
if (0 == result)
{
result = - CompareUtil.compare(getAspirateVolume(), inObj2.getAspirateVolume());
}
}
return result;
}
}
}