Scan specifications and planning#

A ScanSpec contains geometry, captures, and autofocus policy. celigo.plan(spec) compiles it offline; await celigo.execute(plan) performs the inspected operations.

from pathlib import Path

from pylabrobot.resources.corning.plates import cor_96_wellplate_360uL_Fb
from pylabrobot.revvity import (
  Capture,
  Celigo,
  CeligoConfig,
  ScanEstimateModel,
  ScanRegion,
  ScanSpec,
)

Configure without connecting#

Planning reads the installed calibration but does not connect to or move the instrument.

config_root = Path("/path/to/Celigo/ConfigFiles")
config = CeligoConfig.from_install(str(config_root))
celigo = Celigo(config=config)

Cover physical bounds#

full_coverage() chooses the frame grid and groups it into coarse-stage blocks that fit the calibrated galvo reach.

region = ScanRegion.from_bounds_mm(left=5, top=5, right=122, bottom=81)
coverage_spec = ScanSpec.full_coverage(
  region,
  channel="brightfield",
  exposure_ms=1.0,
)
coverage_plan = celigo.plan(coverage_spec)
print(coverage_plan)
first_block = coverage_plan.blocks[0]
first_frame = coverage_plan.frames[0]
{
  "block_stage_mm": (first_block.stage_x_mm, first_block.stage_y_mm),
  "block_shape": first_block.block_shape,
  "frame_sample_mm": (
    first_frame.position.sample_x_mm,
    first_frame.position.sample_y_mm,
  ),
  "channel": first_frame.capture.channel,
}

Scan physical points#

Points need no enclosing bounds or labels. block_shape=(columns, rows) may be any positive shape within the calibrated per-stage reach.

point_spec = ScanSpec.points(
  [(25.0, 20.0), (63.5, 43.0), (102.0, 66.0)],
  block_shape=(2, 3),
  channel="brightfield",
)
point_plan = celigo.plan(point_spec)
[(block.center_x_mm, block.center_y_mm) for block in point_plan.blocks]

Sample reproducible random blocks#

The seed fixes the chosen physical blocks. Non-overlapping blocks are the default.

random_spec = ScanSpec.random(
  region,
  count=10,
  block_shape=(4, 4),
  seed=42,
  channel="brightfield",
)
random_plan = celigo.plan(random_spec)
random_plan.stage_positions_mm

Scan named wells#

ScanSpec.wells() converts names once to labeled physical centers. The resulting specification does not retain the plate.

plate = cor_96_wellplate_360uL_Fb(name="imaging_plate")
well_spec = ScanSpec.wells(
  plate,
  ["A1", "B2", "C3"],
  block_shape=(2, 3),
  channel="brightfield",
  exposure_ms=1.0,
  gain=1.0,
  autofocus="image",
)
well_plan = celigo.plan(well_spec)
[(block.label, block.block_shape) for block in well_plan.blocks]

For the common single-capture case, skip the explicit specification:

async def scan_a1_and_b2():
  return await celigo.scan_wells(
    plate,
    ["A1", "B2"],
    channel="brightfield",
    block_shape=(1, 1),
    exposure_ms=1.0,
    gain=1.0,
  )

Plan multiple captures#

Use Capture when every position needs more than one channel. Capture settings become part of the plan, and the coarse stage moves once per block.

multichannel_spec = ScanSpec.wells(
  plate,
  ["A1", "B2"],
  block_shape=(2, 3),
  captures=[
    Capture(channel="brightfield", exposure_ms=1.0, gain=1.0),
    Capture(channel="green", exposure_ms=20.0, gain=2.0),
  ],
  autofocus="image",
)
multichannel_plan = celigo.plan(multichannel_spec)
print(multichannel_plan)

Use measured throughput#

Exposure times come from the captures. Supply only instrument overhead and storage assumptions.

estimate_model = ScanEstimateModel(
  seconds_per_frame=0.35,
  seconds_per_stage_position=2.0,
  seconds_per_autofocus=5.0,
  bytes_per_pixel=2,
)
estimated_plan = celigo.plan(multichannel_spec, estimate_model=estimate_model)
estimated_plan.estimated_duration, estimated_plan.estimated_storage_bytes

Execute an inspected plan#

The guarded cell below is the only hardware operation in this notebook. execute() accepts no geometry or capture overrides.

RUN_HARDWARE = False

if RUN_HARDWARE:
  await celigo.setup()
  try:
    scan_result = await celigo.execute(multichannel_plan)
  finally:
    await celigo.stop()

  first_result = scan_result.frames[0]
  print(
    first_result.planned.block.label,
    first_result.planned.capture.channel,
    first_result.actual_stage_mm,
    first_result.actual_z_mm,
  )