Skip to content

feat(rf-commissioning): implement the cavity characterization phase backend - #285

Open
lisazacarias wants to merge 3 commits into
slaclab:mainfrom
lisazacarias:feat/cavity-char-phase
Open

feat(rf-commissioning): implement the cavity characterization phase backend#285
lisazacarias wants to merge 3 commits into
slaclab:mainfrom
lisazacarias:feat/cavity-char-phase

Conversation

@lisazacarias

@lisazacarias lisazacarias commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What this changes

Implements the backend for the Cavity Characterization commissioning phase: probe
calibration and loaded-Q measurement, following Ryan's outline. No UI — the
existing placeholder display already covers CAVITY_CHAR, so this lands and can
be exercised on its own.

Read phases/cavity_char.py top to bottom; the module docstring states what is
delegated and why, and the step bodies are meant to be short enough to scan.

Operator-visible

Not yet, in practice. CAVITY_CHAR still shows the generic placeholder display,
so nothing new appears on screen until the phase UI is built. The behaviour this
adds becomes visible then.

One change does reach existing records: a characterization whose loaded Q falls
outside the expected window for that cavity is now recorded as complete but not
passed
. Previously any characterization that produced numbers was recorded as
passed regardless of the value.

Scope

493 changed lines excluding tests — over the 400 target.

474 of those are the single new module phases/cavity_char.py. The remaining ~19
are the loaded_q_in_tolerance field it needs and one export line.

Why it is not split further: the file is one phase's seven-step sequence, and the
steps share the measured state they hand between each other. Splitting it would
mean landing a phase that measures but cannot record, or records but cannot
measure — a half-phase that the workflow would happily let an operator start.
The alternative shape, landing the module with some steps stubbed, moves the same
review burden into a second pass without reducing it.

This is one of a series. #284 (25 lines) went first because the
loaded-Q window it fixes is what this phase flags against.

Decisions worth recording

Wrap Cavity, not SetupCavity. Cavity already implements the whole
measurement: start_characterization(), the characterization_running /
characterization_crashed predicates, calculate_probe_q(), push_loaded_q(),
push_scale_factor(), and the per-cavity-class measured_loaded_q_in_tolerance.
Auto setup's request_characterization() is a twelve-line wrapper over the same
code. Going through SetupCavity instead would drag in request-flag PVs and its
progress reporting, neither of which belong in a gated, operator-driven phase —
so this wraps Cavity at the same level auto setup does. The module docstring
says the phase is meant to stay thin, so that a step body growing past a few
lines of orchestration reads as a warning that something is being duplicated.

Read and push are separate steps. Cavity.characterize() bundles start, wait
and push into one blocking call, and finish_characterization() pushes loaded Q
and scale factor automatically when they are in tolerance. That is right for
unattended setup and wrong here: Ryan's outline asks that the operator be able to
push and save the values, which means seeing them first. So the phase drives the
same shared code at a lower level and stops between measuring and pushing.
read_results touches nothing on the cavity — there is a test asserting the push
methods are never called during it.

Out of tolerance is flagged, not blocked. The measurement succeeded and the
operator needs to see it; whether to push a marginal loaded Q is their judgment.
The record carries the flag either way.

passed now requires the tolerance flag. CavityCharacterization.passed was
is_complete, so a characterization that measured an out-of-range loaded Q
recorded as passed — which would have made "flag loaded Q" cosmetic. The new
loaded_q_in_tolerance field is recorded rather than recomputed because the
limits live on the Cavity object and a record has no access to one. passed is
written as is not False so records predating the field keep their previous
result instead of retroactively failing.

Prerequisite recency warns rather than gates. Characterizing a freshly
calibrated and freshly tuned cavity is best practice, but the workflow already
enforces that SSA_CHAR and FREQUENCY_TUNING ran, and re-driving hardware is
not this phase's call. It reads their timestamps off the record and notes staleness
in the step message.

Two of Ryan's items are left open rather than guessed:

  • Drive level. The outline asks for 10% on 7 kW SSAs and 15% on 3.8 kW ones.
    There is no SSA power rating anywhere in the hardware model — no rated-power PV,
    and fwd_power_lower_limit (3000 / 500) is a calibration floor, not a rating.
    Guessing a class from it would set drive on a cold cavity from an inference. The
    phase starts from the existing SAFE_PULSED_DRIVE_LEVEL and accepts an operator
    override, which the outline wants regardless. The reasoning is recorded on
    CavityCharLimits. Needs an answer: is there a PV that reports SSA power, or
    is the split known from the cryomodule?
  • The characterization plot. CAV:AWF, FWD:AWF and REV:AWF already exist on
    Cavity; drawing them belongs with the phase UI.
  • Probe Q cannot be read back or pushed. QPROBE_CALC1.PROC triggers the
    calculation and is the only probe-Q PV in the codebase — there is no value
    record to read the result from, and no PUSH_QPROBE (only PUSH_QLOADED and
    PUSH_CAV_SCALE exist). The phase triggers the calculation and leaves
    probe_q empty on the record rather than fabricating a number. The outline
    asks for probe Q to be displayed and pushed, so those PV names are presumably
    known to the SRF group and simply missing from the hardware model — what are
    they?
    A test asserts the readback is absent, so it will fail the day the PV
    is added and point at the code to wire up.

Learning reviewer

@hmarts9read_results and push_results are where this phase deliberately
differs from Cavity.characterize(). Worth checking that the review-then-push
split matches how you would actually want to work through a characterization, and
whether anything you rely on is missing from what gets recorded. No approval
needed.

Testing

32 tests, weighted toward delegation and boundaries rather than arithmetic, since
the arithmetic lives in Cavity:

  • the shared implementation is actually called (start_characterization,
    calculate_probe_q, the tolerance check)
  • nothing is pushed during read_results, and pushing before measuring fails
  • an out-of-tolerance result is recorded as complete-but-not-passed
  • a nonsense drive level (0, negative, 99, a string, None) is refused and the
    cavity's drive is left untouched — this writes to a cold cavity
  • the wait step times out rather than hanging on a characterization that never
    finishes
  • dry_run touches no hardware
  • finalizing after a restart keeps values that earlier steps stored
  • probe Q is left empty rather than invented, and the missing readback is
    pinned by a test against the real Cavity

Worth a reviewer's own check: drive the phase against sc-sim and confirm the
loaded Q and scale factor read back as expected. Probe Q will be empty — see the
open item above; that is the behaviour, not a bug in the run.

  • pytest passes
  • Coverage still clears 80%
  • Checked against simulation (PYDM_DEFAULT_PROTOCOL=fake / sc-sim) where applicable

…ackend

Probe calibration and loaded-Q measurement for the CAVITY_CHAR commissioning
phase, following Ryan Porter's outline. No UI — the existing placeholder display
covers CAVITY_CHAR, so this can land and be exercised on its own.

Deliberately thin. Cavity already implements the measurement:
start_characterization(), the characterization_running / _crashed predicates,
calculate_probe_q(), push_loaded_q(), push_scale_factor(), and the
per-cavity-class tolerance check measured_loaded_q_in_tolerance. Auto setup's
request_characterization() is a twelve-line wrapper over the same code and this
phase is a similarly thin one; the module docstring says so, so that a step body
growing past a few lines of orchestration reads as a warning sign.

One deliberate divergence from Cavity.characterize(): that bundles start, wait
and push into a single blocking call, which suits unattended setup.
Commissioning needs the operator to see loaded Q, scale factor and probe Q
before anything is written to the cavity, so read_results and push_results are
separate steps and nothing reaches the hardware until the operator confirms.
An out-of-tolerance loaded Q is flagged, not blocked — whether to push it is
their call.

CavityCharacterization gains loaded_q_in_tolerance, and passed now requires it.
Previously passed was just is_complete, so a characterization that measured an
out-of-range loaded Q recorded as passed — which would have made Ryan's
"flag loaded Q" requirement cosmetic. Written as "is not False" so records
predating the field keep their previous result instead of retroactively failing.

Two items from the outline are deliberately left open rather than guessed:

- Drive level. The outline asks for 10% on 7 kW SSAs and 15% on 3.8 kW ones, but
  there is no SSA power rating in the hardware model — no rated-power PV, and
  fwd_power_lower_limit is a calibration floor, not a rating. The phase starts
  from the existing safe pulsed level and accepts an operator override, which
  the outline asks for anyway. Needs the SSA question answered.
- The characterization plot. CAV/FWD/REV:AWF waveform PVs already exist on
  Cavity; drawing them belongs with the UI work.

Prerequisite recency (SSA calibration and tuning) is a warning, not a gate: the
workflow already enforces that those phases ran, and re-driving hardware from
here is not this phase's decision. It reads their timestamps off the record.

Step results are stored on the record as each step produces them, the same
pattern as the tuning phase and for the same reason — phase_history is
in-memory only and does not survive a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

🟡 PR size: 503 lines — over the 400-line target

+502 / -1 in production code.
1 test and asset files excluded.

Largest files Change
src/sc_linac_physics/applications/rf_commissioning/phases/cavity_char.py +484/-0
src/sc_linac_physics/applications/rf_commissioning/models/data_models.py +15/-1
src/sc_linac_physics/applications/rf_commissioning/phases/__init__.py +3/-0

Target is 400 lines excluding tests, hard stop around 800 — see
"Scope and shipping" in CLAUDE.md. If this PR is over, the description
should say why it could not be split. Advisory only; this check never
fails the build.

lisazacarias and others added 2 commits August 17, 2026 15:14
_read_probe_q() looked for cavity.probe_q / cavity.measured_probe_q. Neither
exists. QPROBE_CALC1.PROC triggers the calculation and is the only probe-Q PV in
the codebase — there is no value record to read the result from, and no
PUSH_QPROBE either; only PUSH_QLOADED and PUSH_CAV_SCALE exist.

So probe Q was always going to be None on real hardware while the code read as
though it were measured. It now returns None explicitly, with the gap documented
in the module docstring and at the call site.

The test suite hid this. The mock cavity set probe_q = 2.0e9, so the assertion
that the record carried a probe Q passed against an API that does not exist. The
fixture no longer invents the attribute, and two tests pin the gap instead: one
that the record leaves probe_q empty, and one asserting the real Cavity has no
probe-Q readback — which will fail, deliberately, the day the PV is added, so
whoever adds it is pointed straight at the code to wire up.

Ryan's outline asks for probe Q to be displayed and pushed, so those PVs are
presumably known to the SRF group and simply absent from the hardware model.
Better an empty field on the record than a fabricated number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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