API Reference
Pourfecto.JENSENLAB_COBRA_PATHPourfecto.cobra_settingsPourfecto.configurationsPourfecto.objectivesPourfecto.ActuatorPourfecto.ChannelPourfecto.CobraPourfecto.ConfigurationPourfecto.ConstrainedPositionPourfecto.ContinuousActuatorPourfecto.DeckPourfecto.DeckPositionPourfecto.DiscreteActuatorPourfecto.DispenseItemPourfecto.DispenseWindowPourfecto.EmptyPositionPourfecto.HeadPourfecto.InstrumentModelPourfecto.InstrumentSettingsPourfecto.MaskPourfecto.MaskRulePourfecto.MultiRepeaterPourfecto.NimbusFourChannelPourfecto.ParameterDictPourfecto.PistonPourfecto.PourcastPourfecto.PriorityDictPourfecto.RepeaterStylePourfecto.SingleRepeaterPourfecto.SlottingDictPourfecto.UnconstrainedPositionCHESSCore.df_to_labwareCHESSCore.df_to_stockCHESSCore.labware_to_dfCHESSCore.stock_to_dfPourfecto.TestUtils._reference_1d_hitsPourfecto.TestUtils._reference_1d_position_countPourfecto.TestUtils._reference_visitsPourfecto.TestUtils._tally_actual_visitsPourfecto.TestUtils.default_test_labware_kindsPourfecto.TestUtils.test_instrument_interfacePourfecto.TestUtils.test_instrument_interfacePourfecto.TestUtils.test_json_roundtripPourfecto.TestUtils.test_json_roundtripPourfecto.TestUtils.test_mask_coveragePourfecto.TestUtils.test_mask_coveragePourfecto.TestUtils.test_pourcast_compilationPourfecto.TestUtils.test_pourcast_compilationPourfecto._batch_travelPourfecto.assemble_windowsPourfecto.batch_designPourfecto.batch_design_four_channelPourfecto.blanket_maskPourfecto.blanket_maskPourfecto.build_mask_from_rulesPourfecto.can_operatePourfecto.can_placePourfecto.channel_event_sequencePourfecto.channel_rowPourfecto.check_solve_status!Pourfecto.cluster_batchesPourfecto.compilePourfecto.compilePourfecto.compute_dispense_windowsPourfecto.compute_synchronized_cyclesPourfecto.config_to_jsonPourfecto.config_to_jsonPourfecto.configsPourfecto.convert_design_four_channelPourfecto.diagnose_infeasibilityPourfecto.effective_head_sizePourfecto.effective_head_sizePourfecto.flowsPourfecto.flows_by_configPourfecto.four_channel_row_spacingPourfecto.grid_distancePourfecto.group_reagentsPourfecto.group_window_countPourfecto.json_to_configPourfecto.json_to_configPourfecto.json_to_pourcastPourfecto.labware_indicesPourfecto.letter_code_to_intPourfecto.mask_rules_forPourfecto.model_solutionPourfecto.nimbus_4ch_wellPourfecto.nimbus_wellPourfecto.order_batchPourfecto.order_exactPourfecto.order_greedyPourfecto.order_windowsPourfecto.packing_greedyPourfecto.packing_greedyPourfecto.paramsPourfecto.place_labwarePourfecto.place_labwarePourfecto.planned_stocksPourfecto.planning_concentrationPourfecto.planning_concentrationPourfecto.plot_flowPourfecto.plot_flowsPourfecto.plot_labwarePourfecto.plot_slottingPourfecto.plot_slottingPourfecto.polish_batchesPourfecto.pourcast_to_jsonPourfecto.pourfectoPourfecto.pourfectoPourfecto.pourfectoPourfecto.pourfectoPourfecto.pourfectoPourfecto.random_adverb_verb_pairsPourfecto.register_instrument!Pourfecto.register_instrument!Pourfecto.round_with_exact_sumPourfecto.same_well_pairsPourfecto.sanitize_tokensPourfecto.scheduling_objective_valuePourfecto.set_cobra_path!Pourfecto.slacksPourfecto.slacksPourfecto.sliding_window_maskPourfecto.sliding_window_maskPourfecto.slotting_greedyPourfecto.slotting_greedyPourfecto.slotting_greedyPourfecto.slotting_greedyPourfecto.slotting_requirementsPourfecto.slottingdict_to_dfPourfecto.slottingdict_to_dfPourfecto.source_labwarePourfecto.source_position_orderPourfecto.source_stocksPourfecto.split_oversizedPourfecto.target_labwarePourfecto.target_stocksPourfecto.target_well_capacitiesPourfecto.tip_change_flagsPourfecto.transfer_indicesPourfecto.transfersPourfecto.transfers_by_configPourfecto.unambiguous_labware_namesPourfecto.write_instrument_filesPourfecto.write_instrument_filesPourfecto.write_instrument_filesPourfecto.write_instrument_files
Full docstrings
Pourfecto.Actuator — Type
abstract type Actuator end
Represents the mechnaism used to move liquid in a Piston.
Pourfecto.Channel — Type
struct Channel capacity::Unitful.Volume end
A Channel defines the liquid handler's capacity for carrying liquid. This can be set independent of the piston.
Pourfecto.Cobra — Type
abstract type Cobra <: InstrumentModel endDispatch tag for the Art Robbins Instruments Cobra liquid handler, configured as run in the Jensen Lab (University of Michigan). This file is Pourfecto's worked example of a complete, non-trivial instrument integration – a real Mask/MaskRule table, deck layout, and custom compiler output (SoftLinx XML protocol files plus a .slvp workflow file) – but it is genuinely lab-specific and will not function correctly in another lab without reconfiguration:
cobra_settings["cobra_path"](the on-instrument-PC protocol directory) must be set viaset_cobra_path!before compiling –JENSENLAB_COBRA_PATHis only a convenience seed value for the Jensen Lab's own path, not a usable default anywhere else.cobra_namesmaps labware kinds to this lab's specific SoftLinx plate-type names (e.g."96 Costar") – these must match your own SoftLinx installation's configured plate library, which may use different names even for physically identical plates.- The XML protocol/
.slvpworkflow templates target Art Robbins' SoftLinx scheduling software specifically – a different Cobra deployment (or a different liquid handler entirely) needs its ownwrite_instrument_filesmethod producing whatever file format its own control software expects. - Deck geometry (
cobra_deck) and mask rules (cobra_mask_rules) reflect the physical layout of this lab's specific Cobra unit and may not generalize even to a different Cobra unit with a different deck configuration.
See the instrument-authoring guide (docs/src/manual/instruments.md) for how to define your own instrument in your own package, using this file as a template, rather than modifying it directly.
Pourfecto.Configuration — Type
struct Configuration{I<:InstrumentModel}
head::Head{I}
deck::Deck
settings::InstrumentSettings
kind::Union{CHESSCore.LocationKind,Nothing}
endAn instance of a liquid handler instrument with definitions for the head, deck, and other settings. kind links this Configuration to a real, physical CHESSCore.LocationKind (e.g. CHESSCore.location_kinds[:Nimbus]) when one exists – nothing for configurations with no database-trackable physical counterpart (e.g. a handheld pipette: SingleChannel/EightChannel configs have no stable individual identity/location the way a Nimbus/Cobra robot does).
See Also: Head, Deck, InstrumentSettings
Pourfecto.ConstrainedPosition — Type
struct ConstrainedPosition <: DeckPosition name::String labware::Set{Symbol} slots::Tuple{Int,Int} aspirate::Bool dispense::Bool plotting_shape::String end
Represents a position that can hold a set of specified labware kinds. labware holds CHESSCore.LocationKind names (e.g. Set([:Conical15])), not Julia types – every labware kind is now a LocationKind value (see kind/location_kinds), not a distinct Julia type. Constrained positions have specified slots for allowable labware
Pourfecto.ContinuousActuator — Type
abstract type ContinuousActuator <: Actuator end
Represents a continuous flow mechanism for liquid handler Piston types
See Also: Actuator
Pourfecto.Deck — Type
Deck = AbstractArray{<:DeckPosition}
Alias for an array of DeckPosition objects
Pourfecto.DeckPosition — Type
abstract type DeckPosition end
Supertype for deck position definitons
Pourfecto.DiscreteActuator — Type
abstract type DiscreteActuator <: Actuator end
a discrete shot mechanism for liquid handler Piston types
See Also: Actuator
Pourfecto.DispenseItem — Type
DispenseItem(col, position, volume)One destination dispense, prior to being grouped into an aspirate batch.
col: index back into the caller's source transfer-list row space (traceability only, unused by the batching/ordering algorithms themselves).position: grid position (CartesianIndex(row,col)) of the destination well.volume: transfer volume, in the same units as the capacity passed tosplit_oversized/cluster_batches.
Pourfecto.DispenseWindow — Type
DispenseWindow(labware_id, dest_kind, column, offset, active)One physically-simultaneous dispense action: active is a list of (channel, item::DispenseItem) pairs, one per participating channel, all sharing column and satisfying the spacing relationship item.position[1] == offset + (channel-1)*spacing (see channel_row) for every channel in active. Produced by compute_dispense_windows, consumed (after order_windows picks a visiting sequence) by the assembly step in batch_design_four_channel.
Pourfecto.EmptyPosition — Type
struct EmptyPosition <: DeckPosition name::String end
Represents an empty or inaccessible position on the deck.
Pourfecto.Head — Type
struct Head{I<:InstrumentModel} pistons::AbstractArray{<:Piston} aspiratechannels::AbstractArray{Channel} aspiratemask::BitMatrix dispensechannels::AbstractArray{Channel} dispensemask::BitMatrix channel_routing::BitMatrix end
An instance of a head for a instrument I. Heads contain pistons that independently control aspiration and dispensation volumes, and separate channels/mask pairs for the aspirate and dispense directions – some instruments have genuinely different channel topology depending on operation direction (e.g. Tempest: 8 pistons share one intake channel while aspirating, but fan out to 8 independent nozzles while dispensing), so pistons is the one thing shared across both directions (the same physical plunger draws and dispenses – see is_valid_flow's a.piston == d.piston check), while channel count/capacity and piston-to-channel connectivity are each direction-specific. The Head{I}(pistons,channels,mask) constructor below covers the common case where both directions share one topology.
julia> abstract type ExSingleChannel <: InstrumentModel end
julia> piston = Piston{ContinuousActuator,SingleRepeater}((20u"µL",200u"µL"),(20u"µL",200u"µL"),1)
julia> head = Head{ExSingleChannel}(
[piston],
[Channel(220u"µL")],
trues(1,1)
)Pourfecto.InstrumentModel — Type
abstract type InstrumentModel end
Abstract Supertype to define new instrument models. Each new class of liquid handler should define a new abstract type. This is a compile-time dispatch tag Pourfecto uses to select the right masking/scheduling method per liquid-handler model (Configuration{I}/Head{I}) – it is unrelated to CHESSCore.Instrument, which represents a physical, database-trackable device located somewhere in the lab. A single InstrumentModel (e.g. Nimbus) is not tied to any one physical instance; a lab could have several physical Nimbus units, all sharing this one dispatch tag.
For example,
julia> abstract type NewInstrument <: InstrumentModel endWe refer to instances of liquid handlers as a Configuration which are parameterized by the abstract instrument type
julia> Configuration{SingleChannel}See Also Configuration
Pourfecto.InstrumentSettings — Type
InstrumentSettings = Dict{String,Any}
Alias for a Dict with settings for a particular instrument
Pourfecto.Mask — Type
struct Mask
head::Head
labware::Labware
asp::Function
disp::Function
asp_positions::Tuple{Integer,Integer}
disp_positions::Tuple{Integer,Integer}
endContains all of the mask related objects used to compute flows. We include separate entries for the aspirate and dispnese mask functions becuase they may be different for some liquid handlers.
Pourfecto.MaskRule — Type
MaskRule(kinds, direction, archetype, kwargs)One declarative rule in an instrument's admissibility/geometry table: kinds (a Set{Symbol} of LocationKind names, or the sentinel :all for an instrument with no kind-based restriction, e.g. SingleChannel) may be used in direction (:aspirate/:dispense) via archetype (:sliding_window/:blanket), built with kwargs (spacing/overhang kwargs for :sliding_window; ignored for :blanket). An instrument's full admissibility+geometry definition is a Vector{MaskRule}; build_mask_from_rules consumes it to build a real Mask, and the same table can be walked generically (by kind, by direction) to derive that instrument's overall admissible-kind set or to independently verify its Mask output – see Pourfecto/test/masks.jl.
Pourfecto.MultiRepeater — Type
abstract type MultiRepeater <: RepeaterStyle end
MultiRepeater styles allow the piston to dispense a single aspiration in multiple shots
See Also: RepeaterStyle
Pourfecto.NimbusFourChannel — Type
NimbusFourChannel <: InstrumentModelThe Hamilton Nimbus running in its 4-channel mode: 4 independent pistons arranged along a fixed vertical (row) axis, in a fixed, unchangeable physical order (channel 1 always "above" channel 2, above channel 3, above channel 4). Each channel's row position is independently adjustable subject to that ordering and a minimum mechanical spacing; all 4 channels share one gantry column position per physical move; an inactive channel still holds a placeholder row position that respects ordering/spacing.
This is deliberately a separate InstrumentModel/Configuration from Nimbus (the existing single-channel mode), not a variant of it – the two modes have very different labware access properties. For scheduling purposes (the MILP in pourfecto_algorithms/algorithms.jl), this instrument's Head/Mask is intentionally indistinguishable from single-channel Nimbus's: channel assignment and parallelism are a compile-time secondary optimization (see compiler/nimbus_four_channel.jl), not something the solver decides.
Pourfecto.ParameterDict — Type
ParameterDict = Dict{Symbol,Any}An Alias for Dict{Symbol,Any} that can be used to store keyword arguments for Pourfecto
Pourfecto.Piston — Type
struct Piston{A <: Actuator, R<: RepeaterStyle} asp::Tuple{Unitful.Volume,Unitful.Volume} disp::Tuple{Unitful.Volume,Unitful.Volume} deadPad::Real end
Pistons control how liquid moves in and out of the Head of a Liquid handler. They are parameterized by an Actuator and a [RepeaterSytle]. The fields of the piston define their maximum and minimum aspirating and dispensing volumes. The deadPad field is a multiplicative factor to account for error and miscalibration in the piston.
Pourfecto.Pourcast — Type
PourcastA result container for the pourfecto workflow. Pourcast bundles the relevant source/target stocks and labware, the instrument configuration set used for scheduling, the parameters supplied to pourfecto, and the resulting model solution and objective value.
Pourfecto.PriorityDict — Type
PriorityDict = Dict{String,UInt64}Dictionary type alias mapping reagent identifiers to a priority rank used during the Pourfecto planning stage.
Lower values represent higher priority (e.g., 1 is higher priority than 2). A common convention is to use typemax(UInt64) to indicate “lowest priority” or “effectively no priority preference”.
Examples
```jldoctest julia> PriorityDict( "sodium" => 1, "potassium" => 1, "ethanol" => 2, "water" => typemax(UInt64), ) Dict{String, UInt64} with 4 entries: "water" => 18446744073709551615 "ethanol" => 2 "sodium" => 1 "potassium" => 1
Pourfecto.RepeaterStyle — Type
abstract type RepeaterStyle end
Represents how a liquid handler handles repeated aspirations and dispenses. For example, a single channel pipette must dispense its entire aspirate in one shot, while other liquid handlers can fire repeated shots.
Pourfecto.SingleRepeater — Type
abstract type SingleRepeater <: RepeaterStyle end
SingleRepeater styles require that when a piston dispenses, it dispenses all of its volume in a single shot
See Also: RepeaterStyle
Pourfecto.SlottingDict — Type
SlottingDict = Dict{Labware,Tuple{DeckPosition,Int}}Alias for a mapping from each piece of labware to the (DeckPosition, slot) it has been assigned to on an instrument's deck. Produced by slotting_greedy/packing_greedy, and consumed by write_instrument_files.
Pourfecto.UnconstrainedPosition — Type
struct UnconstrainedPosition <: DeckPosition name::String aspirate::Bool dispense::Bool plotting_shape::String end
Represents a position that can hold any number of any labware
Pourfecto.JENSENLAB_COBRA_PATH — Constant
JENSENLAB_COBRA_PATHThe local path to protocol files on the JensenLab's Cobra instrument machine. Pass this to set_cobra_path! to configure cobra_path for that machine, e.g. set_cobra_path!(JENSENLAB_COBRA_PATH). Other labs should use their own path instead.
Pourfecto.cobra_settings — Constant
cobra_settingsA dictionary with settings for the cobra
dictionary keys:
pause=true: pause over each well before dispensing. If false, the instrument moves continuously while dispensing.predispenses=0: force the instrument to predispense a shot of liquid before starting the run.cobra_path: the local path the cobra should look for protocol files on the machine running the instrument. Defaults to a persisted Preferences.jl setting if one has been set withset_cobra_path!, otherwisenothing; must be set (viaset_cobra_path!or directly withcobra_settings["cobra_path"] = "...") before compiling a protocol.washtime: set how long the wash system should flush the nozzles forAspPad = 1.125: Force the cobra to overaspirate by a factor of AspPad. Pourfecto takes this into account when planningAspDistance = 1.5: set how many millimeters above the bottom of the labware the cobra should aspirate from.maxShot = 40: the maximum single-shot dispense volume, in µL.
Pourfecto.configurations — Constant
configurationsBy default, Pourfecto provides an assortment of pre-defined instruments. Use keys(configurations) to see available instruments
Pourfecto.objectives — Constant
objectivesPourfecto provides an assortment of pre-defined objectives. Use keys(objectives) to see available options
Note that many non-default options turn the problem into an MILP in the scheduling phase, which can lead to long and unpredictable run times.
Pourfecto._batch_travel — Method
_batch_travel(batch::Vector{DispenseItem}, method::Symbol) -> Float64Sum of consecutive grid_distances in batch after ordering it with order_batch(batch,method). The shared cost function used both by polish_batches's search loop and by the benchmark suite to measure clustering quality – the same metric throughout, so "improvement" during search means the same thing as "improvement" when reported afterward.
Pourfecto.assemble_windows — Method
assemble_windows(ordered_windows, n_channels, source_keys_by_channel, capacity, effective_capacity;
volume_precision, insert_blowouts, waste_target, dead_volume_buffer,
aspirate_buffer, max_tip_use, priming, priming_volume, priming_target) -> DataFramePhase 3: walk ordered_windows (Phase 1/2's globally-ordered, already-grouped dispense sequence) and emit the final wide-schema action rows. Cycle boundaries are decided jointly across all channels by compute_synchronized_cycles (Phase 3a) before this walk begins: whenever any channel needs to reload, every channel with outstanding demand reloads at that same window (ending a cycle early, with unused headroom, if needed to align) – so a channel's TipPickup/Aspirate fires while the gantry is already at the tip/source area for another channel, rather than triggering its own later, isolated trip once its own item happens to come up. At each such synchronized reload window, a trailing Blowout for any channel's just-finished cycle (per has_trailing_blowout) is emitted first, then TipDisposal (mid-run tip changes and any channel's now-due final disposal, deferred here rather than firing in isolation), then TipPickup, then Aspirate, then priming. The window's own Dispense row is then emitted exactly as grouped by Phase 1 – the group is never split, only preceded by whichever channels needed a top-up.
TipPickup/TipDisposal/Blowout always merge across every channel that needs the action at the same point in the walk, since their target Labware ID is always the same constant ("None" for tip actions, the fixed waste_target labware for blowout). Aspirate merges across channels only when they share the same source Labware ID – never based on row/column/spacing compatibility; unlike Dispense (a rigid simultaneous 4-channel motion, hence compute_dispense_windows's spacing check), these are position-per-channel commands whose sequencing the Nimbus's own internal logic handles when they aren't actually simultaneous. See emit_grouped! (local to this function).
This supersedes merge_schedules's role: since Phase 1 already decided which channels fire together, this assembly step no longer needs to discover alignment via a live compatibility check – it just realizes the precomputed plan, inserting per-channel capacity interruptions without disturbing the groups themselves.
Pourfecto.batch_design — Method
batch_design(df::DataFrame, config::Configuration{Nimbus}; batch_ordering::Symbol=:greedy,
volume_precision::Int=1, insert_blowouts::Bool=true,
waste_target::Union{Nothing,Tuple{AbstractString,Union{AbstractString,Integer}}}=nimbus_waste_target,
dead_volume_buffer::Real=20.0, aspirate_buffer::Real=0.01, polish_clustering::Bool=false,
polish_max_iterations::Int=1000) -> DataFrameGroup convert_design's flat source/destination transfer list into one-to-many aspirate/dispense batches, bounded by the Nimbus channel's dispense capacity, and emit them in the unified action-row schema "Labware ID","Labware Position ID","Volume (uL)","Action", "Change Tip Before" (Action is "Aspirate", "Dispense", or "Blowout").
For each distinct source well (grouped in first-appearance row order), destination transfers are volume-split against capacity (split_oversized), packed into capacity-bounded, distance-aware batches (cluster_batches), and ordered within each batch (order_batch; batch_ordering is :greedy or :exact).
Dispense/Blowout volumes within a cycle are rounded to volume_precision decimal places such that they sum back to the unbuffered portion of the aspirate volume exactly at that precision (round_with_exact_sum) – the last value in the cycle (a dispense, or the blowout when present) absorbs whatever rounding remainder is needed, rather than every value being rounded independently, which can otherwise leave a hairline float residual that a strict downstream available >= requested check rejects.
The Aspirate row itself then adds aspirate_buffer on top of that rounded sum (aspirate_volume = sum(rounded dispense/blowout values) + aspirate_buffer) – a small, deliberate physical safety margin against real-world pipetting inaccuracy (an under-aspiration or a dispense that loses slightly more than its nominal volume can otherwise leave the tip short for the last action in a cycle, e.g. a Blowout). Unlike the dispense/blowout rows, the aspirate row is not re-rounded to volume_precision – doing so could erase a buffer finer than that resolution (e.g. the default aspirate_buffer=0.01 vanishes under the default volume_precision=1, since round(x+0.01,digits=1) == round(x,digits=1) in general). This is an unconditional default (unlike every other kwarg here, aspirate_buffer=0.01 changes existing default output) since the margin should always be present regardless of insert_blowouts.
Batch formation reserves headroom for both aspirate_buffer and a rounding margin (0.5 * 10.0^(-volume_precision), the worst a raw value can round up by) in addition to dead_volume_buffer – without the rounding margin, a raw cycle volume just under capacity - aspirate_buffer could round up to something that, once aspirate_buffer is added, exceeds true capacity, defeating the purpose of reserving headroom at all. If a computed aspirate volume is nonetheless ever found to exceed the channel's true capacity, this throws rather than silently clamping (clamping would silently reintroduce the same kind of exact-sum mismatch Bug 1 was fixed to prevent) – with the margins above, this should not be reachable through normal use.
When insert_blowouts=true, a batch that's immediately followed by a re-aspirate under the same tip (no tip change between them) gets a trailing Blowout row at waste_target, sized to drain dead_volume_buffer before the next aspirate – that batch's own aspirate volume is then sized to cover its dispenses plus the buffer (the blowout is the last value in its cycle, so it absorbs the rounding remainder). waste_target defaults to nimbus_waste_target – the permanently-reserved waste conical (TubeRack50ML_WellNames_0006, well A3, see slotting_greedy's Configuration{Nimbus} override) – so callers using the default deck layout don't need to supply it; pass a different (labware id, position) tuple to target something else. A positive dead_volume_buffer (in µL, < capacity, default 20.0) is required when insert_blowouts=true. Batch formation reserves headroom for dead_volume_buffer for every batch in this mode (not just the ones that end up needing a blowout), since which batches need one isn't known until tip-change flags are computed across the whole design, on top of the aspirate_buffer/rounding headroom described above. insert_blowouts defaults to true: a protocol compiled with no blowout-related kwargs at all still gets Blowout rows wherever a re-aspirate happens under the same tip, draining the default 20.0 µL to the reserved waste conical. Pass insert_blowouts=false explicitly to disable it entirely.
When polish_clustering=true, an exchange/local-search pass (polish_batches) runs on each source's clustered batches before within-batch ordering, swapping/relocating items between batches to further reduce total head-travel distance beyond cluster_batches's single greedy pass, capped at polish_max_iterations improving moves. Opt-in; default false preserves current behavior exactly.
Pourfecto.batch_design_four_channel — Method
batch_design_four_channel(df::DataFrame, config::Configuration{NimbusFourChannel};
n_channels, volume_precision, insert_blowouts, waste_target,
dead_volume_buffer, aspirate_buffer, priming, priming_volume,
priming_target) -> DataFrameGroup convert_design_four_channel's flat transfer list by reagent (source), split the reagents into groups of up to n_channels, and run each group through the three-phase sweep-based dispense-ordering pipeline below, one group after another. Within a group, each reagent owns one channel for its whole tip session. Between groups, all tips swap on one trip: a group's last TipDisposal row is immediately followed by the next group's TipPickup row.
Reagents are ordered by physical tube position by default (source_position_order: labware id, then column, then row) and chunked into consecutive runs of n_channels: chunk k is group k, and position within the chunk is the channel. So the tube placement chosen by place_labware decides both grouping and channel order, and a 2-row tube rack's A1, B1, A2, B2 land on channels 1-4 down the head. channel_order overrides this with an explicit list of every source (labware id, position) key, chunked the same way; place_labware uses it to score candidate orders before tubes are moved.
synchronize_reloads=false turns off synchronized reload trips (see compute_synchronized_cycles), so each channel reloads and disposes on its own schedule; it exists to benchmark what synchronization saves.
compute_dispense_windows– per(destination labware, destination kind)target, pooling demand across every channel with items there, greedily group into the fewest possible simultaneous windows.order_windows– find a nearest-neighbor visiting order over each target's windows (reusingbatching.jl's existing tour machinery), concatenated across targets in first-appearance order.assemble_windows– realize the ordered windows as the final wide-schema action sequence, inserting per-channel capacity-drivenTipPickup/Aspirate/Blowout/TipDisposalevents without ever splitting a window's group.
See each function's docstring for the full pipeline detail.
Pourfecto.blanket_mask — Method
blanket_mask(h::Head,l::Labware,direction::Symbol)Build the (predicate,positions) pair for the blanket mask archetype: any in-bounds (well,position,channel) combination is valid, with no further geometric discrimination – for a single, undifferentiated well (e.g. a deep reservoir) that multiple channels/positions may validly touch at once.
Pourfecto.build_mask_from_rules — Method
build_mask_from_rules(h::Head,l::Labware,rules::Vector{MaskRule}) -> MaskBuild a real Mask for (h,l) by finding, per direction, the first rule in rules whose kinds admits kind(l).name (or is :all) and building that rule's archetype. A direction with no matching rule defaults to the trivial always-false mask, exactly matching every instrument's existing "unsupported kind" fallback.
Pourfecto.can_operate — Method
can_operate(h::Head,l::Labware) -> BoolReturn whether h can actually aspirate or dispense with l – i.e. whether Mask(h,l) is non-trivial. Every instrument's Mask method returns the trivial default Mask(h,l,(x,y,z)->false,(x,y,z)->false,(0,0),(0,0)) for an unsupported labware kind, so checking asp_positions/disp_positions against (0,0) is sufficient without invoking the mask functions themselves.
Pourfecto.can_place — Method
can_place(l::Labware, d::DeckPosition) -> Bool
can_place(l::Labware, deck::Deck) -> Bool
can_place(l::Labware, config::Configuration) -> BoolWhether l is admissible on deck position d (or anywhere on deck, or on config's deck). For a ConstrainedPosition, admissibility means kind(l).name is in the position's declared labware set; an UnconstrainedPosition admits anything; an EmptyPosition admits nothing. The Configuration method additionally requires can_operate (a non-trivial Mask) – deck admissibility and head operability are independent checks.
Pourfecto.channel_event_sequence — Method
channel_event_sequence(ordered_windows::Vector{DispenseWindow}, channel::Integer) -> Vector{Tuple{Int,DispenseItem}}Extract one channel's own (window_index, item) pairs from the globally-ordered window sequence, in that same order – this is the channel's effective visiting order, now driven by Phase 1/2 (the sweep + nearest-neighbor tour) rather than an independent cluster_batches/order_batch run.
Pourfecto.channel_row — Method
channel_row(offset::Integer, channel::Integer, spacing::Integer) -> IntThe well-row a given physical channel (1-indexed, respecting the head's fixed order – channel 1 always physically above channel 2, above channel 3, above channel 4) sits at, for a window whose channel-1 row is offset: offset + (channel-1)*spacing. Used by _dispense_compatible as the compatibility check for whether two channels' current rows can be addressed simultaneously – not as a constraint on which rows any one channel may visit (see this file's header comment: under the current "tip tied to reagent" model, a channel's own schedule is free to visit any row its reagent needs, in any order; only simultaneous multi-channel addressing is constrained by physical spacing).
Pourfecto.check_solve_status! — Method
check_solve_status!(model::JuMP.Model, level)Inspect model's termination/primal status after optimize! and raise a clear error if the solve didn't produce a usable solution. level is used only to label the error message (a priority level for the planning solve, or a stage name like "scheduling").
Pourfecto.cluster_batches — Method
cluster_batches(items::Vector{DispenseItem}, capacity::Real; max_items::Union{Nothing,Int}=nothing) -> Vector{Vector{DispenseItem}}Greedily partition items (already split_oversized) into capacity-bounded batches: each batch's summed volume stays ≤ capacity. Batch membership favors spatial locality – each batch opens from an unassigned seed item (largest remaining volume first, a reasonable first-fit-decreasing tiebreak) and then repeatedly pulls in whichever still-unassigned, capacity-fitting item is nearest (by grid_distance) to any item already in the batch, until nothing more fits (or max_items is reached). Every input item ends up in exactly one output batch.
Pourfecto.compile — Method
compile(
directory::AbstractString,
pourcast::Pourcast;
packing_method::Function = packing_greedy,
kwargs...
) -> NothingCompile a Pourcast into one or more protocol folders on disk.
For each configuration in configs(pourcast), this function determines which source/target labware pairs must be simultaneously accessible (“slotted”) and uses packing_method to produce one or more feasible slotting layouts. For each layout, it extracts the corresponding well-to-well transfer submatrix for that configuration and writes instrument files into a protocol-specific subfolder.
Output layout
For each configuration c, files are written under:
directory/<config_type>/<protocol_name>/...
where <config_type> = string(get_config_type(configs(pourcast)[c])) and <protocol_name> is generated by random_adverb_verb_pairs.
Arguments
directory::AbstractString: Root directory where configuration/protocol subdirectories will be created.pourcast::Pourcast: The plan to compile (provides labware, configurations, and flows).
Keyword Arguments
packing_method::Function = packing_greedy: Packing/slotting routine called aspacking_method(slotting_pairs, config). It must return a collection of slotting dictionaries (one per generated protocol).kwargs...: Forwarded keyword arguments passed towrite_instrument_files(...)andplot_slotting(...).
Side Effects
- Creates directories using
mkdirwhen needed. - Writes instrument/protocol files via
write_instrument_files.
Algorithm (per configuration)
- Compute
slotting_reqs = slotting_requirements(pourcast). - Convert
slotting_reqs[c]into a vector of(source::Labware, target::Labware)pairs that require co-slotting. - Generate one or more slotting solutions:
slotting_dicts = packing_method(slotting_pairs, configs(pourcast)[c]). - For each solution
p:- Determine slotted sources/targets from the dict keys.
- Convert labware to well-index selectors using
labware_indices(pourcast). - Slice the configuration transfer matrix:
protocol_design = transfers_by_config(pourcast)[c][s_idxs, t_idxs]. - Convert
protocol_designto aDataFrameand write files.
Pourfecto.compute_dispense_windows — Method
compute_dispense_windows(demand_by_channel::Dict{Int,Vector{DispenseItem}}, n_channels::Integer,
labware_id::AbstractString, dest_kind::Symbol, spacing::Integer) -> Vector{DispenseWindow}Phase 1 of the sweep-based dispense-ordering pipeline: greedily group the pending demand for one (destination labware, destination kind) target, pooled across every channel that has demand there (not per-reagent), into the fewest possible simultaneous windows. demand_by_channel[c] is channel c's already-split_oversized DispenseItems for this target (missing/empty for an unassigned or unaffected channel).
For each column with any pending demand: repeatedly pick the window offset that currently satisfies the most not-yet-claimed (channel, row) pairs (ties broken by iteration order), claim those channels' items, and repeat until every item in that column is claimed. This is a greedy, not provably-optimal, per-column set cover – the same heuristic validated by simulation against the real 4-reagent/50%-coverage example (593 -> 123 dispense-row reduction) – matching this project's existing precedent of cluster_batches also being a heuristic, not an exact solver. Windows are returned in an arbitrary (column-major) order; see order_windows for the actual visiting sequence.
Pourfecto.compute_synchronized_cycles — Method
compute_synchronized_cycles(ordered_windows, channels, channel_items, effective_capacity) ->
(cycles, trigger_window, disposal_window)Phase 3a: jointly compute every channel's aspirate-cycle boundaries in one pass over ordered_windows, instead of packing each channel's own event sequence independently. Whenever any channel would run out of pre-loaded capacity for its next item, every channel with outstanding demand reloads at that same window – ending its current cycle early, with unused headroom, if that's what it takes to align. This is what lets a channel's TipPickup/Aspirate fire while the gantry is already at the tip/source area for another channel, instead of triggering its own later, isolated trip once its own first (or next) item happens to come up in the shared dispense order.
A channel that exhausts all its demand becomes disposal-due but is never given its own trigger – its final TipDisposal is deferred until the next synchronized reload fires for any other channel (piggybacked for free), or, if none remains, flushed in one final wave at the last window.
With synchronized=false (for benchmarking what synchronization saves), each channel reloads on its own schedule instead: a trigger refills only the channel(s) that ran out, and a finished channel disposes right after its own last dispense.
channel_items[c] is channel_event_sequence(ordered_windows,c) for every channel with any demand (precomputed by the caller). Each item is assumed already <= effective_capacity (via split_oversized), so a fresh cycle always accepts at least one item and the walk always terminates. Returns cycles::Dict{Int,Vector{Vector{Tuple{Int,DispenseItem}}}} (same shape channel_cycle_data already consumes), trigger_window::Dict{Int,Vector{Int}} (trigger_window[c][ci] is the window at which cycle ci of channel c starts loading), and disposal::Dict{Int,Tuple{Int,Bool}}: channel c's final tip disposal fires at window w, after that window's dispense if the flag is true, or with that window's reload trip (before the dispense) if false.
Pourfecto.config_to_json — Method
Lower a Configuration to a JSON string.
Pourfecto.configs — Method
configs(p::Pourcast) -> Vector{<:Configuration}Return the instrument configurations used by pourfecto to schedule the liquid handling operations
Pourfecto.convert_design_four_channel — Method
convert_design_four_channel(design, sources, targets, slotting, config::Configuration{NimbusFourChannel}) -> DataFrameFlatten design (a source-well x destination-well volume matrix) into a per-transfer row list, analogous in shape to single-channel Nimbus's convert_design but written independently (per this module's design decisions) and carrying one extra column, "Destination Kind" (the destination labware's CHESSCore.LocationKind name), which batch_design_four_channel needs to look up the correct minimum row spacing (four_channel_row_spacing) per destination labware.
Pourfecto.diagnose_infeasibility — Method
diagnose_infeasibility(model::JuMP.Model, level) -> InfeasibleSolveErrorBuild an InfeasibleSolveError explaining why model (already solved to an infeasible termination status) could not be satisfied. When the active optimizer supports conflict/IIS analysis (e.g. Gurobi), this runs JuMP.compute_conflict! and translates the resulting irreducible inconsistent constraint set back into Pourfecto-level terms (source/target/chemical names) using the registry build_planning_model attaches at model.ext[:pourfecto_constraints]. Falls back to a generic message when conflict analysis isn't supported or the registry has no entry for the conflicting constraints (e.g. the scheduling-stage model).
Pourfecto.effective_head_size — Method
effective_head_size(h::Head,l::Labware,direction::Symbol)The effective channel-grid size to use when computing positions/masks for h/l in direction. Defaults to the raw head size (no collapsing). Some instruments (e.g. EightChannel/PlateMaster) are sometimes paired with a labware kind whose grid is smaller than the head's channel count in some dimension – only ever for a (head,labware kind) pairing already declared safe by the calling Mask branch's own kind list – and override this to clamp the effective size down to the labware's own extent in that dimension. compute_positions and sliding_window_mask both consult this single function, so the position-count and the mask predicate can never drift out of sync with each other.
Pourfecto.flows — Method
flows(p::Pourcast)Return the flow decision variable(s) from a Pourcast solution.
This is an accessor that extracts the solver/model variable stored under the key :Q from the pourcast's model_solution
See also
transfers, get_pourfecto_solution_variable
Pourfecto.flows_by_config — Method
flows_by_config(pc::Pourcast) -> Vector{Matrix{Float64}}Partition the full flow matrix into one flow matrix per configuration.
This function computes aspiration and dispense flow nodes across all configurations, then slices the global flow matrix flows(pc) into C matrices (one per configuration in configs(pc)). For configuration c, only entries corresponding to aspiration/dispense nodes whose configuration equals configs(pc)[c] are copied from the global flow matrix; all other entries remain zero.
Arguments
pc::Pourcast: A pour/transfer plan providing configurations, labware, and a global flow matrix viaflows(pc).
Returns
Vector{Matrix{Float64}}: A length-Cvectorflws_by_configwhereC == length(configs(pc)). Each entryflws_by_config[c]is anA×Dmatrix (A == length(asp_nodes),D == length(disp_nodes)) whose nonzero entries represent flows for configurationc.
Pourfecto.four_channel_row_spacing — Method
four_channel_row_spacing(kind_name::Symbol) -> IntMinimum well-row spacing (in well-grid units) required between two active channels on the 4-channel Nimbus head, for a labware of kind kind_name. The 4-channel head's physical pitch is double that of an 8-channel head, so wherever the existing 8-channel-pitch instruments (EightChannel/Cobra/PlateMaster) use a given v_spacing for a labware kind (v_spacing=1 for :WP96-family kinds, v_spacing=2 for :WP384, see EightChannel.jl's eight_channel_mask_rules), this instrument's minimum row spacing for that same kind is 2x that value – confirmed directly against a real compiled protocol (see Pourfecto/src/instruments/templates/esthetically_sticking_multichannel_rounding.csv): it dispenses into rows A,C,E,G (stride 2) on a 96-well-format plate, then a second full pass into B,D,F,H.
This is an explicit per-kind lookup (mirroring how MaskRule kwargs already hand-set v_spacing per kind, e.g. eight_channel_mask_rules), not a formula derived from labware grid dimensions – that derivation breaks down for non-well-plate labware (e.g. a 2-row tube rack, where physical tube spacing is already coarser than any well-plate row pitch). The values below are the real hardware constraint as described during design and should be validated against the physical instrument before relying on them for a real protocol; kinds not listed default to 2 (the 96-well-pitch baseline).
Pourfecto.grid_distance — Method
grid_distance(a::CartesianIndex, b::CartesianIndex) -> Float64Euclidean distance between two wells in (row,col) grid-index space. There is no physical well-pitch/spacing data in the labware type system, so this is plain grid distance, not a physical mm distance.
Pourfecto.group_reagents — Method
group_reagents(demands, volumes, n_channels, effective_capacity; reload_weight=8.0, max_passes=50) -> Vector{Vector{Int}}Split reagents 1:length(demands) into ceil(n/n_channels) groups that run one after another, minimizing the sum over groups of
(fewest dispense windows over the group's channel orders) + reload_weight × (reload waves)A group's reload waves are ceil(largest reagent volume / effective_capacity), since reloads within a group are synchronized. reload_weight is how many dispense motions one reload trip is worth (8 matches instrument timings). Tips used, swap trips and total volume don't depend on the partition, so they're left out.
Seeded greedily: each group starts from the highest-volume unassigned reagent and repeatedly adds whichever remaining reagent raises the group's score least, until full. Then pairs of reagents are swapped between groups until a full pass finds no improvement or max_passes is reached. A swap-only search from an arbitrary seed can stall on plateaus where every single swap scores the same (two aligned sets that start mixed need two swaps to separate), which the greedy seed avoids. Group sizes stay as seeded (full groups first, any short group last), which keeps the groups readable from tube position.
Pourfecto.group_window_count — Method
group_window_count(demands, order, n_channels) -> IntDispense windows needed by one group when reagent order[c] runs on channel c, summed over destination labware. demands[i] is reagent i's reagent_demand.
Pourfecto.json_to_config — Method
Parse a JSON string back into a Configuration.
Pourfecto.json_to_pourcast — Method
json_to_pourcast(j::String) -> PourcastDeserialize a JSON string produced by pourcast_to_json back into a Pourcast instance.
Pourfecto.labware_indices — Method
labware_indices(pc::Pourcast) -> Tuple{Dict,Dict}Construct dictionaries mapping each source/target labware item to the vector of indices it occupies in the flattened transfer indexing scheme.
This function expands source_labware(pc) and target_labware(pc) into “by-index” vectors where each labware object is repeated length(x) times, then computes, for each labware object x, the positions (via findall) at which it appears in that expanded vector. The result is two dictionaries:
src_dict: maps eachsourcelabware object to a vector of integer indices into the expanded source index space.tgt_dict: maps eachtargetlabware object to a vector of integer indices into the expanded target index space.
These index vectors are intended to be used as selectors when indexing transfer data.
Arguments
pc::Pourcast: A pour/transfer plan providingsource_labware(pc)andtarget_labware(pc).
Returns
(src_dict, tgt_dict): A 2-tuple of dictionaries:src_dict::Dict{<:Any, <:AbstractVector{Int}}mapping each element ofsource_labware(pc)to a vector of indices.tgt_dict::Dict{<:Any, <:AbstractVector{Int}}mapping each element oftarget_labware(pc)to a vector of indices.
Pourfecto.letter_code_to_int — Method
letter_code_to_int(s::AbstractString) -> IntInverse of LabwarePlotting.letter_code: decode a bijective base-26 row label ("A" -> 1, "Z" -> 26, "AA" -> 27, ...) back to its row index. Local to Pourfecto since well_to_cartesian is currently the only caller needing the inverse direction.
Pourfecto.mask_rules_for — Method
mask_rules_for(config::Configuration) -> Union{Vector{MaskRule},Nothing}Return the MaskRule table backing config's instrument, for generic (test-time) inspection – e.g. Pourfecto/test/masks.jl's single generic Mask-coverage testset walks this to independently verify any registered instrument's Mask output, including a new instrument added later, as long as it defines its own mask_rules_for(::Configuration{X}) = x_mask_rules method alongside its Mask. Defaults to nothing for instruments with no kind-based rule table (e.g. SingleChannel, which is deliberately unconstrained – see its own Mask method).
Pourfecto.model_solution — Method
model_solution(p::Pourcast) -> Dict{Symbol,Any}Return the raw JuMP model output from the pourfecto algorithm.
Pourfecto.nimbus_4ch_well — Method
nimbus_4ch_well(position::DeckPosition, slot::Int) -> StringTranslate a bare linear slot index into a well-name string ("A1", "B3", ...), same convention as nimbus_well in instruments/Nimbus.jl – reimplemented independently here rather than called into, per this module's "no shared code paths with Nimbus.jl" design decision.
Pourfecto.nimbus_well — Method
nimbus_well(position::DeckPosition, slot::Int) -> StringTranslate a bare linear slot index (as produced by the shared, Int-typed slotting_greedy/SlottingDict machinery) into an unambiguous well-name string ("A1", "B3", ...) using position's (rows,cols) grid shape. This is the fix for a physical mis-pipetting bug: a bare integer slot number had no documented row-major/column-major convention tying it to a real tube position on the deck, so it could mean different physical slots to Pourfecto than to a human reading the Nimbus software/deck layout. Uses the same CartesianIndices/cartesian_to_well convention already applied to plate wells elsewhere in this file (convert_design's CartesianIndices(CHESSCore.children(...))), so a rack slot i and a plate well i are numbered consistently.
Pourfecto.order_batch — Method
order_batch(batch::Vector{DispenseItem}, method::Symbol) -> Vector{DispenseItem}Dispatches to order_greedy or order_exact based on method (:greedy or :exact).
Pourfecto.order_exact — Method
order_exact(batch::Vector{DispenseItem}) -> Vector{DispenseItem}Exact minimum-total-travel-distance ordering, found by brute-force permutation search over batch, minimizing the sum of consecutive grid_distances. Batches are small (bounded by capacity/typical dispense volume), so this stays cheap; a batch larger than 8 items falls back to order_greedy with a warning rather than paying the factorial cost. No solver is used here: at this scale, per-batch solver startup overhead would dominate a plain permutation search.
Pourfecto.order_greedy — Method
order_greedy(batch::Vector{DispenseItem}) -> Vector{DispenseItem}Nearest-neighbor tour: start from the batch's first item, repeatedly append the unvisited item nearest (by grid_distance) to the last-appended item's position.
Pourfecto.order_windows — Method
order_windows(windows::Vector{DispenseWindow}; method::Symbol=:greedy) -> Vector{DispenseWindow}Phase 2: find a nearest-neighbor visiting order over windows that minimizes total Euclidean travel distance between consecutive windows, treating each window as a point at (offset,column). Directly reuses batching.jl's existing, unmodified order_batch/grid_distance (genuinely instrument-agnostic tour logic) by wrapping each window as a position-bearing DispenseItem stand-in (.col repurposed as an index back into windows, .volume unused). method is :greedy (default, unbounded) or :exact (optimal, but order_exact silently falls back to greedy with a warning above its existing _order_exact_max_items cap).
Pourfecto.packing_greedy — Method
packing_greedy(pairings::Vector{Tuple{Labware,Labware}}, config::Configuration; kwargs...) -> Vector{SlottingDict}Produce one or more SlottingDict layouts covering every (source,target) pairing that must be co-slotted, by repeatedly calling slotting_greedy on the full remaining labware set and peeling off however many pairings that solution satisfies, until every pairing is covered. Each returned SlottingDict becomes one compiled protocol (see compile).
This is the default packing_method for compile and the extension point instruments should override only when they have unusual slotting constraints – e.g. Cobra overrides this to always produce one protocol per pairing (it only has two deck slots and deliberately wants a fresh protocol whenever the labware pairing changes), rather than greedily co-packing as many pairings as possible into each protocol the way this generic implementation does.
Pourfecto.params — Method
params(p::Pourcast) -> ParameterDictReturn the parameter dictionary used to store all metadata from each pourfecto run.
Pourfecto.place_labware — Method
place_labware(slotting, design, sources, targets, config::Configuration{NimbusFourChannel}; reload_weight=8.0) -> SlottingDictPlacement for the 4-channel Nimbus. Grouping and channels follow tube position (source_position_order, chunked by n_channels), so choosing where the reagent tubes sit also chooses each reagent's group and channel:
- Grouping. With more than
n_channelsreagents,group_reagentssplits them into groups that run one after another, scored by dispense windows plusreload_weight× reload waves. - Channel order. Within each group, every assignment of its reagents to channels (24 at most) is compiled with
batch_design_four_channeland scored by action-row count, which captures dispense-window parallelism plus the reload interruptions and window tour that shift with it. The lowest wins; ties keep the incoming order. - Tube placement. The reagents' tubes are re-placed into free 50 mL rack slots. Each group gets its own whole rack columns (both rows free), in its own rack when possible, with racks and columns taken in position order so group 1's tubes sort first, then group 2's, and so on; a group never shares a column with another. Channels aspirate together from one rack column when the lower channel sits on row A, so a crossed column never occurs. Only if too few whole columns are free does placement fall back to single slots.
Aspirate cost here depends only on which slots are free, not on grouping or channel order (any two reagents fit any free column with the lower channel on row A), so optimizing those first and then placing loses nothing. Returns slotting unchanged unless every source is a 50 mL conical, or if not enough free rack slots exist.
Pourfecto.place_labware — Method
place_labware(slotting::SlottingDict, design::DataFrame, sources, targets, config::Configuration) -> SlottingDictPlacement step of compiling, run once per protocol after packing. Packing (packing_greedy) decides which labware share a deck load, i.e. the bounds of each protocol; placement decides where that labware sits within the protocol, now that its transfer design is known. The default returns slotting unchanged. Instruments whose execution cost depends on where labware sits override it (see NimbusFourChannel, which places source tubes to match its channel order). An override must keep the same set of labware and return a valid slotting for config.
Pourfecto.planned_stocks — Method
planned_stocks(p::Pourcast) -> Vector{Stock}Compute the planned target stocks from a Pourcast solution.
This function reconstructs each target stock as a mixture of the source stocks using the planned transfer volumes from the solution. Conceptually, for each target t:
planned[t] = Σₛ ( V[s,t] * source_stocks(p)[s] )
where V is the transfer-volume decision variable.
Details
- Transfer volumes are rounded to a number of digits derived from
params(p)[:min_vol_threshold]to reduce numerical noise:digits = -floor(log10(min_vol_threshold)). - Volumes are interpreted as microliters and converted to
Unitfulquantities viaV .* u"µL".
See Also
Pourfecto.planning_concentration — Method
planning_concentration(stock::CHESSCore.Stock, ingredient::CHESSCore.Organism)CHESSCore.concentration is unambiguous for organisms: Biomass (OD*mL) never shares a stock's plain Mass/Volume total dimension, so it always takes the cross-dimension ratio branch and deterministically returns plain OD – already the fixed unit normalize_inputs needs. This method still forces the unit explicitly, for symmetry with the Solid/Liquid cases above.
Pourfecto.planning_concentration — Method
planning_concentration(stock::CHESSCore.Stock, ingredient::CHESSCore.Solid)
planning_concentration(stock::CHESSCore.Stock, ingredient::CHESSCore.Liquid)Concentration of ingredient in stock, in the fixed convention the pourfecto planning algorithm (normalize_inputs) needs: always g/µL for solids, always a dimensionless percent for liquids – regardless of the stock's own total quantity dimension. This is a different (and narrower) contract than CHESSCore.concentration, which is dimension-aware (returns percent whenever the ingredient's dimension matches the stock total's, a mass/volume-style ratio otherwise) for the dataframe "vc" format. normalize_inputs needs a single fixed unit per ingredient kind so S[:,c]./normfactor is always convertible to µL^-1; using CHESSCore.concentration directly here breaks that for stocks whose total shares a solid ingredient's dimension (e.g. a Mixture), where it returns percent instead of g/µL.
Pourfecto.plot_flow — Method
plot_flow(a::AspNode, d::DispNode, vol::Real; kwargs...) -> AnyCreate a visualization of a single flow from an aspiration node to a dispense node.
This helper plots the aspiration node a and dispense node d (via plot(a; kwargs...) and plot(d; kwargs...)) and combines them into a single figure with a title that includes the node configuration type and the requested volume (in µL).
Arguments
a::AspNode: Aspiration (source-side) flow node to visualize.d::DispNode: Dispense (target-side) flow node to visualize.vol::Real: Flow volume to display in the title (interpreted as µL).
Pourfecto.plot_flows — Method
plot_flows(pc::Pourcast; threshold::Real = 1e-4) -> VectorGenerate per-flow plots for all flows in a solved Pourcast above a threshold.
This function:
- Builds aspiration and dispense node grids with
compute_flow_nodesusing theconfigs(pc)and the corresponding source/target labware. - Reads the flow matrix via
flows(pc). - For each matrix entry
(i, j)whose flow is>= threshold, generates a plot withplot_flowand collects the results.
Arguments
pc::Pourcast: A solvedPourcast
See also
Pourfecto.plot_labware — Method
plot_labware(l::Labware; well_heatmap=false, kwargs...)Pourfecto's decorated view of a Labware: CHESSCore's base grid plot (plot(l), the canonical Labware view shared across the CHESS ecosystem) plus Pourfecto-specific decoration – a circle/square marker per well for non-wellplate-like kinds (Bottle/Conical), and an optional stock-quantity heatmap overlay.
This is a distinct, Pourfecto-owned entry point rather than a plot(::Labware) override: CHESSCore already defines the canonical plot(::Labware) method (via CHESSCoreLabwarePlottingExt), and a second, conflicting definition of the exact same method signature in Pourfecto would be a real collision, not just duplication.
Pourfecto.plot_slotting — Method
plot_slotting(deck::Deck, slotting::SlottingDict;
wrapwidth::Integer=20,
titlefontsize::Integer=18,
fontsize::Integer=14,
plotsize=(1200,800)) -> Plots.PlotPlot a deck with labware arranged according to slotting.
Assumptions about slotting (based on your other plotting code):
slotting[lw][1]is aPositionobject on the deck (e.g.,ConstrainedPosition,StackPosition, ...).slotting[lw][2]is an index (row,col) within that position's slot grid where the labware is placed.
Assumptions about deck:
deckis either a matrix-like container (2D) of positions, or a vector of positions.
Pourfecto.polish_batches — Method
polish_batches(batches::Vector{Vector{DispenseItem}}, capacity::Real;
max_iterations::Int=1000, ordering_method::Symbol=:greedy) -> Vector{Vector{DispenseItem}}Exchange/local-search polishing pass over already-clustered batches (e.g. cluster_batches's output): repeatedly applies the first strictly-improving swap or relocate move found (see _try_swap!/_try_relocate!) – exchanging one item between two batches, or moving one item into another batch's spare capacity – until a full scan finds no improving move or max_iterations is reached. capacity must be whatever bound batches was actually clustered against (e.g. an effective capacity reduced for a dead-volume buffer elsewhere in the pipeline). ordering_method is only the internal distance proxy used to evaluate candidate moves during search (default :greedy, to keep move evaluation cheap); it does not affect how the returned batches are subsequently ordered by the caller.
Every input item ends up in exactly one output batch (no drops/duplicates), every output batch's volume stays ≤ capacity, and total travel distance never increases: since only strictly-improving moves are ever accepted, this holds by construction, but this function double-checks it defensively at the end and falls back to returning the untouched input (with an @warn) if the final recomputed cost is ever found to exceed the initial cost.
Pourfecto.pourcast_to_json — Method
pourcast_to_json(p::Pourcast) -> StringSerialize a Pourcast to a JSON string.
This converter prepares Pourcast contents for JSON encoding by first converting stocks, labware, and configurations into table-like or dictionary-like representations, then assembling a single dictionary and encoding it with JSON.json.
Pourfecto.pourfecto — Method
pourfecto(directory::AbstractString,
source_labware::Vector{<:CHESSCore.Labware},
target_labware::Vector{<:CHESSCore.Labware},
configs::Union{Vector{<:AbstractString},Vector{<:Configuration}};
kwargs...)Run the Pourfecto algorithm in planning and scheduling mode and automatically compile the resulting Pourcast(@ref) in directory.
If the Pourcast fails a solution_quality check, pourfecto will produce an error message while saving the Pourcast and quality report to the directory
Arguments
directory: The output directory (will be made if it doesn't already exist)source_labware: The CHESSCore.Labware objects containing source stocks that pourfecto can use to create the targetstarget_labware: The CHESSCore.Labware objects containing target stocks pourfecto is trying to create using the sourcesconfigs: The the Configuration objects or String identifiers for instrument configurations pourfecto can use to schedule the planned liquid transfers.
Keyword Arguments
objective = min_cost_flow: Set the scheduling objective for Pourfecto. Seekeys(objectives)for options.priority = Dict{String, UInt64}(): aPriorityDictthat specifies which reagents take precedence over others in a plan.quiet = true: Supress the printout of the solver.solver_timelimit = 30: Set the solver's time limit, in seconds.grb_feasibility_tol = 1.0e-6: Set the Gurobi solver'sFeasibilityTolparameter. Also reused as the indicator/big-M epsilon inside every MILP scheduling objective, regardless of which solver is active – so it isn't purely a Gurobi-only setting in practice.min_vol_threshold = 0.1: Set the minimum volume threshold in µL. Any transfer must be at least this large.require_nonzero = true: If a target contains a reagent, require that some amount of that reagent is delivered, even if the optimal solution is none.enforce_minimum_shot = false: Enforce the minimum shot volume constraints for each instrument. Caution turns the problem into an MILP.slack_tol = 0.01: Set the tolerance of the slacks in the solution to stay with in a percentage of the optimal value. A value of 0.01 equates to a 1% tolerance.config_costs = ones(length(configs)): Set the relative cost of using each configuration.solution_tolerance = 0.01: Set the limit for the magnitude of any single slack. Slacks are normalized per chemical, so a value of 1 indicates that a chemical's slack can be as large as the largest target quantity of that chemical, not the largest target in the whole model.allow_in_place = false: Allow the same physical labware to appear in bothsource_labwareandtarget_labware, for in-place transfers (e.g. adding a reagent to a plate's existing stocks). Wells shared between source and target keep their existing content by construction, bounded by physical well capacity rather than the target's declared quantity.optimizer = Gurobi.Optimizer: The JuMP-compatible optimizer used to solve the underlying models (e.g.HiGHS.Optimizer,SCIP.Optimizer).packing_method::Function = packing_greedy: forwarded tocompile; the slotting/packing routine used when compiling the resultingPourcasttodirectory. See the Compiling Pourcasts manual page for details.
Pourfecto.pourfecto — Method
pourfecto(source_labware::Vector{<:CHESSCore.Labware},
target_labware::Vector{<:CHESSCore.Labware},
configs_string::Vector{<:AbstractString};kwargs...)Run the Pourfecto algorithm in planning and scheduling mode.
Arguments
source_labware: The CHESSCore.Labware objects containing source stocks that pourfecto can use to create the targetstarget_labware: The CHESSCore.Labware objects containing target stocks pourfecto is trying to create using the sourcesconfigs_string: The String identifiers for instrument configurations pourfecto can use to schedule the planned liquid transfers.
Keyword Arguments
objective = min_cost_flow: Set the scheduling objective for Pourfecto. Seekeys(objectives)for options.priority = Dict{String, UInt64}(): aPriorityDictthat specifies which reagents take precedence over others in a plan.quiet = true: Supress the printout of the solver.solver_timelimit = 30: Set the solver's time limit, in seconds.grb_feasibility_tol = 1.0e-6: Set the Gurobi solver'sFeasibilityTolparameter. Also reused as the indicator/big-M epsilon inside every MILP scheduling objective, regardless of which solver is active – so it isn't purely a Gurobi-only setting in practice.min_vol_threshold = 0.1: Set the minimum volume threshold in µL. Any transfer must be at least this large.require_nonzero = true: If a target contains a reagent, require that some amount of that reagent is delivered, even if the optimal solution is none.enforce_minimum_shot = false: Enforce the minimum shot volume constraints for each instrument. Caution turns the problem into an MILP.slack_tol = 0.01: Set the tolerance of the slacks in the solution to stay with in a percentage of the optimal value. A value of 0.01 equates to a 1% tolerance.config_costs = ones(length(configs)): Set the relative cost of using each configuration.solution_tolerance = 0.01: Set the limit for the magnitude of any single slack. Slacks are normalized per chemical, so a value of 1 indicates that a chemical's slack can be as large as the largest target quantity of that chemical, not the largest target in the whole model.allow_in_place = false: Allow the same physical labware to appear in bothsource_labwareandtarget_labware, for in-place transfers (e.g. adding a reagent to a plate's existing stocks). Wells shared between source and target keep their existing content by construction, bounded by physical well capacity rather than the target's declared quantity.optimizer = Gurobi.Optimizer: The JuMP-compatible optimizer used to solve the underlying models (e.g.HiGHS.Optimizer,SCIP.Optimizer).
Pourfecto.pourfecto — Method
pourfecto(source_labware::Vector{<:CHESSCore.Labware},
target_labware::Vector{<:CHESSCore.Labware},
configs::Vector{<:Configuration};
kwargs...)Run the Pourfecto algorithm in planning and scheduling mode
Arguments
source_labware: The CHESSCore.Labware objects containing source stocks that pourfecto can use to create the targetstarget_labware: The CHESSCore.Labware objects containing target stocks pourfecto is trying to create using the sourcesconfigs: The instrument configurations pourfecto can use to schedule the planned liquid transfers.
Keyword Arguments
objective = min_cost_flow: Set the scheduling objective for Pourfecto. Seekeys(objectives)for options.priority = Dict{String, UInt64}(): aPriorityDictthat specifies which reagents take precedence over others in a plan.quiet = true: Supress the printout of the solver.solver_timelimit = 30: Set the solver's time limit, in seconds.grb_feasibility_tol = 1.0e-6: Set the Gurobi solver'sFeasibilityTolparameter. Also reused as the indicator/big-M epsilon inside every MILP scheduling objective, regardless of which solver is active – so it isn't purely a Gurobi-only setting in practice.min_vol_threshold = 0.1: Set the minimum volume threshold in µL. Any transfer must be at least this large.require_nonzero = true: If a target contains a reagent, require that some amount of that reagent is delivered, even if the optimal solution is none.enforce_minimum_shot = false: Enforce the minimum shot volume constraints for each instrument. Caution turns the problem into an MILP.slack_tol = 0.01: Set the tolerance of the slacks in the solution to stay with in a percentage of the optimal value. A value of 0.01 equates to a 1% tolerance.config_costs = ones(length(configs)): Set the relative cost of using each configuration.solution_tolerance = 0.01: Set the limit for the magnitude of any single slack. Slacks are normalized per chemical, so a value of 1 indicates that a chemical's slack can be as large as the largest target quantity of that chemical, not the largest target in the whole model.allow_in_place = false: Allow the same physical labware to appear in bothsource_labwareandtarget_labware, for in-place transfers (e.g. adding a reagent to a plate's existing stocks). Wells shared between source and target keep their existing content by construction, bounded by physical well capacity rather than the target's declared quantity.optimizer = Gurobi.Optimizer: The JuMP-compatible optimizer used to solve the underlying models (e.g.HiGHS.Optimizer,SCIP.Optimizer).
Pourfecto.pourfecto — Method
pourfecto(source_labware::Vector{<:CHESSCore.Labware},
target_labware::Vector{<:CHESSCore.Labware};
kwargs...)Run the Pourfecto algorithm in planning mode using just the source and target labware
Arguments
source_labware: The CHESSCore.Labware objects containing source stocks that pourfecto can use to create the targetstarget_labware: The CHESSCore.Labware objects containing target stocks pourfecto is trying to create using the sources
Keyword Arguments
objective = min_cost_flow: Set the scheduling objective for Pourfecto. Seekeys(objectives)for options.priority = Dict{String, UInt64}(): aPriorityDictthat specifies which reagents take precedence over others in a plan.quiet = true: Supress the printout of the solver.solver_timelimit = 30: Set the solver's time limit, in seconds.grb_feasibility_tol = 1.0e-6: Set the Gurobi solver'sFeasibilityTolparameter. Also reused as the indicator/big-M epsilon inside every MILP scheduling objective, regardless of which solver is active – so it isn't purely a Gurobi-only setting in practice.min_vol_threshold = 0.1: Set the minimum volume threshold in µL. Any transfer must be at least this large.require_nonzero = true: If a target contains a reagent, require that some amount of that reagent is delivered, even if the optimal solution is none.enforce_minimum_shot = false: Enforce the minimum shot volume constraints for each instrument. Caution turns the problem into an MILP.slack_tol = 0.01: Set the tolerance of the slacks in the solution to stay with in a percentage of the optimal value. A value of 0.01 equates to a 1% tolerance.config_costs = ones(length(configs)): Set the relative cost of using each configuration.solution_tolerance = 0.01: Set the limit for the magnitude of any single slack. Slacks are normalized per chemical, so a value of 1 indicates that a chemical's slack can be as large as the largest target quantity of that chemical, not the largest target in the whole model.allow_in_place = false: Allow the same physical labware to appear in bothsource_labwareandtarget_labware, for in-place transfers (e.g. adding a reagent to a plate's existing stocks). Wells shared between source and target keep their existing content by construction, bounded by physical well capacity rather than the target's declared quantity.optimizer = Gurobi.Optimizer: The JuMP-compatible optimizer used to solve the underlying models (e.g.HiGHS.Optimizer,SCIP.Optimizer).
Pourfecto.pourfecto — Method
pourfecto(sources::Vector{<:CHESSCore.Stock},
targets::Vector{<:CHESSCore.Stock};
kwargs...)Run the Pourfecto algorithm in planning mode using just the source and target stocks
Arguments
sources: The CHESSCore.Stock objects that pourfecto can use to create the targetstargets: The CHESSCore.Stock objects that pourfecto is trying to create using the sources
Keyword Arguments
objective = min_cost_flow: Set the scheduling objective for Pourfecto. Seekeys(objectives)for options.priority = Dict{String, UInt64}(): aPriorityDictthat specifies which reagents take precedence over others in a plan.quiet = true: Supress the printout of the solver.solver_timelimit = 30: Set the solver's time limit, in seconds.grb_feasibility_tol = 1.0e-6: Set the Gurobi solver'sFeasibilityTolparameter. Also reused as the indicator/big-M epsilon inside every MILP scheduling objective, regardless of which solver is active – so it isn't purely a Gurobi-only setting in practice.min_vol_threshold = 0.1: Set the minimum volume threshold in µL. Any transfer must be at least this large.require_nonzero = true: If a target contains a reagent, require that some amount of that reagent is delivered, even if the optimal solution is none.enforce_minimum_shot = false: Enforce the minimum shot volume constraints for each instrument. Caution turns the problem into an MILP.slack_tol = 0.01: Set the tolerance of the slacks in the solution to stay with in a percentage of the optimal value. A value of 0.01 equates to a 1% tolerance.config_costs = ones(length(configs)): Set the relative cost of using each configuration.solution_tolerance = 0.01: Set the limit for the magnitude of any single slack. Slacks are normalized per chemical, so a value of 1 indicates that a chemical's slack can be as large as the largest target quantity of that chemical, not the largest target in the whole model.allow_in_place = false: Allow the same physical labware to appear in bothsource_labwareandtarget_labware, for in-place transfers (e.g. adding a reagent to a plate's existing stocks). Wells shared between source and target keep their existing content by construction, bounded by physical well capacity rather than the target's declared quantity.optimizer = Gurobi.Optimizer: The JuMP-compatible optimizer used to solve the underlying models (e.g.HiGHS.Optimizer,SCIP.Optimizer).
Pourfecto.random_adverb_verb_pairs — Function
random_adverb_verb_pairs(n=1; adverbs=adverbs, verbs=verbs, rng=Random.default_rng(), make_unique=true) -> Vector{String}Generate n directory-safe random strings of the form adverb_verb by sampling from adverbs and verbs (assumed to be Julia vectors of strings).
Directory-safety rules applied:
- lowercase
- spaces and hyphens become
_ - all other non
[a-z0-9_]characters removed - repeated
_collapsed; leading/trailing_stripped
If make_unique=true, results are unique; throws if n exceeds the number of possible unique combinations after sanitization.
Pourfecto.register_instrument! — Method
register_instrument!(config::Configuration; name::AbstractString, override::Bool=false)Register config in the global configurations registry under name, making it discoverable via configurations["name"] and usable by name in pourfecto.
Errors if name is already registered unless override=true (in which case the previous entry is replaced and a warning is emitted) – this guards against two independently-authored instrument packages accidentally colliding on the same name. config.kind's capability-bearing validation already happens inside the Configuration{I} outer constructor (see Configuration); this function only handles registry-level name bookkeeping, not re-validation.
Third-party instrument packages should call this once per Configuration they define, typically at module top-level (or from __init__() if settings need to be read from Preferences.jl/environment state at load time) right after using Pourfecto.
Examples
julia> abstract type ExInstrument <: InstrumentModel end
julia> piston = Piston{ContinuousActuator,SingleRepeater}((20u"µL",200u"µL"),(20u"µL",200u"µL"),1);
julia> head = Head{ExInstrument}([piston],[Channel(220u"µL")],trues(1,1));
julia> deck = [UnconstrainedPosition("Deck",true,true,"square")];
julia> register_instrument!(Configuration{ExInstrument}(head,deck); name="ex_instrument");
julia> haskey(configurations,"ex_instrument")
truePourfecto.round_with_exact_sum — Method
round_with_exact_sum(values::Vector{<:Real}, digits::Int) -> Vector{Float64}Round every element of values to digits decimal places except the last, which instead absorbs whatever remainder is needed so the rounded values sum exactly to round(sum(values), digits).
Rounding every element independently can leave the rounded sum off from the rounded total by a hairline amount (e.g. -1.42e-14) due to ordinary floating-point summation drift – fatal when a downstream consumer does a strict sum(dispenses) >= aspirated check. Computing the last element as a remainder rather than an independently-rounded value guarantees exact agreement by construction, regardless of how many elements are in values.
Pourfecto.same_well_pairs — Method
same_well_pairs(source_labware::Vector{<:Labware},target_labware::Vector{<:Labware})Index pairs (s,t) into stocks(source_labware)/stocks(target_labware) (equivalently, into the sources/targets vectors build_planning_model operates on) whose wells are the exact same physical well – i.e. same labware name and same well name – appearing in both the source and target labware. Empty whenever no labware name is shared between source_labware and target_labware.
Pourfecto.sanitize_tokens — Method
sanitize_tokens(xs::AbstractVector{<:AbstractString};
lowercase::Bool=true,
replace_space_hyphen::Bool=true,
allow_digits::Bool=true,
keep_underscores::Bool=true,
drop_empty::Bool=true,
de_duplicate::Bool=true,
sort_items::Bool=false) -> Vector{String}Sanitize a vector of strings for directory-safe usage.
Rules (in order):
strip- (optional)
lowercase - (optional) replace whitespace and
-with_ - remove disallowed characters (keeps only
[a-z], optionally digits and_) - collapse consecutive
_ - trim leading/trailing
_ - (optional) drop empty strings
- (optional) de-duplicate (stable)
Examples
```julia adverbss = sanitizetokens(adverbs) verbss = sanitizetokens(verbs)
directory-safe pairings
names = randomadverbverbpairs(50; adverbs=adverbss, verbs=verbs_s)
Pourfecto.scheduling_objective_value — Method
scheduling_objective_value(p::Pourcast) -> RealReturn the objective value achieved by the schedule/plan.
Pourfecto.set_cobra_path! — Method
set_cobra_path!(path)Persist path as the default cobra_path for this project (via Preferences.jl, stored in LocalPreferences.toml) and apply it to the current session immediately.
Pourfecto.slacks — Method
slacks(p::Pourcast)Return the slack decision variable(s) from a Pourcast solution.
This is an accessor that extracts the solver/model variable stored under the key :slacks from the pourcast's model_solution
Pourfecto.sliding_window_mask — Method
sliding_window_mask(h::Head,l::Labware,direction::Symbol; v_spacing=1,h_spacing=1,v_out=false,h_out=false)Build the (predicate,positions) pair for the sliding-window mask archetype – the head's channel grid (spaced v_spacing/h_spacing well-units apart, optionally overhanging the labware edge via v_out/h_out) slides across the labware's well grid. Covers every Mask branch except the undifferentiated-well case (see blanket_mask); a single-channel head reduces to a plain point match automatically (the spacing term vanishes when H[d]=1), so there is no separate case to hand-write.
Per dimension, if effective_head_size collapsed (the head is wider/taller than the labware in that dimension – only for a pairing already declared safe by the calling Mask branch's kind list), the channel term is dropped from the predicate: any channel is valid, matching the position-count that already clamped for the same reason.
Pourfecto.slotting_greedy — Method
slotting_greedy(labware::Vector{<:Labware}, config::Configuration{NimbusFourChannel}) -> SlottingDictPins nimbus_4ch_waste_conical to its fixed physical slot before delegating to the generic slotting_greedy, mirroring single-channel Nimbus's own override (reimplemented independently, per this module's design decisions).
Pourfecto.slotting_greedy — Method
slotting_greedy(labware::Vector{<:Labware}, config::Configuration{Nimbus}) -> SlottingDictNimbus-specific override: pins nimbus_waste_conical to its fixed physical slot (TubeRack50ML_WellNames_0006, well A3, nearest the tip rack) before delegating everything else to the generic slotting_greedy. That slot is a permanent fixture on the real Nimbus deck – excluded from ordinary source/target slotting on every compile, independent of whether the protocol actually uses the Blowout path, exactly like Cobra's packing_greedy override reflects its own instrument-specific deck constraints.
Pourfecto.slotting_greedy — Method
slotting_greedy(labware::Vector{<:Labware}, config::Configuration; pinned::SlottingDict=SlottingDict()) -> SlottingDictGreedily assign each piece of labware to an open, admissible deck slot on config's deck. Labware sharing a name (e.g. the same physical plate used as both a source and a target) is slotted once and shares that slot. Errors if any labware can't be placed on config's deck at all (see can_place); labware that can be placed but for which no slot remains open is returned in the not_placed local (not currently surfaced – see packing_greedy, which calls this repeatedly to produce one or more slotting solutions covering everything).
pinned pre-seeds the result with fixed labware => (position,slot) assignments – e.g. a permanently-mounted fixture that always occupies the same physical deck slot – before the greedy loop runs; those slots are removed from consideration for every other piece of labware, and pinned labware is skipped by the ordinary placement loop. Empty by default, so existing callers see no behavior change. See Nimbus.jl's slotting_greedy(::Vector{<:Labware},::Configuration{Nimbus}) override for a worked example (a reserved waste-conical slot).
Pourfecto.slotting_requirements — Method
slotting_requirements(pc::Pourcast) -> Vector{BitMatrix}Compute per-configuration source→target “slotting” requirements from a Pourcast plan by marking which source labware must be able to transfer to which target labware.
For each configuration c, this function returns a boolean matrix M = slotting_matrices[c] of size (S, T) where:
S == length(source_labware(pc))(number of source labware items)T == length(target_labware(pc))(number of target labware items)M[s, t] == trueif any transfer value associated with transfers fromsources[s]totargets[t]in configurationcis greater than0. OtherwiseM[s, t] == false.
This is useful for determining which source/target pairs must be physically co-slotted/accessible in each configuration.
Returns
Vector{BitMatrix}: A length-Cvector of boolean matrices, one per configuration, whereC == length(configs(pc)). Each matrix has shape(S, T).
Notes
- The comparison is strictly greater than (
>)0. - Requires the following
Pourcast-related functions to be defined:transfers_by_config,configs,source_labware,target_labware,transfer_indices.
Examples
```julia reqs = slotting_requirements(pc)
Pourfecto.slottingdict_to_df — Method
slottingdict_to_df(slotting::SlottingDict) -> DataFrameConvert a SlottingDict describing labware placement into a DataFrame.
The input dictionary is expected to map each labware object s to a 2-tuple of the form (deck_position, slot), where:
deck_positionis an object whose human-readable identifier is given byname(deck_position).slotis aStringorIntegeridentifying the slot within that deck position.
Arguments
slotting::SlottingDict: Dictionary of labware placement info.
Returns
DataFrame: A table with columnsPosition,Slot,LabwareID, andName.
Pourfecto.source_labware — Method
source_labware(p::Pourcast) -> Vector{<:CHESSCore.Labware}Return the source labware associated with p. The labware also provide the source stocks. They are stored separately because not all pourcasts have associated labware.
Pourfecto.source_position_order — Method
source_position_order(key::Tuple) -> TupleSort key for a source (labware id, well): labware id, then column, then row. Channels are handed out in this order, so in a 2-row tube rack A1, B1, A2, B2 map to channels 1-4 down the head, and the two tubes sharing a column always put the lower channel on row A.
Pourfecto.source_stocks — Method
source_stocks(p::Pourcast) -> Vector{<:CHESSCore.Stock}Return the source (input) stocks associated with p.
Pourfecto.split_oversized — Method
split_oversized(items::Vector{DispenseItem}, capacity::Real) -> Vector{DispenseItem}Split any item whose volume exceeds capacity into ceil(volume/capacity) chunks, each ≤ capacity (the final chunk carries the remainder). Items already ≤ capacity pass through unchanged. Chunks keep the same col/position as their source item, so a later batching step is free to either re-merge them into the same batch or spread them across different batches. Preserves total volume exactly.
Pourfecto.target_labware — Method
target_labware(p::Pourcast) -> Vector{<:CHESSCore.Labware}Return the target labware associated with p. The labware also provide the target stocks. They are stored separately because not all pourcasts have associated labware.
Pourfecto.target_stocks — Method
target_stocks(p::Pourcast) -> Vector{<:CHESSCore.Stock}Return the target (output) stocks associated with p.
Pourfecto.target_well_capacities — Method
target_well_capacities(target_labware::Vector{<:Labware})Physical well capacity (via CHESSCore.wellcapacity) for each well in target_labware, in the same flattened order as stocks(target_labware). Entries are missing when the well's LocationKind doesn't define a capacity.
Pourfecto.tip_change_flags — Method
tip_change_flags(batch_sources::Vector, windowsize::Int) -> Vector{Int}Per-batch analogue of a shot-level sliding-window tip-change heuristic: given batch_sources[i] (the source identity of the i-th aspirate batch, in emission order), returns a 0/1 flag per batch. flags[1] is always 0 (no forced change on the very first aspirate – there is no prior tip to distinguish it from). For i > 1, flags[i] == 1 if the source changed from batch i-1, or if no source-change occurred anywhere in the trailing windowsize-batch window ending at i (forcing a periodic refresh even when the same source is reused across many batches).
Pourfecto.transfer_indices — Method
transfer_indices(pc::Pourcast, source::Labware, target::Labware) -> Tuple{AbstractVector,AbstractVector}Return the transfer index selectors for a given source→target labware pair.
This function validates that source is present in source_labware(pc) and that target is present in target_labware(pc). If either is missing, it throws an ArgumentError. On success, it retrieves index selectors from labware_indices(pc) and returns the pair associated with the requested source and target.
Arguments
pc::Pourcast: A pour/transfer plan containing source/target labware lists and index mappings.source::Labware: Source labware to look up.target::Labware: Target labware to look up.
Returns
(src_inds, tgt_inds): A 2-tuple of vectors:src_inds: index selector(s) associated withsourcetgt_inds: index selector(s) associated withtarget
These are intended to be used together for indexing transfer arrays/tensors.
Throws
ArgumentError: Ifsourceis not insource_labware(pc)ortargetis not intarget_labware(pc).
Pourfecto.transfers — Method
transfers(p::Pourcast)Return the transfer decision variable(s) from a Pourcast solution.
This is an accessor that extracts the solver/model variable stored under the key :V from the pourcast's model_solution.
See also
Pourfecto.transfers_by_config — Method
transfers_by_config(pc::Pourcast) -> Vector{Matrix{Float64}}Compute well-to-well transfer volumes per configuration.
This function aggregates the global flow matrix flows(pc) into per-configuration transfer matrices between source wells and target wells.
For each configuration c in configs(pc), it returns an S×T matrix trfs_by_config[c] where:
Sis the total number of source wells across all source labware.Tis the total number of target wells across all target labware.- Entry
trfs_by_config[c][s, t]equals the sum of all flows (fromflows(pc)) along aspiration/dispense node pairs that:- connect source well
sto target wellt(viaPourfecto.compute_flow_connections), and - belong to configuration
c(i.e., both nodes have.configuration == cons[c]).
- connect source well
Arguments
pc::Pourcast: A pour/transfer plan providing configurations, labware, well names, and a global flow matrix viaflows(pc).
Returns
Vector{Matrix{Float64}}: A length-Cvector whereC == length(configs(pc)). Each element is anS×Tdense matrix of aggregated transfer amounts.
Pourfecto.unambiguous_labware_names — Method
unambiguous_labware_names(sources::Vector{<:Labware},targets::Vector{<:Labware})Like all_unique_labware_names, but permits a labware name to appear in both sources and targets (an in-place transfer). Still rejects a name repeated within sources alone, or within targets alone, since that's always ambiguous regardless of in-place mode.
Pourfecto.write_instrument_files — Function
write_instrument_files(directory, design, source, target, config::Configuration{Nimbus}, slotting=slotting_greedy(...);
batch_ordering::Symbol=:greedy, volume_precision::Int=1, insert_blowouts::Bool=true,
waste_target=nimbus_waste_target, dead_volume_buffer::Real=20.0, aspirate_buffer::Real=0.01,
polish_clustering::Bool=false, polish_max_iterations::Int=1000, kwargs...)Compile a design into a Nimbus protocol CSV written to directory. Unlike the generic compiler fallback, the Nimbus supports one-to-many aspirate/dispense: for each source well, as many destination dispenses as fit within the channel's capacity are drawn up in a single aspirate and dispensed together, rather than aspirating once per destination (see batch_design for the full batching/rounding/blowout pipeline). batch_ordering selects how dispenses within a batch are sequenced – :greedy (default, nearest-neighbor) or :exact (optimal, brute-force – only tractable for small batches, see order_exact) – and can be benchmarked against each other by calling pourfecto/compile with either value.
The written CSV uses a unified action-row schema: "Labware ID","Labware Position ID", "Volume (uL)","Action","Change Tip Before", Action being "Aspirate", "Dispense", or "Blowout". An aspirate row is always immediately followed by the rows it feeds (its dispenses, and a trailing blowout when insert_blowouts=true and a re-aspirate follows under the same tip), whose volumes sum to aspirate - aspirate_buffer exactly at volume_precision decimal places (see round_with_exact_sum) – the aspirate carries aspirate_buffer extra, a small unconditional physical safety margin (default 0.01 µL) against real-world pipetting inaccuracy on the last action in a cycle; see batch_design for the full precision/headroom reasoning. "Change Tip Before" is 1 on an aspirate row when the tip should be changed before that aspirate: always on a source change, and periodically thereafter per the instrument's "max_tip_use" setting (see tip_change_flags) – note "max_tip_use" counts aspirate batches, not individual dispense shots. It's always 0 on Dispense/Blowout rows.
insert_blowouts, waste_target, and dead_volume_buffer control the dead-volume Blowout path – see batch_design. insert_blowouts defaults to true, with dead_volume_buffer defaulting to 20.0 µL – compiling with none of these three kwargs supplied still inserts Blowout rows wherever a re-aspirate happens under the same tip. waste_target defaults to the permanently-reserved waste conical (nimbus_waste_target) automatically excluded from ordinary source/target slotting (see slotting_greedy's Configuration{Nimbus} override), so it doesn't need to be supplied explicitly either. Pass insert_blowouts=false to disable the path entirely.
polish_clustering (and polish_max_iterations) enable an exchange/local-search pass over cluster_batches's output to further reduce head-travel distance – see polish_batches and batch_design. Opt-in; default false preserves current behavior exactly.
Pourfecto.write_instrument_files — Function
Compiling function for Mantis
Pourfecto.write_instrument_files — Function
write_instrument_files(directory, design, source, target, config::Configuration{NimbusFourChannel},
slotting=slotting_greedy(...); n_channels, volume_precision,
insert_blowouts, waste_target, dead_volume_buffer, aspirate_buffer,
priming, priming_volume, priming_target, kwargs...)Compile a design into a 4-channel Nimbus protocol CSV written to directory. See batch_design_four_channel for the full sweep/order/assembly pipeline (and for what each named keyword controls). This is a standalone compile entry point, independent of single-channel Nimbus's write_instrument_files (see this file's header comment).
Named keywords are forwarded to batch_design_four_channel; any other keyword (kwargs...) is accepted and silently dropped, not forwarded further – mirroring single-channel Nimbus's own write_instrument_files. This matters because pourfecto(directory,...)'s single-call form forwards the same keyword set to both the solve (e.g. optimizer=SCIP.Optimizer) and the compile stage (compile(directory,pourcast;kwargs...), Pourfecto/src/compiler/compile.jl:69) – without a catch-all here, a solve-only keyword like optimizer would otherwise reach batch_design_four_channel (which has no catch-all of its own) and error.
Pourfecto.TestUtils._reference_1d_hits — Method
_reference_1d_hits(raw_H,Heff,L,spacing,out) -> Vector{Int}Per-well hit-count profile, in one dimension, for a sliding window of Heff channels spaced spacing wells apart, sliding over L wells, optionally overhanging the edge (out). Brute-force: enumerate every candidate window offset over a generously wide range and tally which wells each channel lands on – an independent reference oracle, not a re-typing of sliding_window_mask's own closed-form position-count shortcut.
raw_H is the real channel count in this dimension, which may exceed Heff when the dimension is collapsed (effective_head_size clamped it because the head is wider/taller than the labware in this dimension) – every raw channel in a collapsed dimension maps onto whichever well the (degenerate) window covers, so the per-well count multiplies by raw_H rather than iterating 1:Heff for the channel term (matching sliding_window_mask's own predicate, which drops the channel term entirely for a collapsed dimension).
Pourfecto.TestUtils._reference_1d_position_count — Method
_reference_1d_position_count(Heff,L,spacing,out) -> IntNumber of valid window placements in one dimension, by the same brute-force validity rule as _reference_1d_hits, without tallying per-well hits – used by :blanket, whose predicate doesn't discriminate by well at all.
Pourfecto.TestUtils._reference_visits — Method
_reference_visits(H,Heff,L,archetype;v_spacing,h_spacing,v_out,h_out) -> Matrix{Int}Expected r x c per-well visit-count matrix for a labware of shape L, given raw head size H, effective head size Heff (post row/col-collapsing, if any), and a MaskRule's archetype/kwargs. Row and column dimensions are independent by construction (sliding_window_mask/compute_positions already decompose them this way), so the two 1-D profiles combine via outer product. :blanket ignores spacing/overhang entirely (matching blanket_mask, which passes no kwargs).
Pourfecto.TestUtils._tally_actual_visits — Method
_tally_actual_visits(predicate,P,C,W) -> Matrix{Int}Tally the real Mask predicate's output over every (position,channel) combination, per well.
Pourfecto.TestUtils.default_test_labware_kinds — Method
default_test_labware_kinds() -> Vector{Symbol}The default set of CHESSCore.LocationKind names used to probe an instrument's Mask coverage in test_mask_coverage. Mirrors the fixture Pourfecto's own test/masks.jl uses against its built-in instruments. Pass a narrower/wider kinds list to test_mask_coverage if your instrument only supports a subset, or supports lab-specific labware kinds this default doesn't include.
Pourfecto.TestUtils.test_instrument_interface — Method
test_instrument_interface(config::Configuration; kwargs...) -> BoolConvenience wrapper running all Tier-1 (no-solver) checks: test_mask_coverage and test_json_roundtrip. This is the recommended entry point for a quick, solver-free sanity check on a newly-registered Configuration – e.g. @test test_instrument_interface(my_config) in your own package's test suite. kwargs are forwarded to test_mask_coverage (e.g. kinds=...).
Pourfecto.TestUtils.test_json_roundtrip — Method
test_json_roundtrip(config::Configuration) -> BoolVerify that config survives a JSON serialize/deserialize round trip (json_to_config(config_to_json(config))), catching serialization bugs in a custom instrument's types before they surface at Pourcast-save time. Requires no optimizer.
Configuration has no == method (its settings::Dict field defaults to reference equality), so this checks structural equivalence field-by-field instead of a single ==: instrument type, kind, deck (names/labware-kinds/slots per position), head (piston/channel counts and aspirate/dispense masks), and settings keys.
Pourfecto.TestUtils.test_mask_coverage — Method
test_mask_coverage(config::Configuration; kinds=default_test_labware_kinds()) -> BoolBrute-force-verify that Mask(head(config), labware) for every kind in kinds produces exactly the per-well visit counts implied by mask_rules_for(config). Requires no optimizer.
If mask_rules_for(config) === nothing (no MaskRule table defined), emits a warning and returns true without testing anything – your Mask method exists and is used for scheduling, but hasn't opted into MaskRule-based coverage verification. Define mask_rules_for(::Configuration{YourInstrument}) to enable this check.
Uses @test internally (so it reports through any enclosing @testset), and also returns a Bool so it can be checked directly (e.g. @test test_mask_coverage(config)).
Pourfecto.TestUtils.test_pourcast_compilation — Method
test_pourcast_compilation(name::AbstractString, pourcast::Pourcast) -> NothingSolver-backed integration check: compiles pourcast to a temporary directory and asserts the expected output structure (pourcast.json, a directory per config type, at least one non-empty protocol subfolder per config with slotting requirements) – verifies your instrument's write_instrument_files method actually produces files, not just that its Mask/deck logic is internally consistent.
This function does not run a solver itself – pourcast must already be a solved Pourcast (i.e. you called pourfecto(...) yourself with a real optimizer beforehand, e.g. pourfecto(...; optimizer=SCIP.Optimizer) – SCIP is already a Pourfecto dependency, or any other JuMP-compatible MILP solver you have configured/licensed).
Runs inside its own @testset "$name" block, so wrap the call in your own enclosing @testset if you want it nested under a named group.