Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 18m44s
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m11s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m40s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m41s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m10s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m5s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m23s
Build Packages / build:rpm (rocky8) (push) Successful in 11m30s
Build Packages / build:rpm (rocky9) (push) Successful in 12m51s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m8s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m21s
Build Packages / DIALS test (push) Successful in 13m22s
Build Packages / XDS test (durin plugin) (push) Successful in 9m2s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m55s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m57s
Build Packages / Generate python client (push) Successful in 23s
Build Packages / Build documentation (push) Successful in 57s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 10m24s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * rugnux: Add `--model model.pdb` - score the merged data against an atomic model and compute initial maps. It reports R-work/R-free (scaling the model to the observed amplitudes with an overall scale, an anisotropic B and a flat bulk solvent - the standard few-parameter model, so a batch of maps stays directly comparable) and writes 2Fo-Fc / Fo-Fc electron-density maps (CCP4) plus a map-coefficient MTZ. The structure itself is not refined; the model is only re-fractionalised into the data cell. * rugnux: The merged reflection output now carries French-Wilson amplitudes (|F| and its sigma) next to the intensities - MTZ `F`/`SIGF`, mmCIF `_refln.F_meas_au`, and the text HKL - computed with the correct centric/acentric Wilson prior and epsilon multiplicity, so a downstream program (e.g. phenix.refine) can refine against amplitudes. The intensity columns are unchanged. * rugnux: R-free test-set flags are now assigned deterministically and consistently across symmetry - a Bijvoet pair I(+)/I(-) is never split between the work and free sets, and the assignment is a reproducible per-hkl hash that depends only on the reflection index, so every dataset of one crystal form gets the same ~5% free set (what a multi-dataset campaign such as PanDDA needs). On small data the fraction is floored so the test set stays large enough for a stable R-free (~500 reflections, capped at 10%); it stays flat at 5% on ordinary data. When a reference MTZ carries a `FreeR_flag` column its test set is imported instead, letting a whole campaign inherit one shared free set. * rugnux: A reference MTZ (`--reference-mtz`) can now fix the space group and cell for rotation data too (previously rejected), without being used to scale - the rotation merge stays self-consistent. When the crystal has an indexing (merohedral) ambiguity - a lattice symmetry higher than its Laue symmetry, e.g. P3/P4/P6/C2 - the reference also resolves it: each candidate reindexing (identity plus the twin-law cosets of the metric symmetry) is scored by its intensity correlation against the reference and the data are re-merged in the best-correlating one. This is a metric-preserving relabelling of hkl (the cell is unchanged) and a no-op for a holohedral crystal such as lysozyme. * rugnux: `--model` validation now aligns the data to the model before scoring - the observed reflections are reindexed into the model's enantiomorph when the two differ only by hand (indistinguishable from merged intensities). A merohedral indexing ambiguity is resolved against the reference MTZ when one is given (so a whole campaign shares one indexing convention); only with a model and no reference does validation fall back to fitting each candidate reindexing and keeping the lowest R-free. * rugnux: De-novo symmetry - recover a genuine high-symmetry group whose data are imperfectly scaled. Such a merge's within-orbit chi² lands just past the self-consistency bound (each real symmetry step adds a little systematic scatter), right where a merohedral twin also lands, so the chi² ratio alone cannot separate them. The candidate is now rescued when the extra intensity-proportional systematic error it invokes stays small relative to the confirmed subgroup - a genuine symmetry step gains multiplicity without inflating the merge error model's b, whereas a twin forces non-equivalent reflections together and b balloons. Fixes cubic insulin (I23 instead of I222) with no change to any other crystal in the test battery, including the twins that must stay in their lower symmetry. * Docs: Document the French-Wilson amplitude estimation, R-free flagging, reference-based space-group/ambiguity resolution, and model-based validation/maps in CPU_DATA_ANALYSIS.md. * Frontend: The status-bar pill now shows a progress bar during detector calibration (previously only during measurement), and the calibration state and its button are labelled "Calibration"/"CALIBRATE" (the internal `Pedestal` state name is unchanged for back-compatibility).Reviewed-on: #70 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
255 lines
15 KiB
Markdown
255 lines
15 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## What this is
|
||
|
||
Jungfraujoch is the data-acquisition and analysis system for the PSI JUNGFRAU and EIGER
|
||
X-ray detectors. It receives detector data, runs it through an FPGA-accelerated pipeline
|
||
(spot finding, azimuthal/ROI integration, compression), streams images out over ZeroMQ for
|
||
writing to HDF5, and runs crystallographic analysis (indexing, integration, scaling/merging).
|
||
Most authoritative documentation lives in `docs/` and on Read The Docs
|
||
(https://jungfraujoch.readthedocs.io). When changing CLI behaviour, the program's own
|
||
usage message is the source of truth, not the docs.
|
||
|
||
## Build
|
||
|
||
Out-of-source CMake build, C++20, heavy use of `FetchContent` (spdlog, zstd, HDF5,
|
||
slsDetectorPackage, Catch2, cpp-httplib, libzmq, Ceres, fast-feedback-indexer are downloaded
|
||
and statically linked — the **first configure needs network access** and is slow).
|
||
|
||
```
|
||
mkdir build && cd build
|
||
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||
make -j$(nproc) jfjoch_broker # the main service; build other targets by name
|
||
```
|
||
|
||
Key CMake options:
|
||
- `JFJOCH_USE_CUDA` (default ON) — GPU path. Needs CUDA ≥ 12.8. Provides the `ffbidx` and `fft`
|
||
GPU indexers; without it only the CPU `fftw` indexer is available (requires FFTW at configure
|
||
time, auto-detected). CUDA absence is not a build error.
|
||
- `JFJOCH_WRITER_ONLY` (default OFF) — builds only the HDF5 writer; skips broker, FPGA,
|
||
receiver, analysis, tests, frontend.
|
||
- `JFJOCH_VIEWER_BUILD` (default OFF) — builds the Qt6 `jfjoch_viewer` desktop app.
|
||
- `SLS9` (default OFF) — build against slsDetectorPackage 9.2.0 instead of 8.0.2.
|
||
|
||
The frontend is a separate custom target: `make frontend` (runs `npm ci && npm run build` in
|
||
`frontend/`). It is not built by default unless installing.
|
||
|
||
## Test
|
||
|
||
Tests use **Catch2** and are collected into a single binary `tests/jfjoch_test`.
|
||
|
||
```
|
||
make -j$(nproc) jfjoch_test
|
||
cd tests
|
||
./jfjoch_test # all tests
|
||
./jfjoch_test "<test name>" # one test case (exact name in TEST_CASE)
|
||
./jfjoch_test "[tag]" # by tag
|
||
./jfjoch_test -r junit -o report.xml
|
||
```
|
||
|
||
`make jfjoch_hdf5_test` builds the HDF5 write-speed benchmark, also used by CI to produce files
|
||
that are validated against XDS (Durin/Neggia) and CrystFEL.
|
||
|
||
Lint config is `.clang-tidy` (broad `*` check set with many exceptions; namespaces lower_case,
|
||
classes CamelCase, global constants UPPER_CASE).
|
||
|
||
## Code style
|
||
|
||
The overriding principle is **simple, readable code** — favour the smallest, most direct
|
||
implementation that a reader can verify at a glance. Extra abstraction, speculative guards, and
|
||
clever-but-dense constructs are treated as actively harmful, not as polish. When torn between a
|
||
tidy abstraction and a flat, obvious version, pick the obvious one.
|
||
|
||
This matters most in `image_analysis/pixel_refinement/` (e.g. `PixelRefine.cpp`), which is an
|
||
experimental prototype where readability is how the physics gets verified — keep it especially
|
||
plain. **Do not add defensive/unrequested code** (extra validation, rejection heuristics, "just in
|
||
case" branches) without asking first; if a guard isn't clearly needed, leave it out.
|
||
|
||
Match the surrounding code's idiom, naming, and comment density rather than importing a different
|
||
style.
|
||
|
||
## No sample identities in the repository
|
||
|
||
**Never put sample names or sample-specific measured values into source code, comments,
|
||
documentation, commit messages, or test fixtures.** Datasets belong to users and may be
|
||
confidential or embargoed; anything committed can leak outside the group working on them. This is
|
||
a hard rule, not a preference.
|
||
|
||
- **Forbidden**: sample/dataset names or internal codes (a protein name, a beamline dataset ID, a
|
||
run label), and **measured unit-cell parameters tied to a real sample** (this is the most
|
||
sensitive — never hardcode "protein X has cell a,b,c").
|
||
- **Fine**: general crystallographic descriptors — space group / Laue class ("a P2₁ crystal", "a
|
||
holohedral 422 case"), lattice centering, twinning, "a crystal whose true axis is reported as its
|
||
3× harmonic", pseudo-symmetry, etc. Describe the *crystallographic situation*, not the specimen.
|
||
- **Exception — plain lysozyme.** Lysozyme (HEWL) is the field's universal standard test specimen,
|
||
not a user's confidential dataset, so naming it and using its well-known reference cell
|
||
(tetragonal ~79/79/38, P4₃2₁2) in tests, docs and comments is allowed. This carve-out is only for
|
||
generic lysozyme as a benchmark; a *named user dataset* that happens to be lysozyme is still off
|
||
limits, and every other real sample remains forbidden.
|
||
- **Tests** must otherwise use neutral names (e.g. `tetragonal_uc`, not a specimen-named variable)
|
||
and, where a cell is needed, a synthetic cell chosen for the test — not a real dataset's parameters.
|
||
|
||
When a bug was found on a specific dataset, commit the *behaviour* ("de-novo indexing adopted a
|
||
spurious axis-multiple supercell"), never the dataset.
|
||
|
||
## Local end-to-end run (no detector / no FPGA)
|
||
|
||
The FPGA HLS logic can be simulated on the CPU (`HLSSimulatedDevice`), so the full software
|
||
stack runs without hardware (slowly — fixed-point math on CPU). See
|
||
`docs/JFJOCH_BROKER.md` for the canonical walkthrough.
|
||
|
||
```
|
||
cd build/broker
|
||
./jfjoch_broker ../../etc/broker_local.json 5232 # config JSON + HTTP port
|
||
# then, separately:
|
||
cd tests/test_data && python jfjoch_broker_test.py # feeds a test image, starts collection
|
||
# observe at http://localhost:5232 ; HDF5 is written under build/broker
|
||
```
|
||
|
||
`etc/broker_local.json`, `broker_eiger.json`, `broker_crmx.json` are example broker configs
|
||
(schema = `jfjoch_settings` in `broker/jfjoch_api.yaml`).
|
||
|
||
## Architecture
|
||
|
||
**Data flow (online):** detector → FPGA acquisition (`fpga/`, `acquisition_device/`) →
|
||
`receiver/` builds full images from per-module FPGA output → `image_pusher/` streams CBOR-encoded
|
||
images over ZeroMQ → `jfjoch_writer` (`writer/`) consumes the stream and writes NXmx HDF5. The
|
||
broker also emits a low-rate preview stream and a metadata stream (`preview/`).
|
||
|
||
**Writer file split:** one acquisition produces one `_master.h5` plus many `_data_NNNNNN.h5`
|
||
files. Dataset-wide metadata (geometry, detector config, ROI/azimuthal definitions — anything
|
||
fixed for the whole run) is written to the **master** file in `writer/HDF5NXmx.cpp` (the `NXmx`
|
||
class). Per-image arrays (one entry per frame) are written to the **data** files by the
|
||
`HDF5DataFilePlugin` subclasses in `writer/`. Put shared metadata in `NXmx`, not in a data-file
|
||
plugin.
|
||
|
||
The HDF5 master/data layout is one of three `FileWriterFormat`s (`common/JFJochMessages.h`),
|
||
all NXmx: **`NXmxLegacy`** (master + `_data_NNNNNN.h5` joined by external links),
|
||
**`NXmxVDS`** (master + data joined by HDF5 virtual datasets — the default), and
|
||
**`NXmxIntegrated`** (a single self-contained file, no separate data files). Per-image plugins
|
||
must work for all three; with `NXmxIntegrated` "master" and "data" are the same file.
|
||
|
||
**Two acquisition workflows:** the FPGA-accelerated path (JUNGFRAU at PSI; FPGA does masking,
|
||
summation, spot finding, ROI/azimuthal integration, compression) and the DECTRIS SIMPLON path
|
||
(EIGER), which has no FPGA — masking/ROI/azimuthal analysis then runs on CPU through the shared
|
||
`image_analysis/` library. Treat ROI and azimuthal features as available in **both** workflows,
|
||
not FPGA-only.
|
||
|
||
**`jfjoch_broker`** (`broker/`) is the central online service: HTTP/REST + OpenAPI control plane,
|
||
FPGA configuration, image building, ZeroMQ output. `JFJochStateMachine` drives acquisition state;
|
||
`JFJochServices` wires the pieces; `OpenAPIConvert`/`JFJochBrokerParser` translate between the
|
||
generated API model and internal types.
|
||
|
||
**Three analysis frontends share one analysis library** (`image_analysis/`, built as
|
||
`JFJochImageAnalysis`):
|
||
- `jfjoch_broker` — online, real-time (FPGA + GPU).
|
||
- `jfjoch_viewer` — interactive Qt desktop (`viewer/`), results not persisted.
|
||
- `rugnux` (`tools/rugnux_cli.cpp`, built on the `Rugnux` library in `rugnux/`) — offline batch over
|
||
a stored HDF5; writes `_process.h5` and `.mtz`/`.cif`/`.hkl`. Merging is on by default (`--no-merge`
|
||
to disable); `--azint-only` runs only azimuthal integration and `--scale` re-scales/merges the
|
||
already-integrated reflections in a `_process.h5`. (rugnux = the data-processing half of the system;
|
||
see `docs/NAMING.md`.)
|
||
|
||
**`image_analysis/` pipeline** (subdirs): `spot_finding`, `indexing` (`ffbidx`/`fft` GPU,
|
||
`fftw` CPU), `lattice_search`, `geom_refinement`, `pixel_refinement`, `bragg_prediction`,
|
||
`bragg_integration`, `rotation_indexer`, `azint`, `roi`, `scale_merge`. Least-squares refinement
|
||
uses **Ceres** (fetched, built with miniglog, no MKL, CXX_THREADS). `ffbidx` needs a known cell
|
||
(`-C`) and suits sparse serial stills; `fft`/`fftw` index de novo and suit strong rotation data.
|
||
|
||
**FPGA** (`fpga/`): `hls/` is the Vitis HLS source (image-analysis kernels), `hls_simulation/`
|
||
runs that same HLS on CPU for hardware-free testing, `host_library/` is the host-side driver,
|
||
`pcie_driver/` is the kernel module. The HLS algorithms are documented in
|
||
`docs/FPGA_DATA_ANALYSIS.md`.
|
||
|
||
**Detector control** (`detector_control/`): wrappers for SLS (JUNGFRAU) and DECTRIS SIMPLON
|
||
(EIGER). **`jungfrau/`**: JUNGFRAU ADU→energy gain/pedestal calibration.
|
||
|
||
**Other libs:** `common/` (geometry, diffraction experiment, image buffer, CUDA wrappers — the
|
||
shared core, linked nearly everywhere), `compression/` (zstd + bitshuffle + sqrt lossy),
|
||
`frame_serialize/` (CBOR stream codec), `gemmi_gph/` (vendored GEMMI for MTZ/XDS_ASCII I/O),
|
||
`xds-plugin/` (XDS HDF5 read plugin).
|
||
|
||
## Portability (jfjoch_viewer)
|
||
|
||
Cross-platform support is a goal **only for `jfjoch_viewer` and its dependency tree** — keep that
|
||
code, and any shared library it transitively links (`common/`, `image_analysis/`, `reader/`,
|
||
`gemmi_gph/`, etc.), **MSVC-compatible** so the viewer can build on Windows. The rest of the
|
||
project (broker, receiver, FPGA host, detector control, …) is Linux-only and does not need to be
|
||
portable; don't constrain it for portability's sake.
|
||
|
||
- **Windows/MSVC** is the primary portability target. The end goal is a Windows viewer built
|
||
with **MSVC *and* CUDA** (`JFJOCH_USE_CUDA=ON`): GPU processing is a wanted feature, not
|
||
optional, so the intended Windows config is the full GPU path (`ffbidx`, GPU `fft`), not a
|
||
CPU-only fallback. MSVC is required regardless, because CUDA on Windows requires it. Avoid
|
||
GCC/Clang-only extensions, POSIX-only APIs, and other non-MSVC constructs in viewer-reachable
|
||
code, and keep CUDA-reachable viewer code (`ffbidx`, GPU indexers) MSVC-buildable too.
|
||
- **macOS** is a nice-to-have for the viewer. It rules out CUDA, so anything the viewer depends on
|
||
must also have a working CPU-only / non-CUDA path (the `JFJOCH_USE_CUDA=OFF`, `fftw`-indexer
|
||
configuration). This non-CUDA path must keep working, but it is the macOS fallback — *not* the
|
||
intended Windows configuration.
|
||
|
||
A self-contained Windows build still needs a few dependencies that the Linux build picks up from
|
||
the system and that are **not** auto-provided via `FetchContent` (besides Qt, supplied
|
||
externally): **ZLIB** (pulled by hdf5/libtiff/gemmi/libzmq — a bundled zlib-ng in `ZLIB_COMPAT`
|
||
mode is the intended fix, presented as the `ZLIB::ZLIB` target so the scattered
|
||
`find_package(ZLIB)` calls resolve), **libjpeg-turbo** (used by `preview/`; upstream discourages
|
||
`add_subdirectory`, so bring it in via `ExternalProject`), and **Eigen** (header-only; needed by
|
||
Ceres, by the analysis libs directly, and by `ffbidx` under CUDA — fetch it and point Ceres'
|
||
internal `find_package(Eigen3)` at it). Wire each guarded so the Linux build, which finds these on
|
||
the system, stays unchanged.
|
||
|
||
## OpenAPI is the single source of truth
|
||
|
||
`broker/jfjoch_api.yaml` defines the entire REST API **and the shared data schemas**. From it,
|
||
`update_version.sh` regenerates three clients — **do not hand-edit generated code**:
|
||
- C++ server model → `broker/gen/` (cpp-pistache-server generator; compiled as `JFJochAPI`).
|
||
- Python client → `python-client/` (and `gen_python_client.sh`, published as PyPI `jfjoch-client`).
|
||
- TypeScript frontend client → `frontend/src/client/` (hey-api `openapi-ts`, `npm run openapi`).
|
||
|
||
When you change `jfjoch_api.yaml`, regenerate the relevant client(s); for a version bump run
|
||
`update_version.sh` (also rewrites `VERSION`, `frontend/src/version.ts`, and the Redoc html).
|
||
|
||
## Frontend
|
||
|
||
React 19 + TypeScript + MUI + Vite (`frontend/`). Data layer is generated from the OpenAPI spec
|
||
(`@hey-api/openapi-ts` → fetch client + TanStack Query hooks + zod schemas). Scripts:
|
||
`npm start` (dev server), `npm run build` (tsc + vite), `npm run openapi` (regen client),
|
||
`npm run redocly4broker` (regen `broker/redoc-static.html`).
|
||
|
||
## Adding a per-image scalar quantity
|
||
|
||
A per-image scalar (e.g. `ice_ring_score`, `bkg_estimate`, `mosaicity`) flows analysis → message → CBOR
|
||
→ HDF5 → scan-result/plot → API → viewer/frontend. To add one, mirror an existing float scalar
|
||
(`bkg_estimate` is a clean template) at every layer:
|
||
|
||
1. **Compute** where the azint profile is finalized: `image_analysis/MXAnalysisWithoutFPGA.cpp` (CPU),
|
||
`receiver/JFJochReceiverFPGA.cpp` (FPGA), and the offline azint worker in `rugnux/Rugnux.cpp`.
|
||
2. **Message** (`common/JFJochMessages.h`): `std::optional<float>` in `DataMessage`, `std::vector<float>`
|
||
in `EndMessage`.
|
||
3. **CBOR**: encode in `frame_serialize/CBORStream2Serializer.cpp` (DataMessage block *and* END block),
|
||
decode in `CBORStream2Deserializer.cpp` (both). Optional fields are back-compatible — no version bump.
|
||
4. **HDF5** write: `writer/HDF5DataFilePluginMX.{h,cpp}` — an `AutoIncrVector<float>` with reserve /
|
||
per-image write / `SaveVector("/entry/MX/<name>")` (per-image arrays live in the data-file plugin);
|
||
plus the NXmx master write in `writer/HDF5NXmx.cpp` (`SaveVectorIfMissing(..., end.<name>)`). HDF5
|
||
**read-back** (so a stored file re-opens, e.g. in the viewer) is in `reader/HDF5MetadataSource.cpp`,
|
||
NOT `JFJochHDF5Reader.cpp`: mirror the three `bkgEstimate` sites — master `ReadOptVector`, data-file
|
||
`ReadVector` into the dataset, and the per-image message population.
|
||
5. **Scan result**: `common/ScanResult.h` (`ScanResultElem`) + `common/ScanResultGenerator.cpp`
|
||
(copy in `Add`, resize+fill in `FillEndMessage`).
|
||
6. **Receiver plot**: `common/Plot.h` (`PlotType`) + `common/JFJochReceiverPlots.{h,cpp}` (`StatusVector`
|
||
+ Clear / AddElement / GetPlots / GetPlotRaw cases).
|
||
7. **API**: add to the `plot_type` enum and the `scan_result` images schema in `broker/jfjoch_api.yaml`,
|
||
regenerate the C++ model (`java -jar openapi-generator-cli.jar generate -i broker/jfjoch_api.yaml -o
|
||
broker/gen -g cpp-pistache-server`) and the frontend client (`cd frontend && npm run openapi`), then
|
||
wire `broker/OpenAPIConvert.cpp` (`ConvertPlotType` string→enum and the `Convert(ScanResult)` setter).
|
||
8. **Reader/viewer**: `reader/JFJochReaderDataset.h` + `reader/JFJochHttpReader.cpp` (`GetPlot_i`) and
|
||
`viewer/JFJochViewerDatasetInfo.cpp` (combo item + `ExtractMetric`).
|
||
9. **Frontend**: `frontend/src/components/DataProcessingPlots.tsx` (`MenuItem`) + `DataProcessingPlot.tsx`
|
||
(y-axis label).
|
||
10. **Docs**: `docs/CBOR.md`, `docs/HDF5.md`, `docs/CPU_DATA_ANALYSIS.md`.
|
||
|
||
Gotcha: an existing `build/` dir needs a `cmake .` reconfigure to pick up a newly-added `broker/gen`
|
||
source file (the source list is a configure-time glob).
|