from biohit_pipettor_plus.deck_structure.labware_classes.labware import Labware
from biohit_pipettor_plus.deck_structure.serializable import register_class
[docs]
@register_class
class IndividualPipetteHolder(Labware):
"""
Represents an individual pipette holder position within a PipetteHolder.
Tracks occupancy status and pipette type.
Attributes
----------
is_occupied : bool
Whether this holder position currently contains a pipette.
"""
def __init__(
self,
size_x: float,
size_y: float,
size_z: float,
offset: tuple[float, float] = (0, 0),
is_occupied: bool = True,
row: int = None,
column: int = None,
labware_id: str = None,
can_be_stacked_upon: bool = False,
position: tuple[float, float] = None
):
"""
Initialize an IndividualPipetteHolder instance.
Parameters
----------
size_x : float
Width of the individual holder position in millimeters.
size_y : float
Depth of the individual holder position in millimeters.
size_z : float
Height of the individual holder position in millimeters.
is_occupied : bool, optional
Whether a pipette is currently stored here. Default is False.
labware_id : str, optional
Unique identifier for this holder position. If None, a UUID will be generated.
position : tuple[float, float], optional
(x, y) absolute position coordinates in millimeters.
If None, position is not set.
"""
super().__init__(
size_x=size_x,
size_y=size_y,
size_z=size_z,
offset=offset,
labware_id=labware_id,
position=position,
can_be_stacked_upon=can_be_stacked_upon
)
self.is_occupied = is_occupied
self.row = row
self.column = column
[docs]
def place_pipette(self) -> None:
"""
Mark this holder position as occupied (pipette placed).
Raises ValueError If the holder position is already occupied.
"""
if self.is_occupied:
raise ValueError(
f"Holder position {self.labware_id} is already occupied"
)
self.is_occupied = True
[docs]
def remove_pipette(self) -> None:
"""
Mark this holder position as available (pipette removed).
Raises
------
ValueError
If the holder position is already empty.
"""
if not self.is_occupied:
raise ValueError(f"Holder position {self.labware_id} is already empty")
self.is_occupied = False
[docs]
def get_content_info(self) -> dict:
"""
Standardized content info for Pipette Holders.
Matches the return signature of Plate/Reservoir wells.
"""
return {
"is_occupied": self.is_occupied,
"status_text": "✓ Tip Present" if self.is_occupied else "✗ Empty",
}
[docs]
def is_available(self) -> bool:
"""
Check if this holder position is available for placing a pipette.
Returns bool.True if available (not occupied), False otherwise.
"""
return not self.is_occupied
[docs]
def get_state_snapshot(self) -> dict:
"""Return deep copy of mutable state"""
return {'is_occupied': self.is_occupied}
[docs]
def restore_state_snapshot(self, snapshot: dict) -> None:
"""Restore state from snapshot"""
self.is_occupied = snapshot['is_occupied']
[docs]
def to_dict(self) -> dict:
"""
Serialize the IndividualPipetteHolder to a dictionary.
"""
base = super().to_dict()
base.update({
"is_occupied": self.is_occupied,
"row": self.row,
"column": self.column,
})
return base
@classmethod
def _from_dict(cls, data: dict) -> "IndividualPipetteHolder":
"""
Deserialize an IndividualPipetteHolder instance from a dictionary.
Parameters
data : dict
Dictionary with IndividualPipetteHolder attributes.
Returns IndividualPipetteHolder
Reconstructed IndividualPipetteHolder instance.
"""
# Safely handle position deserialization
position = tuple(data["position"]) if data.get("position") else None
return cls(
size_x=data["size_x"],
size_y=data["size_y"],
size_z=data["size_z"],
offset=data["offset"],
labware_id=data["labware_id"],
can_be_stacked_upon=data.get("can_be_stacked_upon", False),
position=position,
is_occupied=data.get("is_occupied", False),
row=data.get("row"),
column=data.get("column"),
)