Skip to content

fix: refuse to start a run on an uncalibrated embryo - #154

Merged
pskeshu merged 1 commit into
gently-project:developmentfrom
pskeshu:fix/calibration-preflight
Sep 5, 2026
Merged

fix: refuse to start a run on an uncalibrated embryo#154
pskeshu merged 1 commit into
gently-project:developmentfrom
pskeshu:fix/calibration-preflight

Conversation

@pskeshu

@pskeshu pskeshu commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Judgement calls, flagged for reversal

  1. Refuse rather than warn, with allow_uncalibrated=true as an explicit escape. Someone who means it can say so; the point is that they have to say it. A warning in a toast is not a decision anyone records.
  2. Fit quality is not judged. r_squared is hardcoded to 0.85 on the vision-guided path (calibration_tools.py:903), so it is not currently a measurement — a threshold against a literal would be theatre. Presence is checked; quality is left to the operator. Worth its own issue.
  3. embryo_id on acquire/volume is optional. Manual-mode snapping images the current stage position with no embryo in mind, which is legitimate and has nothing to check against. Operate's single mode now sends it, so that path is covered.

What was wrong

Nothing checked. POST /api/devices/timelapse/start validated interval_seconds > 0 and that the embryo ids existed, and started.

The acquisition layer did not fail either. It invented the geometry, and said so in a comment:

# Get calibration parameters (use defaults if not calibrated)
galvo_amplitude = cal.get("galvo_amplitude", 0.5)
galvo_center    = cal.get("galvo_center", 0.0)
piezo_amplitude = cal.get("piezo_amplitude", 25.0)
piezo_center    = cal.get("piezo_center", 50.0)
slope           = cal.get("slope_um_per_deg", 100.0)

So an adaptive timelapse could run to completion on embryos that had never been calibrated, on five literals, and report success. The resulting volumes are indistinguishable from real ones — which is the whole problem. A refusal gets noticed; silently invented scan geometry does not.

It also inverted the ordering stated out loud on 2026-08-07. Ryan: "the main thing is just making sure that we can get the calibration to work". Kesavan: "Calibration has to work. Embryo navigation has to work. Then timelapse setup has to work." The workflow has a hard dependency; now the code has one too.

The gate

gently/harness/calibration_gate.py holds the single predicate, so the answer cannot differ between the surface an operator drives and the tool an agent calls. Calibrated means a finite non-zero slope_um_per_deg — the field every successful fit writes (calibration_tools.py:891) and the number the scan geometry derives from.

Guarded: timelapse/start, operate/run-tactic, acquire/volume, and the acquire_volume agent tool. Server-side on purpose — a check in operate.js is a check the agent walks past. 409, not 400: the request is well formed, the instrument is not in a state to honour it.

This also closes half of audit finding 5: acquire/volume previously received no embryo id at all, so it imaged wherever the stage happened to be and could not have checked anything.

Tests

  1. The predicate against partial calibrations (the invented defaults are not a fit), zero and junk slopes, a missing attribute. The routes against the refusal naming only the bad embryos, the override, embryo_ids: null meaning the whole roster, the ungated no-id case, and that a bad interval is still a 400 rather than masked by the gate.

Refs docs/devices-tab-audit.md.

Nothing checked. `POST /api/devices/timelapse/start` validated
`interval_seconds > 0` and that the embryo ids existed, and started.

The acquisition layer did not fail either. It invented the geometry, and said
so in a comment:

    # Get calibration parameters (use defaults if not calibrated)
    galvo_amplitude = cal.get("galvo_amplitude", 0.5)
    galvo_center    = cal.get("galvo_center", 0.0)
    piezo_amplitude = cal.get("piezo_amplitude", 25.0)
    piezo_center    = cal.get("piezo_center", 50.0)
    slope           = cal.get("slope_um_per_deg", 100.0)

So an adaptive timelapse could run to completion on embryos that had never
been calibrated, on five literals, and report success. The resulting volumes
are indistinguishable from real ones, which is the whole problem — a refusal
gets noticed, silently invented scan geometry does not.

It also inverted the ordering the team stated out loud on 2026-08-07. Ryan:
"the main thing is just making sure that we can get the calibration to work".
Kesavan: "Calibration has to work. Embryo navigation has to work. Then
timelapse setup has to work." The workflow has a hard dependency; now the code
has one too.

`gently/harness/calibration_gate.py` holds the single predicate, so the answer
cannot differ between the surface an operator drives and the tool an agent
calls. Calibrated means a finite non-zero `slope_um_per_deg` — the field every
successful fit writes (`calibration_tools.py:891`) and the number the scan
geometry is derived from.

Guarded: `timelapse/start`, `operate/run-tactic`, `acquire/volume`, and the
`acquire_volume` agent tool. Server-side on purpose: a check in operate.js is
a check the agent walks past. 409 rather than 400 — the request is well formed,
the instrument is not in a state to honour it.

JUDGEMENT CALLS, flagged for reversal:

1. **Refuse rather than warn**, with `allow_uncalibrated=true` as an explicit
   escape. Someone who means it can say so; the point is that they have to say
   it. A warning in a toast is not a decision anyone records.
2. **Fit quality is not judged.** `r_squared` is hardcoded to 0.85 on the
   vision-guided path (`calibration_tools.py:903`), so it is not currently a
   measurement — a threshold against a literal would be theatre. Presence is
   checked; quality is left for the operator. Worth its own issue.
3. **`embryo_id` on `acquire/volume` is optional.** Manual-mode snapping images
   the current stage position with no embryo in mind, which is legitimate and
   has nothing to check against. Operate's single mode now sends it, so that
   path is covered.

Which incidentally closes half of the audit's finding 5: the route previously
received no embryo id at all, so it imaged wherever the stage happened to be
and could not have checked anything even if it had wanted to.

21 tests: the predicate against partial calibrations, zero and junk slopes, and
the routes against the refusal, the override, the null-roster case, and that a
bad interval is still a 400 rather than masked by the gate.

Refs the audit in docs/devices-tab-audit.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pskeshu
pskeshu merged commit 398cbb0 into gently-project:development Sep 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant