Conversation
The metadata readers used `hasattr(ds, 'asstr')` to decide whether a dataset holds strings, but every h5py Dataset has that method regardless of dtype -- it only raises when called on a numeric one. The TypeError was swallowed by the surrounding except, so numeric arrays fell through to the stringifying fallback: voxel_spacing, grid_origin, grid_dimensions_cells and intensity_range all came back as reprs like '[6 6 6]' instead of [6, 6, 6]. Downstream volume code has defensive fallbacks, so this surfaced as volumes silently rendering with default spacing/origin rather than as an error. Use h5py.check_string_dtype(ds.dtype) at all three sites instead.
…ce volume Ports rsMap3D's core capability: gridding every frame of one or more completed scans onto a single interpolated 3D volume via xrayutilities' Gridder3D. DashPVA already converted angles to Q per frame but could only show a bounded live point cloud, never a merged, interpolated volume. Launch with `DashPVA rsmgrid` or the Post Analysis launcher tile. Output uses the existing HDF5 volume format, so results open in the Workbench 3D viewer. Engine (utils/rsm_gridder.py): - Two passes are required, not an optimization. Gridder3D latches fixed_range on its first call when KeepData is set, after which points outside the first batch's range are silently dropped -- so global bounds must be known and set via dataRange(fixed=True) before any gridding. - Masked pixels are excluded from the arrays, never zeroed. Gridder3D.data is a per-bin mean, so a zeroed-but-present pixel would count as a real 'intensity = 0' measurement and bias its voxel low. - Optional I0 normalization from entry/data/metadata/ca/<name>, matching rsMap3D's monitor division. Without it, scans taken at different exposure or attenuation do not match in intensity where they overlap. - Energy/UB differences warn rather than block: each file's own UB is applied, so the merge already lands in a common crystal-fixed HKL frame, and HKL is geometrically energy-independent. rsMap3D does not block either. - Batch size is derived from a byte budget rather than a fixed frame count, which would need ~6 GB for the coordinate arrays on a 2048^2 detector. - grid_origin is shifted half a voxel: Gridder3D's axes are bin centers but the PyVista viewer treats the origin as a cell corner. - Axes are labelled H/K/L, not Q: passing UB to Ang2Q.area returns hkl. utils/rsm_converter.py gains build_file_geometry()/q_for_frames() so QConversion/HXRD/init_area are built once per file instead of once per frame, using xrayutilities' batched angle arguments. create_rsm() and get_sample_and_detector_circles() are unchanged for existing callers. The mask is stored in the viewer's display orientation and that state is never persisted, so the dialog asks rather than guessing. Known gaps vs rsMap3D: no flat-field correction, no user-specified grid crop, no detector tilt (a pre-existing rsm_converter limitation).
Collaborator
Author
|
@Osayi-ANL Please let me know if you have started working on this. Open to hearing your thoughts on this since you have extensively worked on 3D HKL work. This is a new feature that beamlines would find useful. |
Collaborator
Author
|
@Osayi-ANL when you have a chance, please work on this. |
Osayi-ANL
approved these changes
Aug 11, 2026
Osayi-ANL
left a comment
Collaborator
There was a problem hiding this comment.
Doesn't render on the workbench for post analysis
…olume Builder add: physics validation for RSM metadata — energy, UB, detector geometry, circle positions add: non-finite point exclusion with per-build reporting add: source energy, UB, monitor and mask provenance to gridded volume metadata add: Workbench rendering for gridded HKL volumes add: RSM Volume Builder documentation fix: direction-1/2 order in the synthetic scan fixture — SIZE and CENTER_CHANNEL_PIXEL were written direction-2-first remove: cross-circle POSITION fallback from rsm_converter — silently substituted another circle's angle revert: src/dashpva/viewer/config.json from .gitignore — unrelated to this PR
…r QSpinBox, raising TypeError on a float range
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the one thing rsMap3D can do that DashPVA could not: gridding every frame of one or more completed scans onto a single interpolated 3D reciprocal-space volume (
xu.Gridder3D). We already converted angles to Q per frame, but could only show a bounded live point cloud — never a merged volume.Launch with
DashPVA rsmgridor the Post Analysis launcher tile. Output uses the existing HDF5 volume format, so results open in Workbench's 3D viewer.Design notes worth reviewing
Gridder3Dlatchesfixed_rangeon its first call whenKeepDatais set, after which points outside the first batch's range are silently dropped. Global bounds must be known and set viadataRange(fixed=True)before any gridding.Gridder3D.datais a per-bin mean, so a zeroed-but-present pixel would count as a realintensity = 0measurement and bias its voxel low.Ang2Q.areareturns hkl, not Q in Å⁻¹.entry/data/metadata/ca/<name>, matching rsMap3D's monitor division. Without it, scans at different exposure/attenuation won't match in intensity where they overlap.grid_originis shifted half a voxel:Gridder3D's axes are bin centers, but the PyVista viewer treats the origin as a cell corner.Memory safety
The dense result is unavoidably
nx · ny · nzvoxels —Gridder3Dholds two float64 arrays and.datamaterializes a third, so ~24 B/voxel for the grid plus one processing batch.estimate_grid_memory()computes a conservative peak from the grid plus the largest per-file batch, andensure_memory_available()refuses a build exceeding a safe fraction of currently available RAM. The dialog shows the estimate live asnx/ny/nzchange (detector shapes are read once and cached, not re-read per keystroke) and blocks Start rather than letting the process OOM.Thresholds live in
settings.pyper the "no inline config" rule.RSM_GRID_WORKING_BYTES_PER_PIXELdrives both the estimator and_batch_size_for, so the two cannot drift apart. Scripted/HPC callers can bypass the guard withbuild_volume(..., memory_limit_fraction=None).Physics validation
Metadata that the conversion treats as static — photon energy, detector center/size/distance, beam and sample reference directions, UB — is now validated for consistency across frames, along with finiteness, positivity, non-degeneracy, and UB rank.
Tolerances are settings-driven and sized for EPICS readback jitter: these values are written per-frame from live PVs (
hdf5_writer._write_scan_pv_dataset), so an exact-equality check would reject valid scans. Energy usesRSM_GRID_ENERGY_RELATIVE_TOLERANCE(1e-4, ≈1 eV at 10 keV); other static metadata usesRSM_STATIC_METADATA_*. A genuinely swept value is still rejected, since one static geometry cannot represent it.Non-finite coordinates and intensities are excluded from binning and reported. Bounds come from finite coordinates only, so the binned set is always a strict subset of the bounded set — nothing can silently fall outside the latched range.
Circles beyond 4 sample / 2 detector are rejected explicitly, citing #132, rather than silently truncated.
Provenance
Output metadata now records per-file energies and UB matrices (
source_energies_eV,source_ub_matrices,source_ub_matrices_shape) rather than only the first file's, pluscoordinate_system,gridder,monitor_dataset, mask state, and excluded-point counts. A merge across files with differing energy/UB stays auditable after the fact.Breaking changes in
utils/rsm_converter.pybuild_file_geometry()/q_for_frames()are additive —QConversion/HXRD/init_areaare built once per file instead of once per frame, using xrayutilities' batched angle arguments.area_det_viewer.pyandhpc_rsm_consumer.pyhave their own inline setups and are untouched.Signatures are preserved, but three behaviors changed deliberately:
POSITIONfallback is removed. When an axis group had noPOSITIONdataset, the old code walked the whole HKL tree and returned whichever POSITION it found first — i.e. a different circle's angle, silently corrupting HKL. It now raises.float, notint— subpixel centers are no longer truncated.[0].These affect
create_rsm()and the Workbench point-cloud path, not only the new gridder.Workbench volume rendering (consumer side)
workspace_3d.pygainsbuild_volume_grid()and volume detection inload_data(), reusing the existingcloud_volumeactor name thaton_3d_colormap_changedandupdate_intensityalready handle. Worth reviewing as a distinct concern — it's included because the gridder's output is unviewable without it.Separate commit
687e943fixes a pre-existing bug found while testing: the HDF5 metadata readers usedhasattr(ds, 'asstr')to detect string datasets, but every Dataset has that method — it only raises on numeric ones, and the error was swallowed.voxel_spacing/grid_origin/grid_dimensions_cells/intensity_rangewere coming back as reprs like'[6 6 6]'. Reviewable/cherry-pickable on its own.Testing
252 tests pass (34 new).
ruff check src/dashpva/is clean.Grid dimensions in tests are deliberately asymmetric — with
nx==ny==nza transpose bug still produces a correctly-shaped array.Fixture correction:
_synthetic_hdf5.pypreviously wroteSIZEandCENTER_CHANNEL_PIXELdirection-2-first, contradicting all three production paths (rsm_converter.py:157,area_det_viewer.py:2013-2017,hpc_rsm_consumer.py:271-274, which all pairSIZE[0]with detector direction 1). Square fixtures hid it; the new asymmetric (3,5) fixture catches it.test_asymmetric_detector_matches_independent_referencechecks the batched path against a hand-built xrayutilities pipeline on a non-square detector, catching wiring and argument-order bugs inbuild_file_geometry/q_for_framesthat the old batched-vs-per-frame test structurally could not — both sides shared any bug. Note it builds its reference with the samecch1/cch2/Nch1/Nch2convention the converter assumes, so it does not resolve the detector axis-0 question below.Other coverage: hot-pixel→voxel round trip through save/load, mask exclusion vs zeroing, mask transpose, I0 normalization, cell-corner origin, a guard on the
fixed_rangelatch we depend on, EPICS-jitter acceptance vs swept-value rejection, memory estimation and guard bypass, legacy MU/DELTA circle names, and the Workbench grid builder.Widget-level regression caught in review
The Min/Max intensity controls were plain integer
QSpinBoxinworkspace_3d.ui, and_load_volumeseeded them with floats — which PyQt5 rejects outright (TypeError: setRange(self, min: int, max: int)), so every volume load ended in "3D Load Failed". The unit tests passed over it because they only coveredbuild_volume_grid(), the pure helper.This matters specifically for gridded output:
Gridder3D.datais a per-bin mean, not a raw count, and with I0 normalization the whole range can sit below 1 — an integer box would collapse0.0034 → 12.57to0 → 12, or to an empty0 → 0.Both widgets are now
QDoubleSpinBox, seeded byseed_intensity_spinboxes()which picks decimals from the span (2 for counts, 6 for normalized means). The regression test loads the shipped.uiheadless and asserts a fractional range round-trips rather than raising or truncating.Disjoint from #127 — zero file overlap.
Not yet validated on real data
cch1/pw1vs row/col) against a known Bragg peak — pre-existing inrsm_converter, but now load-bearing. A swap would produce a transposed volume that still looks plausible. Only real data closes this.metadata/ca/key is I0 at the beamline.DISTANCEvalidates atrtol=1e-6(0.5 µm on a 500 mm distance). Fine for a static setpoint; if a beamline exposes it as a live encoder readback it will raise a clear error. Commissioning item, not a correctness risk.Known gaps vs rsMap3D
No flat-field correction, no user-specified grid crop, no detector tilt (pre-existing
rsm_converterlimitation), and no out-of-core gridding — the volume is held entirely in RAM, so a ~1000³ grid (~24 GB) is rejected by the memory guard rather than degrading to chunked accumulation.