Lid/temp-holder planner review

Goal

Decide the intended holder-allocation model, then fix branch saahas/lids-temp-planner without preserving accidental complexity.

Context

Branch commit e664744d55 adds two forms of capacity reuse:

  • a temperature-controlled holder may satisfy a plain well_plate_holder spec;
  • a plate-capable holder marked reserved_for_well_plate_lid may hold a plate when the protocol contains no lids.

The underlying invariant should be stated directly: at every point in the plan, plates and concurrently parked lids need a compatible matching to distinct holder slots. The current implementation approximates this with type substitution plus a protocol-global uses_lids boolean.

Review findings

Blocking

  1. Cross-type matches cannot be materialized. candidate_modules can match a WellPlateHolderSpec to TempWellPlateHolderProtoscript, but protospec/compiler.py:_merge_spec_fields then copies entity_type="well_plate_holder" into the temperature-holder model. Pydantic rejects the literal discriminator. Reproduced through both merge_spec_onto_module and merge_spec_fields_onto_physical.

    Likely fix: the physical module owns entity_type; never overwrite it during a spec merge. Cover this through compile_single_deck and full multi-deck planning, not only match_specs_to_modules.

  2. An active temperature block is not behaviorally substitutable for an ambient holder. _substitute_is_placeable admits is_on_at_start=True. Fleet inventory includes ml1-003 holders configured and preflight-checked at 4 °C. Unless cooling an otherwise ambient plate is intentional, substitution needs proof that the physical block is ambient/off; shared cooling loops may require an explicit capability rather than inferring this from is_on_at_start.

Should fix

  1. Lidless reclaim skips later-recruited storage decks. plan_instructions clears the flag only on initially assigned runtime_hardware. Spill selection evaluates recruitable_decks from untouched deck.hardware, and _recruit_storage_deck copies that untouched state. A valid overflow deck is therefore rejected when its only free compatible slot is a plate-capable lid holder.

  2. The relaxation erases an explicit reserved_for_well_plate_lid=True requirement. constraint_data_for_matching drops the field regardless of its value. Reproduced a deliberately lid-reserved spec preferentially matching an ordinary holder. If this mechanism remains, relaxation must be directional: ordinary (False) specs may claim physical True holders under the policy, but explicit True remains a constraint.

  3. reserved_for_well_plate_lid conflates physical capability, preference, and runtime allocation. It selects lid destinations, hides dedicated holders from generic placement/UI paths, and permits pose-less lid rests to omit plate home waypoints. _reclaim_lidless_holders then mutates the same field as if it were temporary allocation state. supported_well_plate_types being nonempty is not a canonical proof that plate waypoints exist.

  4. Matching policy depends on caller memory. uses_lids=True is a defaulted argument repeated across compiler, dry run, resolver, MILP, and candidate-deck paths. Production diagnostic calls omit the protocol context, so feasibility and explanations can disagree. If the boolean remains, make the context required and first-class.

Documentation

The task-planning knowledge page/log and the MILP module header should be corrected after the model is chosen. The current prose overstates round-trip safety and still describes capacity as exact-type/ignorant of compatibility.

Intent questions

  1. May a plain plate intentionally sit on a block actively held at 4 °C or 37 °C, or only on a provably inactive/ambient-safe temperature holder?
  2. Can a lid physically park on any empty holder, or are some positions uniquely calibrated/collision-safe for lids?
  3. For a lidded protocol, should the planner reserve every lid-marked holder for the whole run, or only the peak number of simultaneously parked lids?

Simplification fork

  • If any empty holder is lid-safe, remove reserved_for_well_plate_lid from allocation. Prefer bare lid-only rests, then dynamically match remaining lids and plates to compatible slots.
  • If only certain positions are lid-safe, retain an immutable capability or dedicated subtype, but keep protocol-time reservation in planner state rather than rewriting hardware.

Validation performed

  • bin/lint-diff.sh main: passed.
  • medra_robotics/tests/protoscript/test_protospec.py: 45 passed.
  • Direct reproductions confirmed the cross-type merge failure and the explicit-true constraint reversal.
  • Full planner suite and hardware validation were not run.

No fixes were applied during review.