Skip to content

add: live gridded reciprocal-space volume (4/4) - #139

Open
pecomyint wants to merge 9 commits into
rsm/03-detector-physicsfrom
rsm/04-live-grid
Open

add: live gridded reciprocal-space volume (4/4)#139
pecomyint wants to merge 9 commits into
rsm/03-detector-physicsfrom
rsm/04-live-grid

Conversation

@pecomyint

Copy link
Copy Markdown
Collaborator

Stack 4 of 4. Base rsm/03-detector-physics. This branch contains the whole stack — check this one out for beamline testing and record its SHA in the test notes.

Draft until validated on real PVs. Three reviewable commits.

Why

Live 3D today is a point cloud with amnesia: a 100-frame, 1M-point ring buffer that overwrites its oldest frame. Scan a region twice and the second pass evicts the first rather than reinforcing it. PR #129 gave us the accumulator concept offline; this brings it live, so a voxel means the same thing during the scan as in the file you reopen later.

Commit 1 — Qt-free volume persistence

The accumulator runs in a pvaccess consumer, and this repo documents that mixing pvaccess with PyQt5 core-dumps (the two-process note in consumers/ioc_rsm_parameter). So the write path had to lose Qt first.

volume_io.py imports no Qt and produces the same layout Workbench already reads. HDF5Writer becomes a plain class with an on_finished callback; HDF5Handler re-wraps it as the signal — the split its own docstring already described. hkl_3d_viewer and area_det_viewer used HDF5Writer directly as a threaded QObject and now use HDF5Handler. log_manager was pulling PyQt5 into everything via LogMixin for one type annotation and one .instance() call; both deferred. A subprocess test asserts the write path stays Qt-free.

Empty voxels become NaN with a coverage volume alongside — Gridder3D leaves un-hit bins at zero, indistinguishable from a measured zero. volume_result_to_metadata used volume.min()/max(), which would have returned [nan, nan] the moment empty voxels became NaN, breaking every colour scale that trusts it; now finite-only.

Coverage does not make a flux-weighted mean recoverable: from sum(I/m) and N you cannot recover sum(I) and sum(m) separately unless the monitor was constant across a voxel's contributions.

gridder_access.py is the one place touching Gridder3D._gdata/_gnorm, avoiding .data (which copies the whole numerator and divides on every access — 134 MB per snapshot at 256³). Its guard pins the semantics, sum and count, not just that the attributes exist.

Commit 2 — Fail-closed metadata binding

If a frame's angles belong to a different frame, every pixel lands in the wrong place and the result still looks like a diffraction volume. There is no visual tell. So a frame whose required metadata is missing, stale, non-finite or duplicated is rejected and counted, never gridded with a substituted value.

Channels are classified: STATIC geometry legitimately arrives once and is latched; REQUIRED_DYNAMIC (circle positions, monitor) must be fresh; OPTIONAL never blocks. Rejections are counted per reason, so "the preview is empty" resolves to a cause. Scope is frame-bound and trigger-latched metadata only — fly-scan interpolation and pulse-ID joins are deliberately not attempted, because guessing at them is the exact mis-binding this prevents.

Commit 3 — Accumulator, session, consumer, dock

Bounds are locked before the first frame: Gridder3D latches fixed_range on first use, and rebinning mid-scan would change what a voxel means. Changing bounds needs a new accumulator — Clear() keeps the latched range.

Two grids over the same accepted samples: the full-resolution one that gets saved, and an independently gridded coarse one for the preview. One extra C call per frame, versus sweeping hundreds of MB of accumulator every snapshot in the process running the hot loop. Coarse shape is chosen per axis to fit 4 MiB while preserving aspect ratio.

Aggregation matches the offline builder exactly — unweighted mean, monitor-normalized per frame. Summing would brighten voxels wherever the scan path sampled more often, an artifact of the trajectory, not the scattering.

Nothing drops silently: out-of-range, non-finite and masked samples are counted separately and shown on screen. Masked pixels are excluded, not zeroed.

HpcRsmGridProcessor is an alternative to HpcRsmProcessor, not an addition — that one attaches three float64 Q arrays to every frame (~100 MB/frame at 2048²); this computes Q where it is consumed and publishes only the preview. It subclasses rather than forks the Q path. Single-instance by nature, so drops are counted and the preview marked incomplete: a drop-aware preview, not a lossless record. Full-resolution state never crosses PVA; saving happens in the consumer, confined to OUTPUT_PATH, refusing overwrite, and only from stopped so it cannot capture a torn accumulator.

The HKL3D dock adds bounds/resolution, Estimate/Start/Stop/Clear/Save and live counters. Bounds lock while running. Estimate is labelled as observed bounds — later scan motion can still fall outside, which is why the out-of-grid count sits beside it. Uncovered voxels render transparent.

The assertion that matters

Live and offline agree for identical frames, bounds and resolution — coverage exactly equal, means to 1e-12. build_volume gained an optional fixed_bounds so the comparison is possible at all.

444 tests pass (227 at #129), ruff check src/dashpva/ clean.

Blocked on beam

  • Sustained-rate soak with real PVs; record achievable rate and drop count
  • Confirm which metadata/ca/ key is I0
  • Half-voxel origin visually in Workbench
  • PR 3's axis-0 and tilt-sign gates

Known limits

Single consumer caps throughput; no mid-acquisition snapshot (save requires stop); no full-volume transfer over PVA; assumes shared storage visible to both the consumer and Workbench.

@pecomyint
pecomyint requested a review from Osayi-ANL August 12, 2026 04:23
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from 69b76d2 to 6c8c969 Compare August 12, 2026 04:35

@Osayi-ANL Osayi-ANL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug that I mentioned in PR #137 also prevents me from running DashPVA run and crashes

@pecomyint

Copy link
Copy Markdown
Collaborator Author

The crash you hit here came from the same root cause as #137 — fixed in 2493b8e + 8dc4067 on rsm/02-naxis-issue132, which this branch now contains.

A bad profile no longer makes import dashpva.settings raise, so DashPVA run starts and reports the problem instead of tracebacking. The config tree also no longer corrupts lists or empty tables, which is what produced the bad profile in the first place.

This branch was rebased (it had a duplicate cherry-pick of the fix), so please git pull before re-reviewing. 480 tests pass, ruff clean.

@pecomyint
pecomyint requested a review from Osayi-ANL August 12, 2026 22:12
@Osayi-ANL

Osayi-ANL commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator
  • Version bump from 1.0.5 to 1.1.0 this is a minor update to the HKL setup and the hdf5 writing path -- update pyproject.toml and uv.lock
  • Make apply and save stick to the bottom of the ioc window to not have us scroll all the way down to save -- optional
  • Remove underscore in front of 'Save' in the apply save button in the HKL setup GUI
  • Fix 'IOC out of sync' issue when removing PV's from the list in the HKL Setup and hitting apply save it seems to cause an error
  • Although we add a label to each PV in the HKL Setup when saving, the default SAMPLE_CIRCLE_AXIS_1, SAMPLE_CIRCLE_AXIS_2, and SAMPLE_CIRCLE_AXIS_3 in the HDF5 file still gets saved. I assume the intended hierarchy of the save tree structure is LABEL or RECORD NAME if label is not available, but it seems as though it saves within the SAMPLE_CIRCLE_AXIS. What is the intended save structure? I would suggest making the save structure naming convention within the HDF5 match the labels or record name in the HKL Setup, instead of just the default structure name.
hierchy 132ioc param order

pecomyint added a commit that referenced this pull request Aug 14, 2026
Requested by Osayi-ANL on #139: the HKL Setup editor and the HDF5
writing path both changed enough to warrant a minor bump ahead of
merging the RSM stack.
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from 5cf7012 to d575f4c Compare August 14, 2026 20:12
@pecomyint

Copy link
Copy Markdown
Collaborator Author

Went through each item:

  • Version bump — done, 1.0.51.1.0 in pyproject.toml + uv.lock (9d77d11).
  • Apply & Save scrolling out of view — fixed. It was inside the same QScrollArea as the axis tables; moved the Reload/Retry/Apply & Save row into the window's own layout below the scroll area, so it's always visible (91924cf).
  • Underscore in front of "Save" — found the cause: the button was built as QPushButton("Apply & Save"), a single unescaped & followed by a space. Qt's mnemonic parser consumes the & and assigns the mnemonic to the next character — here a space — which some Qt styles render as a literal underscore glyph instead of an invisible underline. Escaped it to "Apply && Save" (91924cf).
  • "IOC out of sync" when removing a PV and hitting Apply & Save — traced the two places this can legitimately fire (_activate_snapshot/_mark_out_of_sync): either a genuine RAW_CONFIG != snapshot mismatch after reload, or the child IOC process timing out/failing on restart. Both already surface the underlying exception text in the popup and the inline notice, but I don't have that text from your report, so I can't tell which one you hit or write a real fix without guessing. Could you paste the exact error shown in the "IOC out of sync" dialog, and whether it happens on every removal or only sometimes?
  • HDF5 saves under SAMPLE_CIRCLE_AXIS_1/2/3 instead of the label — the numbered group name is intentional (it's the stable, beamline-generic layout the whole canonical refactor is built around), but the code already writes LABEL, RECORD_NAME, ANGLE_UNITS, and a convenience NAME dataset inside each numbered group (hkl_axes.canonical_axis_metadata, since 51cfa30). That only populates when the profile has IOC_RSM_PARAMETER — on a legacy profile (no IOC_RSM_PARAMETER yet, e.g. 19-ID/4-ID/8-ID/SSRL/11-ID configs), there's no LABEL to write, so the group is bare. If the profile in your screenshot was canonical and still missing LABEL/NAME, let me know which config and I'll dig further — otherwise this is working as designed.

Current tip is 9d77d11. Re-requesting your review.

@Osayi-ANL Osayi-ANL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Change these '{"CENTER_CHANNEL_PIXEL": [300.0, 300.0], "DISTANCE": 400.644, "PIXEL_DIRECTION_1": "z-", "PIXEL_DIRECTION_2": "x-", "SIZE": [28.38, 28.38], "UNITS": "mm"}' to a single input in the GUI. You don't have to change the dict structure it was saved in just how we grap the values.

  • Sample Orientation input in the IOC_RSM_PARAMETER is not information that is used in 6IDB. We already have the Primary, Inplane, Sample directions which we don't necessarily use, but can be used in place of the input. So it is just redundant.

  • The HKL Setup does not populate based on the profile selected instead it selects the profile toggled in the setup config. Try opening the config and viewing an selected profile. Then open the HKL Setup. It opens that profile even though it is not selected.

pecomyint added a commit that referenced this pull request Aug 14, 2026
…DETECTOR_SETUP into per-value fields

Apply & Save and Reload both ran one check -- RAW_CONFIG != snapshot -- and
reported every failure as "the IOC did not activate that snapshot", with a
Retry button offered unconditionally. Three unrelated situations were being
described with one wrong sentence, in vocabulary no beamline user can act on.

Reproduced the report from #139: the editor is pinned to the profile it opened
with (the IOC builds its record database from that profile at launch), but
app_settings.reload() re-resolves the locator on every call, so selecting a
different profile in another window makes the two diverge. The save had
succeeded; nothing was corrupted; removing PVs was incidental. Retry could
never have worked because the target had moved.

The three cases are now distinguished and worded for someone who knows
diffraction, not this codebase:

- ActiveProfileChanged -- names both profiles ("HKL Setup is editing 'X', but
  'Y' is now active"), states that the angle IOC is still serving X's PVs so
  live readers are unaffected, tells the user to reopen HKL Setup, and hides
  Retry because it cannot succeed.
- ProfileContentMismatch -- the same profile came back with different values,
  i.e. a concurrent writer. Lists the differing settings in HKL terms
  ("Detector circle 1 - POSITION: saved ..., read back ...") under Show
  Details, capped at 15 with an overflow line. Retry still offered.
- Anything restart_ioc raises -- the IOC genuinely failed to restart. Says the
  PVs still carry the previous values and nothing was rolled back.

Profiles are named, never shown as database row ids.

Detecting a switch needs the *resolved* profile, not app_settings.LOCATOR,
which is None whenever the profile is auto-detected from the database
selection -- comparing locators reports "unchanged" across exactly the switch
this is meant to catch. Adds ConfigSource.resolved_identity() for that.

Two adjacent fixes found on the way:

- set_locator(None) wrote the literal string 'None' into the environment and
  the state file, so the next read returned it as a real, unresolvable
  locator. It now clears both, and _parse_locator treats a stale 'None' as
  unset so already-poisoned state files recover.
- The out-of-sync text claimed "the profile is saved" even on the Reload path,
  which never saves.

Also #139: DETECTOR_SETUP was a single line edit holding raw JSON. It is now
one input per value, generated from DETECTOR_SETUP_FIELDS so the optional
entries (PIXEL_SIZE, ROI, BINNING, DETROT/TILT, per-field units) each get a
row with a placeholder showing the expected shape. Blank means absent. The
stored table is untouched, including int-vs-float types, and unrecognised keys
are carried through rather than dropped.
@pecomyint

Copy link
Copy Markdown
Collaborator Author

Thanks for the error text — that was the whole ballgame. Reproduced it exactly, and it's not what either of us assumed.

The "IOC out of sync" error

It was a profile switch, not a failed save, and nothing to do with removing PVs.

The tell is in your own message: both the prefix and the record names differ — os:Nu:* vs 6idb1:m29_RBV:*. A single profile failing to round-trip can't produce that; I tested that path first and it round-trips clean. Two different profiles can. I built the two-profile scenario against a real SQLite database and the first five lines of my output match yours character for character, PV names included.

What happened: HKL Setup binds to whichever profile was active when it opened, because the IOC builds its EPICS record database from that profile at launch. But settings.reload() re-resolves the active profile on every call. If something selects a different profile while the editor is open — another DashPVA window, a Workflow profile change — the two diverge, and the old code reported that as "the IOC did not activate that snapshot" and offered a Retry that could never succeed.

Your save went through fine. Nothing was corrupted and no rollback was needed. Do you remember having a second window open, or switching profiles in Workflow around then? That would confirm it end to end.

What changed (b500b8b)

Three genuinely different situations were sharing one wrong message. They're now separate, and written for someone reading them at a beamline rather than in the source:

A different profile is now active — names both profiles, says the angle IOC is still serving the original profile's PVs so anything reading them right now is unaffected, and tells you to reopen HKL Setup. Retry is hidden, because it cannot work.

HKL Setup is editing '6IDB motors', but '6IDB canonical' is now the active profile — most likely selected in another DashPVA window.

Nothing was lost: your settings for '6IDB motors' are saved, and the angle IOC is still serving '6IDB motors' PVs, so anything reading them right now is unaffected.

To work on '6IDB canonical', close HKL Setup and open it again. To stay on '6IDB motors', make it the active profile again in the Workflow window.

Saved settings don't match what was read back — the same profile came back with different values, i.e. something else wrote to it concurrently. This one now lists the differing settings in HKL terms under Show Details:

Detector circle 1 - POSITION: saved '6idb1:m29_RBV:Position', read back 'os:Nu:Position'
Detector circle 2 - AXIS_NUMBER: saved '6idb1:m18_RBV:AxisNumber', read back 'os:Delta:AxisNumber'

The angle IOC did not restart — the genuine restart failure, which is the only one where Retry makes sense. Says the PVs still carry the previous values and nothing was rolled back.

Profiles are named throughout, never shown as database row ids.

One thing worth flagging: the reloaded=… saved=… detail in your report doesn't exist in any branch — I grepped rsm/02 through rsm/04, main, and my working tree. You're running a locally patched build, so it's worth re-syncing before the next test. That detail was genuinely useful though, which is why the equivalent is now shipped.

Two adjacent bugs fell out of tracing this:

  • set_locator(None) wrote the literal string 'None' into the environment and the state file, so the next read came back with 'None' as a real, unresolvable locator. Production always passes a real id so it never bit us, but the tests use it as a reset and it silently wasn't resetting.
  • The out-of-sync text claimed "the profile is saved" even on the Reload path, which never saves anything.

Your two new items

DETECTOR_SETUP as separate inputs — done. It's one input per value now, generated from DETECTOR_SETUP_FIELDS, so the optional entries (PIXEL_SIZE, DETECTOR_SHAPE, ROI, BINNING, DETROT/TILT/TILTAZIMUTH, per-field units) each get their own row with a placeholder showing the expected shape — row, column, degrees, and so on. Blank means the key is simply absent from the profile, which is how the optional ones are meant to be expressed. Per your note the stored table is unchanged, right down to int-vs-float types, and any key this build doesn't recognise is carried through rather than dropped.

SAMPLE_ORIENTATION — I'd push back on removing this one. It isn't redundant with the primary/inplane/normal directions; it's passed straight through as sampleor= to the xrayutilities QConversion (utils/rsm_geometry.py:310), where it sets which frame the sample azimuth is referenced to. The three direction vectors define the reference frame's axes; sampleor says how the sample circle stack is mounted relative to it. det, sam, and an explicit [xyz][+-] give materially different Q for the same angles.

You're right that 6IDB never needs to change it — but other beamlines in this stack do, and dropping it would silently pin everyone to one convention. If the goal is "stop asking me for something I never touch", I'd rather move it into a collapsed advanced section alongside the other things 6IDB leaves at defaults. Want me to do that?

507 tests pass, ruff clean. Current tip is b500b8bgit pull before re-reviewing.

@pecomyint
pecomyint requested a review from Osayi-ANL August 14, 2026 22:58

@Osayi-ANL Osayi-ANL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think an advanced section or tab to place the 'Sample Orientation' dropdown is necessary to not overstimulate the user. Let's do it that way.

  • The protected_static_values change in 51cfa30 (rsm/02-naxis-issue132) reasserts profile values back onto the IOC on a timer, so caput can't be used from the terminal. We want all PVs assertable from the terminal.

I already approved #137, so I have a patch ready: it removes protected_static_values and the reassert loop. Static records still get set once at IOC startup. They track the ioc value and if the ioc is changed via caput or the gui a save dialog comes up and lists the changes. So caput changes can be detected by the ioc to be applied and saved to the profile.
Do you agree? I think that we should still allow caput's from the terminal to also be saved to profiles.

  • I don't think we need to save the AXIS_NUMBER as a dataset just a scalar is fine since it doesn'tchange

  • There are three ways we save a name associated to a PV(Label, Record Name, and Spec Motor Name). I don't think it needs to be that much maybe removing spec motor name.

@pecomyint

pecomyint commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

@Osayi-ANL — status update after re-reviewing current stacked head 86e2605. Thank you for the fixes added since the original handoff. Peco is taking over the remaining RSM work; this edited checklist records the merge gate and avoids duplicate implementation.

Resolved on the current head

  • Selected-profile inspection no longer changes the active settings locator.
  • Sample Orientation is under Advanced.
  • Dirty-window close uses Save / Discard / Cancel and keeps the editor open on unsuccessful save paths.
  • Detector round-trip and live construction preserve canonical literals, including PIXEL_SIZE without raw SIZE, and overlay only the six IOC-backed live fields. ROI, binning, tilt, detector rotation, pixel size, and frame-axis order remain available without turning all detector fields into PVs.
  • Focused raw absence/type and detector-parity coverage was added.

Remaining takeover blockers

  • Finish raw-baseline IOC adoption. Keep both the exact loaded raw snapshot and normalized semantic baseline. Preserve absent keys, raw types, and unknown extensions across every writable record. Loaded axes need ephemeral stable identities so reorder/rename plus concurrent IOC changes merge against the original axis rather than list position. Apply and close must use the same pending-change calculation, including IOC-only changes; divergent edits remain conflicts and write nothing.
  • Finish legacy HDF5 compatibility. Canonical axes write ordinal scalar AXIS_NUMBER. Legacy axes must retain their configured channel, validate that observations are invariant, and collapse that series to one scalar. Restore the public optional SpecMotorName IOC/HDF5 contract and its regression coverage.
  • Connect the live-grid viewer to the consumer. Reuse control/status channels resolved through settings.py; add a namespaced rsm_grid request/acknowledgement envelope; and replace the viewer-local accumulator with an injectable remote client. Full Q arrays must not cross PVA.
  • Prevent stale geometry and metadata. Bind required dynamic angles and I0 from the current frame only; cache only static fields. Snapshot static geometry on the first accepted frame and stop fail-closed if UB, directions, energy, or detector geometry changes. Invalidate cached area-viewer geometry on static changes and clear stale Q after conversion failure.
  • Make throughput telemetry truthful and bounded. Replace the unbounded seen-ID set with bounded deduplication and a monotonic high-water mark. Report gap events and actual missing frames separately. Use disjoint session counters for binding and processing rejection, expose upstream pvapy misses separately, keep RPC/PVA names and values consistent, and enforce the frame-accounting invariant in tests.
  • Guard memory and concurrency. Apply the shared grid-memory limit before allocation, bound preview payloads, and serialize frame, control, and save access. Save remains stop-only and must not race accumulation.

Restack and gates

@Osayi-ANL
Osayi-ANL marked this pull request as ready for review August 20, 2026 22:30
@Osayi-ANL
Osayi-ANL self-requested a review August 20, 2026 22:34
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from d575f4c to 3b9b10c Compare August 24, 2026 22:03
@pecomyint
pecomyint requested a review from Osayi-ANL August 24, 2026 22:04
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from 3b9b10c to 5b43d9c Compare August 24, 2026 22:11
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from 5b43d9c to 66fb8e7 Compare August 24, 2026 22:14
@pecomyint
pecomyint force-pushed the rsm/03-detector-physics branch from 66fb8e7 to 8682fdf Compare August 24, 2026 22:16
pecomyint and others added 5 commits August 28, 2026 17:49
add: UChicago Argonne BSD open-source license header to all source files
setup GUI, and resolved-profile-identity handling (PR #139)

The branch was restacked onto a new live-gridded-volume architecture and two
reviewed pieces of prior work never made it back: an optional live-PV source
for the UB matrix (backup/rsm-04-live-grid-pre-restack@2db947b) and a
per-field detector-setup form plus a profile-identity fix for Apply & Save's
out-of-sync messaging (@b500b8b). Ported as design reference only -- the
surrounding change-review/CAS/persistence code has all changed since.

IOC_RSM_PARAMETER gains two optional fields, additive to schema version 1:
UB_MATRIX_SOURCE_PV and DETECTOR_SETUP.DISTANCE_SOURCE_PV. Each is validated
as PV-name-only (a numeric string would pass the axis/energy SOURCE_PV
convention's leniency but can never connect to anything, since UB_MATRIX and
DETECTOR_SETUP.DISTANCE already have their own dedicated literal fields).
UB_MATRIX/DISTANCE stay the validated static fallback either way.

At IOC runtime, source_vector_or_fallback/source_scalar_or_fallback poll
through the same cached EpicsPV(auto_monitor=True) mechanism as the existing
per-axis source_value -- but fall back to the stored profile value instead of
NaN on disconnect: a bad UB matrix or distance silently corrupts every
downstream HKL index, unlike a stale axis angle. Logs only on failure/
recovery transitions. _adoptable_records excludes the UB/distance IOC records
from live-caput adoption whenever a source PV is configured, keeping the
existing unconditional adoption when it isn't.

GUI: "UB Matrix (PV or value)" and "Distance (PV or value)" are dedicated
widgets pulled out of the generic key.replace('_',' ').title() calibration
loop (which produced "Ub Matrix"). Detector setup gets its own two-column
grid generated from DETECTOR_SETUP_FIELDS, one row per declared field,
placeholders showing the expected shape, direction-1/direction-2 wording
throughout (never x/y). Both PV-or-value widgets track the last valid literal
live via textChanged, not just on Apply -- otherwise typing a literal then
switching to a PV in the same session would silently discard the just-typed
value and fall back to whatever was last saved. DISTANCE_SOURCE_PV is
excluded from the detector-setup "unknown key" carry-through, or switching
back to a literal would leak the stale PV name back in on save.

The close-time change-review table collapses each PV-or-value pair into one
logical row instead of two independent flattened keys, so a keep/drop
decision can't update only one half of the pair.

Restores the three-way distinction Apply & Save lost: ActiveProfileChanged
(the profile was switched in another window -- no Retry, it can't succeed),
ProfileContentMismatch (same profile, different values -- a concurrent
writer, Retry still offered), and a genuine restart failure. Detecting a
switch needs ConfigSource.resolved_identity() (new), not app_settings.LOCATOR,
which is None under DB auto-detect. The activation-mismatch diff also fixes
a bug where an empty-dict section and an absent key flattened identically,
letting a genuine mismatch report zero differences.

set_locator(None) now clears the env var and state file instead of writing
the literal string "None" into both; _get_effective_locator treats an
already-poisoned "None" as unset so existing state files self-heal.
staleness warning (PR #139)

Start Metadata Associator built its --metadata-channels string from
whatever app_settings.HKL/METADATA_CA/METADATA_PVA already held in memory --
never reloading first, never checking the result actually covers what
required_rsm_channels() needs to compute Q, and only deduping within the
HKL-derived channels rather than across all sources. A profile missing a
required channel would start a half-configured associator with no warning.

run_associator_consumers now reloads app_settings first, then refuses to
start and lists the missing channel names if required_rsm_channels(HKL)
isn't fully covered by the built channel set. _build_metadata_channel_list
returns the ordered URI list and the bare-name set together, rather than
having the validation step re-split the joined ','-delimited string on ','
then '://' -- a free-text METADATA_CA/METADATA_PVA value containing a literal
comma (a plausible Settings-tree typo) split into a fragment with no '://',
raising an uncaught IndexError before the associator could ever start.

_sync_associator_metadata no longer silently stops and restarts a running
associator when its channel set or resolved profile identity drifts --
that could interrupt live acquisition mid-scan, and that decision belongs to
the user. It now shows one non-modal "Metadata configuration changed --
restart required" notice instead. HKL Setup edits IOC_RSM_PARAMETER from a
separate OS process and never calls back into Workflow, so a new ~10s QTimer
polls for drift while an associator is running -- otherwise a config change
made over there would never be noticed.

The notice compares against the last *observed* state and always advances
that baseline on detection, rather than gating on a run-start one-shot flag:
a persisting drift still only notifies once (the next tick already matches
the new baseline), but a later, different drift -- a second profile switch,
or an earlier transient blip clearing -- is still caught. A one-shot-per-run
flag would have permanently swallowed every notice after the first. Stopping
the associator now also closes any lingering notice box instead of leaving
it on screen telling the user to restart something they already stopped.
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.

2 participants