Skip to content

Finish hklpy2 migration, add 4-ID-G fly scanning, fix startup/logging/SPEC bugs - #76

Open
gfabbris wants to merge 20 commits into
mainfrom
beamline_changes
Open

Finish hklpy2 migration, add 4-ID-G fly scanning, fix startup/logging/SPEC bugs#76
gfabbris wants to merge 20 commits into
mainfrom
beamline_changes

Conversation

@gfabbris

Copy link
Copy Markdown
Collaborator

Summary

Beamline-driven changes accumulated over the 2026-2 run at 4-ID-G, plus the
fixes they forced. Three main threads:

  1. Complete the hklpy → hklpy2 migration for the POLAR diffractometers and
    the HKL user utilities.
  2. Add 4-ID-G piezo fly-scanning (SoftGlue Zynq waveform driving + Eiger /
    position-stream integration).
  3. Fix startup, logging, and SPEC-output bugs hit while running.

Closes #70. Closes #71.


1. hklpy2 migration finished

polar_diffractometer_hklpy2.py is now the real polar_diffractometer.py; the
old gi/hkl-based implementation is kept as
polar_diffractometer_obsolete.py for reference. hkl_utils_hklpy2.py is
likewise renamed to hkl_utils.py. devices.yml, _common_startup.py,
id4_raman/startup.py, utilities.py, and the docs pages follow the rename.

Behind the rename:

  • The four geometries are built with hklpy2.diffractometer_class_factory
    (hkl_soleil / APS POLAR) plus a shared DiffractometerMixin carrying the
    table, area-detector, filter, slit, and analyzer components.
  • DeferredEpicsMonochromatorRO — hklpy2's DiffractometerBase.__init__
    reads beam.wavelength/beam.energy while seeding the solver, which with a
    plain EpicsSignalRO blocks for 60 s and breaks make_devices(connect=False)
    when the VDCM IOC is off. The deferred signals return a sane fallback until
    EPICS connects, so the two-phase startup still works with a dead IOC.
  • UBMatrixSignal (_ub_sync) makes sample.UB a subscribable ophyd signal,
    so Sync_UB_Matrix can mirror UB (and the h2/k2/l2 azimuthal extras and
    simulated motor positions) from the main geometry to its _psi companion.
  • Real/pseudo positioners are set to Kind.config | Kind.normal, and
    auxiliary_axis_names drops nested PseudoPositioner sub-devices — hklpy2's
    wh(full=True)/pa() call round(component.position, ...), and .ana
    returns a namedtuple with no __round__.

hkl_utils.py user-facing changes

  • read_diffractometer_config_scan() replaces restore_huber_from_scan(),
    which was still on the hklpy run_orientation_info / restore_sample /
    restore_reflections path. It now uses hklpy2.run_utils.get_run_orientation
    and diffractometer.restore(..., restore_samples=True, restore_extras=True, restore_constraints=True), prompts when the scan holds a different
    diffractometer, and recomputes UB. Closes Restoring hklpy2 setup from scan not working #71.
  • write_config / read_config renamed to
    write_diffractometer_config_file / read_diffractometer_config_file;
    both file and scan restore share a new _prompt_clear_mode() overwrite-vs-append prompt.
  • reset_constraints() fixed — it called _geom_.reset_constraints() /
    _geom_.show_constraints(), neither of which exists on an hklpy2
    diffractometer.
  • New pa_new(): POLAR-specific full printout (orienting reflections with
    their wavelengths, real and reciprocal lattice, U and UB, azimuthal
    reference, constraints, mono energy/wavelength/d-spacing).
  • New analyzer_get(); ca() takes an optional energy.

Analyzer calibration

AnalyzerDevice.set_energy() now calibrates the tth_trans offset alongside
th, calc() prints to 4 decimals and restores both offsets on release, and
the new convert_energy_to_tth_pseudo() / ath_reset_offset() support that.
The analyzer moved to pseudo-motor (th_motor, tth_trans) addressing.

2. Fly scanning at 4-ID-G

  • plans/flyscans.py — new flyscan() plan: 2D snake over the two Jena
    piezo axes, fast X and slow Y driven from SoftGlue RAM waveforms through
    DAC1/DAC2. The module docstring documents the DAC-swing/modulation-summing
    geometry and the position-unit convention.
  • devices/softgluezynq_g_new.py — new SoftGlueZynq device for the ISN
    setup: UpDown counter, pulse-train generator, per-interferometer trackers,
    dual-RAM components, DAC1 mux, threshold trigger, plus the snake-waveform
    builders. Registered as gsgz_isn; coexists with the existing
    softgluezynq_g and reuses the softgluezynq_parts block library.
  • Eiger — external trigger switched from External Gate to External
    Enable
    (gate is for pump-probe); new setup_flyscan_mode(); stage_sigs
    pop of enable made conditional so re-staging does not raise.
  • pos_stream (PositionStreamDevice) registered in devices.yml and
    given CountersMixin with a no-op plot API so it slots into the counters
    machinery without offering fake plot channels.
  • piezo_jena_setup() in shorts.py to check/toggle the per-axis modulation
    input that gates the FPGA analog drive.

3. Bug fixes

  • Cannot start from /gdata (Cannot start bluesky from gdata #70). apsbits resolves its log directory to
    <cwd>/.logs at import time; at the beamline the cwd is the read-only DM
    experiment directory, so os.makedirs() raised PermissionError and aborted
    the import before we could redirect it. The apsbits import now runs from a
    private temp dir, and the fallback path checks os.access(cwd, os.W_OK)
    before choosing <cwd>/.logs. Closes Cannot start bluesky from gdata #70.
  • Stale-handle log spam. Concurrent sessions shared one
    RotatingFileHandler on NFS, so one process rotating the file others held
    open produced [Errno 116] Stale file handle. Logs are now per-session
    (logging.<host>.<pid>.log, ipython_logs.<host>.<pid>.log), and pymongo —
    whose monitor thread emits a heartbeat DEBUG record every few seconds — is
    pinned to WARNING.
  • SPEC files unreadable by pymca. Non-numeric columns (string HDF5 paths,
    arrays) were substituted inline and reported on a #U line written between
    every data row, breaking specfile column detection. They are now dropped from
    #L entirely so the data block stays a contiguous numeric matrix. Covered by
    the new tests/test_spec_writer.py (labels, #N/column-count consistency,
    block contiguity).
  • te() wrote to the controller device instead of its .setpoint.
  • Dichro/lockin with a scalerstage_dichro_wrapper resolved hinted
    fields via oregistry.find(name.replace("_", ".")), which does not work for
    scaler channels; it now goes through det.channels_name_map.
  • experiment_setup / change_sample default reset_scan_id to None
    (prompt) instead of silently keeping the old scan id.
  • _setup_file_io warns when a detector intends to save images but
    predict_save_path() returns None, instead of silently dropping it from
    the NeXus externals link.
  • ringlight removed from the baseline; MAX_IMAGES for the Xspress3 lowered
    to 12000; fetch_shift_events default 3 → 5.

4. Docs / repo housekeeping

CLAUDE.md split from 326 lines into 136 lines of core guidance plus five
on-demand detail docs under .claude/docs/ (configuration, startup flow,
device loading, data output & logging, documentation site), which .gitignore
now allows through. A separate commit corrects stale claims that audit turned up.

Testing

  • pytest — 62 passed (needs a real apstools; the shared conftest stub makes
    the new SPEC-writer test error at collection otherwise).
  • ruff check / ruff format --check clean on every file this branch touches.
    Two pre-existing issues on main are deliberately untouched: import ordering
    in id4_raman/startup.py (startup import order is load-bearing) and
    formatting in peak_position_legacy.py.
  • Fly scanning, the analyzer calibration, and the diffractometer migration were
    exercised on the real hardware at 4-ID-G; they have no unit coverage.

Known gaps

  • VortexXspress34.setup_flyscan_mode() is a stub — the docstring describes
    num_images / acq_time / hdf_images, but the body only calls
    setup_external_trigger() and ignores all three arguments. The Eiger
    equivalent is implemented.
  • Related open issues this moves toward without closing: Add function to reset the HKL settings #73 (HKL reset) and
    Printing analyzer setup in pa #47 (analyzer info in pa) — commented on each with the specifics.

🤖 Generated with Claude Code

gfabbris and others added 18 commits June 9, 2026 21:39
SpecWriterCallback2 wrote string-valued columns (e.g.
eiger_hdf1_full_file_name) inline as a per-row placeholder plus a
#U label = value line after every data row. With a string column
present this interleaved a #U line between every data row, breaking the
contiguous numeric data block that pymca's specfile parser requires, so
pymca read the file as having a single column.

Filter non-numeric columns (dtype string/array) out of data_labels in
descriptor(), and remove the interleaved #U emission in
write_scan_data_row(). The result is a clean numeric-only data block;
per-point file paths remain available via #MD / #U externals at scan end.

Add tests/test_spec_writer.py covering the dropped string column,
column-count consistency, and a contiguous data block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix bugs found reviewing restore_diffractometer_from_scan (renamed from
restore_huber_from_scan):

- Update stale __all__ entry that still listed restore_huber_from_scan,
  which broke `from hkl_utils_hklpy2 import *` (used in _common_startup
  and id4_raman.startup) with AttributeError at session startup.
- Make the "no hklpy2 configuration" ValueError an actual f-string and
  fix the concatenated "hklpy2configuration" wording.
- Replace non-subscriptable info.keys()[0] with a list, and raise a
  clear error when the user enters a diffractometer name not in the scan.
- Extract the duplicated sample-list + overwrite/append prompt into
  _prompt_clear_mode(); reuse from read_config() and the restore fn.
- After restore(), touch the _psi geometry and recompute UB, matching
  read_config().

Also includes in-progress analyzer/compute_UB updates in the same file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename id4_common/utils/hkl_utils_hklpy2.py to hkl_utils.py and update
all imports and doc references accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Importing apsbits triggers its import-time configure_logging(), which does
os.makedirs("<cwd>/.logs"). At the beamline the cwd is the read-only DM
experiment directory, so this raised PermissionError and aborted startup
before setup_logging() could redirect to the centralized LOG_PATH.

chdir into a private temp dir while importing apsbits so the throwaway
.logs lands somewhere writable, then clean it up in setup_logging(). Also
harden the LOG_PATH fallback to use a temp dir when cwd is not writable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multiple bluesky sessions shared one RotatingFileHandler target
(logging.log) under the NFS LOG_PATH. With rotate_on_startup, a new or
rotating session renames the file others hold open, staling their handle
and producing repeating "OSError: [Errno 116] Stale file handle"
tracebacks. pymongo's monitor thread (heartbeat DEBUG every few seconds,
reaching the file handler because startup forces root to DEBUG) made it
spam constantly.

Give each session its own files (logging.<host>.<pid>.log and
ipython_logs.<host>.<pid>.log) so rotation is self-contained, and silence
pymongo to WARNING via the apsbits `modules` override (explicit level
survives the later root=DEBUG). Both changes live in _build_overrides().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- hkl_utils: rewrite ca() to allow an optional per-call energy override
  without touching the live beam wavelength, add pa_new() for a full
  diffractometer/sample orientation summary, add reciprocal lattice
  and wavelength display to list_reflections/wh, and update eiger
  beam-center/distance PVs in compute_UB.
- polar_diffractometer: reset tth_trans offset when releasing analyzer
  calibration; higher-precision angle printouts.
- local_preprocessors: fix hinted-device lookup for scaler channels in
  stage_dichro_wrapper.
- _local_scan_utils: warn when a detector intends to save images but
  predict_save_path() returns None, instead of silently dropping it.
- logbook_mcr: fetch 5 shift events by default instead of 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trim CLAUDE.md from ~340 to ~135 lines, moving deep-dive material
(config/devices.yml patterns, startup flow, device loading/connection
internals, data output/logging, docs-site build) into .claude/docs/*.md
that get read only when the task at hand touches that area. Un-ignore
.claude/docs/ specifically so these are tracked while
.claude/settings.local.json stays local-only.

Add an "Implementing New Devices & Code" section spelling out the
priority order for talking to hardware: existing device signal via
oregistry, then apstools/ophyd building blocks, then a new/extended
device class, and only as a last resort raw pyepics caget/caput -
which must be explicitly flagged when used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Correct several docs that had drifted from the actual code:
- startup-flow: flag that id4_raman/startup.py doesn't share
  _common_startup.py and has drifted (missing imports, wrong module
  name for polartools_hklpy2_imports); fix cat/cat_full naming; fix
  the crl/gslt "core label" example.
- configuration: clarify ioc_prefix is illustrative, not a real
  convention name; reframe DynamicDeviceComponent factories as the
  mainstream pattern rather than a rare exception; document the third
  kb_generic.py-style type()-based factory pattern.
- device-loading: local_scans.py was split (May 2026, issue #56) into
  base_scans/move_plans/grid_scans/hkl_scans.py and is now just a
  compat shim; add apstools_spec_file_writer.py to the callbacks list.
- data-output-and-logging: document the tempfile.mkdtemp fallback tier
  added after the doc was written; correct the temperature-controller
  claim (the magnet needle valve isn't reachable via temperature_setup).
- documentation-site: fix local build instructions to match CI, add
  the undocumented dev/ deploy case, add missing pages, correct the
  already-gitignored api/ note.
- CLAUDE.md: make the pyepics tech-debt callout self-verifying via
  grep instead of a hardcoded file list; note E501 is ruff-ignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bring the branch back to main's pre-existing lint baseline:

- polar_diffractometer.py: add the missing docstring on
  `AnalyzerDevice.ath_reset_offset` (D102), drop trailing/blank-line
  whitespace (W291/W293).
- flyscans.py: remove the unused `AD_prime_plugin2` import (F401; it is
  only referenced in comments) and the dead `total_scan_points` local
  (F841; `total_images` is what the plan actually uses).
- Run `ruff format` on the six files this branch touched.
- docs/source/{devices_guide,devices_reference}.md: point at
  `polar_diffractometer` after the `polar_diffractometer_hklpy2` rename.

Pre-existing issues on main (id4_raman/startup.py import ordering,
peak_position_legacy.py formatting) are left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "Code style" job failed on `pre-commit run --all-files`: ruff-format
(pinned v0.4.8) collapsed an over-expanded list comprehension in
tests/test_spec_writer.py that fits on one line at the 80-col limit.

Fixing that alone would have moved the failure to the job's next step,
`flake8 src/`, which flags E266 (too many leading '#') on a stray `##`
in a commented-out block in hkl_utils.py, introduced on this branch.
Normalized it to the single `#` the surrounding lines use.

Also shortened an 81-char docstring in the same test file; E501 is in
ruff's ignore list so it slipped past lint, but the project style is
80 columns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gfabbris and others added 2 commits September 1, 2026 10:46
Several functions called get_current_run() directly to pull just the
run name or start/end times, bypassing the DmException fallback that
get_current_run_name() already provided. Introduce get_current_run_info()
as the shared fallback (returning name/startTime/endTime) and route all
current-run lookups through it or get_current_run_name().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix DM-unavailable fallback coverage in dm_utils
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.

Restoring hklpy2 setup from scan not working Cannot start bluesky from gdata

1 participant