Spaces:
Sleeping
Sleeping
BioOps Team
feat: BioOps Twin v1.0 — Enterprise-Grade Digital Twin for Lab Centrifuge Calibration
38ff7a1 | """Command Pattern implementation for centrifuge actions. | |
| Each command is a self-contained action object that can be executed, | |
| undone, and logged. The ``CommandInvoker`` maintains a history stack | |
| for traceability (Rule 6: Command Pattern). | |
| Architecture reference: | |
| Technical Architecture § "The Command Pattern for Laboratory Automation" | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from abc import ABC, abstractmethod | |
| from dataclasses import dataclass, field | |
| from typing import TYPE_CHECKING, Any | |
| if TYPE_CHECKING: | |
| from bioops.simulation_core.simulator import CentrifugeSimulator | |
| logger = logging.getLogger("bioops_twin.commands") | |
| # Physics constants (duplicated here to avoid circular imports) | |
| MAX_SAFE_RPM: int = 15_000 | |
| # --------------------------------------------------------------------------- | |
| # Abstract Command | |
| # --------------------------------------------------------------------------- | |
| class CentrifugeCommand(ABC): | |
| """Abstract base class for all centrifuge commands. | |
| Subclasses must implement ``execute()`` and ``undo()`` to support | |
| full command history and rollback. | |
| """ | |
| def execute(self, simulator: CentrifugeSimulator) -> str: | |
| """Execute the command on the simulator. | |
| Args: | |
| simulator: The centrifuge simulator instance. | |
| Returns: | |
| A human-readable status message. | |
| """ | |
| def undo(self, simulator: CentrifugeSimulator) -> str: | |
| """Reverse the command's effect on the simulator. | |
| Args: | |
| simulator: The centrifuge simulator instance. | |
| Returns: | |
| A human-readable status message. | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Concrete Commands | |
| # --------------------------------------------------------------------------- | |
| class SetRPMCommand(CentrifugeCommand): | |
| """Set the centrifuge rotor to a target RPM. | |
| Stores the previous target for undo support. | |
| Args: | |
| rpm: Desired rotations per minute (0–15 000). | |
| """ | |
| def __init__(self, rpm: int) -> None: | |
| self.rpm = rpm | |
| self._previous_rpm: int = 0 | |
| def execute(self, simulator: CentrifugeSimulator) -> str: | |
| self._previous_rpm = simulator.target_rpm | |
| return simulator._handle_set_rpm(self.rpm) | |
| def undo(self, simulator: CentrifugeSimulator) -> str: | |
| logger.info("Undo SetRPM: reverting to %d RPM", self._previous_rpm) | |
| return simulator._handle_set_rpm(self._previous_rpm) | |
| class StopCommand(CentrifugeCommand): | |
| """Immediately stop the centrifuge rotor.""" | |
| def __init__(self) -> None: | |
| self._previous_rpm: int = 0 | |
| def execute(self, simulator: CentrifugeSimulator) -> str: | |
| self._previous_rpm = simulator.target_rpm | |
| return simulator._handle_stop() | |
| def undo(self, simulator: CentrifugeSimulator) -> str: | |
| logger.info("Undo Stop: resuming at %d RPM", self._previous_rpm) | |
| return simulator._handle_set_rpm(self._previous_rpm) | |
| class ResetCommand(CentrifugeCommand): | |
| """Reset the centrifuge from an error state to STANDBY.""" | |
| def __init__(self) -> None: | |
| self._previous_state: str = "" | |
| def execute(self, simulator: CentrifugeSimulator) -> str: | |
| self._previous_state = simulator.state.value | |
| return simulator._handle_reset() | |
| def undo(self, simulator: CentrifugeSimulator) -> str: | |
| logger.info("Undo Reset: (no-op, cannot revert to error state)") | |
| return "ℹ️ Reset cannot be undone for safety reasons." | |
| # --------------------------------------------------------------------------- | |
| # Command Invoker (history manager) | |
| # --------------------------------------------------------------------------- | |
| class CommandInvoker: | |
| """Manages execution and history of centrifuge commands. | |
| Maintains a LIFO stack of executed commands for undo support | |
| and a full log for traceability. | |
| Attributes: | |
| history: Stack of executed commands (most recent last). | |
| log: Chronological list of (command_class_name, result) tuples. | |
| """ | |
| history: list[CentrifugeCommand] = field(default_factory=list) | |
| log: list[tuple[str, str]] = field(default_factory=list) | |
| def execute( | |
| self, | |
| command: CentrifugeCommand, | |
| simulator: CentrifugeSimulator, | |
| ) -> str: | |
| """Execute a command and push it onto the history stack. | |
| Args: | |
| command: The command object to execute. | |
| simulator: The centrifuge simulator instance. | |
| Returns: | |
| The command's result message. | |
| """ | |
| result = command.execute(simulator) | |
| self.history.append(command) | |
| self.log.append((type(command).__name__, result)) | |
| logger.info("Executed %s -> %s", type(command).__name__, result) | |
| return result | |
| def undo_last(self, simulator: CentrifugeSimulator) -> str: | |
| """Undo the most recent command. | |
| Args: | |
| simulator: The centrifuge simulator instance. | |
| Returns: | |
| The undo result message, or an info message if history is empty. | |
| """ | |
| if not self.history: | |
| return "ℹ️ No commands to undo." | |
| command = self.history.pop() | |
| result = command.undo(simulator) | |
| self.log.append((f"Undo({type(command).__name__})", result)) | |
| return result | |
| # --------------------------------------------------------------------------- | |
| # Factory: JSON -> Command | |
| # --------------------------------------------------------------------------- | |
| def command_from_json(data: dict[str, Any]) -> CentrifugeCommand | None: | |
| """Create a command object from a parsed JSON dict. | |
| Expected schema:: | |
| {"action": "set_rpm" | "stop" | "reset", "value": <int>} | |
| Args: | |
| data: Parsed JSON with ``action`` and optional ``value``. | |
| Returns: | |
| A command instance or ``None`` if the action is unknown. | |
| """ | |
| action = data.get("action", "") | |
| if action == "set_rpm": | |
| return SetRPMCommand(rpm=int(data.get("value", 0))) | |
| if action == "stop": | |
| return StopCommand() | |
| if action == "reset": | |
| return ResetCommand() | |
| return None | |