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=...).
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 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:
Iis anInstrumentModeltype,headis aHeaddescribing pistons and channels,deckis a collection ofDeckPositions,settingsis anInstrumentSettingsdictionary.
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 endInstrument 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 <: Actuatordescribes the actuation mechanism,R <: RepeaterStyledescribes how repeated dispenses are handled,aspis a tuple of minimum and maximum aspiration volumes,dispis a tuple of minimum and maximum dispense volumes,deadPadis 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") # errorsChannels 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:
| Field | Meaning |
|---|---|
name | Position name |
aspirate | Whether aspiration is allowed from this position |
dispense | Whether dispense is allowed into this position |
plotting_shape | Shape 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:
| Field | Meaning |
|---|---|
name | Position name |
labware | Set{Symbol} of allowed LocationKind names |
slots | Number of labware slots available |
aspirate | Whether aspiration is allowed |
dispense | Whether dispensing is allowed |
plotting_shape | Shape 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)Pourfecto.config_to_json — Function
Lower a Configuration to a JSON string.
Pourfecto.json_to_config — Function
Parse a JSON string back into a Configuration.
Best practices and reminders
When defining configurations:
- Use an
InstrumentModelsubtype for each distinct instrument class. - Set realistic aspiration and dispense limits on each
Piston. - Keep
Channelcapacity 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_aspirateandcan_dispensebehavior to separate source and target positions. - Store extra parameters in
InstrumentSettings.