Source code for biohit_pipettor_plus.deck_structure.slot

from typing import Dict, List
from biohit_pipettor_plus.deck_structure.labware_classes import *
from biohit_pipettor_plus.deck_structure.serializable import Serializable, register_class
from biohit_pipettor_plus.deck_structure.position import PositionAllocator

[docs] @register_class class Slot(Serializable): """ Represents a single slot on a Deck. A slot can hold multiple Labware objects stacked vertically with specified Z-ranges Attributes --------- range_x : tuple[float, float] Minimum and maximum x coordinates of the slot. range_y : tuple[float, float] Minimum and maximum y coordinates of the slot. range_z : float Maximum height of the slot. slot_id : str Unique identifier for the slot. labware_stack : dict[str, list[Labware, tuple[float, float]]] Dictionary mapping Labware IDs to a list containing the Labware object and its Z-range (min_z, max_z) within the slot. """ def __init__(self, range_x: tuple[float, float], range_y: tuple[float, float], range_z: float, slot_id: str) -> None: self.range_x = range_x self.range_y = range_y self.range_z = range_z self.slot_id = slot_id # Dictionary storing stacked labware: {labware_id: [Labware, (min_z, max_z)]} self.labware_stack: Dict[str, List[Labware, tuple[float, float]]] = {} def _place_labware(self, lw: Labware, min_z: float): """ Add a Labware object to the slot at a specific Z-range. Parameters ---------- lw : Labware Labware object to place. min_z : float Minimum Z coordinate within the slot for this Labware. Raises ------ ValueError If the Labware exceeds the slot's Z range. """ self.is_compatible_labware(lw=lw, min_z=min_z) max_z = min_z + lw.size_z self.labware_stack[lw.labware_id] = [lw, (min_z, max_z)] def _remove_labware(self, labware_id: str): """ Remove a specific Labware from the stack. Parameters ---------- labware_id : str ID of the Labware to remove. """ if labware_id in self.labware_stack: del self.labware_stack[labware_id] def _allocate_position( self, lw: Labware, ): """ if labware is placed in slot, x & y coordinates of the labware + offset is slot. """ if lw.labware_id not in self.labware_stack: raise ValueError("Labware not in labware stack") offset_x, offset_y = lw.offset x_corner = max(self.range_x[0], self.range_x[1]) - offset_x y_corner = min(self.range_y[0], self.range_y[1]) + offset_y #position is slot corner + offset of the labware. lw.position = (x_corner, y_corner) #if not none, then labware contains labware within them. Like ReservoirHolder - reservoirs, Plate - wells, pipetteHolder -IndividualPipetteHolder if hasattr(lw, "_rows") and hasattr(lw, "_columns"): if not isinstance(lw, (Plate, ReservoirHolder, PipetteHolder)): raise ValueError("Only (plate, reservoir, pipetteHolder) contain labware within them (wells, reservoirs, zone). Update the code for your labware") else: position_allocator = PositionAllocator() position_allocator.calculate_multi( lw, lw.position[0], lw.position[1], lw._rows, lw._columns, )
[docs] def is_compatible_labware(self, lw: Labware, min_z: float) -> None: """ Checks if a Labware object can be placed at the given Z position, raising a ValueError if any constraint (XY fit, Z range, stacking, or overlap) is violated. """ slot_width = abs(self.range_x[1] - self.range_x[0]) slot_depth = abs(self.range_y[1] - self.range_y[0]) max_z_new = min_z + lw.size_z # --- 1 & 2. Basic XY Fit & Z-Range Check (Remains the same) --- if lw.size_x > slot_width or lw.size_y > slot_depth: raise ValueError( f"Labware '{lw.labware_id}' (Size X:{lw.size_x}, Y:{lw.size_y}) " f"is too large for slot '{self.slot_id}' (Size X:{slot_width}, Y:{slot_depth})." ) if max_z_new > self.range_z: raise ValueError( f"Placement of Labware '{lw.labware_id}' at min_z={min_z} " f"exceeds the slot's total height capacity ({self.range_z})." ) # --- 3. Stacking Constraint Check (UPDATED LOGIC) --- if self.labware_stack: top_lw = None max_z_seen = -1.0 # Find the existing labware that occupies the highest Z-level. for existing_lw, (min_z_exist, max_z_exist) in self.labware_stack.values(): if max_z_exist > max_z_seen: max_z_seen = max_z_exist top_lw = existing_lw # Use getattr to safely check the property, defaulting to False # if the attribute is missing (which matches the default behavior of blocking stacking). can_stack_upon = getattr(top_lw, 'can_be_stacked_upon', False) if not can_stack_upon: # If can_be_stacked_upon is False, raise an error. raise ValueError( f"Stacking error in slot '{self.slot_id}': Labware '{top_lw.labware_id}' " f"is not designed to have other labware placed on top of it (can_be_stacked_upon=False)." ) # --- 4. Z-Overlap Check (Remains the same) --- for existing_lw, (min_z_exist, max_z_exist) in self.labware_stack.values(): if max_z_exist > min_z and min_z_exist < max_z_new: raise ValueError( f"Z-Overlap error in slot '{self.slot_id}': Placement range Z:[{min_z}, {max_z_new}] " f"overlaps with existing labware '{existing_lw.labware_id}' Z-range:[{min_z_exist}, {max_z_exist}]." ) # If all checks pass, the function returns silently (None). return
[docs] def get_highest_z(self) -> float: """Returns the maximum Z coordinate currently occupied in this slot.""" if not self.labware_stack: return 0.0 # Extract the max_z (index 1 of the tuple) from all items in the stack return max(zr[1] for _, zr in self.labware_stack.values())
[docs] def to_dict(self) -> dict: """ Serialize the Slot instance to a dictionary, including stacked labware and their Z-ranges. Returns ------- dict Dictionary representation of the slot. """ return { "class": self.__class__.__name__, "slot_id": self.slot_id, "range_x": list(self.range_x), "range_y": list(self.range_y), "range_z": self.range_z, "labware_stack": { lw_id: [lw.to_dict(), zr] for lw_id, (lw, zr) in self.labware_stack.items() } }
@classmethod def _from_dict(cls, data: dict) -> "Slot": """ Deserialize a Slot instance from a dictionary. Note: Labware objects should be populated by Deck after all labware is created. """ slot = cls( range_x=tuple(data["range_x"]), range_y=tuple(data["range_y"]), range_z=data["range_z"], slot_id=data["slot_id"] ) # ✅ Don't create labware here - just store the raw data temporarily slot._pending_labware_data = data.get("labware_stack", {}) return slot