Movement & Occupancy
This page describes how locations move within a hierarchy.
CHESS already includes the location kinds Lab, Bench, and WP96. Its Incubator kind has shelves, so this page defines a simpler incubator that holds up to four plates directly:
julia> @location_kind SmallIncubator Symbol[] nothing nothing nothing nothing nothing 2//1 0//1
LocationKind(SmallIncubator)
julia> set_occupancy_cost!(:SmallIncubator, :WP96, 1//4)move_into!
move_into!(parent, child) builds and changes the hierarchy. It reassigns child's parent to parent, removing it from its previous parent.
julia> lab = build_location(loc"Lab", "Lab");
julia> bench = build_location(loc"Bench", "Bench");
julia> incubator = build_location(SmallIncubator, "Incubator");
julia> plate = build_location(loc"WP96", "Plate 1");
julia> move_into!(lab, bench)
julia> move_into!(lab, incubator)
julia> move_into!(bench, plate)parent(x) returns the parent of x, or nothing if x is at the root of its tree. children(x) returns its children.
julia> children(lab)
2-element Vector{Location}:
Bench
Incubator
julia> print(parent(plate))
BenchOver an experiment, a plate might start on a bench, move into an incubator overnight, and then move into a plate reader to be measured. Each is the same kind of event: a location moving into a new parent.
Moving the plate into the incubator:
julia> move_into!(incubator, plate)The tree changes accordingly.
Bench no longer has Plate as a child and Incubator does. Lab does not change, because the Lab/Plate relationship was never recorded. A move changes exactly one relationship, a location's parent, and everything nested inside that location moves with it.
The children confirm the change: bench has no children, incubator has plate, and lab's children are unchanged:
julia> children(bench)
Location[]
julia> children(incubator)
1-element Vector{Location}:
Plate 1
julia> children(lab)
2-element Vector{Location}:
Bench
IncubatorOccupancy and locking
Not every move is allowed. move_into! calls can_move_into(parent, child), which refuses a move for one of these reasons:
LockedLocationError:childisis_locked.AlreadyLocatedInError:childis already insideparent.OccupancyError: the move would over-fillparent.FixedMembershipError:parent's slots are fixed (LabwareorWell), orchildis aWell, which is permanently fused to itsLabware(see Locations).
Occupancy. A location's occupancy is a rational number from 0 to 1 describing how full it is. Every (parent kind, child kind) pair has an occupancy_cost, the fraction of the parent's capacity that one instance of the child consumes. move_into! refuses any move that would push the parent's occupancy plus that cost above 1. set_occupancy_cost! registers a cost. This is the rule registered at the top of this page, which lets the incubator hold up to four plates:
set_occupancy_cost!(:SmallIncubator, :WP96, 1//4) # holds up to four platesCosts are rational numbers so that floating-point rounding cannot make a location appear over-full or under-full. A cost greater than 1 blocks the move whatever the current occupancy, which rejects physically impossible pairings such as a bench into a well. A cost defaults to zero unless the kind's @location_kind declaration sets default_parent_cost/default_child_cost, or an exact or category-based rule is registered with set_occupancy_cost!.
The explicit rules are held in occupancy_rules, keyed by the names of the parent and child kinds. Change them with set_occupancy_cost! and not by editing the dictionary:
julia> occupancy_rules[(:SmallIncubator, :WP96)]
1//4The occupancy of a Labware or Well is always 1 (full), however many of its wells hold material. Their slots are fixed at construction, so no partial occupancy exists. The occupancy of a GenericLocation is derived from occupancy_cost.
Locking and activity. is_locked says whether a location can be moved out of its current parent. Children of a locked location can still be moved. is_active is a general on/off flag. lock!, unlock!, and toggle_lock! change the lock, and activate!, deactivate!, and toggle_activity! change the active flag.
julia> lock!(plate);
julia> move_into!(bench, plate)
ERROR: Locked Location Error with: Plate 1Checking a move before making it
can_move_into never returns false. It returns true or throws one of the four errors above. To check a move without performing it, call can_move_into and catch the error:
julia> try
can_move_into(bench, plate)
catch e
println("can't move: ", e)
end
can't move: LockedLocationError(Plate 1)Detaching a location
move_into!(nothing, child) removes a location from the hierarchy, leaving it with no parent:
julia> unlock!(plate);
julia> move_into!(nothing, plate)
julia> parent(plate) === nothing
trueQuerying the hierarchy
Four functions answer questions about the hierarchy:
ancestorsreturns the chain of parents of a location, from the nearest to the farthest. The keywordrev=truereverses the order.get_all_withinreturns every location of a given type inside a location, at any depth.children_namedreturns every direct child with a given name as a vector, which can be empty. Names are not unique. Indexing a location by name, as inplate["A1"], throws an error if no child or more than one child has the name, andchildren_namedleaves that case to the caller.childtypereturns the type of the locations that fill the slots of aLabware.
julia> move_into!(bench, plate)
julia> ancestors(plate)
3-element Vector{Location}:
Plate 1
Bench
Lab
julia> ancestors(plate; rev=true)
3-element Vector{Location}:
Lab
Bench
Plate 1
julia> get_all_within(lab, Labware)
1-element Vector{Labware}:
Plate 1
julia> length(get_all_within(lab, Well))
96
julia> children_named(lab, "Bench")
1-element Vector{Location}:
Bench
julia> children_named(lab, "Shelf")
Location[]
julia> childtype(plate)
WellEnvironmental Attributes & Inheritance describes the environment a location carries in addition to its place in the hierarchy.