Datasets may be confidential; sample names and measured unit cells committed to the repo can leak outside the group working on them. Scrub existing occurrences and add a "No sample identities in the repository" section to CLAUDE.md (forbidden: sample/dataset names, internal codes, measured cells tied to a sample; fine: space group / lattice / twinning descriptors). - Comments: replace internal dataset codes and protein names with the crystallographic situation they illustrate (centred vs pseudo-symmetric, holohedral, cubic, F-cubic/hexagonal, ...). - Docs: same, in the analysis/writer/stream references and example configs. - Tests: rename sample-named identifiers, TEST_CASE names, file prefixes and asserted labels to neutral crystallographic names (e.g. tetragonal_uc); behaviour unchanged. Reduce the CrystFEL reference PDB to a bare CRYST1 cell file (cell.pdb) and rename the reference data file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
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 theffbidxandfftGPU indexers; without it only the CPUfftwindexer 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 Qt6jfjoch_viewerdesktop 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.
- Tests must 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 FileWriterFormats (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 theRugnuxlibrary inrugnux/) — offline batch over a stored HDF5; writes_process.h5and.mtz/.cif/.hkl. Merging is on by default (--no-mergeto disable);--azint-onlyruns only azimuthal integration and--scalere-scales/merges the already-integrated reflections in a_process.h5. (rugnux = the data-processing half of the system; seedocs/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, GPUfft), 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 asJFJochAPI). - Python client →
python-client/(andgen_python_client.sh, published as PyPIjfjoch-client). - TypeScript frontend client →
frontend/src/client/(hey-apiopenapi-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:
- Compute where the azint profile is finalized:
image_analysis/MXAnalysisWithoutFPGA.cpp(CPU),receiver/JFJochReceiverFPGA.cpp(FPGA), and the offline azint worker inrugnux/Rugnux.cpp. - Message (
common/JFJochMessages.h):std::optional<float>inDataMessage,std::vector<float>inEndMessage. - CBOR: encode in
frame_serialize/CBORStream2Serializer.cpp(DataMessage block and END block), decode inCBORStream2Deserializer.cpp(both). Optional fields are back-compatible — no version bump. - HDF5 write:
writer/HDF5DataFilePluginMX.{h,cpp}— anAutoIncrVector<float>with reserve / per-image write /SaveVector("/entry/MX/<name>")(per-image arrays live in the data-file plugin); plus the NXmx master write inwriter/HDF5NXmx.cpp(SaveVectorIfMissing(..., end.<name>)). HDF5 read-back (so a stored file re-opens, e.g. in the viewer) is inreader/HDF5MetadataSource.cpp, NOTJFJochHDF5Reader.cpp: mirror the threebkgEstimatesites — masterReadOptVector, data-fileReadVectorinto the dataset, and the per-image message population. - Scan result:
common/ScanResult.h(ScanResultElem) +common/ScanResultGenerator.cpp(copy inAdd, resize+fill inFillEndMessage). - Receiver plot:
common/Plot.h(PlotType) +common/JFJochReceiverPlots.{h,cpp}(StatusVector- Clear / AddElement / GetPlots / GetPlotRaw cases).
- API: add to the
plot_typeenum and thescan_resultimages schema inbroker/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 wirebroker/OpenAPIConvert.cpp(ConvertPlotTypestring→enum and theConvert(ScanResult)setter). - Reader/viewer:
reader/JFJochReaderDataset.h+reader/JFJochHttpReader.cpp(GetPlot_i) andviewer/JFJochViewerDatasetInfo.cpp(combo item +ExtractMetric). - Frontend:
frontend/src/components/DataProcessingPlots.tsx(MenuItem) +DataProcessingPlot.tsx(y-axis label). - 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).