Skip to content

Commit fa40bea

Browse files
authored
different plate types on cytation5 (#403)
1 parent c3165f9 commit fa40bea

File tree

10 files changed

+142
-99
lines changed

10 files changed

+142
-99
lines changed

pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
from pylabrobot.liquid_handling import LiquidHandler
66
from pylabrobot.liquid_handling.standard import GripDirection, Pickup
77
from pylabrobot.plate_reading import PlateReader
8-
from pylabrobot.plate_reading.plate_reader_tests import (
9-
MockPlateReaderBackend,
10-
)
8+
from pylabrobot.plate_reading.chatterbox import PlateReaderChatterboxBackend
119
from pylabrobot.resources import (
1210
HT,
1311
HTF,
@@ -696,7 +694,7 @@ async def test_iswap(self):
696694
async def test_iswap_plate_reader(self):
697695
plate_reader = PlateReader(
698696
name="plate_reader",
699-
backend=MockPlateReaderBackend(),
697+
backend=PlateReaderChatterboxBackend(),
700698
size_x=0,
701699
size_y=0,
702700
size_z=0,

pylabrobot/plate_reading/backend.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pylabrobot.machines.backends import MachineBackend
77
from pylabrobot.plate_reading.standard import Exposure, FocalPosition, Gain, ImagingMode
8+
from pylabrobot.resources.plate import Plate
89

910

1011
class PlateReaderBackend(MachineBackend, metaclass=ABCMeta):
@@ -28,18 +29,19 @@ async def close(self) -> None:
2829
"""Close the plate reader. Also known as plate in."""
2930

3031
@abstractmethod
31-
async def read_luminescence(self, focal_height: float) -> List[List[float]]:
32+
async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]:
3233
"""Read the luminescence from the plate reader. This should return a list of lists, where the
3334
outer list is the columns of the plate and the inner list is the rows of the plate."""
3435

3536
@abstractmethod
36-
async def read_absorbance(self, wavelength: int) -> List[List[float]]:
37+
async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]:
3738
"""Read the absorbance from the plate reader. This should return a list of lists, where the
3839
outer list is the columns of the plate and the inner list is the rows of the plate."""
3940

4041
@abstractmethod
4142
async def read_fluorescence(
4243
self,
44+
plate: Plate,
4345
excitation_wavelength: int,
4446
emission_wavelength: int,
4547
focal_height: float,
@@ -58,6 +60,7 @@ async def capture(
5860
exposure_time: Exposure,
5961
focal_height: FocalPosition,
6062
gain: Gain,
63+
plate: Plate,
6164
) -> List[List[float]]:
6265
"""Capture an image of the plate in the specified mode."""
6366

pylabrobot/plate_reading/biotek_backend.py

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import time
55
from typing import List, Literal, Optional
66

7+
from pylabrobot.resources.plate import Plate
8+
79
try:
810
from pylibftdi import Device
911

@@ -126,6 +128,7 @@ def __init__(self, timeout: float = 20, camera_serial_number: Optional[float] =
126128
self.camera_serial_number = camera_serial_number
127129
self.max_image_read_attempts = 8
128130

131+
self._plate: Optional[Plate] = None
129132
self._exposure: Optional[Exposure] = None
130133
self._focal_height: Optional[FocalPosition] = None
131134
self._gain: Optional[Gain] = None
@@ -137,10 +140,16 @@ async def setup(self, use_cam: bool = False) -> None:
137140
logger.info("[cytation5] setting up")
138141

139142
self.dev.open()
140-
# self.dev.baudrate = 9600 # worked in the past
141-
self.dev.baudrate = 38400
143+
self.dev.ftdi_fn.ftdi_usb_reset()
144+
self.dev.ftdi_fn.ftdi_set_latency_timer(16) # 0x10
145+
146+
self.dev.baudrate = 9600 # 0x38 0x41
147+
# self.dev.baudrate = 38400
142148
self.dev.ftdi_fn.ftdi_set_line_property(8, 2, 0) # 8 bits, 2 stop bits, no parity
143149
SIO_RTS_CTS_HS = 0x1 << 8
150+
self.dev.ftdi_fn.ftdi_setdtr(1)
151+
self.dev.ftdi_fn.ftdi_setrts(1)
152+
144153
self.dev.ftdi_fn.ftdi_setflowctrl(SIO_RTS_CTS_HS)
145154
self.dev.ftdi_fn.ftdi_setrts(1)
146155

@@ -265,31 +274,36 @@ async def _read_until(self, char: bytes, timeout: Optional[float] = None) -> byt
265274
return res
266275

267276
async def send_command(
268-
self, command: str, parameter: Optional[str] = None, wait_for_response=True
277+
self,
278+
command: str,
279+
parameter: Optional[str] = None,
280+
wait_for_response=True,
281+
timeout: Optional[float] = None,
269282
) -> Optional[bytes]:
270283
await self._purge_buffers()
271284
self.dev.write(command.encode())
272285
logger.debug("[cytation5] sent %s", command)
273286
response: Optional[bytes] = None
274287
if wait_for_response or parameter is not None:
275-
# print("reading until", b"\x06" if parameter is not None else b"\x03")
276-
response = await self._read_until(b"\x06" if parameter is not None else b"\x03")
288+
response = await self._read_until(
289+
b"\x06" if parameter is not None else b"\x03", timeout=timeout
290+
)
277291

278292
if parameter is not None:
279293
self.dev.write(parameter.encode())
280294
logger.debug("[cytation5] sent %s", parameter)
281295
if wait_for_response:
282-
response = await self._read_until(b"\x03")
296+
response = await self._read_until(b"\x03", timeout=timeout)
283297

284298
return response
285299

286300
async def get_serial_number(self) -> str:
287-
resp = await self.send_command("C")
301+
resp = await self.send_command("C", timeout=1)
288302
assert resp is not None
289303
return resp[1:].split(b" ")[0].decode()
290304

291305
async def get_firmware_version(self) -> str:
292-
resp = await self.send_command("e")
306+
resp = await self.send_command("e", timeout=1)
293307
assert resp is not None
294308
return " ".join(resp[1:-1].decode().split(" ")[0:4])
295309

@@ -323,11 +337,62 @@ def _parse_body(self, body: bytes) -> List[List[float]]:
323337
parsed_data[row_idx].append(value)
324338
return parsed_data
325339

326-
async def read_absorbance(self, wavelength: int) -> List[List[float]]:
340+
async def set_plate(self, plate: Plate):
341+
"""
342+
08120112207434014351135308559127881422
343+
^^^^ plate size z
344+
^^^^^ plate size x
345+
^^^^^ plate size y
346+
^^^^^ bottom right x
347+
^^^^^ top left x
348+
^^^^^ bottom right y
349+
^^^^^ top left y
350+
^^ columns
351+
^^ rows
352+
"""
353+
354+
if plate is self._plate:
355+
return
356+
357+
rows = plate.num_items_y
358+
columns = plate.num_items_x
359+
360+
bottom_right_well = plate.get_item(plate.num_items - 1)
361+
assert bottom_right_well.location is not None
362+
bottom_right_well_center = bottom_right_well.location + bottom_right_well.get_anchor(
363+
x="c", y="c"
364+
)
365+
top_left_well = plate.get_item(0)
366+
assert top_left_well.location is not None
367+
top_left_well_center = top_left_well.location + top_left_well.get_anchor(x="c", y="c")
368+
369+
plate_size_y = plate.get_size_y()
370+
plate_size_x = plate.get_size_x()
371+
plate_size_z = plate.get_size_z()
372+
373+
top_left_well_center_y = plate.get_size_y() - top_left_well_center.y # invert y axis
374+
bottom_right_well_center_y = plate.get_size_y() - bottom_right_well_center.y # invert y axis
375+
376+
cmd = (
377+
f"{rows:02}"
378+
f"{columns:02}"
379+
f"{int(top_left_well_center_y*100):05}"
380+
f"{int(bottom_right_well_center_y*100):05}"
381+
f"{int(top_left_well_center.x*100):05}"
382+
f"{int(bottom_right_well_center.x*100):05}"
383+
f"{int(plate_size_y*100):05}"
384+
f"{int(plate_size_x*100):05}"
385+
f"{int(plate_size_z*100):04}"
386+
"\x03"
387+
)
388+
389+
return await self.send_command("y", cmd)
390+
391+
async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]:
327392
if not 230 <= wavelength <= 999:
328393
raise ValueError("Wavelength must be between 230 and 999")
329394

330-
await self.send_command("y", "08120112207434014351135308559127881772\x03")
395+
await self.set_plate(plate)
331396

332397
wavelength_str = str(wavelength).zfill(4)
333398
cmd = f"00470101010812000120010000110010000010600008{wavelength_str}1"
@@ -343,14 +408,14 @@ async def read_absorbance(self, wavelength: int) -> List[List[float]]:
343408
assert resp is not None
344409
return self._parse_body(body)
345410

346-
async def read_luminescence(self, focal_height: float) -> List[List[float]]:
411+
async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]:
347412
if not 4.5 <= focal_height <= 13.88:
348413
raise ValueError("Focal height must be between 4.5 and 13.88")
349414

350415
cmd = f"3{14220 + int(1000*focal_height)}\x03"
351416
await self.send_command("t", cmd)
352417

353-
await self.send_command("y", "08120112207434014351135308559127881772\x03")
418+
await self.set_plate(plate)
354419

355420
cmd = "008401010108120001200100001100100000123000500200200-001000-00300000000000000000001351092"
356421
await self.send_command("D", cmd)
@@ -364,6 +429,7 @@ async def read_luminescence(self, focal_height: float) -> List[List[float]]:
364429

365430
async def read_fluorescence(
366431
self,
432+
plate: Plate,
367433
excitation_wavelength: int,
368434
emission_wavelength: int,
369435
focal_height: float,
@@ -378,7 +444,7 @@ async def read_fluorescence(
378444
cmd = f"{614220 + int(1000*focal_height)}\x03"
379445
await self.send_command("t", cmd)
380446

381-
await self.send_command("y", "08120112207434014351135308559127881772\x03")
447+
await self.set_plate(plate)
382448

383449
excitation_wavelength_str = str(excitation_wavelength).zfill(4)
384450
emission_wavelength_str = str(emission_wavelength).zfill(4)
@@ -512,14 +578,14 @@ async def led_off(self):
512578
async def set_focus(self, focal_position: FocalPosition):
513579
"""focus position in mm"""
514580

515-
if focal_position == self._focal_height:
516-
logger.debug("Focus position is already set to %s", focal_position)
517-
return
518-
519581
if focal_position == "auto":
520582
await self.auto_focus()
521583
return
522584

585+
if focal_position == self._focal_height:
586+
logger.debug("Focus position is already set to %s", focal_position)
587+
return
588+
523589
# There is a difference between the number in the program and the number sent to the machine,
524590
# which is modelled using the following linear relation. R^2=0.999999999
525591
# convert from mm to um
@@ -534,6 +600,9 @@ async def set_focus(self, focal_position: FocalPosition):
534600
self._focal_height = focal_position
535601

536602
async def auto_focus(self, timeout: float = 30):
603+
plate = self._plate
604+
if plate is None:
605+
raise RuntimeError("Plate not set. Run set_plate() first.")
537606
imaging_mode = self._imaging_mode
538607
if imaging_mode is None:
539608
raise RuntimeError("Imaging mode not set. Run set_imaging_mode() first.")
@@ -557,6 +626,7 @@ async def auto_focus(self, timeout: float = 30):
557626
# objective function: variance of laplacian
558627
async def evaluate_focus(focus_value):
559628
image = await self.capture(
629+
plate=plate,
560630
row=row,
561631
column=column,
562632
mode=imaging_mode,
@@ -769,6 +839,7 @@ async def capture(
769839
exposure_time: Exposure,
770840
focal_height: FocalPosition,
771841
gain: Gain,
842+
plate: Plate,
772843
color_processing_algorithm: int = SPINNAKER_COLOR_PROCESSING_ALGORITHM_HQ_LINEAR,
773844
pixel_format: int = PixelFormat_Mono8,
774845
) -> List[List[float]]:
@@ -788,6 +859,8 @@ async def capture(
788859
if self.cam is None:
789860
raise ValueError("Camera not initialized. Run setup(use_cam=True) first.")
790861

862+
await self.set_plate(plate)
863+
791864
await self.select(row, column)
792865
await self.set_imaging_mode(mode)
793866
await self.set_exposure(exposure_time)

pylabrobot/plate_reading/biotek_tests.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Iterator
44

55
from pylabrobot.plate_reading.biotek_backend import Cytation5Backend
6+
from pylabrobot.resources import CellVis_24_wellplate_3600uL_Fb
67

78

89
def _byte_iter(s: str) -> Iterator[bytes]:
@@ -18,11 +19,12 @@ async def asyncSetUp(self):
1819
self.backend.dev = unittest.mock.MagicMock()
1920
self.backend.dev.open.return_value = 0
2021
self.backend.dev.write.return_value = 0
22+
self.plate = CellVis_24_wellplate_3600uL_Fb(name="plate")
2123

2224
async def test_setup(self):
2325
await self.backend.setup()
2426
assert self.backend.dev.open.called
25-
assert self.backend.dev.baudrate == 38400
27+
assert self.backend.dev.baudrate == 9600
2628
self.backend.dev.ftdi_fn.ftdi_set_line_property.assert_called_with(8, 2, 0)
2729
self.backend.dev.ftdi_fn.ftdi_setflowctrl.assert_called_with(0x100)
2830
self.backend.dev.ftdi_fn.ftdi_setrts.assert_called_with(1)
@@ -77,10 +79,10 @@ async def test_read_absorbance(self):
7779
)
7880
)
7981

80-
resp = await self.backend.read_absorbance(wavelength=580)
82+
resp = await self.backend.read_absorbance(plate=self.plate, wavelength=580)
8183

8284
self.backend.dev.write.assert_any_call(b"y")
83-
self.backend.dev.write.assert_any_call(b"08120112207434014351135308559127881772\x03")
85+
self.backend.dev.write.assert_any_call(b"04060136807158017051135508525127501610\x03")
8486
self.backend.dev.write.assert_any_call(b"D")
8587
self.backend.dev.write.assert_any_call(
8688
b"004701010108120001200100001100100000106000080580113\x03"
@@ -233,6 +235,7 @@ async def test_read_fluorescence(self):
233235
)
234236

235237
resp = await self.backend.read_fluorescence(
238+
plate=self.plate,
236239
excitation_wavelength=485,
237240
emission_wavelength=528,
238241
focal_height=7.5,
@@ -241,7 +244,7 @@ async def test_read_fluorescence(self):
241244
self.backend.dev.write.assert_any_call(b"t")
242245
self.backend.dev.write.assert_any_call(b"621720\x03")
243246
self.backend.dev.write.assert_any_call(b"y")
244-
self.backend.dev.write.assert_any_call(b"08120112207434014351135308559127881772\x03")
247+
self.backend.dev.write.assert_any_call(b"04060136807158017051135508525127501610\x03")
245248
self.backend.dev.write.assert_any_call(b"D")
246249
self.backend.dev.write.assert_any_call(
247250
b"0084010101081200012001000011001000001350001002002000485000052800000000000000000021001119"

pylabrobot/plate_reading/chatterbox.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import List
22

33
from pylabrobot.plate_reading.backend import PlateReaderBackend
4+
from pylabrobot.resources.plate import Plate
45

56

67
class PlateReaderChatterboxBackend(PlateReaderBackend):
@@ -24,16 +25,17 @@ async def open(self) -> None:
2425
async def close(self) -> None:
2526
print("Closing the plate reader.")
2627

27-
async def read_luminescence(self, focal_height: float) -> List[List[float]]:
28+
async def read_luminescence(self, plate: Plate, focal_height: float) -> List[List[float]]:
2829
print(f"Reading luminescence at focal height {focal_height}.")
2930
return self.dummy_luminescence
3031

31-
async def read_absorbance(self, wavelength: int) -> List[List[float]]:
32+
async def read_absorbance(self, plate: Plate, wavelength: int) -> List[List[float]]:
3233
print(f"Reading absorbance at wavelength {wavelength}.")
3334
return self.dummy_absorbance
3435

3536
async def read_fluorescence(
3637
self,
38+
plate: Plate,
3739
excitation_wavelength: int,
3840
emission_wavelength: int,
3941
focal_height: float,

0 commit comments

Comments
 (0)