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,
)