
com.hfg.automation.tecan.TecanProtocol Maven / Gradle / Ivy
package com.hfg.automation.tecan;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import com.hfg.automation.LabwareContainer;
import com.hfg.automation.PlateLocation;
import com.hfg.automation.PlateUtil;
import com.hfg.automation.VolumeCheckStringency;
import com.hfg.automation.WellRef;
import com.hfg.automation.platelayer.PlateDataLayer;
import com.hfg.automation.plateop.AddReagentPlateOp;
import com.hfg.automation.plateop.LiquidTransferPlateOp;
import com.hfg.automation.plateop.PlateOp;
import com.hfg.automation.plateop.VolumeMask;
import com.hfg.automation.tecan.plateop.TecanAddReagentPlateOp;
import com.hfg.automation.tecan.plateop.TecanCopyPlateOp;
import com.hfg.automation.tecan.plateop.TecanPlateOp;
import com.hfg.automation.tecan.plateop.TecanRearrayPlateOp;
import com.hfg.automation.tecan.worklistcmd.TecanWorklistCommentCommand;
import com.hfg.datetime.DateUtil;
import com.hfg.units.Quantity;
import com.hfg.util.CompareUtil;
import com.hfg.util.FileUtil;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.collection.OrderedMap;
import com.hfg.util.io.FileBytes;
//------------------------------------------------------------------------------
/**
A collection of Tecan Plate Operations intended to be executed as a set.
@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 TecanProtocol
{
private String mName;
private TecanDeckConfig mDeckConfig;
private List mPlateOps;
private Mode mMode = Mode.worklist_per_plate_op;
private String mComment;
private boolean mAddTimestampToComment = true;
private VolumeCheckStringency mVolumeCheckStringency = VolumeCheckStringency.error;
private List mWarnings;
private boolean mGenerateTransferSummaryTSV;
private Map> mTransferMap;
public enum Mode
{
single_worklist
, worklist_per_plate_op
// , worklist_per_source_plate
}
private static final String REAGENT_DISPENSE_VOL_DATA_LAYER_NAME = "Reagent Dispense Volume";
//###########################################################################
// CONSTRUCTORS
//###########################################################################
//---------------------------------------------------------------------------
public TecanProtocol()
{
}
//---------------------------------------------------------------------------
public TecanProtocol(String inValue)
{
this();
setName(inValue);
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//---------------------------------------------------------------------------
public TecanProtocol setDeckConfig(TecanDeckConfig inValue)
{
mDeckConfig = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanDeckConfig getDeckConfig()
{
return mDeckConfig;
}
//---------------------------------------------------------------------------
public TecanProtocol setVolumeCheckStringency(VolumeCheckStringency inValue)
{
mVolumeCheckStringency = inValue;
return this;
}
//---------------------------------------------------------------------------
public VolumeCheckStringency getVolumeCheckStringency()
{
return mVolumeCheckStringency;
}
//---------------------------------------------------------------------------
public boolean producedWarnings()
{
return CollectionUtil.hasValues(mWarnings);
}
//---------------------------------------------------------------------------
public List getWarnings()
{
return mWarnings;
}
//---------------------------------------------------------------------------
public TecanProtocol setAddTimestampToComment(boolean inValue)
{
mAddTimestampToComment = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanProtocol setGenerateRearrayTSV(boolean inValue)
{
mGenerateTransferSummaryTSV = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanProtocol setComment(String inValue)
{
mComment = inValue;
return this;
}
//---------------------------------------------------------------------------
public TecanProtocol setName(String inValue)
{
mName = inValue;
return this;
}
//---------------------------------------------------------------------------
public String name()
{
return mName;
}
//---------------------------------------------------------------------------
@Override
public String toString()
{
return name();
}
//---------------------------------------------------------------------------
public TecanProtocol setMode(Mode inValue)
{
mMode = inValue;
return this;
}
//---------------------------------------------------------------------------
public Mode getMode()
{
return mMode;
}
//---------------------------------------------------------------------------
public TecanProtocol addPlateOp(TecanPlateOp inValue)
{
if (null == mPlateOps)
{
mPlateOps = new ArrayList<>(5);
}
mPlateOps.add(inValue);
return this;
}
//---------------------------------------------------------------------------
public List getPlateOps()
{
return mPlateOps;
}
//--------------------------------------------------------------------------
public List generateWorklists(String inWorklistFileBaseName)
throws IOException
{
Map opWorklistMap = new HashMap<>(getPlateOps().size());
List outputFiles = new ArrayList<>(5);
/* This shouldn't be necessary any more since the rearray is now based on a particular sample layer
Map> origOccupiedWellMap = new HashMap<>();
for (int i = 0; i < mPlateOps.size(); i++)
{
PlateOp plateOp = mPlateOps.get(i);
if (TecanRearrayPlateOp.class.isAssignableFrom(plateOp.getClass()))
{
for (TecanPlate destPlate : (Collection) plateOp.getDispenseDestinations())
{
origOccupiedWellMap.put(destPlate, destPlate.getOccupiedWells());
}
}
}
*/
mTransferMap = new OrderedMap<>();
for (int i = 0; i < mPlateOps.size(); i++)
{
TecanPlateOp plateOp = mPlateOps.get(i);
if (getDeckConfig() != null)
{
plateOp.setDeckConfig(getDeckConfig());
}
/* This shouldn't be necessary any more since the rearray is now based on a particular sample layer
if (TecanRearrayPlateOp.class.isAssignableFrom(plateOp.getClass()))
{
for (TecanPlate destPlate : (List) plateOp.getDispenseDestinations())
{
destPlate.setWells(origOccupiedWellMap.get(destPlate));
}
}
*/
if (plateOp instanceof AddReagentPlateOp)
{
TecanAddReagentPlateOp addReagentPlateOp = (TecanAddReagentPlateOp) plateOp;
// Do we need to populate volume masks?
if (null == addReagentPlateOp.getAspirateVolumeMask()
&& null == addReagentPlateOp.getDispenseVolumeMask()
&& (addReagentPlateOp.getTotalVolumeTarget() != null
|| (StringUtil.isSet(addReagentPlateOp.getDispenseVolumeSrcDataLayerName()))))
{
if (addReagentPlateOp.getTotalVolumeTarget() != null)
{
// Use the total volume target to calculate a dispense data layer per-destination plate for the reagent transfer
for (TecanPlate destPlate : (List) addReagentPlateOp.getDispenseDestinations())
{
VolumeMask volumeMask = new VolumeMask();
for (PlateOp otherPlateOp : mPlateOps)
{
if (otherPlateOp != addReagentPlateOp
&& otherPlateOp instanceof LiquidTransferPlateOp)
{
// TODO: Check aspirate sources as well
for (LabwareContainer otherPlate : ((LiquidTransferPlateOp) otherPlateOp).getDispenseDestinations())
{
if (otherPlate.equals(destPlate))
{
VolumeMask otherPlateVolumeMask = ((LiquidTransferPlateOp) otherPlateOp).calculatePostOpTotalVolumeMask(destPlate);
volumeMask.addVolume(otherPlateVolumeMask);
break;
}
}
}
}
// The reagent transfer volume is the target total volume minus the combined total volume from the other operations
volumeMask.subtractFrom(addReagentPlateOp.getTotalVolumeTarget());
PlateDataLayer dispenseVolumeDataLayer = new PlateDataLayer(REAGENT_DISPENSE_VOL_DATA_LAYER_NAME, Quantity.class);
if (CollectionUtil.hasValues(volumeMask.getOccupiedWellRefs()))
{
for (WellRef wellRef : volumeMask.getOccupiedWellRefs())
{
dispenseVolumeDataLayer.addData(wellRef, volumeMask.getVolume(wellRef));
}
}
destPlate.addLayer(dispenseVolumeDataLayer);
}
}
else if (StringUtil.isSet(addReagentPlateOp.getDispenseVolumeSrcDataLayerName()))
{
for (TecanPlate destPlate : (List) addReagentPlateOp.getDispenseDestinations())
{
PlateDataLayer dispenseVolumeDataLayer = new PlateDataLayer("Reagent Dispense Volume", Quantity.class);
for (WellRef wellRef : destPlate.getLayout().getWellRefs())
{
PlateLocation destPlateLoc = new PlateLocation(destPlate, wellRef);
for (PlateOp otherPlateOp : mPlateOps)
{
if (otherPlateOp != addReagentPlateOp
&& otherPlateOp instanceof LiquidTransferPlateOp)
{
Quantity transferVolume = ((LiquidTransferPlateOp) otherPlateOp).getDestinationVolumeFromSrcDataLayer(destPlateLoc, addReagentPlateOp.getDispenseVolumeSrcDataLayerName());
if (transferVolume != null)
{
dispenseVolumeDataLayer.addData(wellRef, transferVolume);
}
}
}
}
destPlate.addLayer(dispenseVolumeDataLayer);
}
}
addReagentPlateOp.setDispenseVolumeDataLayerName(REAGENT_DISPENSE_VOL_DATA_LAYER_NAME);
}
}
// TODO: Handle other plateOp types
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(50 * 1024);
plateOp.generateWorklist(byteStream);
// Were there any warnings that we need to bubble up?
if (plateOp.producedWarnings())
{
addWarnings(plateOp.getWarnings());
}
String worklistName = inWorklistFileBaseName + " " + (i + 1) + ".gwl";
FileBytes fileBytes = new FileBytes(worklistName);
fileBytes.setData(new ByteArrayInputStream(byteStream.toByteArray()));
opWorklistMap.put(plateOp, fileBytes);
byteStream.reset();
if (TecanRearrayPlateOp.class.isAssignableFrom(plateOp.getClass()))
{
mTransferMap.putAll(((TecanRearrayPlateOp)plateOp).getTransferMap());
}
else if (TecanCopyPlateOp.class.isAssignableFrom(plateOp.getClass()))
{
mTransferMap.putAll(((TecanCopyPlateOp)plateOp).getTransferMap());
}
}
if (mGenerateTransferSummaryTSV)
{
outputFiles.add(PlateUtil.generateTransferSummaryTSV(inWorklistFileBaseName + ".tsv", mTransferMap));
}
if (getMode().equals(Mode.single_worklist))
{
List sortedPlateOps = new ArrayList<>(mPlateOps);
Collections.sort(sortedPlateOps, new OpOutputOrderComparator());
// TODO: Sanity check the output ordered ops.
// Ex: there shouldn't be 2 set tip operations next to ea. other
FileBytes fileBytes = new FileBytes(inWorklistFileBaseName + ".gwl");
if (StringUtil.isSet(mComment))
{
StringBuilderPlus commentBlock = new StringBuilderPlus();
commentBlock.appendln(new TecanWorklistCommentCommand(mComment).toString());
if (mAddTimestampToComment)
{
commentBlock.appendln(new TecanWorklistCommentCommand("Generated " + DateUtil.getYYYY_MM_DD_HH_mm_ss()).toString());
}
commentBlock.appendln(new TecanWorklistCommentCommand("*****************************************************").toString());
fileBytes.appendData(commentBlock.toString().getBytes());
}
for (TecanPlateOp plateOp : sortedPlateOps)
{
fileBytes.appendData(opWorklistMap.get(plateOp));
}
outputFiles.add(fileBytes);
}
else
{
outputFiles.addAll(opWorklistMap.values());
}
return outputFiles;
}
//--------------------------------------------------------------------------
public void writeWorklists(File inWorklistFile)
throws IOException
{
List worklists = generateWorklists(FileUtil.getNameMinusExtension(inWorklistFile));
if (1 == worklists.size())
{
FileUtil.write(inWorklistFile, worklists.get(0).getDataStream());
}
else
{
createZipFile(inWorklistFile, worklists);
}
}
//---------------------------------------------------------------------------
public Map> getTransferMap()
{
return mTransferMap;
}
//---------------------------------------------------------------------------
protected void addWarnings(Collection inValues)
{
if (null == mWarnings)
{
mWarnings = new ArrayList<>(10);
}
mWarnings.addAll(inValues);
}
//--------------------------------------------------------------------------
private void createZipFile(File inWorklistFile, List inWorklists)
throws IOException
{
File zipFile = new File(FileUtil.getNameMinusExtension(inWorklistFile) + ".zip");
ZipOutputStream zipStream = null;
try
{
zipStream = new ZipOutputStream(new FileOutputStream(zipFile));
for (FileBytes worklist : inWorklists)
{
ZipEntry zipEntry = new ZipEntry(FileUtil.getNameMinusExtension(zipFile) + "/" + worklist.name());
zipStream.putNextEntry(zipEntry);
worklist.writeData(zipStream);
zipStream.closeEntry();
}
}
finally
{
if (zipStream != null)
{
zipStream.finish();
}
}
}
private class OpOutputOrderComparator implements Comparator
{
//-----------------------------------------------------------------------
@Override
public int compare(TecanPlateOp inObj1, TecanPlateOp inObj2)
{
return CompareUtil.compare(inObj1.getOutputOrder(), inObj2.getOutputOrder());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy