Configurations

Pourfecto uses configurations to describe the liquid-handling instruments available to a scheduling problem. A Configuration defines the physical and operational constraints of an instrument, including:

  • how liquid can be aspirated and dispensed by the instrument at once,
  • how much liquid each channel can carry,
  • which pistons are connected to which channels,
  • which labware can be placed on the deck,
  • which deck positions can aspirate or dispense,
  • and any additional instrument-specific settings.

Configurations are supplied to Pourfecto when solving for a Pourcast. They tell the algorithm what operations are physically possible.

A Configuration is not the same as a capability-bearing instrument location in CHESSCore (see Reads & Instrument Measurements). In CHESSCore, an instrument's capabilities control which operations can be recorded against a physical device in the ledger. A Configuration describes a device for scheduling only. The two are not linked: compiling a Pourcast writes protocol files and does not call upload(...; instrument=...).

Note

Most users need only Using pre-defined Configurations below. The rest of this page describes building Piston, Head, and Configuration objects, which adds support for a new physical liquid handler. See Defining a New Instrument.

Using pre-defined Configurations

The configurations dictionary holds pre-defined instrument configurations. They are the defaults commonly used on the Jensen Lab's liquid handling platform.

configurations 
Warning

Pre-defined configurations may need changes for other labs or workflows. For example, the ARI Cobra configuration contains Jensen Lab file paths in its settings.

Creating New Configurations

A Configuration is built from three main pieces:

Configuration{I}(
    head,
    deck,
    settings,
)

where:

Instruments

An InstrumentModel is an abstract type used to identify a class of liquid handler.

To define a new instrument type, create a subtype of InstrumentModel:

abstract type MyNewInstrument <: InstrumentModel end

Instrument types are the type parameters of Head and Configuration.

Heads

A Head describes the liquid-handling apparatus of an instrument. A head contains:

  • an array of pistons,
  • an array of channels,
  • a mask defining which pistons are connected to which channels.

The head defines a Configuration's ability to move liquid and how it interacts with labware.

Pistons

A Piston describes how liquid is moved by an instrument head.

Piston{A, R}(
    asp,
    disp,
    deadPad,
)

where:

  • A <: Actuator describes the actuation mechanism,
  • R <: RepeaterStyle describes how repeated dispenses are handled,
  • asp is a tuple of minimum and maximum aspiration volumes,
  • disp is a tuple of minimum and maximum dispense volumes,
  • deadPad is a multiplicative factor used to account for piston error or miscalibration.

For example:

using Unitful

piston = Piston{ContinuousActuator, SingleRepeater}(
    (20u"µL", 200u"µL"),
    (20u"µL", 200u"µL"),
    1.0,
)

This piston can aspirate between 20 µL and 200 µL, dispense between 20 µL and 200 µL, and uses no additional dead-padding correction.

Actuator types

An Actuator describes the mechanism used by a piston to move liquid.

Pourfecto currently defines two abstract actuator styles:

A continuous actuator represents a mechanism that can move liquid as a continuous flow. A discrete actuator represents a mechanism that dispenses in discrete shots. The mechanism type is passed to the scheduling algorithm as a constraint.

Repeater styles

A RepeaterStyle describes how an instrument handles repeated aspiration and dispense operations.

Pourfecto defines two repeater styles:

A SingleRepeater must dispense its aspirated volume in a single shot. A MultiRepeater can dispense a single aspiration across multiple shots. The Repeater style is useful for creating objectives that minimize the number of individual aspirations and dispenses in a liquid handling plan. For example, a MultiRepeater can execute multiple dispenses per aspiration, and may thus be faster than a SingleRepeater.

Channels

A Channel describes how much liquid an instrument channel can carry.

channel = Channel(220u"µL")

The capacity must be positive:

Channel(0u"µL")      # errors
Channel(-10u"µL")    # errors

Channels are independent of pistons. This allows an instrument to have a piston with one volume range and a channel with a separate carrying capacity.

Head Masks

The Head definition contains arrays of Piston and Channel definitions along with a binary mask, H, that indicates how the pistons and channels are connected. The mask should be of size P × C, where P is the number of pistons, and C is the number of channels. The mask is true for element [p,c] if piston p is connected to channel c, and false otherwise.

For example, a single-piston, single-channel head uses a 1 × 1 mask to indicate that the single piston is connected to the single channel:

H = trues(1, 1)

The full Head definition becomes

head = Head{SingleChannelPipette}(
    [piston],
    [channel],
    H,
)

Other instruments may have different mask shapes. For example, an eight channel pipette has the following head definition

head = Head{EightChannelPipette}(
    [piston],
    fill(channel,8),
    trues(1,8),
)

All eight channels are connected to a single piston, and the head mask is 1 × 8.

Decks

A Deck represents the physical platform that the Head can reach. The head is assumed to be able to visit every deck position. A deck is an array of deck positions.

Deck positions

A DeckPosition describes a physical position on the instrument deck.

Pourfecto defines three deck position types:

Empty positions

An EmptyPosition represents a position that is empty, inaccessible, or unavailable.

pos = EmptyPosition("unused")

Empty positions:

  • cannot aspirate,
  • cannot dispense,
  • cannot hold labware,
  • have zero slots.
can_aspirate(pos)   # false
can_dispense(pos)   # false
slots(pos)          # (0, 0)

Unconstrained positions

An UnconstrainedPosition represents a deck position that can hold any labware

pos = UnconstrainedPosition(
    "deck_position_1",
    true,      # aspirate
    true,      # dispense
    "rectangle",    # plotting shape
)

The fields are:

FieldMeaning
namePosition name
aspirateWhether aspiration is allowed from this position
dispenseWhether dispense is allowed into this position
plotting_shapeShape used for plotting/visualization

By default, slots(pos) for an unconstrained position returns:

(4, 6)

which provides a practical cap of 24 labware slots.

Constrained positions

A ConstrainedPosition represents a deck position that can hold only specified labware types.

pos = ConstrainedPosition(
    "plate_position",
    Set([:MyPlateKind]),
    (1, 1),
    true,
    true,
    "rectangle",
)

The fields are:

FieldMeaning
namePosition name
labwareSet{Symbol} of allowed LocationKind names
slotsNumber of labware slots available
aspirateWhether aspiration is allowed
dispenseWhether dispensing is allowed
plotting_shapeShape used for plotting/visualization

For example, a position that can hold only SLAS plates and supports both aspiration and dispense might look like:

plate_position = ConstrainedPosition(
    "plate_position",
    Set([:MySLASPlateKind]),
    (1, 1),
    true,
    true,
    "rectangle",
)

Creating Decks from multiple positions

As mentioned, a Deck is an array of deck positions.

deck = [
    UnconstrainedPosition("source", true, false, "rectangle"),
    UnconstrainedPosition("target", false, true, "rectangle"),
]

In this example:

  • "source" can be aspirated from but not dispensed into,
  • "target" can be dispensed into but not aspirated from.

A more constrained deck might look like:

deck = [
    ConstrainedPosition("source_plate_slot", Set([:SourcePlateKind]), (1, 1), true, false, "rectangle"),
    ConstrainedPosition("target_plate_slot", Set([:TargetPlateKind]), (1, 1), false, true, "rectangle"),
    EmptyPosition("unused"),
]

Instrument settings

InstrumentSettings is an alias for:

Dict{String, Any}

It stores additional configuration values that do not belong directly in the head or deck definitions.

For example:

settings = InstrumentSettings(
    "name" => "example instrument",
    "aspiration_speed" => 1.0,
    "default_liquid_class" => "water", 
    "description" => "single-channel example configuration",
)

Settings are typically used by instrument-specific compiling functions to pass important parameters to different liquid handler control software.

Example: Creating a new configuration

A complete configuration combines an instrument type, head, deck, and settings.

using Unitful
using Pourfecto

abstract type ExamplePipette <: InstrumentModel end

piston = Piston{ContinuousActuator, SingleRepeater}(
    (20u"µL", 200u"µL"),
    (20u"µL", 200u"µL"),
    1.0,
)

channel = Channel(220u"µL")

head = Head{ExamplePipette}(
    [piston],
    [channel],
    trues(1, 1),
)

deck = [
    UnconstrainedPosition("source", true, false, "rect"),
    UnconstrainedPosition("target", false, true, "rect"),
]

settings = InstrumentSettings(
    "name" => "example single-channel pipette",
)

config = Configuration{ExamplePipette}(
    head,
    deck,
    settings,
)

This configuration describes a simple single-channel pipette that can aspirate from one deck position and dispense into another.

config cannot yet be scheduled or tested. It still needs a Mask method, registration in the configurations registry, and, for a custom protocol file format, a compiler hook. Defining a New Instrument describes masks, compiling, registration, and testing, including how to do all of this from an external package without modifying Pourfecto.

JSON Serialization

Configurations can be converted to JSON and back. config_to_json serializes a configuration:

j_config  = config_to_json(config)

json_to_config reconstructs it:

json_to_config(j_config)

Best practices and reminders

When defining configurations:

  • Use an InstrumentModel subtype for each distinct instrument class.
  • Set realistic aspiration and dispense limits on each Piston.
  • Keep Channel capacity consistent with physical hardware.
  • Use the head mask to represent real piston-channel connectivity.
  • Use ConstrainedPositions when only certain labware types are valid.
  • Use can_aspirate and can_dispense behavior to separate source and target positions.
  • Store extra parameters in InstrumentSettings.