API Reference

Full docstrings

DataFrames.DataFrame — Method
DataFrame(pms::Vector{PlateMap}) -> DataFrame

Compile a multi-plate schedule into a single DataFrame: DataFrame(pm) for each plate, stacked with a leading plate column (1-based, matching pms' order) to disambiguate rows across plates (well "A1" exists on every plate). See platemaps_from_dataframe for the reverse.

source
PlateMaps.PlacementError — Type
struct PlacementError <: Exception

Thrown when a PlateMap's occupant field is invalid: a node assigned to an inactive well, or the same node assigned to more than one well.

source
PlateMaps.PlateMap — Type
struct PlateMap{T}
    wells::BitMatrix
    occupant::Matrix{Union{Missing,T}}
end

A PlateMap is the physical realization of a scheduling problem: which node (of caller-chosen identity type T) occupies which well of a plate. It stores nothing about why – no roles, no run/control distinction, no relationships between nodes – only where. PlateMap is deliberately the solution, not the problem statement; combine it with whatever produced the schedule (e.g. a RunMaps.RunMap, sharing the same node identities) to resolve what any given occupant actually is.

occupant is missing at every unassigned well (including every inactive one).

source
PlateMaps.PlateMap — Method
PlateMap{T}(wells::AbstractMatrix) where T

Construct an empty PlateMap (no nodes placed) over the given active-well mask.

source
PlateMaps.NODE_TYPE_REGISTRY — Constant
NODE_TYPE_REGISTRY

Maps a serialized type-tag string to the concrete Julia type it should reconstruct as when parsing a PlateMap's JSON occupant field back in – JSON has no native way to represent arbitrary Julia types (a Symbol-typed occupant, for instance, would otherwise silently come back as a String). Mirrors Pourfecto's INSTRUMENT_TYPES tag-to-type registry pattern for the same "which concrete type do I rebuild" problem. Extend it (NODE_TYPE_REGISTRY["MyType"] = MyType) for custom node-identity types.

source
Base.getindex — Method
Base.getindex(pm::PlateMap, i, j)
Base.getindex(pm::PlateMap, I::CartesianIndex)

Return the node occupying well (i,j), or missing if that well is empty/inactive. Reserved strictly for well-based access – for the reverse lookup (given a node, which well is it in), see well_position; pm[node] is deliberately not defined, since if the node identity type T is e.g. Int, pm[5] would be ambiguous between "well 5" and "node 5".

source
Base.iterate — Function
Base.iterate(pm::PlateMap)

Iterate over pm's occupied wells as node => well pairs (CartesianIndex), skipping empty/inactive wells – mirrors ControlMap's own philosophy of iterating over the meaningful relationships rather than the raw underlying matrix.

source
PlateMaps._component_groups — Method
_component_groups(edges, fixed_nodes) -> Vector{Set}

The atomic node-groups a multi-plate schedule must never split: every edge-connected component of edges (see components), plus a singleton Set for any fixed_nodes entry not already covered by one (a fixed node with no edges at all is still its own atomic unit of size 1).

source
PlateMaps._exchange_solve — Method
_exchange_solve(wells, edges, fixed_pos; objective=:hybrid, minimize=true, restarts=10, iterations=1000,
                 lambda=0.5, rng=Random.default_rng(), kwargs...) -> Dict{Any,CartesianIndex{2}}

Heuristic local-search solver: place every node referenced by edges that isn't already in fixed_pos (i.e. every control node, with run nodes already fixed by place_runs) onto wells's remaining active wells, minimizing (or maximizing) the chosen objective. Returns the full position dict (fixed_pos merged with the placed controls) – the internal counterpart MILP's _milp_solve mirrors; schedule_platemap is the public entry point that calls one of these via solver=. Accepts and ignores extra kwargs (e.g. optimizer/timelimit, meant for plate_solver="MILP"), since schedule_platemap forwards one shared keyword bag to both the plate solver and this control solver.

source
PlateMaps._local_search — Method
_local_search(wells, nodes, free_wells, fixed_pos, score, minimize, restarts, iterations, rng)
    -> (Dict{Any,CartesianIndex{2}}, Real)

Generic restart/local-move search: place nodes onto free_wells, minimizing (or maximizing) score (called on merge(fixed_pos, candidate)). Each restart starts from a random placement and repeatedly either relocates one node into a currently-empty free well or swaps two nodes' positions, keeping a move only if it improves the score. Used both for control placement (relative to already-fixed runs) and for run placement itself (relative to nothing, i.e. fixed_pos=Dict(), minimizing run_compactness).

source
PlateMaps._milp_solve — Method
_milp_solve(wells, edges, fixed_pos; objective=:distance, optimizer=_default_optimizer[], timelimit=100,
            rng=Random.default_rng()) -> Dict{Any,CartesianIndex{2}}

Exact solver for control placement relative to already-fixed run positions, generalizing PlateArrays' old ring-trick MILP from two hardcoded roles to however many are present. This is the two-stage design that reopens MILP tractability after an earlier, fully-joint formulation was found intractable (see the design notes): with run positions fixed before the solve, a run's own nearest-same-role-control term is exactly the old ring-trick (a fixed query point, an unknown-position target), and a control's own nearest-same-role-run term is even simpler than before – every run it could link to sits at a known, fixed position, so "distance to the nearest one" is a constant, precomputable per candidate well, not a ring at all.

Requires a bipartite edge set: every control's same-role neighbors must themselves be fixed nodes (never another control) – true whenever edges comes from a ControlMap (or anything shaped like one), since its edges only ever connect a run to a control. Throws ArgumentError if this doesn't hold.

Only objective=:distance is supported – :evenness/:hybrid aren't yet formulated for the exact solver (they'd need the per-role row/column margin terms linearized too); use solver="exchange" for those.

Accepts and ignores extra kwargs (e.g. run_restarts/run_iterations, meant for place_runs), since schedule_platemap forwards one shared keyword bag to both the plate solver and this control solver.

source
PlateMaps._neighboring_disk — Method
_neighboring_disk(row,col,ring,R,C) -> Vector{Tuple{Int,Int}}

Every grid cell reachable within ring 4-connected hops of (row,col) (a cumulative disk, not a shell) – ported from PlateArrays' old neighboring_ring (the ring-trick's building block).

source
PlateMaps._plate_assign_MILP — Method
_plate_assign_MILP(weights::Vector{Int}, capacity::Int; optimizer=_default_optimizer[], timelimit=100) -> Vector{Int}

MILP formulation of the same weighted plate-assignment problem as _plate_assign_greedy: partition items into the minimum number of plates whose total weight never exceeds capacity. Exact (given enough time) for general/mixed weights, unlike the greedy heuristic. The candidate plate count G is sized from running greedy first as a valid upper bound. Throws ArgumentError if any weight exceeds capacity. Accepts and ignores extra kwargs (e.g. the control solver's objective/restarts/...), same as _plate_assign_greedy, since schedule_platemap forwards one shared keyword bag to both the plate solver and the control solver.

source
PlateMaps._plate_assign_greedy — Method
_plate_assign_greedy(weights::Vector{Int}, capacity::Int; kwargs...) -> Vector{Int}

Partition items (given by weights, e.g. atomic-component sizes) into groups (plates) whose total weight never exceeds capacity, using first-fit-decreasing (FFD): sort items by weight descending, place each into the first group with enough remaining capacity, opening a new group otherwise. Exact optimal group count for uniform weights; a good heuristic (not guaranteed exact) for mixed weights – see _plate_assign_MILP for an exact alternative. Throws ArgumentError if any weight exceeds capacity (a component that can never fit on any plate). Accepts and ignores extra kwargs so it is interchangeable with _plate_assign_MILP behind plate_solvers.

source
PlateMaps._schedule_single_plate — Method
_schedule_single_plate(wells, edges, fixed_nodes; solver, run_restarts, run_iterations, rng, kwargs...)
    -> PlateMap

Two-stage single-plate scheduler: fix fixed_nodes' positions first (via place_runs, clustering edge-connected groups of them together), then place every other node referenced by edges around them, minimizing (or maximizing) the chosen objective. schedule_platemap is the public entry point; this is the per-plate primitive it calls once per plate a multi-plate schedule needs.

This two-stage shape is what makes an exact solver viable: PlateArrays' old MILP solver was tractable specifically because run positions were fixed before it ran; an earlier, fully-joint design of this package (every node placed simultaneously) made exact solving intractable even at modest scale.

source
PlateMaps.components — Method
components(edges) -> Vector{Set}

The connected components of the graph formed by edges (both endpoints of every edge, regardless of role) – a plain union-find over node identities. Nodes that never appear in edges are not included.

source
PlateMaps.describe — Function
describe(pm::PlateMap, cm, node)

Resolve what node (or the occupant at a given well) actually is, using whatever produced the schedule – e.g. a RunMaps.RunMap (what relation_types touch it? what metadata?). PlateMap itself stores none of this; it's provided by the RunMaps package extension (using RunMaps activates it) rather than the core, since it needs both structures at once.

source
PlateMaps.evenness — Method
evenness(wells::BitMatrix,pos,role::Symbol,edges) -> Real

Sum of squared deviations between one role's actual per-row/per-column well counts and the count expected if that role's nodes were spread uniformly across the plate's active wells – generalizes PlateArrays' old LHS metric from two hardcoded roles to whichever roles are actually present.

source
PlateMaps.expected_margins — Method
expected_margins(wells::BitMatrix,count::Integer) -> (rowexp,colexp)

Expected per-row/per-column count if count items were spread uniformly across wells' active cells.

source
PlateMaps.hybrid — Method
hybrid(wells,pos,edges;lambda=0.5,lb_dist=0,ub_dist=1,lb_even=0,ub_even=1) -> Real

Weighted combination of the (normalized) distance and evenness objectives – generalizes PlateArrays' hybrid, now over however many roles/edges are actually present rather than two hardcoded ones.

source
PlateMaps.margins — Method
margins(wells::BitMatrix) -> (rowsums, colsums)

Active-well counts per row and per column.

source
PlateMaps.mkedge — Function
mkedge(node1, node2, role::Symbol=:default; metadata=Dict{Symbol,Any}())

Construct an edge in PlateMaps' native, dependency-free scheduling input format: a NamedTuple (node1=.., node2=.., role=.., metadata=..). This shape deliberately mirrors RunMaps.edges(rm)'s (run, linked_run, relation_type, metadata) output (with neutral field names, since PlateMaps doesn't know about a run/control distinction) – a RunMap's edges can be handed to the scheduler with only a field-name translation, no real conversion logic.

Any iterable of NamedTuples with node1/node2/role/metadata fields is a valid edges argument to schedule_platemap; this constructor is just a convenience for hand-building them.

source
PlateMaps.node_roles — Method
node_roles(edges) -> Dict{Any,Set{Symbol}}

The set of roles each node participates in, derived from edges (a node's roles are the union of .role over every edge touching it, from either side – PlateMaps doesn't track a run/control-style asymmetry between an edge's two endpoints).

source
PlateMaps.nodes — Method
nodes(pm::PlateMap{T}) where T -> Vector{T}

All nodes currently placed on pm, in no particular order.

source
PlateMaps.place_runs — Method
place_runs(wells::BitMatrix, run_nodes, edges; restarts::Int=10, iterations::Int=1000,
           rng::AbstractRNG=Random.default_rng()) -> Dict{Any,CartesianIndex{2}}

Fix run-node positions before control placement, minimizing run_compactness – edge-connected groups of runs are clustered together (via the same restart/local-move search schedule_platemap uses for control placement) so that a run's specific linked control isn't left stranded far from it. Runs with no edges, or whose whole component has no other run member, are unconstrained by the objective and land wherever the initial random draw puts them.

Keyword Arguments

  • restarts/iterations: search effort, independent of the control solver's own restarts/iterations.
  • rng: random number generator used for the initial placement and every search move.
source
PlateMaps.platemaps_to_json — Method
platemaps_to_json(pms::Vector{PlateMap}) -> String

Serialize a multi-plate schedule to a single JSON string: a __type__-tagged "PlateMapBatch" wrapper around each plate's own JSON.lower form, matching the tagged-Dict convention every other JSON shape in this package uses. Deliberately not a JSON.lower(::Vector{PlateMap}) method, so an unrelated Vector{PlateMap} nested inside some other JSON structure isn't silently reshaped by it.

See also: json_to_platemaps

source
PlateMaps.role_neighbors — Method
role_neighbors(edges) -> Dict{Any,Dict{Symbol,Set{Any}}}

For every node, the set of other nodes it shares a same-role edge with, grouped by role – the adjacency structure total_distance needs to find each node's nearest same-role neighbor.

source
PlateMaps.run_compactness — Method
run_compactness(pos, edges, run_nodes) -> Real

For every edge-connected component (see components) with 2 or more run_nodes members, sum each member's Manhattan distance to the component's centroid (mean row, mean col of its members' pos). Components with fewer than 2 run-node members (a run with no edges, or whose whole component has no other run) contribute 0 – there is nothing to be compact relative to. Used by place_runs to cluster edge-linked runs together before control placement, so a run's specific control isn't left stranded far from it.

source
PlateMaps.scale — Method
scale(x,lb,ub) -> Real

Normalize x into [0,1] given known bounds lb/ub (matches PlateArrays' identically-named helper).

source
PlateMaps.schedule_platemap — Method
schedule_platemap(wells::BitMatrix, edges, fixed_nodes; plate_solver::String="greedy",
                   solver::String="exchange", run_restarts::Int=10, run_iterations::Int=1000,
                   rng::AbstractRNG=Random.default_rng(), kwargs...) -> Vector{PlateMap}

Schedule edges/fixed_nodes across one or more identically-shaped wells plates. Edge-connected components (see components) are atomic – each one lands wholly on a single plate, never split – so the first stage is a bin-packing problem (which components share which plate, minimizing plate count), and the second stage is _schedule_single_plate's existing two-stage placement, run once per plate on that plate's subset of edges/fixed_nodes.

Returns one PlateMap per plate actually used (never zero – an entirely empty edges/fixed_nodes input still returns a 1-element vector holding one empty PlateMap).

Arguments

  • wells: active-well mask, shared by every plate (this package doesn't support heterogeneous plate shapes in one call).
  • edges: an iterable of (node1,node2,role,metadata)-shaped NamedTuples (see mkedge).
  • fixed_nodes: the nodes to place first on their plate, before any optimization (typically "runs").

Keyword Arguments

  • plate_solver: "greedy" (default, first-fit-decreasing, exact for uniform component sizes) or "MILP" (exact minimum plate count) – see plate_solvers/_plate_assign_greedy/ _plate_assign_MILP. Throws ArgumentError if any single component is larger than one plate's active-well count.
  • solver: "exchange" (default, heuristic, supports every objective) or "MILP" (exact, objective=:distance only – see _milp_solve's docstring for why, and its bipartite requirement on edges).
  • run_restarts/run_iterations/rng: passed to place_runs to fix fixed_nodes' positions on each plate (kept separate from the control solver's own restarts/iterations below).
  • other keywords (objective, minimize, restarts, iterations, lambda, timelimit, optimizer, ...) are forwarded to the chosen solver (and, for plate_solver="MILP", to _plate_assign_MILP too – optimizer/timelimit are shared across both MILP stages when both are selected).
source
PlateMaps.total_distance — Method
total_distance(pos, edges) -> Real

For every node, and every role it participates in, the minimum distance to any same-role neighbor, summed over every (node, role) pair – the core generalized "minimax"-style objective. This is not a sum over every edge: a node linked to several same-role neighbors is only scored by its distance to the closest one, matching PlateArrays' old distance_score_brute (which scored every active well's distance to its nearest positive/negative control) generalized to however many roles are present, rather than penalizing a node for being far from every same-role neighbor it has.

source
PlateMaps.well_position — Method
well_position(pm::PlateMap, node) -> Union{CartesianIndex,Nothing}

The well node occupies, or nothing if it isn't placed on pm. A linear scan (O(wells)) – plate sizes here are small enough (hundreds of wells) that a cached reverse index isn't worth the complexity.

source
PlateMaps.wells_from_locationkind — Function
wells_from_locationkind(kind)

Derive an active-well BitMatrix from a registered plate kind's shape. Provided by the CHESSCore package extension (using CHESSCore activates it).

source
RecipesBase.plot — Method
plot(pm::PlateMap; inactive="darkgray", empty_color="white", occupied_color="steelblue")

Basic layout visualization: inactive wells, empty active wells, and occupied wells in three flat colors, labeled by raw node identity where displayable. This is the dependency-free view – it has no notion of what an occupant is (role, relationships), since PlateMap doesn't store that. For a role-aware, colored-by-role plot, see the RunMaps extension's plot(pm, rm).

source