Source code for AFL.automation.prepare.VirtualOT2HTTPDriver

import uuid
from AFL.automation.APIServer.Driver import Driver
from AFL.automation.shared.utilities import listify
from AFL.automation.prepare.OT2HTTPDriver import OT2HTTPDriver, TIPRACK_WELLS


[docs] class VirtualOT2HTTPDriver(OT2HTTPDriver): """In-memory mock of :class:`OT2HTTPDriver` for CI testing. This driver inherits from :class:`OT2HTTPDriver` but overrides all network-facing functionality. Calls simply update internal state and log actions, allowing test suites to exercise OT2-dependent code without requiring hardware or the HTTP server. """
[docs] def __init__(self, overrides=None): super().__init__(overrides=overrides) self.name = "VirtualOT2HTTPDriver" self.reset()
# ------------------------------------------------------------------ # Internal helpers def _initialize_robot(self): """Skip hardware initialization and prepare empty state.""" self.pipette_info = {} self.available_tips = {} self.min_transfer = None self.max_transfer = None self.log_info("Initialized virtual OT2 robot") def _update_pipettes(self): """Update cached pipette info from loaded instruments.""" self.min_transfer = None self.max_transfer = None for mount, info in self.pipette_info.items(): if not info: continue min_v = info.get("min_volume", 1) max_v = info.get("max_volume", 1000) if self.min_transfer is None or self.min_transfer > min_v: self.min_transfer = min_v if self.max_transfer is None or self.max_transfer < max_v: self.max_transfer = max_v def _generate_id(self, prefix: str) -> str: return f"{prefix}_{uuid.uuid4().hex[:8]}" # ------------------------------------------------------------------
[docs] def reset(self): """Reset all stored state.""" self.session_id = "virtual" self.protocol_id = "virtual" self.loaded_labware = {} self.loaded_instruments = {} self.loaded_modules = {} self.available_tips = {} self.pipette_info = {} self.has_tip = False self.last_pipette = None self.log_info("Virtual OT2 reset")
# ------------------------------------------------------------------
[docs] @Driver.quickbar(qb={"button_text": "Home"}) def home(self, **kwargs): self.log_info("Virtual home executed")
[docs] def load_labware(self, name, slot, module=None, **kwargs): labware_id = self._generate_id("labware") definition = { "definition": { "wells": {w: {} for w in TIPRACK_WELLS}, "metadata": {"displayName": name}, } } self.loaded_labware[str(slot)] = (labware_id, name, definition) self.log_info(f"Loaded labware {name} into slot {slot} with id {labware_id}") return labware_id
[docs] def load_instrument(self, name, mount, tip_rack_slots, **kwargs): pipette_id = self._generate_id("pipette") tip_racks = [self.loaded_labware[str(s)][0] for s in listify(tip_rack_slots)] self.loaded_instruments[mount] = { "name": name, "pipette_id": pipette_id, "tip_racks": tip_racks, } self.available_tips[mount] = [] for rack in tip_racks: for well in TIPRACK_WELLS: self.available_tips[mount].append((rack, well)) self.pipette_info[mount] = { "id": pipette_id, "name": name, "min_volume": 1, "max_volume": 1000, "aspirate_flow_rate": 150, "dispense_flow_rate": 300, } self._update_pipettes() self.log_info(f"Loaded instrument {name} on {mount} with id {pipette_id}") return pipette_id
# ------------------------------------------------------------------
[docs] def pick_up_tip(self, mount): if self.has_tip: self.log_warning("Tip already picked up") return if mount not in self.available_tips or not self.available_tips[mount]: raise RuntimeError(f"No tips available on {mount} mount") tiprack_id, well = self.available_tips[mount].pop(0) self.has_tip = True self.last_pipette = mount self.log_info(f"Picked up tip from {tiprack_id} well {well} on {mount}")
[docs] def drop_tip(self, mount): if not self.has_tip: self.log_warning("No tip to drop") return self.has_tip = False self.log_info(f"Dropped tip from {mount}")
[docs] @Driver.quickbar( qb={ "button_text": "Transfer", "params": { "source": {"label": "Source Well", "type": "text", "default": "1A1"}, "dest": {"label": "Dest Well", "type": "text", "default": "1A1"}, "volume": {"label": "Volume (uL)", "type": "float", "default": 300}, }, } ) def transfer(self, source, dest, volume, drop_tip=True, **kwargs): mount = next(iter(self.loaded_instruments.keys())) self.pick_up_tip(mount) self.log_info(f"Aspirating {volume}uL from {source}") self.log_info(f"Dispensing {volume}uL to {dest}") if drop_tip: self.drop_tip(mount)
if __name__ == "__main__": from AFL.automation.shared.launcher import *