Skip to content

Commit c8b2c19

Browse files
rickwierengaclaude
andcommitted
Remove PySpin/Spinnaker dependency, use Aravis exclusively for camera access
Replace all PySpin camera code in CytationBackend with AravisCamera calls. Simplify Cytation1/Cytation5 to always use CytationAravisDriver. Fix aravis_camera.py to accept serial_number=None (first available camera). Update docs notebook to use Microscopy.capture(well=...) API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 774ac62 commit c8b2c19

7 files changed

Lines changed: 1094 additions & 1318 deletions

File tree

docs/user_guide/agilent/biotek/cytation/hello-world.ipynb

Lines changed: 168 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,135 +3,258 @@
33
{
44
"cell_type": "markdown",
55
"id": "wez9bxdabm",
6-
"source": "# Agilent BioTek Cytation\n\nThe Cytation is an Agilent BioTek multi-mode plate reader with optional microscopy imaging. Depending on the model it supports:\n\n- [Absorbance](../../../capabilities/absorbance)\n- [Fluorescence](../../../capabilities/fluorescence)\n- [Luminescence](../../../capabilities/luminescence)\n- [Microscopy](../../../capabilities/microscopy)\n- [Temperature control](../../../capabilities/temperature-control)\n\n| Model | PLR Name | Plate Reading | Microscopy | Temperature |\n|---|---|---|---|---|\n| Cytation 5 | `Cytation5` | Absorbance, Fluorescence, Luminescence | yes | yes |\n| Cytation 1 | `Cytation1` | -- | yes | yes |\n\nBoth models share the `CytationBackend` driver, which communicates over FTDI USB. The Cytation 5 adds plate-reading capabilities on top of the shared microscopy and temperature-control features.",
7-
"metadata": {}
6+
"metadata": {},
7+
"source": [
8+
"# Agilent BioTek Cytation\n",
9+
"\n",
10+
"The Cytation is an Agilent BioTek multi-mode plate reader with optional microscopy imaging. Depending on the model it supports:\n",
11+
"\n",
12+
"- [Absorbance](../../../capabilities/absorbance)\n",
13+
"- [Fluorescence](../../../capabilities/fluorescence)\n",
14+
"- [Luminescence](../../../capabilities/luminescence)\n",
15+
"- [Microscopy](../../../capabilities/microscopy)\n",
16+
"- [Temperature control](../../../capabilities/temperature-control)\n",
17+
"\n",
18+
"| Model | PLR Name | Plate Reading | Microscopy | Temperature |\n",
19+
"|---|---|---|---|---|\n",
20+
"| Cytation 5 | `Cytation5` | Absorbance, Fluorescence, Luminescence | yes | yes |\n",
21+
"| Cytation 1 | `Cytation1` | -- | yes | yes |\n",
22+
"\n",
23+
"Both models share the `CytationBackend` driver, which communicates over FTDI USB. The Cytation 5 adds plate-reading capabilities on top of the shared microscopy and temperature-control features."
24+
]
825
},
926
{
1027
"cell_type": "markdown",
1128
"id": "0rn94ubvq8dj",
12-
"source": "## Setup\n\nThe examples below use a Cytation 5. For a Cytation 1, replace `Cytation5` with `Cytation1` (the Cytation 1 does not have `.absorbance`, `.fluorescence`, or `.luminescence` attributes).",
13-
"metadata": {}
29+
"metadata": {},
30+
"source": [
31+
"## Setup\n",
32+
"\n",
33+
"The examples below use a Cytation 5. For a Cytation 1, replace `Cytation5` with `Cytation1` (the Cytation 1 does not have `.absorbance`, `.fluorescence`, or `.luminescence` attributes)."
34+
]
1435
},
1536
{
1637
"cell_type": "code",
38+
"execution_count": null,
1739
"id": "ia4t5ga2ldg",
18-
"source": "from pylabrobot.agilent.biotek.cytation import Cytation5\n\nc5 = Cytation5(name=\"cytation5\")\nawait c5.setup()",
1940
"metadata": {},
20-
"execution_count": null,
21-
"outputs": []
41+
"outputs": [],
42+
"source": [
43+
"from pylabrobot.agilent.biotek.cytation import Cytation5\n",
44+
"\n",
45+
"c5 = Cytation5(name=\"cytation5\")\n",
46+
"await c5.setup()"
47+
]
2248
},
2349
{
2450
"cell_type": "markdown",
2551
"id": "35rmpdivj44",
26-
"source": "Open and close the tray door. Pass `slow=True` for slower motor travel if needed.",
27-
"metadata": {}
52+
"metadata": {},
53+
"source": [
54+
"Open and close the tray door. Pass `slow=True` for slower motor travel if needed."
55+
]
2856
},
2957
{
3058
"cell_type": "code",
59+
"execution_count": null,
3160
"id": "l85qt1z6hdf",
32-
"source": "await c5.open()\n\nfrom pylabrobot.resources import Cor_96_wellplate_360ul_Fb\nplate = Cor_96_wellplate_360ul_Fb(name=\"plate\")\nc5.plate_holder.assign_child_resource(plate)\n\nawait c5.close()",
3361
"metadata": {},
34-
"execution_count": null,
35-
"outputs": []
62+
"outputs": [],
63+
"source": [
64+
"await c5.open()\n",
65+
"\n",
66+
"from pylabrobot.resources import Cor_96_wellplate_360ul_Fb\n",
67+
"plate = Cor_96_wellplate_360ul_Fb(name=\"plate\")\n",
68+
"c5.plate_holder.assign_child_resource(plate)\n",
69+
"\n",
70+
"await c5.close()"
71+
]
3672
},
3773
{
3874
"cell_type": "markdown",
3975
"id": "hxkf2luxk9n",
40-
"source": "## Plate reading (Cytation 5 only)\n\nThe Cytation 5 exposes `.absorbance`, `.fluorescence`, and `.luminescence` capability objects. For the full API, see [Absorbance](../../../capabilities/absorbance), [Fluorescence](../../../capabilities/fluorescence), and [Luminescence](../../../capabilities/luminescence).",
41-
"metadata": {}
76+
"metadata": {},
77+
"source": [
78+
"## Plate reading (Cytation 5 only)\n",
79+
"\n",
80+
"The Cytation 5 exposes `.absorbance`, `.fluorescence`, and `.luminescence` capability objects. For the full API, see [Absorbance](../../../capabilities/absorbance), [Fluorescence](../../../capabilities/fluorescence), and [Luminescence](../../../capabilities/luminescence)."
81+
]
4282
},
4383
{
4484
"cell_type": "code",
85+
"execution_count": null,
4586
"id": "hwipa2rwkzl",
46-
"source": "# Absorbance\ndata = await c5.absorbance.read_absorbance(wavelength=450)",
4787
"metadata": {},
48-
"execution_count": null,
49-
"outputs": []
88+
"outputs": [],
89+
"source": [
90+
"# Absorbance\n",
91+
"data = await c5.absorbance.read_absorbance(wavelength=450)"
92+
]
5093
},
5194
{
5295
"cell_type": "code",
96+
"execution_count": null,
5397
"id": "jmvn8du2t5",
54-
"source": "# Fluorescence\ndata = await c5.fluorescence.read_fluorescence(\n excitation_wavelength=485, emission_wavelength=528, focal_height=7.5\n)",
5598
"metadata": {},
56-
"execution_count": null,
57-
"outputs": []
99+
"outputs": [],
100+
"source": [
101+
"# Fluorescence\n",
102+
"data = await c5.fluorescence.read_fluorescence(\n",
103+
" excitation_wavelength=485, emission_wavelength=528, focal_height=7.5\n",
104+
")"
105+
]
58106
},
59107
{
60108
"cell_type": "code",
109+
"execution_count": null,
61110
"id": "oxn123gjoh",
62-
"source": "# Luminescence\ndata = await c5.luminescence.read_luminescence(focal_height=4.5)",
63111
"metadata": {},
64-
"execution_count": null,
65-
"outputs": []
112+
"outputs": [],
113+
"source": [
114+
"# Luminescence\n",
115+
"data = await c5.luminescence.read_luminescence(focal_height=4.5)"
116+
]
66117
},
67118
{
68119
"cell_type": "markdown",
69120
"id": "1qn3t2pqvw",
70-
"source": "## Microscopy\n\nBoth the Cytation 5 and Cytation 1 expose a `.microscopy` capability. For imaging, pass `use_cam=True` during setup so the Spinnaker camera is initialized. For the full API, see [Microscopy](../../../capabilities/microscopy).\n\nUse {class}`~pylabrobot.agilent.biotek.cytation.CytationBackend.CaptureParams` to control LED intensity, coverage tiling, and pixel format.",
71-
"metadata": {}
121+
"metadata": {},
122+
"source": [
123+
"## Microscopy\n",
124+
"\n",
125+
"Both the Cytation 5 and Cytation 1 expose a `.microscopy` capability. For imaging, pass `use_cam=True` during setup so the Spinnaker camera is initialized. For the full API, see [Microscopy](../../../capabilities/microscopy).\n",
126+
"\n",
127+
"Use {class}`~pylabrobot.agilent.biotek.cytation.CytationBackend.CaptureParams` to control LED intensity, coverage tiling, and pixel format."
128+
]
72129
},
73130
{
74131
"cell_type": "code",
132+
"execution_count": null,
75133
"id": "qr1jm6691a",
76-
"source": "from pylabrobot.agilent.biotek.cytation import CytationBackend, CytationImagingConfig\nfrom pylabrobot.capabilities.microscopy.standard import ImagingMode, Objective\n\nres = await c5.microscopy.capture(\n row=1,\n column=2,\n mode=ImagingMode.BRIGHTFIELD,\n objective=Objective.O_4X_PL_FL_Phase,\n focal_height=0.833,\n exposure_time=5,\n gain=16,\n plate=plate,\n backend_params=CytationBackend.CaptureParams(led_intensity=10),\n)",
77134
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"from pylabrobot.agilent.biotek.cytation import CytationBackend, CytationImagingConfig\n",
138+
"from pylabrobot.capabilities.microscopy.standard import ImagingMode, Objective\n",
139+
"\n",
140+
"res = await c5.microscopy.capture(\n",
141+
" well=(1, 2),\n",
142+
" mode=ImagingMode.BRIGHTFIELD,\n",
143+
" objective=Objective.O_4X_PL_FL_Phase,\n",
144+
" focal_height=0.833,\n",
145+
" exposure_time=5,\n",
146+
" gain=16,\n",
147+
" plate=plate,\n",
148+
" backend_params=CytationBackend.CaptureParams(led_intensity=10),\n",
149+
")"
150+
]
151+
},
152+
{
153+
"cell_type": "code",
78154
"execution_count": null,
79-
"outputs": []
155+
"id": "b7125e49",
156+
"metadata": {},
157+
"outputs": [],
158+
"source": [
159+
"from PIL import Image\n",
160+
"img = Image.fromarray(res.images[0])\n",
161+
"img"
162+
]
80163
},
81164
{
82165
"cell_type": "markdown",
83166
"id": "km95iou30f",
84-
"source": "Tile multiple fields of view with the `coverage` parameter:",
85-
"metadata": {}
167+
"metadata": {},
168+
"source": [
169+
"Tile multiple fields of view with the `coverage` parameter:"
170+
]
86171
},
87172
{
88173
"cell_type": "code",
174+
"execution_count": null,
89175
"id": "fi1o94l1uni",
90-
"source": "res = await c5.microscopy.capture(\n row=1,\n column=2,\n mode=ImagingMode.BRIGHTFIELD,\n objective=Objective.O_4X_PL_FL_Phase,\n focal_height=0.833,\n exposure_time=5,\n gain=16,\n plate=plate,\n backend_params=CytationBackend.CaptureParams(\n led_intensity=10,\n coverage=(4, 4),\n ),\n)\nprint(f\"{len(res.images)} images captured\")",
91176
"metadata": {},
92-
"execution_count": null,
93-
"outputs": []
177+
"outputs": [],
178+
"source": [
179+
"res = await c5.microscopy.capture(\n",
180+
" well=(1, 2),\n",
181+
" mode=ImagingMode.BRIGHTFIELD,\n",
182+
" objective=Objective.O_4X_PL_FL_Phase,\n",
183+
" focal_height=0.833,\n",
184+
" exposure_time=5,\n",
185+
" gain=16,\n",
186+
" plate=plate,\n",
187+
" backend_params=CytationBackend.CaptureParams(\n",
188+
" led_intensity=10,\n",
189+
" coverage=(4, 4),\n",
190+
" ),\n",
191+
")\n",
192+
"print(f\"{len(res.images)} images captured\")"
193+
]
94194
},
95195
{
96196
"cell_type": "markdown",
97197
"id": "sa9pdeeo51",
98-
"source": "## Temperature control\n\nBoth models expose a `.temperature` controller. For the full API, see [Temperature Control](../../../capabilities/temperature-control).",
99-
"metadata": {}
198+
"metadata": {},
199+
"source": [
200+
"## Temperature control\n",
201+
"\n",
202+
"Both models expose a `.temperature` controller. For the full API, see [Temperature Control](../../../capabilities/temperature-control)."
203+
]
100204
},
101205
{
102206
"cell_type": "code",
207+
"execution_count": null,
103208
"id": "qhsjnerhl3",
104-
"source": "await c5.temperature.set_temperature(37.0)\n\ncurrent = await c5.temperature.request_temperature()\nprint(f\"{current:.1f} \\u00b0C\")\n\nawait c5.temperature.deactivate()",
105209
"metadata": {},
106-
"execution_count": null,
107-
"outputs": []
210+
"outputs": [],
211+
"source": [
212+
"await c5.temperature.set_temperature(37.0)\n",
213+
"\n",
214+
"current = await c5.temperature.request_temperature()\n",
215+
"print(f\"{current:.1f} \\u00b0C\")\n",
216+
"\n",
217+
"await c5.temperature.deactivate()"
218+
]
108219
},
109220
{
110221
"cell_type": "markdown",
111222
"id": "f667qnt4occ",
112-
"source": "## Teardown",
113-
"metadata": {}
223+
"metadata": {},
224+
"source": [
225+
"## Teardown"
226+
]
114227
},
115228
{
116229
"cell_type": "code",
230+
"execution_count": null,
117231
"id": "xcqz2zwu04g",
118-
"source": "await c5.stop()",
119232
"metadata": {},
120-
"execution_count": null,
121-
"outputs": []
233+
"outputs": [],
234+
"source": [
235+
"await c5.stop()"
236+
]
122237
}
123238
],
124239
"metadata": {
125240
"kernelspec": {
126-
"display_name": "Python 3 (ipykernel)",
241+
"display_name": "env",
127242
"language": "python",
128243
"name": "python3"
129244
},
130245
"language_info": {
246+
"codemirror_mode": {
247+
"name": "ipython",
248+
"version": 3
249+
},
250+
"file_extension": ".py",
251+
"mimetype": "text/x-python",
131252
"name": "python",
132-
"version": "3.11.0"
253+
"nbconvert_exporter": "python",
254+
"pygments_lexer": "ipython3",
255+
"version": "3.10.15"
133256
}
134257
},
135258
"nbformat": 4,
136259
"nbformat_minor": 5
137-
}
260+
}

pylabrobot/agilent/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from .biotek import (
22
EL406,
3+
AravisImagingConfig,
34
BioTekBackend,
45
Cytation1,
56
Cytation5,
7+
CytationAravisDriver,
68
CytationBackend,
79
CytationImagingConfig,
810
EL406Driver,

0 commit comments

Comments
 (0)