// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.model.cherrypicks;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import javax.persistence.Version;
import org.apache.log4j.Logger;
import org.hibernate.annotations.Parameter;
import edu.harvard.med.screensaver.model.AbstractEntity;
import edu.harvard.med.screensaver.model.AbstractEntityVisitor;
import edu.harvard.med.screensaver.model.BusinessRuleViolationException;
import edu.harvard.med.screensaver.model.Volume;
import edu.harvard.med.screensaver.model.annotations.ToMany;
import edu.harvard.med.screensaver.model.libraries.Copy;
import edu.harvard.med.screensaver.model.libraries.Reagent;
import edu.harvard.med.screensaver.model.libraries.Well;
import edu.harvard.med.screensaver.model.libraries.WellName;
import edu.harvard.med.screensaver.model.libraries.WellVolumeAdjustment;
import edu.harvard.med.screensaver.model.meta.Cardinality;
import edu.harvard.med.screensaver.model.meta.RelationshipPath;
/**
* Represents the transferal of a particular volume of reagent from a source
* well of a library plate to a well position on a
* {@link CherryPickAssayPlate CherryPickAssayPlate}. LabCherryPicks are
* managed by a {@link CherryPickRequest}. The set of LabCherryPicks for a
* particular library well copy can be used to calculate the total consumed
* reagent volume of that well copy, and thus the remaining volume.
* <p>
* The LabCherryPick concept is needed in addition to {@link ScreenerCherryPick}
* for the following reasons:
* <ul>
* <li>A ScreenerCherryPick can be "deconvoluted" into <i>multiple</i>
* reagents, where each reagent is to taken from a separate source well, as is
* the case with ThermoFisher SMARTPool RNAi libraries. LCPs thus represent the
* multiple, deconvoluted reagents of the SCP.</li>
* <li>LCPs for a given CPAP can be cloned and then assigned to a new CPAP when
* the creation of the original the CPAP failed (in the lab). Cloning the set of
* LCPs is critical for accurate well volume accounting.
* <p>
* LCPs progress through a range of states, as a CPR is processed by the lab.
* LabCPs can have the following states:
* <p>
* <table border="1">
* <tr>
* <td>State</td>
* <td>Description</td>
* <td>State Type</td>
* <td>Valid Transition(s)</td>
* <td>Affected Properties/Relationships</td>
* </tr>
* <tr>
* <td>Unfulfilled</td>
* <td>Liquid has not yet been allocated for the LabCP</td>
* <td>Initial</td>
* <td>Allocated</td>
* <td></td>
* </tr>
* <tr>
* <td>Allocated</td>
* <td>Liquid has been allocated for the LabCP</td>
* <td>Intermediate</td>
* <td>Mapped+Allocated</td>
* <td>sourceWell</td>
* </tr>
* <tr>
* <td>Mapped+Unallocated</td>
* <td>The LabCP has been assigned (mapped) to a particular well on a
* particular assay plate, but has not been allocated. This occurs if a lab
* cherry pick was created for a subsequent creation attempt of an assay plate,
* but for which there was insufficient volume in any library copy.</td>
* <td>Initial</td>
* <td>Map+Allocated, Canceled</td>
* <td>assayPlate, assayPlateRow, assayPlateColumn</td>
* </tr>
* <tr>
* <td>Mapped+Allocated</td>
* <td>The allocated LabCP has been assigned (mapped) to a particular well on a
* particular assay plate.
* <td>Intermediate</td>
* <td>Failed, Canceled, Plated</td>
* <td>assayPlate, assayPlateRow, assayPlateColumn</td>
* </tr>
* <tr>
* <td>Failed</td>
* <td>The LabCP was allocated and mapped, but the plate it was assigned to was
* later marked as failed (workflow rules dictate that LabCPs can only be
* canceled on a per-plate basis)</td>
* <td>Terminal</td>
* <td></td>
* <td>assayPlate.cherryPickLiquidTransfer.isSuccessful</td>
* </tr>
* <tr>
* <td>Canceled</td>
* <td>The LabCP was previously allocated and mapped, but the plate it was
* assigned to was later canceled (workflow rules dictate that LabCPs can only
* be canceled on a per-plate basis)</td>
* <td>Terminal</td>
* <td></td>
* <td>sourceWell, assayPlate.isCanceled</td>
* </tr>
* <tr>
* <td>Plated</td>
* <td>The LabCP has been allocated and mapped, and the assay plate it belongs
* to was marked as "plated"</td>
* <td>Terminal</td>
* <td></td>
* <td>assayPlate.cherryPickLiquidTransfer.isSuccessful</td>
* </tr>
* </table>
*
* @see ScreenerCherryPick
* @see CherryPickRequest
* @see CherryPickAssayPlate
* @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a>
*/
@Entity
@org.hibernate.annotations.Proxy
@edu.harvard.med.screensaver.model.annotations.ContainedEntity(containingEntityClass=ScreenerCherryPick.class)
public class LabCherryPick extends AbstractEntity<Integer>
{
// private static data
private static final Logger log = Logger.getLogger(LabCherryPick.class);
private static final long serialVersionUID = 0L;
public static final RelationshipPath<LabCherryPick> cherryPickRequest = RelationshipPath.from(LabCherryPick.class).to("cherryPickRequest", Cardinality.TO_ONE);
public static final RelationshipPath<LabCherryPick> screenerCherryPick = RelationshipPath.from(LabCherryPick.class).to("screenerCherryPick", Cardinality.TO_ONE);
public static final RelationshipPath<LabCherryPick> sourceWell = RelationshipPath.from(LabCherryPick.class).to("sourceWell", Cardinality.TO_ONE);
public static final RelationshipPath<LabCherryPick> wellVolumeAdjustments = RelationshipPath.from(LabCherryPick.class).to("wellVolumeAdjustments");
public static final RelationshipPath<LabCherryPick> assayPlate = RelationshipPath.from(LabCherryPick.class).to("assayPlate", Cardinality.TO_ONE);
// private instance data
private Integer _version;
private CherryPickRequest _cherryPickRequest;
private ScreenerCherryPick _screenerCherryPick;
private Well _sourceWell;
private Set<WellVolumeAdjustment> _wellVolumeAdjustments = new HashSet<WellVolumeAdjustment>();
private CherryPickAssayPlate _assayPlate;
private Integer _assayPlateRow;
private Integer _assayPlateColumn;
public enum LabCherryPickStatus {
Unfulfilled,
Reserved,
Mapped,
Canceled,
Failed,
Plated
};
// public instance methods
@Override
public Object acceptVisitor(AbstractEntityVisitor visitor)
{
return visitor.visit(this);
}
/**
* Get the id for the lab cherry pick.
* @return the id for the lab cherry pick
*/
@Id
@org.hibernate.annotations.GenericGenerator(
name="lab_cherry_pick_id_seq",
strategy="sequence",
parameters = { @Parameter(name="sequence", value="lab_cherry_pick_id_seq") }
)
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="lab_cherry_pick_id_seq")
public Integer getLabCherryPickId()
{
return getEntityId();
}
@Transient
public LabCherryPickStatus getStatus()
{
return isPlated() ? LabCherryPickStatus.Plated :
isFailed() ? LabCherryPickStatus.Failed :
isCancelled() ? LabCherryPickStatus.Canceled :
isMapped() ? LabCherryPickStatus.Mapped :
isAllocated() ? LabCherryPickStatus.Reserved :
LabCherryPickStatus.Unfulfilled;
}
/**
* Get the cherry pick request.
* @return the cherry pick request
*/
@ManyToOne
@JoinColumn(name="cherryPickRequestId", nullable=false, updatable=false)
@org.hibernate.annotations.ForeignKey(name="fk_lab_cherry_pick_to_cherry_pick_request")
public CherryPickRequest getCherryPickRequest()
{
return _cherryPickRequest;
}
/**
* Get the screener cherry pick for this lab cherry pick.
* @return the screener cherry pick for this lab cherry pick
*/
@ManyToOne
@JoinColumn(name="screenerCherryPickId", nullable=false, updatable=false)
@org.hibernate.annotations.ForeignKey(name="fk_lab_cherry_pick_to_screener_cherry_pick")
public ScreenerCherryPick getScreenerCherryPick()
{
return _screenerCherryPick;
}
/**
* Get the source well for this cherry pick. The source well corresponds to
* the well that will provide the reagent used to produce the cherry pick
* assay plate. For small molecule screens, the screened well will be the same as
* the source well. For RNAi screens, the screened well will map to a set of
* source wells (to accommodate pool-to-duplex mapping).
* <p>
* Note: Since we must allow a LabCherryPick to be plate mapped after
* instantiation time, we instantiate it with only a sourceWell, but not with
* a sourceCopy. This means we cannot create an association with a
* WellVolumeAdjustment until the sourceCopy is specified via
* {@link #setAllocated}. So we must redundantly store the sourceWell in both
* the LabCherryPick and, later on, in the related wellVolumeAdjustment
* entity.
*
* @return the source well
* @see ScreenerCherryPick#getScreenedWell()
*/
@ManyToOne
@JoinColumn(name="sourceWellId", nullable=false, updatable=false)
@org.hibernate.annotations.ForeignKey(name="fk_lab_cherry_pick_to_source_well")
@org.hibernate.annotations.LazyToOne(value=org.hibernate.annotations.LazyToOneOption.PROXY)
@edu.harvard.med.screensaver.model.annotations.ToOne(unidirectional=true)
public Well getSourceWell()
{
return _sourceWell;
}
/**
* Get the well volume adjustments associated with this lab cherry pick.
* <p>
* Note: Currently, we only allow for 1 WellVolumeAdjustment per
* LabCherryPick. However, declaring this relationship as a one-to-many set
* allows for:
* <ul>
* <li>automatic deletion of the associated WellVolumeAdjustment, if removed
* from this set</li>
* <li>future possibility of allowing multiple WellVolumeAdjustment per
* LabCherryPick; e.g., now that we have WellVolumeAdjustment in our data
* model, it may be possible to get rid of CherryPickAssayPlate "attempts", so
* that if a CherryPickAssayPlate attempt fails, we do not create a new
* CherryPickAssayPlate entity with a duplicate set of LabCherryPick; instead
* we just add more WellVolumeAdjustments to the plate's set of
* LabCherryPicks, as necessary.</li>
* <li>it's possible that the lab might (manually) perform a secondary
* reagent transfer for a given LabCherryPick, say, if they encountered a
* problem; this model would accommodate such an activity</li>
* </ul>
*/
@OneToMany(
cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE },
orphanRemoval = true,
fetch=FetchType.LAZY
)
@ToMany(hasNonconventionalMutation=true)
@JoinColumn(name="labCherryPickId")
@org.hibernate.annotations.ForeignKey(name="fk_well_volume_adjustment_to_lab_cherry_pick")
public Set<WellVolumeAdjustment> getWellVolumeAdjustments()
{
return _wellVolumeAdjustments;
}
/**
* Create and return a new well volume adjustment for the lab cherry pick.
* @param copy the copy
* @param well the well
* @param volume the volume
* @return true the new well volume adjustment
*/
public WellVolumeAdjustment createWellVolumeAdjustment(
Copy copy,
Well well,
Volume volume)
{
WellVolumeAdjustment wellVolumeAdjustment = new WellVolumeAdjustment(
copy,
well,
volume,
this);
_wellVolumeAdjustments.add(wellVolumeAdjustment);
return wellVolumeAdjustment;
}
/**
* Get the source copy.
* @return the source copy
*/
@Transient
public Copy getSourceCopy()
{
if (_wellVolumeAdjustments.size() == 0) {
return null;
}
return _wellVolumeAdjustments.iterator().next().getCopy();
}
/**
* Mark the cherry pick as having well volume allocated from a particular
* source library plate copy.
*
* @param sourceCopy the source copy from which the well volume was allocated;
* if null the well volume currently allocated to this lab cherry
* pick will be deallocated
*/
public void setAllocated(Copy sourceCopy)
{
if (sourceCopy != null && isAllocated()) {
throw new BusinessRuleViolationException("cannot (re)allocate a cherry pick that has already been allocated");
}
if (sourceCopy != null && isCancelled()) {
throw new BusinessRuleViolationException("cannot (re)allocate a cherry pick that has been canceled");
}
if (sourceCopy == null && !isAllocated()) {
throw new BusinessRuleViolationException("cannot deallocate a cherry pick that has not been allocated");
}
if (isPlated()) {
throw new BusinessRuleViolationException("cannot allocate or deallocate a cherry pick after it has been plated");
}
boolean wasFulfilled = !isUnfulfilled();
_wellVolumeAdjustments.clear();
if (sourceCopy != null) {
createWellVolumeAdjustment(
sourceCopy,
getSourceWell(),
getCherryPickRequest().getTransferVolumePerWellApproved().negate());
}
boolean nowFulfilled = !isUnfulfilled();
if (!wasFulfilled && nowFulfilled) {
_cherryPickRequest.decUnfulfilledLabCherryPicks();
}
else if (wasFulfilled && !nowFulfilled) {
_cherryPickRequest.incUnfulfilledLabCherryPicks();
}
}
/**
* Mark the cherry pick as having well volume allocated for a particular assay plate, specifying
* the assay plate and well that the liquid volume has been allocated to.
* @param assayPlate the cherry pick assay plate
* @param assayPlateRow the assay plate row
* @param assayPlateColumn the assay plate column
*/
public void setMapped(CherryPickAssayPlate assayPlate,
int assayPlateRow,
int assayPlateColumn)
{
// if (!isAllocated()) {
// throw new BusinessRuleViolationException("cannot map a cherry pick to an assay plate before it has been allocated");
// }
if (isMapped() || isPlated()) {
throw new BusinessRuleViolationException("cannot map a cherry pick to an assay plate if it has already been mapped or plated");
}
// TODO: for [#3380] Implement manual edit of Lab Cherry Picks: Cherry pick assay plate destination well
// if (isPlated()) {
// throw new BusinessRuleViolationException("cannot map a cherry pick to an assay plate if it has already been plated");
// }
_assayPlate = assayPlate;
_assayPlate.getLabCherryPicks().add(this);
_assayPlateRow = assayPlateRow;
_assayPlateColumn = assayPlateColumn;
}
/**
* Get the volume.
* @return the volume
*/
@Transient
public Volume getVolume()
{
if (!isPlated()) {
throw new IllegalStateException("a cherry pick does not have a transferred volume before it has been transferred");
}
return _cherryPickRequest.getTransferVolumePerWellApproved();
}
/**
* Get the cherry pick assay plate. Can be null, if the lab cherry pick has
* not been mapped to an assay plate. This value should only be updated via
* {@link #setMapped(CherryPickAssayPlate, int, int)}.
* @return the cherry pick assay plate
*/
@ManyToOne(
cascade={ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }
)
@JoinColumn(name="cherryPickAssayPlateId", nullable=true)
@org.hibernate.annotations.ForeignKey(name="fk_lab_cherry_pick_to_cherry_pick_assay_plate")
@org.hibernate.annotations.LazyToOne(value=org.hibernate.annotations.LazyToOneOption.PROXY)
@edu.harvard.med.screensaver.model.annotations.ToOne(hasNonconventionalSetterMethod=true)
@org.hibernate.annotations.Cascade(value={
org.hibernate.annotations.CascadeType.SAVE_UPDATE,
org.hibernate.annotations.CascadeType.DELETE
})
public CherryPickAssayPlate getAssayPlate()
{
return _assayPlate;
}
/**
* Get the 0-based indexed assay plate row.
* @return the assay plate row
*/
@org.hibernate.annotations.Type(type="integer")
@edu.harvard.med.screensaver.model.annotations.Column(hasNonconventionalSetterMethod=true)
public Integer getAssayPlateRow()
{
return _assayPlateRow;
}
/**
* Get the 0-based indexed assay plate column.
* @return the assay plate column
*/
@org.hibernate.annotations.Type(type="integer")
@edu.harvard.med.screensaver.model.annotations.Column(hasNonconventionalSetterMethod=true)
public Integer getAssayPlateColumn()
{
return _assayPlateColumn;
}
@Transient
public WellName getAssayPlateWellName()
{
if (_assayPlateRow != null &&
_assayPlateColumn != null) {
return new WellName(_assayPlateRow, _assayPlateColumn);
}
return null;
}
/**
* Return true iff the lab cherry pick is unfulfilled. A lab cherry pick is unfulfilled
* when it is neither allocated nor cancelled.
* @return true iff the lab cherry pick is unfulfilled
*/
@Transient
public boolean isUnfulfilled()
{
// note: a failed labCherryPick will be unallocated, so an isFailed() check would be redundant
return !isAllocated() && !isCancelled() /*&& !isFailed()*/;
}
/**
* Get whether liquid volume for this cherry pick has been allocated from a
* source plate well.
* @return true iff source plate well liquid volume has been allocated
*/
@Transient
public boolean isAllocated()
{
return _wellVolumeAdjustments.size() > 0;
}
/**
* Get whether this cherry pick has been mapped to an assay plate well. A
* mapped lab cherry pick can be either allocated or unallocated.
* @return true iff this cherry pick has been mapped to an assay plate well
*/
@Transient
public boolean isMapped()
{
return _assayPlate != null;
}
/**
* Get whether this cherry pick has been cancelled. A cancelled lab cherry pick
* is one that was allocated, then mapped, and later deallocated (due to its
* entire cherry pick assay plate having been canceled).
* @return true iff this cherry pick has been cancelled
*/
@Transient
public boolean isCancelled()
{
return isMapped() && _assayPlate.isCancelled();
}
/**
* Get whether liquid volume for this cherry pick has been transferred from a
* source copy plate to a cherry pick assay plate.
* @return true iff source plate well liquid volume has been transferred
*/
@Transient
public boolean isPlated()
{
return isAllocated() && isMapped() && _assayPlate.isPlated();
}
/**
* Get whether this cherry pick is located on a failed assay plate.
* @return true iff this cherry pick is located on a failed assay plate
*/
@Transient
public boolean isFailed()
{
return isAllocated() && isMapped() && _assayPlate.isFailed();
}
// package constructor
/**
* Construct an initialized <code>LabCherryPick</code> with an association to the
* <code>ScreenerCherryPick</code>. Intended only for use by {@link CherryPickRequest}.
* @param sourceWell the source well
* @param screenerCherryPick the screener cherry pick
*/
LabCherryPick(ScreenerCherryPick screenerCherryPick, Well sourceWell)
{
if (screenerCherryPick == null || sourceWell == null) {
throw new NullPointerException();
}
_cherryPickRequest = screenerCherryPick.getCherryPickRequest();
if (sourceWell.<Reagent>getLatestReleasedReagent() == null) {
throw new BusinessRuleViolationException(sourceWell + " is not a valid source well (does not contain a reagent)");
}
_sourceWell = sourceWell;
_screenerCherryPick = screenerCherryPick;
}
// protected constructor
/**
* Construct an uninitialized <code>LabCherryPick</code>.
* @motivation for hibernate and proxy/concrete subclass constructors
*/
protected LabCherryPick() {}
// private instance methods
/**
* Set the id for the lab cherry pick.
* @param labCherryPickId the new id for the lab cherry pick
* @motivation for hibernate
*/
private void setLabCherryPickId(Integer labCherryPickId)
{
setEntityId(labCherryPickId);
}
/**
* Set the screener cherry pick.
* @param screenerCherryPick the new screener cherry pick
* @motivation for hibernate
*/
private void setScreenerCherryPick(ScreenerCherryPick screenerCherryPick)
{
_screenerCherryPick = screenerCherryPick;
}
/**
* Get the version for the lab cherry pick.
* @return the version for the lab cherry pick
* @motivation for hibernate
*/
@Column(nullable=false)
@Version
private Integer getVersion()
{
return _version;
}
/**
* Set the version for the lab cherry pick.
* @param version the new version for the lab cherry pick
* @motivation for hibernate
*/
private void setVersion(Integer version)
{
_version = version;
}
/**
* Set the cherry pick request.
* @param cherryPickRequest the new cherry pick request
* @motivation for hibernate
*/
private void setCherryPickRequest(CherryPickRequest cherryPickRequest)
{
_cherryPickRequest = cherryPickRequest;
}
/**
* Set the source well.
* @param well the new source well
* @motivation for hibernate
*/
private void setSourceWell(Well sourceWell)
{
_sourceWell = sourceWell;
}
/**
* Set the well volume adjustments.
* @param wellVolumeAdjustments the well volume adjustments
* @motivation for hibernate
*/
private void setWellVolumeAdjustments(Set<WellVolumeAdjustment> wellVolumeAdjustments)
{
_wellVolumeAdjustments = wellVolumeAdjustments;
}
/**
* Set the assay plate.
* @param assayPlate the new assay plate
* @motivation for hibernate
*/
private void setAssayPlate(CherryPickAssayPlate assayPlate)
{
_assayPlate = assayPlate;
}
/**
* Set the assay plate row.
* @param assayPlateRow the new assay plate row
* @motivation for hibernate
*/
private void setAssayPlateRow(Integer assayPlateRow)
{
_assayPlateRow = assayPlateRow;
}
/**
* Set the assay plate column.
* @param assayPlateColumn the new assay plate column
* @motivation for hibernate
*/
private void setAssayPlateColumn(Integer assayPlateColumn)
{
_assayPlateColumn = assayPlateColumn;
}
}