FZGPUModules 2.0
GPU-accelerated modular compression pipelines
Loading...
Searching...
No Matches
ROIBinSplitStage

Header: modules/structural/roibin_split/roibin_split_stage.h Class: fz::ROIBinSplitStage<TData>TData is float or double Category: Structural

Common instantiation:

p.setDims(1552, 1480, 1); // before addStage
auto* split = p.addStage<fz::ROIBinSplitStage<float>>();
split->setRoiHalfWidth(4); // 9x9 box per peak
split->setBinFactor(2); // 2x2 background binning; 1 = off
split->setPeaksFile("frame.roi"); // compress side only
Definition roibin_split_stage.h:118
void setRoiHalfWidth(uint32_t hw)
ROI box half-width in pixels; the box is (2*hw+1)^2. Default 4 → 9x9.
Definition roibin_split_stage.h:133

What it does

Splits a detector field into two independently compressible streams plus the metadata that ties them back together:

direction ports
forward (1 → 3) field → roi, bg, peaks
inverse (3 → 1) roi, bg, peaks → field
  • **roi** — the (2*hw+1)^2 box around every peak, concatenated in peak order, at full resolution and the field's element type.
  • **bg** — the background, box-averaged by bin_factor within each z-slice: ceil(nx/b) * ceil(ny/b) * nz values. b = 1 copies the field through.
  • **peaks** — the peak record table, 8 * npeaks bytes (UINT8).

On the inverse, the background is un-binned first and the ROI boxes are then pasted over it, so the tight-bound values win wherever the two overlap.

Why it exists

Serial-crystallography frames are almost all background; the science lives in a few hundred Bragg peaks covering well under 1 % of the pixels. A single-bound compressor must apply the tight ROI bound to the entire frame, and pays for the other 99 % at that bound.

This stage turns that into a graph problem. Because it is 1 → 3, the two data streams become two DAG branches, and each branch can carry its own Quantizer at its own error bound and its own coder chain:

┌── roi ──> Quantizer(eb_tight) ──> AdaptiveBitpack ──┐
input ──> split ── bg ──> Quantizer(eb_loose) ──> TiledLorenzo ──> AdaptiveBitpack ─┤──> archive
└── peaks ─────────────────────────────────────────────┘

There is no way to say "compress this part tightly and that part loosely" to a monolithic compressor; expressing it is the point. The design follows ROIBIN-SZ (Underwood et al.).

Measured contributions of the branch-specific stages (3 EXAFEL frames, eb 10/100): TiledLorenzo on the background is worth +9.3 % (bin=1) / +13.6 % (bin=2); a 1-D predictor on the roi branch is worth +0.4 %, which is why the ROI branch has none. roi is a peak-major concatenation of boxes rather than an image, so a 2-D predictor has no meaningful stride over it — a 1-D one is valid but does not pay.

Where the peak list comes from

It is not derived from the data. It is the output of the experiment's own peak finder, which in a real light-source pipeline has already run upstream — the same assumption ROIBIN-SZ makes. At compress time setPeaksFile() reads it; the stage validates every record against the field bounds and throws if the file's nx/ny/nz disagree with the pipeline's dimensions, because a silent mismatch would relocate every ROI box and still produce output that round-trips.

The table is then re-emitted on the peaks port, so it lands inside the archive and is counted in the compressed size. The decompressor never needs the .roi file. At 8 B/peak this is ≈0.01 % of a frame.

<tt>.roi</tt> file format

magic char[8] "FZROI1\0\0"
nx uint32 fast axis
ny uint32 slow axis
nz uint32 frames
npeaks uint32
records npeaks x { uint32 z; uint16 x; uint16 y; } (8 bytes each)

Geometry: redundancy instead of stream compaction

Boxes belonging to nearby peaks overlap, and overlapping pixels are stored more than once. That is deliberate. It makes the output size exactly npeaks * box * sizeof(TData), known before the first kernel launch, so estimateOutputSizes() is exact and PREALLOCATE needs no slack. The alternative — a per-pixel mask plus a device-wide exclusive scan — costs a 4-byte offset per pixel (1.2 GB on a 130-frame 1480x1552 volume) to remove a redundancy that measures well under 1 % of the ROI stream. getRoiOverlapFraction() reports the measured duplicate fraction.

Duplicates are safe because scatter is idempotent: every copy of a source pixel takes the same value, passes through the same quantizer, and reconstructs to the same number, so write order cannot matter. Edge boxes are clamped, not truncated, which keeps the box size fixed; a clamped box reads and writes the same border pixel several times, which is idempotent for the same reason.

Binning bounds nothing — read this before quoting a background number

bin_factor = b > 1 replaces each b x b block with its mean. This is a resolution reduction, not an error bound. The background reconstruction error is the binning error plus the quantization error, and it is not bounded by the background branch's error bound.

bin_factor background guarantee how to report background fidelity
1 satisfies its stated bound pixel-wise max abs error and PSNR; the bound gate is meaningful
> 1 no bound PSNR only; never claim a satisfied error bound

The stage emits a getRunNotes() warning whenever bin_factor > 1 so this cannot be lost between the run and the write-up. The ROI branch satisfies its bound in both cases — that is the invariant the science depends on.


Stage settings

setter TOML key default meaning
setRoiHalfWidth(hw) roi_half_width 4 box is (2*hw+1)^2
setBinFactor(b) bin_factor 1 background binning; 1 disables
setPeaksFile(path) peaks_file compress-side peak list
data_type float32 float32 or float64

Dimensions come from Pipeline::setDims(). On the decompress path they are recovered from the archive header, and later pipeline dim pushes are ignored — finalize() pushes {0,0,1} on that path and would otherwise erase them.

TOML configuration

[[stage]]
name = "split"
type = "ROIBinSplit"
data_type = "float32"
roi_half_width = 4
bin_factor = 2
peaks_file = "frame.roi"

Downstream stages connect to the named ports:

[[stage]]
type = "TiledLorenzo"
inputs = [{from = "split", port = "roi"}] # or "bg", or "peaks"

If a downstream predictor is dimension-aware and bin_factor > 1, give it an explicit dim_x/dim_y/dim_z override. The binned background is not the pipeline's input shape, and finalize() re-pushes the global dims over anything set at construction — the predictor would then use the wrong row stride and still round-trip, silently costing ratio. See TiledLorenzoStage.

Serialized config header

[0..3] uint32 nx
[4..7] uint32 ny
[8..11] uint32 nz
[12..15] uint32 npeaks
[16..17] uint16 roi_half_width
[18..19] uint16 bin_factor
[20] uint8 element DataType
[21] uint8 reserved

22 bytes. Peak values travel on the peaks port, not here — the 128-byte stage config slot cannot hold a few thousand records.