{ "cells": [ { "cell_type": "markdown", "id": "title-status", "metadata": {}, "source": [ "# Celigo hello world\n", "\n", "This walkthrough uses the PyLabRobot driver directly; no Celigo vendor application is required. Keep the stage and drawer paths clear whenever motion is enabled." ] }, { "cell_type": "markdown", "id": "device-card", "metadata": {}, "source": [ "```{device-card} revvity-celigo\n", "```" ] }, { "cell_type": "markdown", "id": "gaps", "metadata": {}, "source": [ "## Prepare\n", "\n", "### Current driver scope\n", "\n", "The Lumenera opens at `2464x2056`; `Celigo.setup()` applies and reads back the centered `2048x2048` ROI required by `CalibrationConfig.xml` (offset `208, 4`), then homes Z, X, Y, and the dichroic filter. `Celigo.acquire()` rejects any later geometry mismatch rather than silently producing uncalibrated data.\n", "\n", "Image autofocus has been exercised on an A1 cell sample; hardware displacement-sensor autofocus is not implemented. Well navigation uses the geometry of the assigned PyLabRobot plate resource. All five configured illumination channels have completed acquisition. Camera-trigger diagnostics are verified, while externally triggered frame acquisition is not. Laser support is disabled by default, and firing has not been exercised." ] }, { "cell_type": "markdown", "id": "communication", "metadata": {}, "source": [ "### Connections and configuration\n", "\n", "One plain `Celigo` object owns both connections. It talks to the USB-I/O controller through PyLabRobot's FTDI transport, manages the `LumeneraCamera` lifecycle through `liblucamapi`, and loads motor limits, homing profiles, illumination channels, filter mappings, galvo centers, and coordinate transforms from the copied instrument configuration." ] }, { "cell_type": "markdown", "id": "physical-setup", "metadata": {}, "source": [ "### Physical setup\n", "\n", "1. Power on the Celigo and clear the stage, objective, and drawer paths.\n", "2. Connect both USB devices: FTDI `0403:6001` for the controller and Lumenera `1724:0645` for the camera.\n", "3. If more than one matching FTDI is attached, find the controller's local topology with `lsusb -t` and set `usb_address` below.\n", "4. Copy the instrument's `ConfigFiles` directory and identify the matching plate model in `pylabrobot.resources`." ] }, { "cell_type": "markdown", "id": "imports-note", "metadata": {}, "source": [ "## Configure the driver\n", "\n", "### Imports\n", "\n", "The public device API is `Celigo`. It owns the camera internally, and a normal PyLabRobot `Plate` is assigned after construction." ] }, { "cell_type": "code", "execution_count": null, "id": "imports", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "from pylabrobot.revvity import Celigo, CeligoConfig\n", "from pylabrobot.resources.corning.plates import cor_96_wellplate_360uL_Fb" ] }, { "cell_type": "markdown", "id": "configuration-note", "metadata": {}, "source": [ "### Load this instrument's configuration\n", "\n", "Load the complete instrument configuration with `CeligoConfig.from_install(config_root)` and pass it to `Celigo`. `config_root` may be the Celigo installation root, its `ConfigFiles` directory, or `USBIOHardwareConfig.config` itself. Use the PyLabRobot model matching the physical plate. `Celigo.setup()` initializes the controller and camera, applies the calibrated camera ROI, configures the motors and galvos, and homes Z, X, Y, and the dichroic filter." ] }, { "cell_type": "code", "execution_count": null, "id": "configuration", "metadata": {}, "outputs": [], "source": [ "config_root = Path(\"/path/to/Celigo/ConfigFiles\")\n", "lucam_sdk = Path(\"/path/to/liblucamapi.so\")\n", "usb_address = \"3-2\" # local -[....] from lsusb/pyusb\n", "\n", "config = CeligoConfig.from_install(str(config_root))\n", "plate = cor_96_wellplate_360uL_Fb(name=\"imaging_plate\")\n", "celigo = Celigo(\n", " config=config,\n", " usb_address=usb_address,\n", " lucam_sdk=str(lucam_sdk),\n", ")\n", "celigo.set_plate(plate)" ] }, { "cell_type": "markdown", "id": "setup-note", "metadata": {}, "source": [ "## Initialize and verify\n", "\n", "### Connect and initialize\n", "\n", "The single setup call opens the controller and camera, performs the binary handshake, reads identity, discovers motors, applies safe outputs, initializes configured motor parameters, calibrates the galvos, applies the calibrated camera ROI, and homes Z, X, Y, and the dichroic filter." ] }, { "cell_type": "code", "execution_count": null, "id": "setup", "metadata": {}, "outputs": [], "source": [ "await celigo.setup()\n", "celigo.controller_info, await celigo.request_detected_motor_addresses()" ] }, { "cell_type": "markdown", "id": "self-test-note", "metadata": {}, "source": [ "### Run the read-only self-test\n", "\n", "The default self-test reads controller status, identity, motor mapping and encoder ratios, digital inputs, and galvo calibration metadata without moving the stage. The tested instrument reports generic interlock flag `4`; that flag remains a laser-safety condition but does not by itself indicate a general controller failure." ] }, { "cell_type": "code", "execution_count": null, "id": "self-test", "metadata": {}, "outputs": [], "source": [ "self_test_report = await celigo.run_self_test()\n", "self_test_report.passed, self_test_report.failures, self_test_report.checks" ] }, { "cell_type": "markdown", "id": "geometry-note", "metadata": {}, "source": [ "### Check camera geometry\n", "\n", "Setup should make these equal by applying and verifying the centered native ROI. Calibrated acquisition also checks every returned frame." ] }, { "cell_type": "code", "execution_count": null, "id": "geometry", "metadata": {}, "outputs": [], "source": [ "actual_format = (celigo.camera.width, celigo.camera.height)\n", "expected_format = (\n", " celigo.config.calibration.image_width_pixels,\n", " celigo.config.calibration.image_height_pixels,\n", ")\n", "actual_format, expected_format" ] }, { "cell_type": "markdown", "id": "home-note", "metadata": {}, "source": [ "### Re-home the mechanisms\n", "\n", "Home Z first for vertical clearance, then X and Y. Each linear routine checks encoder response, negative-limit activation and release, datum establishment, controller-mode restoration, and final encoder arrival. Filter homing uses its encoder index and physical opto tab." ] }, { "cell_type": "code", "execution_count": null, "id": "home", "metadata": {}, "outputs": [], "source": [ "home_positions = {\n", " \"z\": await celigo.z_axis.home(),\n", " \"x\": await celigo.x_axis.home(),\n", " \"y\": await celigo.y_axis.home(),\n", " \"filter\": await celigo.dichroic_filter.home(),\n", "}\n", "home_positions" ] }, { "cell_type": "markdown", "id": "open-note", "metadata": {}, "source": [ "## Load the sample\n", "\n", "### Open the drawer\n", "\n", "This retracts Z, moves to Y clearance, and drives X/Y to their configured loading limits. Keep hands clear until it finishes. Repeating `open_drawer()` is safe because active destination limits are checked." ] }, { "cell_type": "code", "execution_count": null, "id": "open-drawer", "metadata": {}, "outputs": [], "source": [ "await celigo.open_drawer()" ] }, { "cell_type": "markdown", "id": "load-plate", "metadata": {}, "source": [ "### Seat the plate\n", "\n", "Place the Corning 3603 plate in the carrier in the instrument's expected orientation. Confirm it is seated flat, then keep clear before running the next cell." ] }, { "cell_type": "markdown", "id": "close-note", "metadata": {}, "source": [ "### Close the drawer to A1\n", "\n", "The return position is derived from the copied calibration, hardware defaults, and assigned PyLabRobot plate resource. For a custom carrier without a `Plate`, use sample-relative millimeters instead: `await celigo.close_drawer_to_sample_mm(x_mm=63.5, y_mm=43.0)`. Both methods turn off illumination, retract Z, and move through Y clearance before positioning the sample." ] }, { "cell_type": "code", "execution_count": null, "id": "close-drawer", "metadata": {}, "outputs": [], "source": [ "await celigo.close_drawer(well=\"A1\")" ] }, { "cell_type": "markdown", "id": "camera-settings-note", "metadata": {}, "source": [ "## Acquire images\n", "\n", "### Set a conservative brightfield exposure\n", "\n", "The live camera retained settings across reopen. One millisecond at gain 1 was unsaturated during the first hardware check; tune this for the sample." ] }, { "cell_type": "code", "execution_count": null, "id": "camera-settings", "metadata": {}, "outputs": [], "source": [ "await celigo.set_camera_exposure_and_gain(\n", " exposure_ms=1.0,\n", " gain=1.0,\n", " restart_camera_stream=True,\n", ")" ] }, { "cell_type": "markdown", "id": "brightfield-note", "metadata": {}, "source": [ "### Acquire one calibrated brightfield image\n", "\n", "This single call moves to A1, selects the configured brightfield filter and illumination, centers the calibrated galvos, moves Z to the installed brightfield plane, and captures a geometry-checked frame." ] }, { "cell_type": "code", "execution_count": null, "id": "brightfield", "metadata": {}, "outputs": [], "source": [ "result = await celigo.acquire(\n", " \"A1\",\n", " \"brightfield\",\n", " exposure_ms=1.0,\n", " gain=1.0,\n", ")\n", "result.z_mm, result.galvo_hardware_voltages" ] }, { "cell_type": "markdown", "id": "auto-exposure-note", "metadata": {}, "source": [ "### Let the instrument choose an exposure\n", "\n", "Set `machine_auto_exposure=True` when the sample brightness is unknown. The driver tests bounded exposure candidates and returns the selected exposure in the frame metadata." ] }, { "cell_type": "code", "execution_count": null, "id": "auto-exposure", "metadata": {}, "outputs": [], "source": [ "auto_exposed = await celigo.acquire(\n", " \"A1\",\n", " \"brightfield\",\n", " gain=1.0,\n", " machine_auto_exposure=True,\n", ")\n", "auto_exposed.frame.exposure_ms, auto_exposed.frame.statistics()" ] }, { "cell_type": "markdown", "id": "raw-capture-note", "metadata": {}, "source": [ "### Save and inspect the picture\n", "\n", "PGM preserves the monochrome pixels without adding an image dependency." ] }, { "cell_type": "code", "execution_count": null, "id": "raw-capture", "metadata": {}, "outputs": [], "source": [ "result.frame.save_pgm(\"A1-brightfield.pgm\")\n", "result.frame.statistics(), result.frame.sharpness(sample_step=8)" ] }, { "cell_type": "markdown", "id": "focus-stack-note", "metadata": {}, "source": [ "### Run image autofocus when the sample has visible structure\n", "\n", "Image autofocus scans around the calibrated channel-specific Z plane, scores each frame, and leaves Z at the best plane. It fails closed when there is no measurable contrast or the optimum lies at the scan boundary." ] }, { "cell_type": "code", "execution_count": null, "id": "focus-stack", "metadata": {}, "outputs": [], "source": [ "focused = await celigo.acquire(\n", " \"A1\",\n", " \"brightfield\",\n", " exposure_ms=1.0,\n", " gain=1.0,\n", " autofocus=\"image\",\n", ")\n", "focused.frame.save_pgm(\"A1-brightfield-focused.pgm\")\n", "focused.focus.z_mm, focused.focus.score" ] }, { "cell_type": "markdown", "id": "calibrated-acquisition-gap", "metadata": {}, "source": [ "### Change filters and acquire another channel\n", "\n", "The channel name selects the installed dichroic position, illumination output and intensity, galvo offsets, and calibrated Z correction. For example:\n", "\n", "```python\n", "result = await celigo.acquire(\n", " \"A1\",\n", " \"green\",\n", " exposure_ms=1.0,\n", " gain=1.0,\n", " autofocus=\"image\",\n", ")\n", "result.frame.save_pgm(\"A1-green.pgm\")\n", "```\n", "\n", "Available installed names are `brightfield`, `green`, `red`, `blue`, and `far_red`. All five channel-control and acquisition paths have been exercised. Because that test did not use a fluorescent reference sample, begin with conservative exposure and use `require_lamp_ready=True` when the instrument has a switchable fluorescence lamp." ] }, { "cell_type": "markdown", "id": "well-scan-note", "metadata": {}, "source": [ "### Scan named wells\n", "\n", "`scan_wells()` is the simple path for one capture at each requested well. `block_shape=(1, 1)` captures the centered field only." ] }, { "cell_type": "code", "execution_count": null, "id": "well-scan", "metadata": {}, "outputs": [], "source": [ "scan_result = await celigo.scan_wells(\n", " plate,\n", " [\"A1\", \"B2\"],\n", " channel=\"brightfield\",\n", " block_shape=(1, 1),\n", " exposure_ms=1.0,\n", " gain=1.0,\n", ")\n", "[(item.planned.block.label, item.frame.statistics()) for item in scan_result.frames]" ] }, { "cell_type": "markdown", "id": "trigger-diagnostics-note", "metadata": {}, "source": [ "## Inspect and stop\n", "\n", "### Read camera-trigger diagnostics\n", "\n", "These methods inspect the controller's camera synchronization lines. A result of `None` means that this firmware does not expose that input; on the tested controller the integration input reports `None`. This verifies diagnostics, not externally triggered frame acquisition." ] }, { "cell_type": "code", "execution_count": null, "id": "trigger-diagnostics", "metadata": {}, "outputs": [], "source": [ "camera_signals = {\n", " \"busy\": await celigo.request_is_camera_busy(),\n", " \"integrating\": await celigo.request_is_camera_integrating(),\n", " \"trigger_encoder_ticks\": await celigo.request_camera_trigger_encoder_ticks(),\n", "}\n", "camera_signals" ] }, { "cell_type": "markdown", "id": "teardown-note", "metadata": {}, "source": [ "### Stop safely\n", "\n", "Always run this cell, including after an exception. `stop()` aborts controller work, clears analog and digital illumination outputs, closes the camera, and releases FTDI." ] }, { "cell_type": "code", "execution_count": null, "id": "teardown", "metadata": {}, "outputs": [], "source": [ "await celigo.stop()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3" } }, "nbformat": 4, "nbformat_minor": 5 }