Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions pylabrobot/resources/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ def compute_height_from_volume(v: float) -> float:

self.max_volume = max_volume or (size_x * size_y * size_z)
self.tracker = VolumeTracker(thing=f"{self.name}_volume_tracker", max_volume=self.max_volume)
# Notify state-update subscribers (e.g. the Visualizer) on volume changes; without this
# a bare Container like a Trough updates its volume internally but never broadcasts it.
self.tracker.register_callback(self._state_updated)
self._compute_volume_from_height = compute_volume_from_height
self._compute_height_from_volume = compute_height_from_volume
self.no_go_zones: List[Tuple[Coordinate, Coordinate]] = self._validate_no_go_zones(
Expand Down Expand Up @@ -147,10 +150,14 @@ def serialize(self) -> dict:
}

def serialize_state(self) -> Dict[str, Any]:
return self.tracker.serialize()
return {**super().serialize_state(), **self.tracker.serialize()}

def load_state(self, state: Dict[str, Any]):
self.tracker.load_state(state)
super().load_state(state)
# The tracker only consumes its own keys; extras (e.g. "rotation") are
# already handled by ``super().load_state``.
tracker_state = {k: v for k, v in state.items() if k != "rotation"}
self.tracker.load_state(tracker_state)

def supports_compute_height_volume_functions(self) -> bool:
return (
Expand Down
13 changes: 13 additions & 0 deletions pylabrobot/resources/container_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ def test_no_go_zones_multiple(self):
c = Container(name="c", size_x=10, size_y=142, size_z=10, no_go_zones=zones)
self.assertEqual(len(c.no_go_zones), 3)

def test_tracker_change_notifies_state_update_callbacks(self):
"""A bare Container (e.g. a Trough) must forward volume-tracker changes to its
state-update callbacks, so subscribers like the Visualizer see the level change and do
not show a frozen volume."""
c = Container(name="c", size_x=10, size_y=10, size_z=10, max_volume=1000)
received: list = []
c.register_state_update_callback(received.append)
c.tracker.set_volume(500)
c.tracker.remove_liquid(100)
c.tracker.commit()
self.assertGreaterEqual(len(received), 2) # at least set_volume and remove_liquid fired
self.assertEqual(c.tracker.get_used_volume(), 400)


class TestNoGoZoneCollision(unittest.TestCase):
def _make_container(self, size_y, no_go_zones=None):
Expand Down
19 changes: 16 additions & 3 deletions pylabrobot/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,9 @@ def rotate(self, x: float = 0, y: float = 0, z: float = 0):
self.rotation.x = (self.rotation.x + x) % 360
self.rotation.y = (self.rotation.y + y) % 360
self.rotation.z = (self.rotation.z + z) % 360
# Rotation is part of the resource's state; notify subscribers (e.g. the
# Visualizer) so they can re-render.
self._state_updated()

def copy(self) -> Self:
resource_copy = self.__class__.deserialize(self.serialize(), allow_marshal=True)
Expand Down Expand Up @@ -847,8 +850,13 @@ def serialize_state(self) -> Dict[str, Any]:

Use :meth:`pylabrobot.resources.resource.Resource.serialize_all_state` to serialize the state of
this resource and all children.

The base implementation includes ``"rotation"`` so that subscribers
(e.g. the Visualizer) are notified of orientation changes through the
standard state channel. Subclasses overriding this method should merge
in ``super().serialize_state()``.
"""
return {}
return {"rotation": self.rotation.serialize()}

# Developer note: you probably don't need to override this method. Instead, override
# `serialize_state`.
Expand All @@ -871,8 +879,13 @@ def serialize_all_state(self) -> Dict[str, Dict[str, Any]]:
# Developer note: this method deserializes the state of this resource only. If you want to
# deserialize a custom state for a resource, override this method in the subclass.
def load_state(self, state: Dict[str, Any]) -> None:
"""Load state for this resource only."""
# no state to load by default
"""Load state for this resource only.

The base implementation reads ``"rotation"`` if present. Subclasses
overriding this method should call ``super().load_state(state)``.
"""
if "rotation" in state:
self.rotation = deserialize(state["rotation"])

# Developer note: you probably don't need to override this method. Instead, override `load_state`.
def load_all_state(self, state: Dict[str, Dict[str, Any]]) -> None:
Expand Down
6 changes: 4 additions & 2 deletions pylabrobot/resources/tip_rack.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,12 @@ def make_tip(name: str) -> Tip:
)

def serialize_state(self) -> Dict[str, Any]:
return self.tracker.serialize()
return {**super().serialize_state(), **self.tracker.serialize()}

def load_state(self, state: Dict[str, Any]):
self.tracker.load_state(state)
super().load_state(state)
tracker_state = {k: v for k, v in state.items() if k != "rotation"}
self.tracker.load_state(tracker_state)

def get_identifier(self) -> str:
"""Get the (canonical) identifier, like `"A1"` of the tip spot in the parent tip rack. If the
Expand Down
1 change: 0 additions & 1 deletion pylabrobot/resources/tube.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def __init__(
height_volume_data=height_volume_data,
no_go_zones=no_go_zones,
)
self.tracker.register_callback(self._state_updated)

def serialize(self) -> dict:
return {**super().serialize(), "max_volume": self.max_volume}
Expand Down
2 changes: 0 additions & 2 deletions pylabrobot/resources/well.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ def __init__(
self.bottom_type = bottom_type
self.cross_section_type = cross_section_type

self.tracker.register_callback(self._state_updated)

def serialize(self):
return {
**super().serialize(),
Expand Down
Loading
Loading