From be0562528df41b4c63967722979770df899fed23 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Thu, 13 Feb 2025 18:07:48 -0800 Subject: [PATCH 1/3] different plate types on cytation5 --- pylabrobot/plate_reading/backend.py | 6 +- pylabrobot/plate_reading/biotek_backend.py | 94 ++++++++++++++++++---- pylabrobot/plate_reading/clario_star.py | 5 +- pylabrobot/plate_reading/imager.py | 31 ++++--- pylabrobot/plate_reading/plate_reader.py | 13 ++- pylabrobot/plate_reading/standard.py | 4 + 6 files changed, 112 insertions(+), 41 deletions(-) diff --git a/pylabrobot/plate_reading/backend.py b/pylabrobot/plate_reading/backend.py index fc9d86691f3..2274109199d 100644 --- a/pylabrobot/plate_reading/backend.py +++ b/pylabrobot/plate_reading/backend.py @@ -5,6 +5,7 @@ from pylabrobot.machines.backends import MachineBackend from pylabrobot.plate_reading.standard import Exposure, FocalPosition, Gain, ImagingMode +from pylabrobot.resources.plate import Plate class PlateReaderBackend(MachineBackend, metaclass=ABCMeta): @@ -28,12 +29,12 @@ async def close(self) -> None: """Close the plate reader. Also known as plate in.""" @abstractmethod - async def read_luminescence(self, focal_height: float) -> List[List[float]]: + async def read_luminescence(self, focal_height: float, plate: Plate) -> List[List[float]]: """Read the luminescence from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" @abstractmethod - async def read_absorbance(self, wavelength: int) -> List[List[float]]: + async def read_absorbance(self, wavelength: int, plate: Plate) -> List[List[float]]: """Read the absorbance from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" @@ -43,6 +44,7 @@ async def read_fluorescence( excitation_wavelength: int, emission_wavelength: int, focal_height: float, + plate: Plate, ) -> List[List[float]]: """Read the fluorescence from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" diff --git a/pylabrobot/plate_reading/biotek_backend.py b/pylabrobot/plate_reading/biotek_backend.py index a1c6d373e37..9aaf56cca05 100644 --- a/pylabrobot/plate_reading/biotek_backend.py +++ b/pylabrobot/plate_reading/biotek_backend.py @@ -4,6 +4,8 @@ import time from typing import List, Literal, Optional +from pylabrobot.resources.plate import Plate + try: from pylibftdi import Device @@ -137,10 +139,16 @@ async def setup(self, use_cam: bool = False) -> None: logger.info("[cytation5] setting up") self.dev.open() - # self.dev.baudrate = 9600 # worked in the past - self.dev.baudrate = 38400 + self.dev.ftdi_fn.ftdi_usb_reset() + self.dev.ftdi_fn.ftdi_set_latency_timer(16) # 0x10 + + self.dev.baudrate = 9600 # 0x38 0x41 + # self.dev.baudrate = 38400 self.dev.ftdi_fn.ftdi_set_line_property(8, 2, 0) # 8 bits, 2 stop bits, no parity SIO_RTS_CTS_HS = 0x1 << 8 + self.dev.ftdi_fn.ftdi_setdtr(1) + self.dev.ftdi_fn.ftdi_setrts(1) + self.dev.ftdi_fn.ftdi_setflowctrl(SIO_RTS_CTS_HS) self.dev.ftdi_fn.ftdi_setrts(1) @@ -265,31 +273,36 @@ async def _read_until(self, char: bytes, timeout: Optional[float] = None) -> byt return res async def send_command( - self, command: str, parameter: Optional[str] = None, wait_for_response=True + self, + command: str, + parameter: Optional[str] = None, + wait_for_response=True, + timeout: Optional[float] = None, ) -> Optional[bytes]: await self._purge_buffers() self.dev.write(command.encode()) logger.debug("[cytation5] sent %s", command) response: Optional[bytes] = None if wait_for_response or parameter is not None: - # print("reading until", b"\x06" if parameter is not None else b"\x03") - response = await self._read_until(b"\x06" if parameter is not None else b"\x03") + response = await self._read_until( + b"\x06" if parameter is not None else b"\x03", timeout=timeout + ) if parameter is not None: self.dev.write(parameter.encode()) logger.debug("[cytation5] sent %s", parameter) if wait_for_response: - response = await self._read_until(b"\x03") + response = await self._read_until(b"\x03", timeout=timeout) return response async def get_serial_number(self) -> str: - resp = await self.send_command("C") + resp = await self.send_command("C", timeout=1) assert resp is not None return resp[1:].split(b" ")[0].decode() async def get_firmware_version(self) -> str: - resp = await self.send_command("e") + resp = await self.send_command("e", timeout=1) assert resp is not None return " ".join(resp[1:-1].decode().split(" ")[0:4]) @@ -323,10 +336,58 @@ def _parse_body(self, body: bytes) -> List[List[float]]: parsed_data[row_idx].append(value) return parsed_data - async def read_absorbance(self, wavelength: int) -> List[List[float]]: + async def _define_plate(self, plate: Plate): + """ + 08120112207434014351135308559127881422 + ^^^^ plate size z + ^^^^^ plate size x + ^^^^^ plate size y + ^^^^^ bottom right x + ^^^^^ top left x + ^^^^^ bottom right y + ^^^^^ top left y + ^^ columns + ^^ rows + """ + + rows = plate.num_items_y + columns = plate.num_items_x + + bottom_right_well = plate.get_item(plate.num_items - 1) + bottom_right_well_center = bottom_right_well.location + bottom_right_well.get_anchor( + x="c", y="c" + ) + top_left_well = plate.get_item(0) + top_left_well_center = top_left_well.location + top_left_well.get_anchor(x="c", y="c") + + plate_size_y = plate.get_size_y() + plate_size_x = plate.get_size_x() + plate_size_z = plate.get_size_z() + + top_left_well_center_y = plate.get_size_y() - top_left_well_center.y # invert y axis + bottom_right_well_center_y = plate.get_size_y() - bottom_right_well_center.y # invert y axis + + cmd = ( + f"{rows:02}" + f"{columns:02}" + f"{int(top_left_well_center_y*100):05}" + f"{int(bottom_right_well_center_y*100):05}" + f"{int(top_left_well_center.x*100):05}" + f"{int(bottom_right_well_center.x*100):05}" + f"{int(plate_size_y*100):05}" + f"{int(plate_size_x*100):05}" + f"{int(plate_size_z*100):04}" + "\x03" + ) + + return await self.send_command("y", cmd) + + async def read_absorbance(self, wavelength: int, plate: Plate) -> List[List[float]]: if not 230 <= wavelength <= 999: raise ValueError("Wavelength must be between 230 and 999") + await self._define_plate(plate) + await self.send_command("y", "08120112207434014351135308559127881772\x03") wavelength_str = str(wavelength).zfill(4) @@ -343,10 +404,12 @@ async def read_absorbance(self, wavelength: int) -> List[List[float]]: assert resp is not None return self._parse_body(body) - async def read_luminescence(self, focal_height: float) -> List[List[float]]: + async def read_luminescence(self, focal_height: float, plate: Plate) -> List[List[float]]: if not 4.5 <= focal_height <= 13.88: raise ValueError("Focal height must be between 4.5 and 13.88") + await self._define_plate(plate) + cmd = f"3{14220 + int(1000*focal_height)}\x03" await self.send_command("t", cmd) @@ -367,6 +430,7 @@ async def read_fluorescence( excitation_wavelength: int, emission_wavelength: int, focal_height: float, + plate: Plate, ) -> List[List[float]]: if not 4.5 <= focal_height <= 13.88: raise ValueError("Focal height must be between 4.5 and 13.88") @@ -375,6 +439,8 @@ async def read_fluorescence( if not 250 <= emission_wavelength <= 700: raise ValueError("Emission wavelength must be between 250 and 700") + await self._define_plate(plate) + cmd = f"{614220 + int(1000*focal_height)}\x03" await self.send_command("t", cmd) @@ -512,14 +578,14 @@ async def led_off(self): async def set_focus(self, focal_position: FocalPosition): """focus position in mm""" - if focal_position == self._focal_height: - logger.debug("Focus position is already set to %s", focal_position) - return - if focal_position == "auto": await self.auto_focus() return + if focal_position == self._focal_height: + logger.debug("Focus position is already set to %s", focal_position) + return + # There is a difference between the number in the program and the number sent to the machine, # which is modelled using the following linear relation. R^2=0.999999999 # convert from mm to um diff --git a/pylabrobot/plate_reading/clario_star.py b/pylabrobot/plate_reading/clario_star.py index 50184c3ce26..ac0367b7feb 100644 --- a/pylabrobot/plate_reading/clario_star.py +++ b/pylabrobot/plate_reading/clario_star.py @@ -8,6 +8,7 @@ from typing import List, Optional, Union from pylabrobot import utils +from pylabrobot.resources.plate import Plate from .backend import PlateReaderBackend @@ -261,7 +262,7 @@ async def _status_hw(self): async def _get_measurement_values(self): return await self.send(b"\x02\x00\x0f\x0c\x05\x02\x00\x00\x00\x00\x00\x00") - async def read_luminescence(self, focal_height: float = 13) -> List[List[float]]: + async def read_luminescence(self, plate: Plate, focal_height: float = 13) -> List[List[float]]: """Read luminescence values from the plate reader.""" await self._mp_and_focus_height_value() @@ -293,6 +294,7 @@ async def read_luminescence(self, focal_height: float = 13) -> List[List[float]] async def read_absorbance( self, wavelength: int, + plate: Plate, report: Literal["OD", "transmittance"] = "OD", ) -> List[List[float]]: """Read absorbance values from the device. @@ -357,5 +359,6 @@ async def read_fluorescence( excitation_wavelength: int, emission_wavelength: int, focal_height: float, + plate: Plate, ) -> List[List[float]]: raise NotImplementedError("Not implemented yet") diff --git a/pylabrobot/plate_reading/imager.py b/pylabrobot/plate_reading/imager.py index 9953bb9700c..ade1285c519 100644 --- a/pylabrobot/plate_reading/imager.py +++ b/pylabrobot/plate_reading/imager.py @@ -2,7 +2,13 @@ from pylabrobot.machines import Machine from pylabrobot.plate_reading.backend import ImagerBackend -from pylabrobot.plate_reading.standard import Exposure, FocalPosition, Gain, ImagingMode +from pylabrobot.plate_reading.standard import ( + Exposure, + FocalPosition, + Gain, + ImagingMode, + NoPlateError, +) from pylabrobot.resources import Plate, Resource, Well @@ -30,25 +36,19 @@ def __init__( ) Machine.__init__(self, backend=backend) self.backend: ImagerBackend = backend # fix type - self.plate: Optional[Plate] = None - self.register_did_unassign_resource_callback(self._did_unassign_resource) - self.register_did_assign_resource_callback(self._did_assign_resource) self.register_will_assign_resource_callback(self._will_assign_resource) def _will_assign_resource(self, resource: Resource): - if self.plate is not None: + if len(self.children) >= 1: raise ValueError( - f"Imager {self} already has a plate assigned " f"(attemping to assign {resource})" + f"Imager {self} already has a plate assigned " f"(attempting to assign {resource})" ) - def _did_assign_resource(self, resource: Resource): - if isinstance(resource, Plate): - self.plate = resource - - def _did_unassign_resource(self, resource: Resource): - if resource == self.plate: - self.plate = None + def get_plate(self) -> Plate: + if len(self.children) == 0: + raise NoPlateError("There is no plate in the plate reader.") + return cast(Plate, self.children[0]) async def capture( self, @@ -62,11 +62,9 @@ async def capture( if isinstance(well, tuple): row, column = well else: - if self.plate is None: - raise ValueError(f"Imager {self} has no plate assigned") idx = cast(Plate, well.parent).index_of_item(well) if idx is None: - raise ValueError(f"Well {well} not in plate {self.plate}") + raise ValueError(f"Well {well} not in plate {well.parent}") row, column = divmod(idx, cast(Plate, well.parent).num_items_x) return await self.backend.capture( @@ -76,5 +74,6 @@ async def capture( exposure_time=exposure_time, focal_height=focal_height, gain=gain, + plate=self.get_plate(), **backend_kwargs, ) diff --git a/pylabrobot/plate_reading/plate_reader.py b/pylabrobot/plate_reading/plate_reader.py index 116e522bdba..98d40cf3622 100644 --- a/pylabrobot/plate_reading/plate_reader.py +++ b/pylabrobot/plate_reading/plate_reader.py @@ -2,14 +2,11 @@ from pylabrobot.machines.machine import Machine, need_setup_finished from pylabrobot.plate_reading.backend import PlateReaderBackend +from pylabrobot.plate_reading.standard import NoPlateError from pylabrobot.resources import Coordinate, Plate, Resource from pylabrobot.resources.resource_holder import ResourceHolder -class NoPlateError(Exception): - pass - - class PlateReader(ResourceHolder, Machine): """The front end for plate readers. Plate readers are devices that can read luminescence, absorbance, or fluorescence from a plate. @@ -66,11 +63,11 @@ def get_plate(self) -> Plate: raise NoPlateError("There is no plate in the plate reader.") return cast(Plate, self.children[0]) - async def open(self) -> None: - await self.backend.open() + async def open(self, **backend_kwargs) -> None: + await self.backend.open(**backend_kwargs) - async def close(self) -> None: - await self.backend.close() + async def close(self, **backend_kwargs) -> None: + await self.backend.close(**backend_kwargs) @need_setup_finished async def read_luminescence(self, focal_height: float) -> List[List[float]]: diff --git a/pylabrobot/plate_reading/standard.py b/pylabrobot/plate_reading/standard.py index df7a3737538..2b93bf38683 100644 --- a/pylabrobot/plate_reading/standard.py +++ b/pylabrobot/plate_reading/standard.py @@ -10,6 +10,10 @@ class ImagingMode(enum.Enum): COLOR_BRIGHTFIELD = enum.auto() +class NoPlateError(Exception): + pass + + Exposure = Union[float, Literal["auto"]] FocalPosition = Union[float, Literal["auto"]] Gain = Union[float, Literal["auto"]] From b38e0a33ba46419f1480899635ebb90c06171866 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Thu, 13 Feb 2025 18:21:27 -0800 Subject: [PATCH 2/3] fixes --- .../backends/hamilton/STAR_tests.py | 6 +-- pylabrobot/plate_reading/backend.py | 7 +-- pylabrobot/plate_reading/biotek_backend.py | 33 +++++++------ pylabrobot/plate_reading/biotek_tests.py | 9 ++-- pylabrobot/plate_reading/chatterbox.py | 6 ++- pylabrobot/plate_reading/clario_star.py | 4 +- pylabrobot/plate_reading/plate_reader.py | 5 +- .../plate_reading/plate_reader_tests.py | 46 ++----------------- 8 files changed, 44 insertions(+), 72 deletions(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py index 67477be9269..5a4990d9421 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py @@ -5,9 +5,7 @@ from pylabrobot.liquid_handling import LiquidHandler from pylabrobot.liquid_handling.standard import GripDirection, Pickup from pylabrobot.plate_reading import PlateReader -from pylabrobot.plate_reading.plate_reader_tests import ( - MockPlateReaderBackend, -) +from pylabrobot.plate_reading.chatterbox import PlateReaderChatterboxBackend from pylabrobot.resources import ( HT, HTF, @@ -696,7 +694,7 @@ async def test_iswap(self): async def test_iswap_plate_reader(self): plate_reader = PlateReader( name="plate_reader", - backend=MockPlateReaderBackend(), + backend=PlateReaderChatterboxBackend(), size_x=0, size_y=0, size_z=0, diff --git a/pylabrobot/plate_reading/backend.py b/pylabrobot/plate_reading/backend.py index 2274109199d..68bda360215 100644 --- a/pylabrobot/plate_reading/backend.py +++ b/pylabrobot/plate_reading/backend.py @@ -29,22 +29,22 @@ async def close(self) -> None: """Close the plate reader. Also known as plate in.""" @abstractmethod - async def read_luminescence(self, focal_height: float, plate: Plate) -> List[List[float]]: + async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]: """Read the luminescence from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" @abstractmethod - async def read_absorbance(self, wavelength: int, plate: Plate) -> List[List[float]]: + async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]: """Read the absorbance from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" @abstractmethod async def read_fluorescence( self, + plate: Plate, excitation_wavelength: int, emission_wavelength: int, focal_height: float, - plate: Plate, ) -> List[List[float]]: """Read the fluorescence from the plate reader. This should return a list of lists, where the outer list is the columns of the plate and the inner list is the rows of the plate.""" @@ -60,6 +60,7 @@ async def capture( exposure_time: Exposure, focal_height: FocalPosition, gain: Gain, + plate: Plate, ) -> List[List[float]]: """Capture an image of the plate in the specified mode.""" diff --git a/pylabrobot/plate_reading/biotek_backend.py b/pylabrobot/plate_reading/biotek_backend.py index 9aaf56cca05..ab7913d8d6f 100644 --- a/pylabrobot/plate_reading/biotek_backend.py +++ b/pylabrobot/plate_reading/biotek_backend.py @@ -128,6 +128,7 @@ def __init__(self, timeout: float = 20, camera_serial_number: Optional[float] = self.camera_serial_number = camera_serial_number self.max_image_read_attempts = 8 + self._plate: Optional[Plate] = None self._exposure: Optional[Exposure] = None self._focal_height: Optional[FocalPosition] = None self._gain: Optional[Gain] = None @@ -336,7 +337,7 @@ def _parse_body(self, body: bytes) -> List[List[float]]: parsed_data[row_idx].append(value) return parsed_data - async def _define_plate(self, plate: Plate): + async def set_plate(self, plate: Plate): """ 08120112207434014351135308559127881422 ^^^^ plate size z @@ -350,14 +351,19 @@ async def _define_plate(self, plate: Plate): ^^ rows """ + if plate is self._plate: + return + rows = plate.num_items_y columns = plate.num_items_x bottom_right_well = plate.get_item(plate.num_items - 1) + assert bottom_right_well.location is not None bottom_right_well_center = bottom_right_well.location + bottom_right_well.get_anchor( x="c", y="c" ) top_left_well = plate.get_item(0) + assert top_left_well.location is not None top_left_well_center = top_left_well.location + top_left_well.get_anchor(x="c", y="c") plate_size_y = plate.get_size_y() @@ -382,13 +388,11 @@ async def _define_plate(self, plate: Plate): return await self.send_command("y", cmd) - async def read_absorbance(self, wavelength: int, plate: Plate) -> List[List[float]]: + async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]: if not 230 <= wavelength <= 999: raise ValueError("Wavelength must be between 230 and 999") - await self._define_plate(plate) - - await self.send_command("y", "08120112207434014351135308559127881772\x03") + await self.set_plate(plate) wavelength_str = str(wavelength).zfill(4) cmd = f"00470101010812000120010000110010000010600008{wavelength_str}1" @@ -404,16 +408,14 @@ async def read_absorbance(self, wavelength: int, plate: Plate) -> List[List[floa assert resp is not None return self._parse_body(body) - async def read_luminescence(self, focal_height: float, plate: Plate) -> List[List[float]]: + async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]: if not 4.5 <= focal_height <= 13.88: raise ValueError("Focal height must be between 4.5 and 13.88") - await self._define_plate(plate) - cmd = f"3{14220 + int(1000*focal_height)}\x03" await self.send_command("t", cmd) - await self.send_command("y", "08120112207434014351135308559127881772\x03") + await self.set_plate(plate) cmd = "008401010108120001200100001100100000123000500200200-001000-00300000000000000000001351092" await self.send_command("D", cmd) @@ -427,10 +429,10 @@ async def read_luminescence(self, focal_height: float, plate: Plate) -> List[Lis async def read_fluorescence( self, + plate: Plate, excitation_wavelength: int, emission_wavelength: int, focal_height: float, - plate: Plate, ) -> List[List[float]]: if not 4.5 <= focal_height <= 13.88: raise ValueError("Focal height must be between 4.5 and 13.88") @@ -439,12 +441,10 @@ async def read_fluorescence( if not 250 <= emission_wavelength <= 700: raise ValueError("Emission wavelength must be between 250 and 700") - await self._define_plate(plate) - cmd = f"{614220 + int(1000*focal_height)}\x03" await self.send_command("t", cmd) - await self.send_command("y", "08120112207434014351135308559127881772\x03") + await self.set_plate(plate) excitation_wavelength_str = str(excitation_wavelength).zfill(4) emission_wavelength_str = str(emission_wavelength).zfill(4) @@ -600,6 +600,9 @@ async def set_focus(self, focal_position: FocalPosition): self._focal_height = focal_position async def auto_focus(self, timeout: float = 30): + plate = self._plate + if plate is None: + raise RuntimeError("Plate not set. Run set_plate() first.") imaging_mode = self._imaging_mode if imaging_mode is None: raise RuntimeError("Imaging mode not set. Run set_imaging_mode() first.") @@ -623,6 +626,7 @@ async def auto_focus(self, timeout: float = 30): # objective function: variance of laplacian async def evaluate_focus(focus_value): image = await self.capture( + plate=plate, row=row, column=column, mode=imaging_mode, @@ -835,6 +839,7 @@ async def capture( exposure_time: Exposure, focal_height: FocalPosition, gain: Gain, + plate: Plate, color_processing_algorithm: int = SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR, pixel_format: int = PixelFormat_Mono8, ) -> List[List[float]]: @@ -854,6 +859,8 @@ async def capture( if self.cam is None: raise ValueError("Camera not initialized. Run setup(use_cam=True) first.") + await self.set_plate(plate) + await self.select(row, column) await self.set_imaging_mode(mode) await self.set_exposure(exposure_time) diff --git a/pylabrobot/plate_reading/biotek_tests.py b/pylabrobot/plate_reading/biotek_tests.py index 5da02f63459..c9dc7e94fba 100644 --- a/pylabrobot/plate_reading/biotek_tests.py +++ b/pylabrobot/plate_reading/biotek_tests.py @@ -3,6 +3,7 @@ from typing import Iterator from pylabrobot.plate_reading.biotek_backend import Cytation5Backend +from pylabrobot.resources import CellVis_24_wellplate_3600uL_Fb def _byte_iter(s: str) -> Iterator[bytes]: @@ -18,6 +19,7 @@ async def asyncSetUp(self): self.backend.dev = unittest.mock.MagicMock() self.backend.dev.open.return_value = 0 self.backend.dev.write.return_value = 0 + self.plate = CellVis_24_wellplate_3600uL_Fb(name="plate") async def test_setup(self): await self.backend.setup() @@ -77,10 +79,10 @@ async def test_read_absorbance(self): ) ) - resp = await self.backend.read_absorbance(wavelength=580) + resp = await self.backend.read_absorbance(plate=self.plate, wavelength=580) self.backend.dev.write.assert_any_call(b"y") - self.backend.dev.write.assert_any_call(b"08120112207434014351135308559127881772\x03") + self.backend.dev.write.assert_any_call(b"04060136807158017051135508525127501610\x03") self.backend.dev.write.assert_any_call(b"D") self.backend.dev.write.assert_any_call( b"004701010108120001200100001100100000106000080580113\x03" @@ -233,6 +235,7 @@ async def test_read_fluorescence(self): ) resp = await self.backend.read_fluorescence( + plate=self.plate, excitation_wavelength=485, emission_wavelength=528, focal_height=7.5, @@ -241,7 +244,7 @@ async def test_read_fluorescence(self): self.backend.dev.write.assert_any_call(b"t") self.backend.dev.write.assert_any_call(b"621720\x03") self.backend.dev.write.assert_any_call(b"y") - self.backend.dev.write.assert_any_call(b"08120112207434014351135308559127881772\x03") + self.backend.dev.write.assert_any_call(b"04060136807158017051135508525127501610\x03") self.backend.dev.write.assert_any_call(b"D") self.backend.dev.write.assert_any_call( b"0084010101081200012001000011001000001350001002002000485000052800000000000000000021001119" diff --git a/pylabrobot/plate_reading/chatterbox.py b/pylabrobot/plate_reading/chatterbox.py index 758b65e182d..64cd9fe127e 100644 --- a/pylabrobot/plate_reading/chatterbox.py +++ b/pylabrobot/plate_reading/chatterbox.py @@ -1,6 +1,7 @@ from typing import List from pylabrobot.plate_reading.backend import PlateReaderBackend +from pylabrobot.resources.plate import Plate class PlateReaderChatterboxBackend(PlateReaderBackend): @@ -24,16 +25,17 @@ async def open(self) -> None: async def close(self) -> None: print("Closing the plate reader.") - async def read_luminescence(self, focal_height: float) -> List[List[float]]: + async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]: print(f"Reading luminescence at focal height {focal_height}.") return self.dummy_luminescence - async def read_absorbance(self, wavelength: int) -> List[List[float]]: + async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]: print(f"Reading absorbance at wavelength {wavelength}.") return self.dummy_absorbance async def read_fluorescence( self, + plate: Plate, excitation_wavelength: int, emission_wavelength: int, focal_height: float, diff --git a/pylabrobot/plate_reading/clario_star.py b/pylabrobot/plate_reading/clario_star.py index ac0367b7feb..d250639901e 100644 --- a/pylabrobot/plate_reading/clario_star.py +++ b/pylabrobot/plate_reading/clario_star.py @@ -293,8 +293,8 @@ async def read_luminescence(self, plate: Plate, focal_height: float = 13) -> Lis async def read_absorbance( self, - wavelength: int, plate: Plate, + wavelength: int, report: Literal["OD", "transmittance"] = "OD", ) -> List[List[float]]: """Read absorbance values from the device. @@ -356,9 +356,9 @@ async def read_absorbance( async def read_fluorescence( self, + plate: Plate, excitation_wavelength: int, emission_wavelength: int, focal_height: float, - plate: Plate, ) -> List[List[float]]: raise NotImplementedError("Not implemented yet") diff --git a/pylabrobot/plate_reading/plate_reader.py b/pylabrobot/plate_reading/plate_reader.py index 98d40cf3622..a9c6a5662a0 100644 --- a/pylabrobot/plate_reading/plate_reader.py +++ b/pylabrobot/plate_reading/plate_reader.py @@ -77,7 +77,7 @@ async def read_luminescence(self, focal_height: float) -> List[List[float]]: focal_height: The focal height to read the luminescence at, in micrometers. """ - return await self.backend.read_luminescence(focal_height=focal_height) + return await self.backend.read_luminescence(plate=self.get_plate(), focal_height=focal_height) @need_setup_finished async def read_absorbance(self, wavelength: int) -> List[List[float]]: @@ -87,7 +87,7 @@ async def read_absorbance(self, wavelength: int) -> List[List[float]]: wavelength: The wavelength to read the absorbance at, in nanometers. """ - return await self.backend.read_absorbance(wavelength=wavelength) + return await self.backend.read_absorbance(plate=self.get_plate(), wavelength=wavelength) @need_setup_finished async def read_fluorescence( @@ -105,6 +105,7 @@ async def read_fluorescence( """ return await self.backend.read_fluorescence( + plate=self.get_plate(), excitation_wavelength=excitation_wavelength, emission_wavelength=emission_wavelength, focal_height=focal_height, diff --git a/pylabrobot/plate_reading/plate_reader_tests.py b/pylabrobot/plate_reading/plate_reader_tests.py index 91ab6bb9780..1cea4ac7b87 100644 --- a/pylabrobot/plate_reading/plate_reader_tests.py +++ b/pylabrobot/plate_reading/plate_reader_tests.py @@ -1,48 +1,18 @@ import unittest from pylabrobot.plate_reading import PlateReader -from pylabrobot.plate_reading.backend import PlateReaderBackend +from pylabrobot.plate_reading.chatterbox import PlateReaderChatterboxBackend from pylabrobot.resources import Plate -class MockPlateReaderBackend(PlateReaderBackend): - """A mock backend for testing.""" - - async def setup(self): - pass - - async def stop(self): - pass - - async def open(self): - pass - - async def close(self): - pass - - async def read_luminescence(self, focal_height: float): - return [[1, 2, 3], [4, 5, 6]] - - async def read_absorbance(self, wavelength: int): - return [[1, 2, 3], [4, 5, 6]] - - async def read_fluorescence( - self, - excitation_wavelength: int, - emission_wavelength: int, - focal_height: float, - ): - raise NotImplementedError - - class TestPlateReaderResource(unittest.TestCase): - """Test plate reade as a resource.""" + """Test plate reader as a resource.""" def setUp(self) -> None: super().setUp() self.pr = PlateReader( name="pr", - backend=MockPlateReaderBackend(), + backend=PlateReaderChatterboxBackend(), size_x=1, size_y=1, size_z=1, @@ -65,13 +35,3 @@ def test_get_plate(self): self.pr.assign_child_resource(plate) self.assertEqual(self.pr.get_plate(), plate) - - def test_serialization(self): - backend = MockPlateReaderBackend() - self.assertEqual( - backend.serialize(), - { - "type": "MockPlateReaderBackend", - }, - ) - self.assertIsInstance(backend.deserialize(backend.serialize()), MockPlateReaderBackend) From e8e1cc3203cb007676728debf9d748198e5d8099 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Thu, 13 Feb 2025 18:28:46 -0800 Subject: [PATCH 3/3] ugh --- pylabrobot/plate_reading/biotek_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylabrobot/plate_reading/biotek_tests.py b/pylabrobot/plate_reading/biotek_tests.py index c9dc7e94fba..f4de49b6ca9 100644 --- a/pylabrobot/plate_reading/biotek_tests.py +++ b/pylabrobot/plate_reading/biotek_tests.py @@ -24,7 +24,7 @@ async def asyncSetUp(self): async def test_setup(self): await self.backend.setup() assert self.backend.dev.open.called - assert self.backend.dev.baudrate == 38400 + assert self.backend.dev.baudrate == 9600 self.backend.dev.ftdi_fn.ftdi_set_line_property.assert_called_with(8, 2, 0) self.backend.dev.ftdi_fn.ftdi_setflowctrl.assert_called_with(0x100) self.backend.dev.ftdi_fn.ftdi_setrts.assert_called_with(1)