Skip to content

Container: accept lids via a Liddable mixin, centred on the top face#1160

Merged
BioCam merged 9 commits into
PyLabRobot:mainfrom
BioCam:generalize-lid-support
Jul 14, 2026
Merged

Container: accept lids via a Liddable mixin, centred on the top face#1160
BioCam merged 9 commits into
PyLabRobot:mainfrom
BioCam:generalize-lid-support

Conversation

@BioCam

@BioCam BioCam commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Plate was the only resource that could hold a lid, and placement was origin-aligned - so a lid that isn't the plate's exact footprint (a trough lid, or a slightly-undersized plate lid) sat in a corner.

Proposes a Liddable mixin shared by Plate and Container, with the lid centred on the parent's top face. (Where the mixin lives is open - it's small and easy to relocate.)

  • lid.py: Lid (moved from plate.py, re-exported for back-compat) + Liddable (.lid, has_lid(), centred get_lid_location, double-lid and minimum-size guards).
  • Plate(Liddable, ItemizedResource["Well"]) and Container(Liddable, Resource) - troughs, tubes, petri dishes and wells inherit lids; drop_resource / move_lid gate on Liddable.

Footprint-matched lids are byte-identical (verified across every with_lid plate); off-footprint lids now centre - sub-mm (the one rotated STAR lid test is re-pinned). Rotation alignment is left to the move/arm layer, so the resource stays geometry-only.

Tests: lid_tests.py (centred placement, container lids, guards, serialize round-trip); full suite, ruff and mypy clean.

🤖 Generated with Claude Code

@BioCam BioCam force-pushed the generalize-lid-support branch 2 times, most recently from 396a611 to 659b93d Compare July 8, 2026 21:32

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making lid a lazily computed property would really simplify this class

Comment thread pylabrobot/resources/lid.py Outdated
Comment on lines +122 to +123
else:
assert location is not None, "Location must be specified if resource is not a lid."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use value error not assert

BioCam added a commit to BioCam/pylabrobot that referenced this pull request Jul 10, 2026
…d location

`assert` statements are removed by the interpreter under `python -O`, so the
guard against a non-lid child arriving without a location silently vanished on
optimised runs and `None` was passed straight through to
`Resource.assign_child_resource`. Raise a `ValueError` so the check always
holds. Message and behaviour are otherwise unchanged.

Addresses a review comment on PR PyLabRobot#1160.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BioCam added a commit to BioCam/pylabrobot that referenced this pull request Jul 10, 2026
The lid is already a child resource, so tracking it in a separate `_lid` field
duplicated state that had to be kept in sync on every assign and unassign.
Compute `lid` from `children` on access, matching `ResourceHolder.resource` and
`PetriDishHolder.dish`, which hold a single child the same way.

Removes the `_lid` field, its writes in `assign_child_resource` and the `lid`
setter, and the `unassign_child_resource` override, which existed only to clear
the field. Unassigning the lid child directly now updates `has_lid()` on its own.

Also closes a state desync: `assign_child_resource` wrote `_lid` before calling
`super()`, so a rejected assignment (a naming conflict, a callback that raises)
left the parent reporting `has_lid()` as True with no lid among its children,
and every later lid assignment failed with "already has a lid".

`has_lid()` now scans the children rather than reading a field, costing a few
microseconds on a 384-well plate. Serialization is unchanged: `_lid` was never
serialized, and a round-tripped plate rebuilds its lid from `children`.

Addresses a review comment on PR PyLabRobot#1160.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BioCam and others added 9 commits July 13, 2026 10:00
Lid support lived only on `Plate`; a `Container` (trough, tube, petri dish, well) could not take a lid, and placement was origin-aligned so an off-footprint lid landed in a corner rather than centred.

This proposes a `Liddable` mixin as the shared home for lid behaviour on `Plate` and `Container`, and centres lid placement on the parent's top face. The mixin is one option for where this lives; it is small and easy to relocate.

- `resources/lid.py`: `Lid` (moved from `plate.py`, re-exported for back-compat) + the `Liddable` mixin (`_lid`, `lid`, `has_lid`, centred `get_lid_location`, assign/unassign, double-lid and size guards).
- `Plate(Liddable, ItemizedResource["Well"])` and `Container(Liddable, Resource)` so troughs, tubes, petri dishes and wells inherit lids.
- `drop_resource` and `move_lid` gate on `Liddable` instead of `Plate`.

Behaviour: footprint-matched lids (every existing lidded plate) are byte-identical at all rotations; off-footprint lids now centre (sub-mm, the rotated STAR lid test is re-pinned accordingly). Rotation alignment stays a move/arm concern, so the resource layer is geometry-only.

Tests: `lid_tests.py` covers centred placement, container lids, the double-lid and size guards, and a serialize round-trip; full suite, ruff and mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… just plates

The lid guard only fired when the well's parent was a lidded Plate, so aspirating or
dispensing directly from a lidded Container (e.g. a Trough) slipped through. Generalize it:
check the resource's own has_lid() and, for wells, an isinstance(parent, Liddable) parent.
The 96-head paths gain the same check on their Container branch. Error messages now name the
offending resource and say to remove the lid. Adds aspirate-with-lid tests for a Hamilton
trough and a petri dish, and a trough lid-centring test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lid guard only checked the resource and its direct parent, so a lid on
a grandparent or nested holder slipped through. Walk the full parent chain
in a shared `_lidded_ancestor` helper and route every aspirate/dispense
(incl. 96-head) check through `_check_no_lid`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d location

`assert` statements are removed by the interpreter under `python -O`, so the
guard against a non-lid child arriving without a location silently vanished on
optimised runs and `None` was passed straight through to
`Resource.assign_child_resource`. Raise a `ValueError` so the check always
holds. Message and behaviour are otherwise unchanged.

Addresses a review comment on PR PyLabRobot#1160.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lid is already a child resource, so tracking it in a separate `_lid` field
duplicated state that had to be kept in sync on every assign and unassign.
Compute `lid` from `children` on access, matching `ResourceHolder.resource` and
`PetriDishHolder.dish`, which hold a single child the same way.

Removes the `_lid` field, its writes in `assign_child_resource` and the `lid`
setter, and the `unassign_child_resource` override, which existed only to clear
the field. Unassigning the lid child directly now updates `has_lid()` on its own.

Also closes a state desync: `assign_child_resource` wrote `_lid` before calling
`super()`, so a rejected assignment (a naming conflict, a callback that raises)
left the parent reporting `has_lid()` as True with no lid among its children,
and every later lid assignment failed with "already has a lid".

`has_lid()` now scans the children rather than reading a field, costing a few
microseconds on a 384-well plate. Serialization is unchanged: `_lid` was never
serialized, and a round-tripped plate rebuilds its lid from `children`.

Addresses a review comment on PR PyLabRobot#1160.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PyLabRobot measures distances in millimetres by default, so distance names
carry no unit suffix. Rename the constant to match; value unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…card path

`Trash` is a `Container`, which the lid work made `Liddable`, so `move_lid(lid,
trash)` matched the "seat a lid on a Liddable" branch: it aimed at the centre of
the trash's top face and then tried to assign the lid as a child, which the
undersize guard rejected - after the backend drop had already run and the lid
had been unassigned. Match `Trash` ahead of every branch that places a resource
on its destination, in both the location and assignment chains, so a lid (or any
resource) sent to the trash is simply discarded, as it was before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry container

Moving lid handling into `Liddable` also moved the "a non-lid child must be
given a location" check onto `Container`, so troughs, tubes, wells and the trash
began rejecting `assign_child_resource(child)` calls that were valid before.
Only lid placement belongs in `Liddable`; drop the check there and keep it on
`Plate`, where it lived, raising `ValueError` rather than the previous `assert`
(which is stripped under `python -O`). Containers regain their prior behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mments

`unparented` fails the spell check; reword to `parentless`. Also trim the two
`Trash`-branch comments and the discard test's comment to the surrounding style.
No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BioCam BioCam force-pushed the generalize-lid-support branch from d516a4a to ad011ee Compare July 13, 2026 09:01
@BioCam BioCam merged commit ce32021 into PyLabRobot:main Jul 14, 2026
21 checks passed
@BioCam BioCam deleted the generalize-lid-support branch July 14, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants