Celigo hello world#

This walkthrough uses the PyLabRobot driver directly; no Celigo vendor application is required. Keep the stage and drawer paths clear whenever motion is enabled.

Revvity
Celigo
Basicv1
microscope
fluorescencemicroscopy
APIpylabrobot.revvity.Celigo

Prepare#

Current driver scope#

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.

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.

Connections and configuration#

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.

Physical setup#

  1. Power on the Celigo and clear the stage, objective, and drawer paths.

  2. Connect both USB devices: FTDI 0403:6001 for the controller and Lumenera 1724:0645 for the camera.

  3. If more than one matching FTDI is attached, find the controller’s local topology with lsusb -t and set usb_address below.

  4. Copy the instrument’s ConfigFiles directory and identify the matching plate model in pylabrobot.resources.

Configure the driver#

Imports#

The public device API is Celigo. It owns the camera internally, and a normal PyLabRobot Plate is assigned after construction.

from pathlib import Path

from pylabrobot.revvity import Celigo, CeligoConfig
from pylabrobot.resources.corning.plates import cor_96_wellplate_360uL_Fb

Load this instrument’s configuration#

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.

config_root = Path("/path/to/Celigo/ConfigFiles")
lucam_sdk = Path("/path/to/liblucamapi.so")
usb_address = "3-2"  # local <bus>-<port>[.<port>...] from lsusb/pyusb

config = CeligoConfig.from_install(str(config_root))
plate = cor_96_wellplate_360uL_Fb(name="imaging_plate")
celigo = Celigo(
  config=config,
  usb_address=usb_address,
  lucam_sdk=str(lucam_sdk),
)
celigo.set_plate(plate)

Initialize and verify#

Connect and initialize#

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.

await celigo.setup()
celigo.controller_info, await celigo.request_detected_motor_addresses()

Run the read-only self-test#

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.

self_test_report = await celigo.run_self_test()
self_test_report.passed, self_test_report.failures, self_test_report.checks

Check camera geometry#

Setup should make these equal by applying and verifying the centered native ROI. Calibrated acquisition also checks every returned frame.

actual_format = (celigo.camera.width, celigo.camera.height)
expected_format = (
  celigo.config.calibration.image_width_pixels,
  celigo.config.calibration.image_height_pixels,
)
actual_format, expected_format

Re-home the mechanisms#

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.

home_positions = {
  "z": await celigo.z_axis.home(),
  "x": await celigo.x_axis.home(),
  "y": await celigo.y_axis.home(),
  "filter": await celigo.dichroic_filter.home(),
}
home_positions

Load the sample#

Open the drawer#

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.

await celigo.open_drawer()

Seat the plate#

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.

Close the drawer to A1#

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.

await celigo.close_drawer(well="A1")

Acquire images#

Set a conservative brightfield exposure#

The live camera retained settings across reopen. One millisecond at gain 1 was unsaturated during the first hardware check; tune this for the sample.

await celigo.set_camera_exposure_and_gain(
  exposure_ms=1.0,
  gain=1.0,
  restart_camera_stream=True,
)

Acquire one calibrated brightfield image#

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.

result = await celigo.acquire(
  "A1",
  "brightfield",
  exposure_ms=1.0,
  gain=1.0,
)
result.z_mm, result.galvo_hardware_voltages

Let the instrument choose an exposure#

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.

auto_exposed = await celigo.acquire(
  "A1",
  "brightfield",
  gain=1.0,
  machine_auto_exposure=True,
)
auto_exposed.frame.exposure_ms, auto_exposed.frame.statistics()

Save and inspect the picture#

PGM preserves the monochrome pixels without adding an image dependency.

result.frame.save_pgm("A1-brightfield.pgm")
result.frame.statistics(), result.frame.sharpness(sample_step=8)

Run image autofocus when the sample has visible structure#

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.

focused = await celigo.acquire(
  "A1",
  "brightfield",
  exposure_ms=1.0,
  gain=1.0,
  autofocus="image",
)
focused.frame.save_pgm("A1-brightfield-focused.pgm")
focused.focus.z_mm, focused.focus.score

Change filters and acquire another channel#

The channel name selects the installed dichroic position, illumination output and intensity, galvo offsets, and calibrated Z correction. For example:

result = await celigo.acquire(
  "A1",
  "green",
  exposure_ms=1.0,
  gain=1.0,
  autofocus="image",
)
result.frame.save_pgm("A1-green.pgm")

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.

Scan named wells#

scan_wells() is the simple path for one capture at each requested well. block_shape=(1, 1) captures the centered field only.

scan_result = await celigo.scan_wells(
  plate,
  ["A1", "B2"],
  channel="brightfield",
  block_shape=(1, 1),
  exposure_ms=1.0,
  gain=1.0,
)
[(item.planned.block.label, item.frame.statistics()) for item in scan_result.frames]

Inspect and stop#

Read camera-trigger diagnostics#

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.

camera_signals = {
  "busy": await celigo.request_is_camera_busy(),
  "integrating": await celigo.request_is_camera_integrating(),
  "trigger_encoder_ticks": await celigo.request_camera_trigger_encoder_ticks(),
}
camera_signals

Stop safely#

Always run this cell, including after an exception. stop() aborts controller work, clears analog and digital illumination outputs, closes the camera, and releases FTDI.

await celigo.stop()