API Reference
PlateMaps.NODE_TYPE_REGISTRYPlateMaps.plate_solversDataFrames.DataFramePlateMaps.PlacementErrorPlateMaps.PlateMapPlateMaps.PlateMapBase.getindexBase.iteratePlateMaps._component_groupsPlateMaps._exchange_solvePlateMaps._local_searchPlateMaps._milp_solvePlateMaps._neighboring_diskPlateMaps._plate_assign_MILPPlateMaps._plate_assign_greedyPlateMaps._schedule_single_platePlateMaps.componentsPlateMaps.describePlateMaps.edge_nodesPlateMaps.evennessPlateMaps.expected_marginsPlateMaps.hybridPlateMaps.json_to_platemapPlateMaps.json_to_platemapsPlateMaps.manhattan_distancePlateMaps.marginsPlateMaps.mkedgePlateMaps.node_rolesPlateMaps.nodesPlateMaps.place_runsPlateMaps.platemap_to_jsonPlateMaps.platemaps_from_dataframePlateMaps.platemaps_to_jsonPlateMaps.role_neighborsPlateMaps.rolesPlateMaps.run_compactnessPlateMaps.scalePlateMaps.schedule_platemapPlateMaps.total_distancePlateMaps.total_evennessPlateMaps.well_positionPlateMaps.wells_from_locationkindRecipesBase.plot
Full docstrings
DataFrames.DataFrame — Method
DataFrame(pms::Vector{PlateMap}) -> DataFrameCompile 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.
PlateMaps.PlacementError — Type
struct PlacementError <: ExceptionThrown 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.
PlateMaps.PlateMap — Type
struct PlateMap{T}
wells::BitMatrix
occupant::Matrix{Union{Missing,T}}
endA 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).
PlateMaps.PlateMap — Method
PlateMap{T}(wells::AbstractMatrix) where TConstruct an empty PlateMap (no nodes placed) over the given active-well mask.
PlateMaps.NODE_TYPE_REGISTRY — Constant
NODE_TYPE_REGISTRYMaps 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.
PlateMaps.plate_solvers — Constant
plate_solversMaps a plate_solver string to the function that implements it: "greedy" => _plate_assign_greedy, "MILP" => _plate_assign_MILP. Selected via schedule_platemap's plate_solver keyword.
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".
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.
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).
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.
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).
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.
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).
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.
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.
PlateMaps._schedule_single_plate — Method
_schedule_single_plate(wells, edges, fixed_nodes; solver, run_restarts, run_iterations, rng, kwargs...)
-> PlateMapTwo-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.
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.
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.
PlateMaps.edge_nodes — Method
edge_nodes(edges) -> SetThe distinct nodes referenced by edges (either endpoint).
PlateMaps.evenness — Method
evenness(wells::BitMatrix,pos,role::Symbol,edges) -> RealSum 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.
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.
PlateMaps.hybrid — Method
hybrid(wells,pos,edges;lambda=0.5,lb_dist=0,ub_dist=1,lb_even=0,ub_even=1) -> RealWeighted 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.
PlateMaps.json_to_platemap — Method
json_to_platemap(s::AbstractString) -> PlateMapParse a JSON string (produced by platemap_to_json) back into a PlateMap. The node-identity type is reconstructed via NODE_TYPE_REGISTRY.
PlateMaps.json_to_platemaps — Method
json_to_platemaps(s::AbstractString) -> Vector{PlateMap}Parse a JSON string (produced by platemaps_to_json) back into a Vector{PlateMap}.
PlateMaps.manhattan_distance — Method
manhattan_distance(p1::CartesianIndex,p2::CartesianIndex) -> IntL1 distance between two well positions.
PlateMaps.margins — Method
margins(wells::BitMatrix) -> (rowsums, colsums)Active-well counts per row and per column.
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.
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).
PlateMaps.nodes — Method
nodes(pm::PlateMap{T}) where T -> Vector{T}All nodes currently placed on pm, in no particular order.
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 ownrestarts/iterations.rng: random number generator used for the initial placement and every search move.
PlateMaps.platemap_to_json — Method
platemap_to_json(pm::PlateMap) -> StringSerialize a PlateMap to a JSON string.
See also: json_to_platemap
PlateMaps.platemaps_from_dataframe — Method
platemaps_from_dataframe(df::DataFrame) -> Vector{PlateMap}Reverse of DataFrame(pms::Vector{PlateMap}): splits df on its required plate column (sorted ascending) and reconstructs each plate via the existing PlateMap(df) on that group's well,row,col,active,occupant columns.
PlateMaps.platemaps_to_json — Method
platemaps_to_json(pms::Vector{PlateMap}) -> StringSerialize 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
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.
PlateMaps.roles — Method
roles(edges) -> Set{Symbol}The distinct roles referenced by edges.
PlateMaps.run_compactness — Method
run_compactness(pos, edges, run_nodes) -> RealFor 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.
PlateMaps.scale — Method
scale(x,lb,ub) -> RealNormalize x into [0,1] given known bounds lb/ub (matches PlateArrays' identically-named helper).
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)-shapedNamedTuples (seemkedge).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) – seeplate_solvers/_plate_assign_greedy/_plate_assign_MILP. ThrowsArgumentErrorif any single component is larger than one plate's active-well count.solver:"exchange"(default, heuristic, supports everyobjective) or"MILP"(exact,objective=:distanceonly – see_milp_solve's docstring for why, and its bipartite requirement onedges).run_restarts/run_iterations/rng: passed toplace_runsto fixfixed_nodes' positions on each plate (kept separate from the control solver's ownrestarts/iterationsbelow).- other keywords (
objective,minimize,restarts,iterations,lambda,timelimit,optimizer, ...) are forwarded to the chosensolver(and, forplate_solver="MILP", to_plate_assign_MILPtoo –optimizer/timelimitare shared across both MILP stages when both are selected).
PlateMaps.total_distance — Method
total_distance(pos, edges) -> RealFor 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.
PlateMaps.total_evenness — Method
total_evenness(wells::BitMatrix,pos,edges) -> Realevenness summed over every distinct role present in edges.
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.
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).
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).