Skip to content

Commit 533e5e8

Browse files
committed
make biotek imaging more robust (retries)
1 parent 784d85e commit 533e5e8

File tree

1 file changed

+36
-21
lines changed

1 file changed

+36
-21
lines changed

pylabrobot/plate_reading/biotek_backend.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import math
66
import re
77
import time
8+
from contextlib import contextmanager
89
from dataclasses import dataclass
910
from typing import Any, Callable, Coroutine, Dict, List, Literal, Optional, Tuple, Union, cast
1011

@@ -96,13 +97,26 @@ async def cached_func(x: float) -> float:
9697
@dataclass
9798
class Cytation5ImagingConfig:
9899
camera_serial_number: Optional[str] = None
99-
max_image_read_attempts: int = 8
100+
max_image_read_attempts: int = 50
100101

101102
# if not specified, these will be loaded from machine configuration (register with gen5.exe)
102103
objectives: Optional[List[Optional[Objective]]] = None
103104
filters: Optional[List[Optional[ImagingMode]]] = None
104105

105106

107+
@contextmanager
108+
def try_often(times: int = 50):
109+
"""needed because the pyspin api is extremely unreliable."""
110+
for i in range(times):
111+
try:
112+
yield
113+
break
114+
except PySpin.SpinnakerException as e:
115+
print(f"Attempt {i+1}/{times}: Unable to use spinnaker: {e}")
116+
else:
117+
raise RuntimeError("Unable to do it")
118+
119+
106120
class Cytation5Backend(ImageReaderBackend):
107121
"""Backend for biotek cytation 5 image reader.
108122
@@ -1024,13 +1038,15 @@ async def set_auto_exposure(self, auto_exposure: Literal["off", "once", "continu
10241038

10251039
if self.cam.ExposureAuto.GetAccessMode() != PySpin.RW:
10261040
raise RuntimeError("unable to write ExposureAuto")
1027-
self.cam.ExposureAuto.SetValue(
1028-
{
1029-
"off": PySpin.ExposureAuto_Off,
1030-
"once": PySpin.ExposureAuto_Once,
1031-
"continuous": PySpin.ExposureAuto_Continuous,
1032-
}[auto_exposure]
1033-
)
1041+
1042+
with try_often():
1043+
self.cam.ExposureAuto.SetValue(
1044+
{
1045+
"off": PySpin.ExposureAuto_Off,
1046+
"once": PySpin.ExposureAuto_Once,
1047+
"continuous": PySpin.ExposureAuto_Continuous,
1048+
}[auto_exposure]
1049+
)
10341050

10351051
async def set_exposure(self, exposure: Exposure):
10361052
"""exposure (integration time) in ms, or "machine-auto" """
@@ -1049,19 +1065,23 @@ async def set_exposure(self, exposure: Exposure):
10491065
self._exposure = "machine-auto"
10501066
return
10511067
raise ValueError("exposure must be a number or 'auto'")
1052-
self.cam.ExposureAuto.SetValue(PySpin.ExposureAuto_Off)
1068+
with try_often():
1069+
self.cam.ExposureAuto.SetValue(PySpin.ExposureAuto_Off)
10531070

10541071
# set exposure time (in microseconds)
10551072
if self.cam.ExposureTime.GetAccessMode() != PySpin.RW:
10561073
raise RuntimeError("unable to write ExposureTime")
10571074
exposure_us = int(exposure * 1000)
1058-
min_et = self.cam.ExposureTime.GetMin()
1075+
with try_often():
1076+
min_et = self.cam.ExposureTime.GetMin()
10591077
if exposure_us < min_et:
10601078
raise ValueError(f"exposure must be >= {min_et}")
1061-
max_et = self.cam.ExposureTime.GetMax()
1079+
with try_often():
1080+
max_et = self.cam.ExposureTime.GetMax()
10621081
if exposure_us > max_et:
10631082
raise ValueError(f"exposure must be <= {max_et}")
1064-
self.cam.ExposureTime.SetValue(exposure_us)
1083+
with try_often():
1084+
self.cam.ExposureTime.SetValue(exposure_us)
10651085
self._exposure = exposure
10661086

10671087
async def select(self, row: int, column: int):
@@ -1206,17 +1226,9 @@ async def _acquire_image(
12061226
node_softwaretrigger_cmd = PySpin.CCommandPtr(nodemap.GetNode("TriggerSoftware"))
12071227
if not PySpin.IsWritable(node_softwaretrigger_cmd):
12081228
raise RuntimeError("unable to execute software trigger")
1209-
num_trigger_tries = 5
1210-
for _ in range(num_trigger_tries):
1211-
try:
1212-
node_softwaretrigger_cmd.Execute()
1213-
break
1214-
except SpinnakerException:
1215-
continue
1216-
else:
1217-
raise RuntimeError(f"Failed to execute software trigger after {num_trigger_tries} attempts")
12181229

12191230
try:
1231+
node_softwaretrigger_cmd.Execute()
12201232
timeout = int(self.cam.ExposureTime.GetValue() / 1000 + 1000) # from example
12211233
image_result = self.cam.GetNextImage(timeout)
12221234
if not image_result.IsIncomplete():
@@ -1228,6 +1240,9 @@ async def _acquire_image(
12281240
except SpinnakerException as e:
12291241
# the image is not ready yet, try again
12301242
logger.debug("Failed to get image: %s", e)
1243+
self.stop_acquisition()
1244+
self.start_acquisition()
1245+
12311246
num_tries += 1
12321247
await asyncio.sleep(0.3)
12331248
raise TimeoutError("max_image_read_attempts reached")

0 commit comments

Comments
 (0)