v1.0.0-rc.175 (#85)
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`). * Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection. * Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS). * Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead. * Rugnux: On macOS, Bragg integration runs on the Apple GPU. * Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand. * Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`. * Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit. * Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed. * Rugnux: The beam-stop shadow is found correctly on tilted detectors. * Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement. * jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps. * jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection. * jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images. * Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges. --------- Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home> Reviewed-on: #85 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #85.
This commit is contained in:
1 parent
a395f358ef
commit
19b135186d
779 files changed
+201230
-17809
No files matched your search
@@ -36,13 +36,6 @@ inline void prefetch_row(const void *first, const void *last) {
|
||||
prefetch(last);
|
||||
}
|
||||
|
||||
// Identity sampler over the preprocessed int32 buffer (already in that convention).
|
||||
struct BufferSampler {
|
||||
const int32_t *p;
|
||||
int32_t operator[](size_t i) const { return p[i]; }
|
||||
const void *address(size_t i) const { return p + i; }
|
||||
};
|
||||
|
||||
// Sampler over a raw detector image of pixel type T: masked pixels carry the type minimum, saturated
|
||||
// the type maximum (the FPGA image has no lossy-codec +/-1 band). Only the pixels actually read - the
|
||||
// reflection disks - are converted, so there is no whole-image pass.
|
||||
@@ -60,36 +53,36 @@ struct ImageSampler {
|
||||
const void *address(size_t i) const { return p + i; }
|
||||
};
|
||||
|
||||
// The reflections in detector order: by band of 16 rows (a mask tile's height), then by column. The
|
||||
// prediction lists them by Miller index, which scatters consecutive reflections over the whole frame, so
|
||||
// each box waited on memory for its image rows and mask tiles; in this order neighbours share them in
|
||||
// cache. Only for the loops whose results do not depend on the order the reflections are visited in.
|
||||
std::vector<uint32_t> FrameOrder(const std::vector<Reflection> &predicted, size_t npredicted) {
|
||||
std::vector<std::pair<uint64_t, uint32_t>> keyed(npredicted);
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
const auto band = static_cast<uint64_t>(std::max(0.0f, predicted[i].predicted_y)) / 16;
|
||||
const auto col = static_cast<uint64_t>(std::max(0.0f, predicted[i].predicted_x));
|
||||
keyed[i] = {(band << 32) | col, static_cast<uint32_t>(i)};
|
||||
}
|
||||
std::sort(keyed.begin(), keyed.end());
|
||||
std::vector<uint32_t> order(npredicted);
|
||||
for (size_t k = 0; k < npredicted; ++k)
|
||||
order[k] = keyed[k].second;
|
||||
return order;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BraggIntegrationEngineCPU::BraggIntegrationEngineCPU(const DiffractionExperiment &experiment,
|
||||
const PixelMask &mask)
|
||||
: BraggIntegrationEngine(experiment),
|
||||
static_mask(mask.GetPackedMask()),
|
||||
refl_mask(static_cast<int>(xpixel), static_cast<int>(ypixel), 0),
|
||||
owner(static_cast<int>(xpixel), static_cast<int>(ypixel), BRAGG_OWNER_NONE) {}
|
||||
|
||||
template <class Sampler>
|
||||
std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, int64_t image_number) {
|
||||
std::vector<BraggFitResult> results(npredicted);
|
||||
if (npredicted == 0)
|
||||
return Finalize(predicted, npredicted, results, image_number);
|
||||
counts.predicted += npredicted;
|
||||
owner(static_cast<int>(xpixel), static_cast<int>(ypixel), BRAGG_OWNER_NONE),
|
||||
static_mask(mask.GetPackedMask()) {}
|
||||
|
||||
void BraggIntegrationEngineCPU::MarkNeighbours(const std::vector<Reflection> &predicted,
|
||||
const std::vector<uint32_t> &order) {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
const bool do_clip = bkg_clip_nsigma > 0.0f && mode != IntegratorMode::BoxSum;
|
||||
|
||||
// Symmetric trimmed-mean background fraction (BraggIntegrationSettings, rugnux --background-trim):
|
||||
// replaces the r2..r3 ring MEAN with an f-trimmed mean, robust to the high-side contamination
|
||||
// (neighbour wings, tails) that biases the plain mean up and makes it over-subtract weak high-angle
|
||||
// reflections. The base ctor has already forced it to 0 whenever the clip below is in force, which
|
||||
// is the default - the two are alternatives.
|
||||
const double bkg_trim_frac = bkg_trim;
|
||||
|
||||
auto grid_idx = [this](int dx, int dy) { return (dy + R) * G + (dx + R); };
|
||||
|
||||
// --- Reflection mask: mark the r2 signal region of every predicted reflection, so a neighbour's
|
||||
// signal is excluded from this reflection's r2..r3 background ring. MASK_FLUX where the
|
||||
// neighbour puts flux on this frame (NEIGHBOUR_MASK_MIN_PARTIALITY) - only those are excluded
|
||||
@@ -98,8 +91,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
// neighbour's OWN frame - an elongated neighbour whose streak is still marked as a disk would
|
||||
// leak its tails into this reflection's ring. ---
|
||||
refl_mask.Clear();
|
||||
owner.Clear();
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
for (const uint32_t i : order) {
|
||||
const auto &r = predicted[i];
|
||||
const uint8_t level = r.partiality >= NEIGHBOUR_MASK_MIN_PARTIALITY ? MASK_FLUX : MASK_TAIL;
|
||||
const BraggStencil st = MakeBraggStencil(r.predicted_x, r.predicted_y, stencil);
|
||||
@@ -116,6 +108,55 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BraggIntegrationEngineCPU::CountNeighbourCrowding(const std::vector<Reflection> &predicted,
|
||||
size_t npredicted) {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
counts.predicted += npredicted;
|
||||
const std::vector<uint32_t> order = FrameOrder(predicted, npredicted);
|
||||
MarkNeighbours(predicted, order);
|
||||
for (const uint32_t i : order) {
|
||||
const auto &r = predicted[i];
|
||||
const BraggStencil st = MakeBraggStencil(r.predicted_x, r.predicted_y, stencil);
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(r.predicted_x - st.ex_out - 1.0f)));
|
||||
const int x1 = std::min(W - 1, static_cast<int>(std::ceil(r.predicted_x + st.ex_out + 1.0f)));
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(r.predicted_y - st.ey_out - 1.0f)));
|
||||
const int y1 = std::min(H - 1, static_cast<int>(std::ceil(r.predicted_y + st.ey_out + 1.0f)));
|
||||
int n_ring = 0, n_clean = 0;
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const auto d = BraggStencilDistances(st, x - r.predicted_x, y - r.predicted_y);
|
||||
if (BraggStencilInSignal(st, x - r.predicted_x, y - r.predicted_y, d.signal, r1_sq)
|
||||
|| d.inner < r2_sq || d.outer >= r3_sq)
|
||||
continue;
|
||||
const size_t idx = static_cast<size_t>(y) * W + x;
|
||||
if (idx / 32 < static_mask.size() && ((static_mask[idx / 32] >> (idx % 32)) & 1U))
|
||||
continue;
|
||||
++n_ring;
|
||||
if (refl_mask.Get(x, y) == 0)
|
||||
++n_clean;
|
||||
}
|
||||
if (n_clean <= 5 && n_ring > 5)
|
||||
++counts.bkg_starved_by_neighbour;
|
||||
}
|
||||
}
|
||||
|
||||
template <class Sampler>
|
||||
std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, int64_t image_number) {
|
||||
if (npredicted == 0)
|
||||
return Finalize(predicted, npredicted, std::vector<BraggFitResult>(npredicted), image_number);
|
||||
counts.predicted += npredicted;
|
||||
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
const bool do_clip = bkg_clip_nsigma > 0.0f && mode != IntegratorMode::BoxSum;
|
||||
|
||||
// Every loop below whose results do not depend on the order walks the reflections in this one.
|
||||
const std::vector<uint32_t> order = FrameOrder(predicted, npredicted);
|
||||
MarkNeighbours(predicted, order);
|
||||
owner.Clear();
|
||||
|
||||
// --- Signal-region ownership: a pixel inside two reflections' signal regions belongs to the
|
||||
// NEARER predicted centre. The union mask above cannot answer that - it also marks a
|
||||
@@ -126,7 +167,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
const bool use_owner = overlap != OverlapMode::Off;
|
||||
if (use_owner) {
|
||||
const float claim_sq = claim * claim;
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
for (const uint32_t i : order) {
|
||||
const auto &r = predicted[i];
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(r.predicted_x - claim)));
|
||||
const int x1 = std::min(W - 1, static_cast<int>(std::ceil(r.predicted_x + claim)));
|
||||
@@ -148,20 +189,6 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
};
|
||||
|
||||
// --- Pass A: box-sum every reflection (rough I, background, centroid, strong flag). ---
|
||||
struct Rough {
|
||||
double I = 0.0, sigma = NAN, bkg = 0.0, obs_x = 0.0, obs_y = 0.0;
|
||||
double bkg_var = 0.0; // variance of the background ESTIMATE itself, bkg / n_bkg
|
||||
double var_bkg = 0.0; // total non-signal variance carried to the merge
|
||||
int64_t I_sum = 0; // kept so I can be rebuilt after the radial background correction
|
||||
int n_inner = 0;
|
||||
int n_disk = 0, n_own = 0; // signal-disk pixels, and how many of them are this reflection's
|
||||
int r_bin = 0; // rounded distance from the beam centre, indexes the radial curve
|
||||
int k_bin = 0; // which radial-background kernel this reflection's stencil needs
|
||||
int cx = 0, cy = 0, shell = -1;
|
||||
bool ok = false, strong = false, has_obs = false;
|
||||
bool full = false; // every pixel of the signal disk was readable
|
||||
bool overloaded = false; // a pixel of the signal disk was saturated
|
||||
};
|
||||
// A saturated pixel, or one unreadable on this frame alone - the overload marker of a detector
|
||||
// that writes its error value for a pixel it could not count (EIGER), which the preprocessor turns
|
||||
// into a masked pixel like a gap's. The run's mask holds the gaps, so whatever is unreadable beyond
|
||||
@@ -169,21 +196,12 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
auto static_masked = [&](size_t idx) {
|
||||
return idx / 32 < static_mask.size() && ((static_mask[idx / 32] >> (idx % 32)) & 1U);
|
||||
};
|
||||
std::vector<Rough> rough(npredicted);
|
||||
double inv_d2_min = std::numeric_limits<double>::max(), inv_d2_max = 0.0;
|
||||
PassA a = StartPassA(npredicted);
|
||||
std::vector<int32_t> bkg_vals; // reused per reflection for the trimmed-mean background (idea 1)
|
||||
|
||||
// Radial background curve, accumulated from the annulus pixels this pass already reads. A pixel's
|
||||
// radius is the reflection's radius plus the pixel's projection on the beam->reflection direction,
|
||||
// so no per-pixel sqrt is needed.
|
||||
const int n_rad = static_cast<int>(std::ceil(std::hypot(std::max<double>(beam_x, W - beam_x),
|
||||
std::max<double>(beam_y, H - beam_y)))) + 2;
|
||||
std::vector<double> rad_sum;
|
||||
std::vector<int> rad_cnt;
|
||||
if (bkg_radial) {
|
||||
rad_sum.assign(n_rad, 0.0);
|
||||
rad_cnt.assign(n_rad, 0);
|
||||
}
|
||||
// The ring pixels the background clip below reads, with their radial offsets, gathered while pass
|
||||
// A walks the box anyway.
|
||||
struct ClipPixel { int32_t px; float rad; };
|
||||
std::vector<ClipPixel> clip_ring;
|
||||
|
||||
// The box pass A reads: the OUTER ellipse, tightly - taking the largest semi-axis in both
|
||||
// directions instead would read up to 60% more pixels for nothing.
|
||||
@@ -194,199 +212,364 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
std::max(0, static_cast<int>(std::floor(r.predicted_y - st.ey_out - 1.0f))),
|
||||
std::min(H - 1, static_cast<int>(std::ceil(r.predicted_y + st.ey_out + 1.0f)))};
|
||||
};
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
for (size_t k = 0; k < npredicted; ++k) {
|
||||
const size_t i = order[k];
|
||||
// The next reflection's image rows, asked for while this one is worked on: the reflections
|
||||
// are spread over a frame far larger than the caches, so each box would otherwise wait on
|
||||
// memory row by row. (Written out here: GCC drops a call to a function that does nothing but
|
||||
// prefetch.)
|
||||
if (i + 1 < npredicted) {
|
||||
const Reflection &n = predicted[i + 1];
|
||||
if (k + 1 < npredicted) {
|
||||
const Reflection &n = predicted[order[k + 1]];
|
||||
const Box b = box_of(n, MakeBraggStencil(n.predicted_x, n.predicted_y, stencil));
|
||||
for (int y = b.y0; y <= b.y1 && b.x0 <= b.x1; ++y)
|
||||
prefetch_row(img.address(static_cast<size_t>(y) * W + b.x0),
|
||||
img.address(static_cast<size_t>(y) * W + b.x1));
|
||||
}
|
||||
const auto &r = predicted[i];
|
||||
Rough out;
|
||||
// This reflection's stencil: the r1 signal disk, and the r2..r3 ring elongated radially by
|
||||
// the analytic smear.
|
||||
const BraggStencil st = MakeBraggStencil(r.predicted_x, r.predicted_y, stencil);
|
||||
const Box box = box_of(r, st);
|
||||
const int x0 = box.x0, x1 = box.x1, y0 = box.y0, y1 = box.y1;
|
||||
|
||||
// Both from the stencil's own radius, so nothing downstream is derived from a second one.
|
||||
out.r_bin = std::clamp(static_cast<int>(std::lround(st.r0)), 0, n_rad - 1);
|
||||
out.k_bin = BraggStencilKernelIndex(st, n_kern);
|
||||
|
||||
int64_t I_sum = 0, I_sum_x = 0, I_sum_y = 0, n_inner = 0, n_inner_valid = 0;
|
||||
int64_t x_sum = 0, y_sum = 0; // positions of the pixels behind I_sum, for the centroid
|
||||
int n_disk = 0, n_own = 0; // pixels in the signal disk, and how many are this reflection's
|
||||
double bkg_sum = 0.0;
|
||||
int n_bkg = 0;
|
||||
// Readable ring pixels, and those of them no predicted neighbour's signal region covers.
|
||||
// refl_mask marks d.inner < r2_sq and the ring is d.inner >= r2_sq, so the two are
|
||||
// complementary and a masked ring pixel is always someone else's - never this reflection's
|
||||
// n_ring / n_clean: readable ring pixels, and those of them no predicted neighbour's signal
|
||||
// region covers. refl_mask marks d.inner < r2_sq and the ring is d.inner >= r2_sq, so the two
|
||||
// are complementary and a masked ring pixel is always someone else's - never this reflection's
|
||||
// own core. That makes the difference an exact count of what the pattern's density took,
|
||||
// separable from what the detector took.
|
||||
int n_ring = 0, n_clean = 0;
|
||||
BraggBoxSums s{};
|
||||
bkg_vals.clear();
|
||||
clip_ring.clear();
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const auto d = BraggStencilDistances(st, x - r.predicted_x, y - r.predicted_y);
|
||||
const int32_t px = img[y * W + x];
|
||||
if (do_clip && d.inner >= r2_sq && d.outer < r3_sq && valid(px) && refl_mask.Get(x, y) != MASK_FLUX)
|
||||
clip_ring.push_back({px, d.rad});
|
||||
if (BraggStencilInSignal(st, x - r.predicted_x, y - r.predicted_y, d.signal, r1_sq)) {
|
||||
// A pixel a nearer neighbour owns carries that neighbour's flux, so in Exclude
|
||||
// mode it leaves the disk entirely - the sum, the pixel count the background is
|
||||
// subtracted with, and the all-or-nothing validity gate alike. The box sum only
|
||||
// counts how many were lost, which is all it can act on.
|
||||
++n_disk;
|
||||
++s.n_disk;
|
||||
if (overlap != OverlapMode::Off) {
|
||||
if (clean(x, y, i)) ++n_own;
|
||||
if (clean(x, y, i)) ++s.n_own;
|
||||
else if (exclude) continue;
|
||||
}
|
||||
++n_inner;
|
||||
++s.n_inner;
|
||||
if (px == INT32_MAX
|
||||
|| (px == INT32_MIN && !static_masked(static_cast<size_t>(y) * W + x)))
|
||||
out.overloaded = true;
|
||||
s.overloaded = 1;
|
||||
if (!valid(px)) continue;
|
||||
I_sum += px;
|
||||
I_sum_x += static_cast<int64_t>(x) * px;
|
||||
I_sum_y += static_cast<int64_t>(y) * px;
|
||||
s.I_sum += px;
|
||||
s.I_sum_x += static_cast<int64_t>(x) * px;
|
||||
s.I_sum_y += static_cast<int64_t>(y) * px;
|
||||
// Position sums over the very same pixels, so the background can be taken out of
|
||||
// the centroid below. It is not known yet - the ring is read in this same loop.
|
||||
x_sum += x;
|
||||
y_sum += y;
|
||||
++n_inner_valid;
|
||||
s.x_sum += x;
|
||||
s.y_sum += y;
|
||||
++s.n_inner_valid;
|
||||
} else if (d.inner >= r2_sq && d.outer < r3_sq) {
|
||||
if (!valid(px)) continue;
|
||||
++n_ring;
|
||||
++s.n_ring;
|
||||
const uint8_t m = refl_mask.Get(x, y);
|
||||
if (m == 0) ++n_clean;
|
||||
if (m == 0) ++s.n_clean;
|
||||
if (m == MASK_FLUX) continue;
|
||||
bkg_sum += static_cast<double>(px);
|
||||
if (bkg_trim_frac > 0.0) bkg_vals.push_back(px);
|
||||
++n_bkg;
|
||||
s.bkg_sum += px;
|
||||
if (bkg_trim > 0.0f) bkg_vals.push_back(px);
|
||||
++s.n_bkg;
|
||||
}
|
||||
}
|
||||
|
||||
int n_bkg_used = n_bkg; // pixels behind the FINAL background value (trim/clip shrink it)
|
||||
// A masked, untrusted, gapped or overloaded pixel inside the signal disk used to discard the
|
||||
// reflection outright. A profile fit does not need it to: the fit is the amplitude of a
|
||||
// NORMALISED profile, so leaving pixels out renormalises the estimator by construction and
|
||||
// widens sigma by the information they carried - the same argument Exclude makes for a
|
||||
// neighbour's pixels. Pass B keeps the reflection only while enough of the expected profile
|
||||
// survived to constrain the amplitude (XDS's MINPK, dials' valid_foreground_threshold). A box
|
||||
// sum has no profile to renormalise with, so there it stays all or nothing.
|
||||
const bool full = n_inner_valid == n_inner;
|
||||
// A ring left with five or fewer readable pixels cannot estimate a background, so the
|
||||
// reflection is dropped whole - the one thing the stencil geometry does to the DATA rather
|
||||
// than to a measurement. Counted here because it is the only direct evidence of a radius that has
|
||||
// outgrown the pattern it is integrating (BraggIntegrationCounts).
|
||||
const bool keep_partial = full || mode != IntegratorMode::BoxSum || out.overloaded;
|
||||
if (keep_partial && n_bkg <= 5) ++counts.bkg_starved;
|
||||
// Would every predicted neighbour, the tails included, leave the ring starved that the
|
||||
// detector alone would not? That is the pattern's density, what the guard on a widened
|
||||
// radius reads (BraggIntegrationCounts).
|
||||
if (keep_partial && n_clean <= 5 && n_ring > 5) ++counts.bkg_starved_by_neighbour;
|
||||
if (keep_partial && n_bkg > 5) {
|
||||
out.bkg = bkg_sum / n_bkg;
|
||||
if (bkg_trim_frac > 0.0 && bkg_vals.size() > 5
|
||||
&& bkg_vals.size() <= static_cast<size_t>(bragg_engine::BKG_TRIM_MAX)) {
|
||||
// Symmetric trimmed mean over the background ring (idea 1): drop the lowest and highest
|
||||
// bkg_trim_frac of the pixels, average the rest. Robust to the high-side contamination
|
||||
// that biases the plain ring mean and makes it over-subtract at high resolution.
|
||||
std::sort(bkg_vals.begin(), bkg_vals.end());
|
||||
const size_t lo = static_cast<size_t>(bkg_vals.size() * bkg_trim_frac);
|
||||
const size_t hi = bkg_vals.size() - lo;
|
||||
if (hi > lo) {
|
||||
double s = 0.0;
|
||||
for (size_t t = lo; t < hi; ++t) s += bkg_vals[t];
|
||||
out.bkg = s / static_cast<double>(hi - lo);
|
||||
n_bkg_used = static_cast<int>(hi - lo);
|
||||
}
|
||||
} else if (do_clip) {
|
||||
// One high-outlier sigma-clip pass on the background ring: reject pixels above
|
||||
// mean + n*sqrt(mean) to strip a neighbour core or a zinger that biases the mean.
|
||||
const double thr = out.bkg + bkg_clip_nsigma * std::sqrt(std::max(out.bkg, 1.0));
|
||||
double s = 0.0;
|
||||
int n = 0;
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const auto d = BraggStencilDistances(st, x - r.predicted_x, y - r.predicted_y);
|
||||
if (!(d.inner >= r2_sq && d.outer < r3_sq)) continue;
|
||||
if (refl_mask.Get(x, y) == MASK_FLUX) continue;
|
||||
const int32_t px = img[y * W + x];
|
||||
if (!valid(px)) continue;
|
||||
if (static_cast<double>(px) <= thr) {
|
||||
s += px;
|
||||
++n;
|
||||
if (bkg_radial) {
|
||||
// The radial curve is binned on the TRUE detector radius, so the
|
||||
// offset here is the unshrunk radial projection.
|
||||
const int b = std::clamp(static_cast<int>(std::lround(st.r0 + d.rad)), 0, n_rad - 1);
|
||||
rad_sum[b] += static_cast<double>(px);
|
||||
++rad_cnt[b];
|
||||
}
|
||||
}
|
||||
// One high-outlier sigma-clip pass on the background ring (see ClipThreshold), and the radial
|
||||
// background curve from the pixels it keeps.
|
||||
BraggClipSums clip{};
|
||||
if (ClipsRing(s)) {
|
||||
const int32_t thr = ClipThreshold(s);
|
||||
for (const auto &[px, rad] : clip_ring)
|
||||
if (px <= thr) {
|
||||
clip.sum += px;
|
||||
++clip.n;
|
||||
if (bkg_radial) {
|
||||
// The radial curve is binned on the TRUE detector radius, so the
|
||||
// offset here is the unshrunk radial projection.
|
||||
const int b = std::clamp(static_cast<int>(std::lround(st.r0 + rad)), 0, a.n_rad - 1);
|
||||
a.rad_sum[b] += px;
|
||||
++a.rad_cnt[b];
|
||||
}
|
||||
if (n > 5) { out.bkg = s / n; n_bkg_used = n; }
|
||||
}
|
||||
// The sum is over the pixels actually READ, so that is the count the background is
|
||||
// subtracted with; with nothing missing it is the whole disk, exactly as before.
|
||||
out.I = static_cast<double>(I_sum) - static_cast<double>(n_inner_valid) * out.bkg;
|
||||
// I = I_sum - n_inner*bkg, and bkg is itself estimated from n_bkg_used pixels, so its
|
||||
// error enters n_inner times over: var(I) = I_sum + n_inner^2 * bkg/n_bkg_used. Leaving
|
||||
// the second term out understates sigma by sqrt(1 + n_inner/n_bkg) - 1.109x at the
|
||||
// default r1=4/r2=6/r3=10 stencil, on every reflection of every dataset.
|
||||
out.bkg_var = out.bkg / n_bkg_used;
|
||||
out.var_bkg = static_cast<double>(n_inner_valid) * out.bkg
|
||||
+ static_cast<double>(n_inner_valid) * n_inner_valid * out.bkg_var;
|
||||
out.I_sum = I_sum;
|
||||
out.n_inner = static_cast<int>(n_inner_valid);
|
||||
out.n_disk = n_disk;
|
||||
out.n_own = n_own;
|
||||
const double var_bkg_term = static_cast<double>(n_inner_valid) * n_inner_valid * out.bkg_var;
|
||||
out.sigma = 1.0;
|
||||
if (I_sum > 0) {
|
||||
out.sigma = std::max(out.sigma, std::sqrt(static_cast<double>(I_sum) + var_bkg_term));
|
||||
// The centroid of the SIGNAL, not of the disk. Weighting by the raw counts weights by
|
||||
// signal plus background, and the background is flat over a disk centred on the
|
||||
// PREDICTION - so its own centroid is the prediction exactly, and it pulls the answer
|
||||
// there. The measured displacement comes out shrunk by I/(I + n*bkg), which is worst
|
||||
// where the background dominates, i.e. at high resolution. Post-refinement fits the
|
||||
// beam and the distance on these centroids, so it under-corrects by that factor and
|
||||
// does so resolution-dependently. Subtracting a flat pedestal from a first moment is
|
||||
// exact: sum(x*(px-bkg)) = I_sum_x - bkg*x_sum over the same pixels.
|
||||
const double net = static_cast<double>(I_sum) - n_inner_valid * out.bkg;
|
||||
if (net > 0.0) {
|
||||
out.obs_x = (static_cast<double>(I_sum_x) - out.bkg * x_sum) / net;
|
||||
out.obs_y = (static_cast<double>(I_sum_y) - out.bkg * y_sum) / net;
|
||||
} else {
|
||||
// Nothing above background to take a centroid of; the raw one is all there is.
|
||||
out.obs_x = static_cast<double>(I_sum_x) / static_cast<double>(I_sum);
|
||||
out.obs_y = static_cast<double>(I_sum_y) / static_cast<double>(I_sum);
|
||||
}
|
||||
// A disk with a hole in it gives a centroid pulled away from the hole, and the hole
|
||||
// sits at a fixed place on the detector - post-refinement would read that as geometry.
|
||||
out.has_obs = full;
|
||||
}
|
||||
out.cx = static_cast<int>(std::lround(r.predicted_x));
|
||||
out.cy = static_cast<int>(std::lround(r.predicted_y));
|
||||
out.ok = true;
|
||||
out.full = full;
|
||||
// The profile, its resolution shells and their widths are learned from COMPLETE
|
||||
// reflections: a partial one has no intensity to normalise its grid by and a hole to
|
||||
// stack into it. So the learning sees exactly what it saw before this rescue existed.
|
||||
out.strong = full && out.sigma > 0.0 && out.I / out.sigma >= STRONG_I_OVER_SIGMA;
|
||||
if (full && r.d > 0.0f) {
|
||||
const double inv_d2 = 1.0 / (static_cast<double>(r.d) * r.d);
|
||||
inv_d2_min = std::min(inv_d2_min, inv_d2);
|
||||
inv_d2_max = std::max(inv_d2_max, inv_d2);
|
||||
}
|
||||
}
|
||||
rough[i] = out;
|
||||
FinishPassA(a, i, r, st, s, clip, bkg_vals);
|
||||
}
|
||||
|
||||
return ProfilePasses(img, owner, predicted, npredicted, a, image_number);
|
||||
}
|
||||
|
||||
BraggIntegrationEngineCPU::PassA BraggIntegrationEngineCPU::StartPassA(size_t npredicted) const {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
PassA a;
|
||||
a.rough.resize(npredicted);
|
||||
// A pixel's radius is the reflection's radius plus the pixel's projection on the beam->reflection
|
||||
// direction, so no per-pixel sqrt is needed.
|
||||
a.n_rad = static_cast<int>(std::ceil(std::hypot(std::max<double>(beam_x, W - beam_x),
|
||||
std::max<double>(beam_y, H - beam_y)))) + 2;
|
||||
if (bkg_radial) {
|
||||
a.rad_sum.assign(a.n_rad, 0);
|
||||
a.rad_cnt.assign(a.n_rad, 0);
|
||||
}
|
||||
a.inv_d2_min = std::numeric_limits<double>::max();
|
||||
a.inv_d2_max = 0.0;
|
||||
return a;
|
||||
}
|
||||
|
||||
bool BraggIntegrationEngineCPU::ClipsRing(const BraggBoxSums &s) const {
|
||||
// The clip is the alternative to the trimmed mean, so where it is on the trim is off, and the
|
||||
// profile modes keep every reflection with a background (FinishPassA).
|
||||
return bkg_clip_nsigma > 0.0f && mode != IntegratorMode::BoxSum && s.n_bkg > 5;
|
||||
}
|
||||
|
||||
int32_t BraggIntegrationEngineCPU::ClipThreshold(const BraggBoxSums &s) const {
|
||||
// Reject pixels above mean + n*sqrt(gain*mean) to strip a neighbour core or a zinger that biases
|
||||
// the mean. The gain is 1 for counted photons, so this is then exactly the Poisson clip. A pixel
|
||||
// value is an integer, so it is at or below the threshold exactly when it is at or below its floor.
|
||||
const double bkg = static_cast<double>(s.bkg_sum) / s.n_bkg;
|
||||
const double thr = bkg + bkg_clip_nsigma * std::sqrt(static_cast<double>(bkg_pixel_gain) * std::max(bkg, 1.0));
|
||||
return static_cast<int32_t>(std::clamp(std::floor(thr), static_cast<double>(INT32_MIN),
|
||||
static_cast<double>(INT32_MAX)));
|
||||
}
|
||||
|
||||
void BraggIntegrationEngineCPU::FinishPassA(PassA &a, size_t i, const Reflection &r, const BraggStencil &st,
|
||||
const BraggBoxSums &s, const BraggClipSums &clip,
|
||||
std::vector<int32_t> &bkg_vals) {
|
||||
Rough out;
|
||||
// Both from the stencil's own radius, so nothing downstream is derived from a second one.
|
||||
out.r_bin = std::clamp(static_cast<int>(std::lround(st.r0)), 0, a.n_rad - 1);
|
||||
out.k_bin = BraggStencilKernelIndex(st, n_kern);
|
||||
out.overloaded = s.overloaded != 0;
|
||||
|
||||
if (overlap != OverlapMode::Off && s.n_own < s.n_disk) {
|
||||
++counts.overlapped;
|
||||
if (s.n_own < overlap_min_peak * s.n_disk) ++counts.overlapped_major;
|
||||
}
|
||||
|
||||
int n_bkg_used = s.n_bkg; // pixels behind the FINAL background value (trim/clip shrink it)
|
||||
// A masked, untrusted, gapped or overloaded pixel inside the signal disk used to discard the
|
||||
// reflection outright. A profile fit does not need it to: the fit is the amplitude of a
|
||||
// NORMALISED profile, so leaving pixels out renormalises the estimator by construction and
|
||||
// widens sigma by the information they carried - the same argument Exclude makes for a
|
||||
// neighbour's pixels. Pass B keeps the reflection only while enough of the expected profile
|
||||
// survived to constrain the amplitude (XDS's MINPK, dials' valid_foreground_threshold). A box
|
||||
// sum has no profile to renormalise with, so there it stays all or nothing.
|
||||
const bool full = s.n_inner_valid == s.n_inner;
|
||||
// A ring left with five or fewer readable pixels cannot estimate a background, so the
|
||||
// reflection is dropped whole - the one thing the stencil geometry does to the DATA rather
|
||||
// than to a measurement. Counted here because it is the only direct evidence of a radius that has
|
||||
// outgrown the pattern it is integrating (BraggIntegrationCounts).
|
||||
const bool keep_partial = full || mode != IntegratorMode::BoxSum || out.overloaded;
|
||||
if (keep_partial && s.n_bkg <= 5) ++counts.bkg_starved;
|
||||
// Would every predicted neighbour, the tails included, leave the ring starved that the
|
||||
// detector alone would not? That is the pattern's density, what the guard on a widened
|
||||
// radius reads (BraggIntegrationCounts).
|
||||
if (keep_partial && s.n_clean <= 5 && s.n_ring > 5) ++counts.bkg_starved_by_neighbour;
|
||||
if (keep_partial && s.n_bkg > 5) {
|
||||
out.bkg = static_cast<double>(s.bkg_sum) / s.n_bkg;
|
||||
// Symmetric trimmed-mean background fraction (BraggIntegrationSettings, rugnux --background-trim):
|
||||
// replaces the r2..r3 ring MEAN with an f-trimmed mean, robust to the high-side contamination
|
||||
// (neighbour wings, tails) that biases the plain mean up and makes it over-subtract weak
|
||||
// high-angle reflections. The base ctor has already forced it to 0 whenever the clip is in
|
||||
// force, which is the default - the two are alternatives.
|
||||
const double bkg_trim_frac = bkg_trim;
|
||||
if (bkg_trim_frac > 0.0 && bkg_vals.size() > 5
|
||||
&& bkg_vals.size() <= static_cast<size_t>(bragg_engine::BKG_TRIM_MAX)) {
|
||||
// Symmetric trimmed mean over the background ring (idea 1): drop the lowest and highest
|
||||
// bkg_trim_frac of the pixels, average the rest. Robust to the high-side contamination
|
||||
// that biases the plain ring mean and makes it over-subtract at high resolution.
|
||||
std::sort(bkg_vals.begin(), bkg_vals.end());
|
||||
const size_t lo = static_cast<size_t>(bkg_vals.size() * bkg_trim_frac);
|
||||
const size_t hi = bkg_vals.size() - lo;
|
||||
if (hi > lo) {
|
||||
double sum = 0.0;
|
||||
for (size_t t = lo; t < hi; ++t) sum += bkg_vals[t];
|
||||
out.bkg = sum / static_cast<double>(hi - lo);
|
||||
n_bkg_used = static_cast<int>(hi - lo);
|
||||
}
|
||||
} else if (ClipsRing(s)) {
|
||||
if (clip.n > 5) { out.bkg = static_cast<double>(clip.sum) / clip.n; n_bkg_used = clip.n; }
|
||||
}
|
||||
// The sum is over the pixels actually READ, so that is the count the background is
|
||||
// subtracted with; with nothing missing it is the whole disk, exactly as before.
|
||||
out.I = static_cast<double>(s.I_sum) - static_cast<double>(s.n_inner_valid) * out.bkg;
|
||||
// I = I_sum - n_inner*bkg, and bkg is itself estimated from n_bkg_used pixels, so its
|
||||
// error enters n_inner times over: var(I) = I_sum + n_inner^2 * bkg/n_bkg_used. Leaving
|
||||
// the second term out understates sigma by sqrt(1 + n_inner/n_bkg) - 1.109x at the
|
||||
// default r1=4/r2=6/r3=10 stencil, on every reflection of every dataset.
|
||||
out.bkg_var = out.bkg / n_bkg_used;
|
||||
out.var_bkg = static_cast<double>(s.n_inner_valid) * out.bkg
|
||||
+ static_cast<double>(s.n_inner_valid) * s.n_inner_valid * out.bkg_var;
|
||||
out.I_sum = s.I_sum;
|
||||
out.n_inner = static_cast<int>(s.n_inner_valid);
|
||||
out.n_disk = s.n_disk;
|
||||
out.n_own = s.n_own;
|
||||
const double var_bkg_term = static_cast<double>(s.n_inner_valid) * s.n_inner_valid * out.bkg_var;
|
||||
out.sigma = 1.0;
|
||||
if (s.I_sum > 0) {
|
||||
out.sigma = std::max(out.sigma, std::sqrt(static_cast<double>(s.I_sum) + var_bkg_term));
|
||||
// The centroid of the SIGNAL, not of the disk. Weighting by the raw counts weights by
|
||||
// signal plus background, and the background is flat over a disk centred on the
|
||||
// PREDICTION - so its own centroid is the prediction exactly, and it pulls the answer
|
||||
// there. The measured displacement comes out shrunk by I/(I + n*bkg), which is worst
|
||||
// where the background dominates, i.e. at high resolution. Post-refinement fits the
|
||||
// beam and the distance on these centroids, so it under-corrects by that factor and
|
||||
// does so resolution-dependently. Subtracting a flat pedestal from a first moment is
|
||||
// exact: sum(x*(px-bkg)) = I_sum_x - bkg*x_sum over the same pixels.
|
||||
const double net = static_cast<double>(s.I_sum) - s.n_inner_valid * out.bkg;
|
||||
if (net > 0.0) {
|
||||
out.obs_x = (static_cast<double>(s.I_sum_x) - out.bkg * s.x_sum) / net;
|
||||
out.obs_y = (static_cast<double>(s.I_sum_y) - out.bkg * s.y_sum) / net;
|
||||
} else {
|
||||
// Nothing above background to take a centroid of; the raw one is all there is.
|
||||
out.obs_x = static_cast<double>(s.I_sum_x) / static_cast<double>(s.I_sum);
|
||||
out.obs_y = static_cast<double>(s.I_sum_y) / static_cast<double>(s.I_sum);
|
||||
}
|
||||
// A disk with a hole in it gives a centroid pulled away from the hole, and the hole
|
||||
// sits at a fixed place on the detector - post-refinement would read that as geometry.
|
||||
out.has_obs = full;
|
||||
}
|
||||
out.cx = static_cast<int>(std::lround(r.predicted_x));
|
||||
out.cy = static_cast<int>(std::lround(r.predicted_y));
|
||||
out.ok = true;
|
||||
out.full = full;
|
||||
// The profile, its resolution shells and their widths are learned from COMPLETE
|
||||
// reflections: a partial one has no intensity to normalise its grid by and a hole to
|
||||
// stack into it. So the learning sees exactly what it saw before this rescue existed.
|
||||
out.strong = full && out.sigma > 0.0 && out.I / out.sigma >= STRONG_I_OVER_SIGMA;
|
||||
if (full && r.d > 0.0f) {
|
||||
const double inv_d2 = 1.0 / (static_cast<double>(r.d) * r.d);
|
||||
a.inv_d2_min = std::min(a.inv_d2_min, inv_d2);
|
||||
a.inv_d2_max = std::max(a.inv_d2_max, inv_d2);
|
||||
}
|
||||
}
|
||||
a.rough[i] = out;
|
||||
}
|
||||
|
||||
bool BraggIntegrationEngineCPU::FitKept(const BraggFitOut &o) const {
|
||||
if (o.p_valid < overlap_min_peak * o.p_grid) return false;
|
||||
// A hole in the profile's PEAK is a different defect from a hole in its wings, and the mass
|
||||
// fraction above cannot tell them apart - the peak of a broad spot is a few percent of the
|
||||
// mass, so MINPK passes a reflection that has lost the one part of the profile its amplitude
|
||||
// is determined by. See the header.
|
||||
if (o.p_lost_peak > MINPK_MAX_MISSING_PEAK * o.p_peak) return false;
|
||||
if (overlap == OverlapMode::Reject && o.p_own < overlap_min_peak) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class Sampler, class OwnerMap>
|
||||
BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const OwnerMap &owner_map,
|
||||
const BraggFitSetup &s, size_t i,
|
||||
const std::vector<float> &shell_profiles,
|
||||
std::vector<float> &cells,
|
||||
std::vector<FitPixel> &fit_pixels) const {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
const bool exclude = overlap == OverlapMode::Exclude;
|
||||
const bool use_owner = overlap != OverlapMode::Off;
|
||||
const int Rf = s.Rf, Gf = 2 * Rf + 1;
|
||||
BraggFitOut o{};
|
||||
|
||||
const float *P;
|
||||
if (s.shell >= 0)
|
||||
P = shell_profiles.data() + static_cast<size_t>(s.shell) * GG;
|
||||
else {
|
||||
// The grid's image rows, asked for now (as in pass A): building the profile below takes long
|
||||
// enough for them to arrive before the fit reads them.
|
||||
const int gx0 = std::max(0, s.cx - Rf), gx1 = std::min(W - 1, s.cx + Rf);
|
||||
for (int y = std::max(0, s.cy - Rf); y <= std::min(H - 1, s.cy + Rf); ++y)
|
||||
prefetch_row(img.address(static_cast<size_t>(y) * W + gx0),
|
||||
img.address(static_cast<size_t>(y) * W + gx1));
|
||||
cells.resize(static_cast<size_t>(Gf) * Gf);
|
||||
// The cells a row at a time, a loop that vectorises; then their sum, in the same order.
|
||||
for (int dy = -Rf; dy <= Rf; ++dy) {
|
||||
float *row = cells.data() + static_cast<size_t>(dy + Rf) * Gf;
|
||||
for (int dx = -Rf; dx <= Rf; ++dx)
|
||||
row[dx + Rf] = BraggGaussian(dx, dy, s.fx, s.fy, s.ux, s.uy, s.inv_2s2r, s.inv_2s2t);
|
||||
}
|
||||
float gs = 0.0f;
|
||||
for (const float g : cells) gs += g;
|
||||
const float inv_gs = 1.0f / gs;
|
||||
for (float &p : cells) p *= inv_gs;
|
||||
P = cells.data();
|
||||
}
|
||||
|
||||
// --- How much of the expected profile the fit can actually see. p_valid is the readable
|
||||
// fraction of it - XDS's MINPK, dials' valid_foreground_threshold - measured against the
|
||||
// mass that falls on the detector at all, so a reflection is judged on the pixels that
|
||||
// exist and not on where the sensor ends. p_own is the same quantity over neighbour-owned
|
||||
// pixels, what Reject cuts on. m_read / m_all is the fraction of the r1 disk the
|
||||
// summation seed the runaway guard compares against actually summed; with nothing missing
|
||||
// and nothing excluded it is 1 and the guard is untouched. ---
|
||||
const BraggStencil st = MakeBraggStencil(s.px, s.py, stencil);
|
||||
fit_pixels.clear();
|
||||
for (int dy = -Rf; dy <= Rf; ++dy)
|
||||
for (int dx = -Rf; dx <= Rf; ++dx) {
|
||||
const float Pp = P[(dy + Rf) * Gf + (dx + Rf)];
|
||||
if (Pp <= 0.0f) continue;
|
||||
const int x = s.cx + dx, y = s.cy + dy;
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||
// The region the summation seed was taken over (pass A), so the guard compares the two
|
||||
// over the same pixels.
|
||||
const bool in_disk = s.footprint
|
||||
? BraggStencilInSignal(st, x - s.px, y - s.py,
|
||||
(x - s.px) * (x - s.px) + (y - s.py) * (y - s.py), r1_sq)
|
||||
: dx * dx + dy * dy < r1_sq;
|
||||
o.p_grid += Pp;
|
||||
o.p_peak = std::max(o.p_peak, Pp);
|
||||
if (in_disk) o.m_all += Pp;
|
||||
const int32_t v = img[y * W + x];
|
||||
if (!valid(v)) {
|
||||
o.p_lost_peak = std::max(o.p_lost_peak, Pp);
|
||||
continue;
|
||||
}
|
||||
o.p_valid += Pp;
|
||||
const bool own = !use_owner || BraggOwnedBy(owner_map.Get(x, y), static_cast<int>(i));
|
||||
if (own) o.p_own += Pp;
|
||||
if (in_disk && (own || !exclude)) o.m_read += Pp;
|
||||
if (own || !exclude)
|
||||
fit_pixels.push_back({Pp, static_cast<float>(v) - s.bkg});
|
||||
}
|
||||
if (!FitKept(o)) return o;
|
||||
|
||||
float If = s.I0;
|
||||
for (int iter = 0; iter < 4; ++iter) {
|
||||
float num = 0.0f;
|
||||
o.den = 0.0f;
|
||||
o.wsum = 0.0f;
|
||||
for (const auto &[Pp, signal] : fit_pixels) {
|
||||
const float iv = 1.0f / std::max(s.B + If * Pp, s.v_min);
|
||||
num += Pp * signal * iv;
|
||||
o.den += Pp * Pp * iv;
|
||||
o.wsum += Pp * iv;
|
||||
}
|
||||
if (o.den > 0.0f) If = num / o.den; else break;
|
||||
}
|
||||
o.I = If;
|
||||
return o;
|
||||
}
|
||||
|
||||
template <class Sampler, class OwnerMap>
|
||||
std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &img, const OwnerMap &owner_map,
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, PassA &a,
|
||||
int64_t image_number) {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
std::vector<BraggFitResult> results(npredicted);
|
||||
std::vector<Rough> &rough = a.rough;
|
||||
const bool exclude = overlap == OverlapMode::Exclude && mode != IntegratorMode::BoxSum;
|
||||
const bool use_owner = overlap != OverlapMode::Off;
|
||||
auto clean = [&](int x, int y, size_t i) {
|
||||
return !use_owner
|
||||
|| BraggOwnedBy(owner_map.Get(x, y), static_cast<int>(i));
|
||||
};
|
||||
auto grid_idx = [this](int dx, int dy) { return (dy + R) * G + (dx + R); };
|
||||
|
||||
// --- Radial background curvature correction. The annulus mean is blind to the curvature of the
|
||||
// radial background (a linear background cancels between the concentric disk and annulus), so
|
||||
// correct it by mean_annulus(B) - mean_disk(B) taken from the curve just accumulated. Reads no
|
||||
@@ -400,8 +583,8 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
const float *kern = k_diff.data() + static_cast<size_t>(rh.k_bin) * k_len;
|
||||
double corr = 0.0;
|
||||
for (int k = 0; k < k_len; ++k) {
|
||||
const int b = std::clamp(rh.r_bin + k - k_off, 0, n_rad - 1);
|
||||
const double v = rad_cnt[b] > 0 ? rad_sum[b] / rad_cnt[b] : rh.bkg;
|
||||
const int b = std::clamp(rh.r_bin + k - k_off, 0, a.n_rad - 1);
|
||||
const double v = a.rad_cnt[b] > 0 ? static_cast<double>(a.rad_sum[b]) / a.rad_cnt[b] : rh.bkg;
|
||||
corr += static_cast<double>(kern[k]) * v;
|
||||
}
|
||||
rh.bkg -= corr; // annulus mean -> mean over the signal disk
|
||||
@@ -430,8 +613,8 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
}
|
||||
|
||||
auto shell_of = [&](float d) {
|
||||
if (!(d > 0.0f) || inv_d2_max <= inv_d2_min) return 0;
|
||||
const double t = (1.0 / (static_cast<double>(d) * d) - inv_d2_min) / (inv_d2_max - inv_d2_min);
|
||||
if (!(d > 0.0f) || a.inv_d2_max <= a.inv_d2_min) return 0;
|
||||
const double t = (1.0 / (static_cast<double>(d) * d) - a.inv_d2_min) / (a.inv_d2_max - a.inv_d2_min);
|
||||
return std::clamp(static_cast<int>(t * N_SHELL), 0, N_SHELL - 1);
|
||||
};
|
||||
for (size_t i = 0; i < npredicted; ++i)
|
||||
@@ -494,22 +677,22 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
};
|
||||
// Normalised empirical profile (sum = 1), the average grid over the strong spots of a shell.
|
||||
// ProfileGaussian does not use it - it rebuilds a per-reflection Gaussian in Pass B.
|
||||
// Stored in float, as the fit below reads it (and as the GPU engine keeps it).
|
||||
auto build_profile = [&](const std::vector<double> &grid) {
|
||||
std::vector<double> P(GG, 0.0);
|
||||
std::vector<float> P(GG, 0.0f);
|
||||
double sum = 0.0;
|
||||
for (int k = 0; k < GG; ++k) {
|
||||
P[k] = std::max(0.0, grid[k]);
|
||||
sum += P[k];
|
||||
}
|
||||
for (int k = 0; k < GG; ++k)
|
||||
sum += std::max(0.0, grid[k]);
|
||||
if (sum > 0.0)
|
||||
for (double &p : P) p /= sum;
|
||||
for (int k = 0; k < GG; ++k)
|
||||
P[k] = static_cast<float>(std::max(0.0, grid[k]) / sum);
|
||||
return P;
|
||||
};
|
||||
|
||||
const std::vector<double> global_P = empirical && global_n > 0 ? build_profile(global_grid)
|
||||
: std::vector<double>(GG, 0.0);
|
||||
const std::vector<float> global_P = empirical && global_n > 0 ? build_profile(global_grid)
|
||||
: std::vector<float>(GG, 0.0f);
|
||||
const Sigma2 global_sigma2 = widths(global_mom);
|
||||
std::vector<std::vector<double>> shell_P(N_SHELL, global_P);
|
||||
std::vector<std::vector<float>> shell_P(N_SHELL, global_P);
|
||||
std::vector<Sigma2> shell_sigma2(N_SHELL, global_sigma2);
|
||||
for (int s = 0; s < N_SHELL; ++s) {
|
||||
if (shell_n[s] < MIN_STRONG_PER_SHELL) continue;
|
||||
@@ -519,14 +702,22 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
|
||||
// --- Pass B: profile-fit each reflection (Kabsch, de-biased variance v = B + I*P; iterate). The
|
||||
// reweighting is the Kabsch/Otwinowski iteration: Kabsch, Acta Cryst D66, 133-144 (2010);
|
||||
// Otwinowski & Minor, Methods Enzymol 276, 307-326 (1997). ---
|
||||
std::vector<double> Pbuf;
|
||||
// The pixels the fit reads, gathered once while p_valid walks the grid: the profile value and the
|
||||
// background-subtracted count, in the grid's order.
|
||||
struct FitPixel { double Pp, signal; };
|
||||
std::vector<FitPixel> fit_pixels;
|
||||
// Otwinowski & Minor, Methods Enzymol 276, 307-326 (1997).
|
||||
// Per reflection in three steps: what the fit needs, worked out in double (BraggFitSetup); the
|
||||
// fit in single precision (FitProfile, or the Metal engine's kernel), with the GPU engine's
|
||||
// arithmetic (BraggIntegrationEngineGPU.cu, fit) - the profile from exp with the widths'
|
||||
// reciprocals taken once, normalised by a reciprocal, and one reciprocal of the variance for the
|
||||
// three weights; and its error, the summation guard and the result, in double. ---
|
||||
std::vector<float> shell_profiles; // the learned profiles, shell by shell (ProfileEmpirical)
|
||||
if (empirical)
|
||||
for (int s = 0; s < N_SHELL; ++s)
|
||||
shell_profiles.insert(shell_profiles.end(), shell_P[s].begin(), shell_P[s].end());
|
||||
std::vector<BraggFitSetup> setups(npredicted);
|
||||
uint32_t cells = 0;
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
const auto &rh = rough[i];
|
||||
BraggFitSetup &fs = setups[i];
|
||||
fs = {};
|
||||
if (!rh.ok) continue;
|
||||
// An overloaded reflection is not measured by any fit - its brightest pixels are what is
|
||||
// missing. It is passed on with its box sum and the flag, for the merge to drop its rocking
|
||||
@@ -539,10 +730,15 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
continue;
|
||||
}
|
||||
const int sh = rh.shell < 0 ? 0 : rh.shell;
|
||||
|
||||
int Rf = R;
|
||||
const std::vector<double> *Pvec = &shell_P[sh];
|
||||
const BraggStencil st = MakeBraggStencil(predicted[i].predicted_x, predicted[i].predicted_y, stencil);
|
||||
fs.fit = 1;
|
||||
fs.px = predicted[i].predicted_x;
|
||||
fs.py = predicted[i].predicted_y;
|
||||
fs.cx = rh.cx;
|
||||
fs.cy = rh.cy;
|
||||
fs.Rf = R;
|
||||
fs.shell = sh;
|
||||
fs.footprint = st.fp_s2t > 0.0f;
|
||||
if (!empirical) {
|
||||
const double rx = predicted[i].predicted_x - beam_x, ry = predicted[i].predicted_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry);
|
||||
@@ -571,94 +767,41 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
s2r = std::max<double>(s2r, st.fp_s2r);
|
||||
elong = true;
|
||||
}
|
||||
// Build the Gaussian per reflection, centred on the sub-pixel predicted position and (when
|
||||
// The Gaussian is built per reflection, centred on the sub-pixel predicted position and (when
|
||||
// needed) elongated, on a grid grown to hold the wider of its two axes.
|
||||
const double fx = predicted[i].predicted_x - rh.cx, fy = predicted[i].predicted_y - rh.cy;
|
||||
Rf = elong ? std::min(3 * R, static_cast<int>(std::ceil(r2 + 2.0 * std::sqrt(std::max(s2r, s2t))))) : R;
|
||||
// The grid's image rows, asked for now (as in pass A): building the profile below takes
|
||||
// long enough for them to arrive before the fit reads them.
|
||||
const int gx0 = std::max(0, rh.cx - Rf), gx1 = std::min(W - 1, rh.cx + Rf);
|
||||
for (int y = std::max(0, rh.cy - Rf); y <= std::min(H - 1, rh.cy + Rf); ++y)
|
||||
prefetch_row(img.address(static_cast<size_t>(y) * W + gx0),
|
||||
img.address(static_cast<size_t>(y) * W + gx1));
|
||||
const int Gf = 2 * Rf + 1;
|
||||
Pbuf.assign(static_cast<size_t>(Gf) * Gf, 0.0);
|
||||
double gs = 0.0;
|
||||
for (int dy = -Rf; dy <= Rf; ++dy)
|
||||
for (int dx = -Rf; dx <= Rf; ++dx) {
|
||||
const double ex = dx - fx, ey = dy - fy;
|
||||
const double rad = ex * ux + ey * uy, tn = -ex * uy + ey * ux;
|
||||
const double g = std::exp(-rad * rad / (2.0 * s2r) - tn * tn / (2.0 * s2t));
|
||||
Pbuf[(dy + Rf) * Gf + (dx + Rf)] = g;
|
||||
gs += g;
|
||||
}
|
||||
for (double &p : Pbuf) p /= gs;
|
||||
Pvec = &Pbuf;
|
||||
fs.Rf = elong ? std::min(3 * R, static_cast<int>(std::ceil(r2 + 2.0 * std::sqrt(std::max(s2r, s2t))))) : R;
|
||||
fs.shell = -1;
|
||||
fs.cells = cells;
|
||||
cells += (2 * fs.Rf + 1) * (2 * fs.Rf + 1);
|
||||
fs.fx = static_cast<float>(predicted[i].predicted_x - rh.cx);
|
||||
fs.fy = static_cast<float>(predicted[i].predicted_y - rh.cy);
|
||||
fs.ux = static_cast<float>(ux);
|
||||
fs.uy = static_cast<float>(uy);
|
||||
fs.inv_2s2r = 0.5f / static_cast<float>(s2r);
|
||||
fs.inv_2s2t = 0.5f / static_cast<float>(s2t);
|
||||
}
|
||||
fs.bkg = static_cast<float>(rh.bkg);
|
||||
fs.B = static_cast<float>(std::max(rh.bkg, PIXEL_VARIANCE_FLOOR));
|
||||
fs.v_min = static_cast<float>(WEIGHT_VARIANCE_MIN_FRACTION) * fs.B;
|
||||
fs.I0 = static_cast<float>(rh.I);
|
||||
}
|
||||
|
||||
const int Gf = 2 * Rf + 1;
|
||||
std::vector<BraggFitOut> fits(npredicted);
|
||||
if (!FitOnDevice(setups, shell_profiles, fits)) {
|
||||
std::vector<float> cells_buf;
|
||||
std::vector<FitPixel> fit_pixels;
|
||||
for (const uint32_t i : FrameOrder(predicted, npredicted))
|
||||
if (setups[i].fit)
|
||||
fits[i] = FitProfile(img, owner_map, setups[i], i, shell_profiles, cells_buf, fit_pixels);
|
||||
}
|
||||
|
||||
// --- How much of the expected profile the fit can actually see. p_valid is the readable
|
||||
// fraction of it - XDS's MINPK, dials' valid_foreground_threshold - measured against the
|
||||
// mass that falls on the detector at all, so a reflection is judged on the pixels that
|
||||
// exist and not on where the sensor ends. p_own is the same quantity over neighbour-owned
|
||||
// pixels, what Reject cuts on. m_read / m_all is the fraction of the r1 disk the
|
||||
// summation seed the runaway guard compares against actually summed; with nothing missing
|
||||
// and nothing excluded it is 1 and the guard is untouched. ---
|
||||
double p_grid = 0.0, p_valid = 0.0, p_own = 0.0, m_all = 0.0, m_read = 0.0;
|
||||
double p_peak = 0.0, p_lost_peak = 0.0;
|
||||
fit_pixels.clear();
|
||||
for (int dy = -Rf; dy <= Rf; ++dy)
|
||||
for (int dx = -Rf; dx <= Rf; ++dx) {
|
||||
const double Pp = (*Pvec)[(dy + Rf) * Gf + (dx + Rf)];
|
||||
if (Pp <= 0.0) continue;
|
||||
const int x = rh.cx + dx, y = rh.cy + dy;
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||
// The region the summation seed was taken over (pass A), so the guard below
|
||||
// compares the two over the same pixels.
|
||||
const bool in_disk = st.fp_s2t > 0.0f
|
||||
? BraggStencilInSignal(st, x - predicted[i].predicted_x, y - predicted[i].predicted_y,
|
||||
(x - predicted[i].predicted_x) * (x - predicted[i].predicted_x)
|
||||
+ (y - predicted[i].predicted_y) * (y - predicted[i].predicted_y),
|
||||
r1_sq)
|
||||
: dx * dx + dy * dy < r1_sq;
|
||||
p_grid += Pp;
|
||||
p_peak = std::max(p_peak, Pp);
|
||||
if (in_disk) m_all += Pp;
|
||||
if (!valid(img[y * W + x])) {
|
||||
p_lost_peak = std::max(p_lost_peak, Pp);
|
||||
continue;
|
||||
}
|
||||
p_valid += Pp;
|
||||
const bool own = clean(x, y, i);
|
||||
if (own) p_own += Pp;
|
||||
if (in_disk && (own || !exclude)) m_read += Pp;
|
||||
if (own || !exclude)
|
||||
fit_pixels.push_back({Pp, static_cast<double>(img[y * W + x]) - rh.bkg});
|
||||
}
|
||||
if (p_valid < overlap_min_peak * p_grid) continue;
|
||||
// A hole in the profile's PEAK is a different defect from a hole in its wings, and the mass
|
||||
// fraction above cannot tell them apart - the peak of a broad spot is a few percent of the
|
||||
// mass, so MINPK passes a reflection that has lost the one part of the profile its amplitude
|
||||
// is determined by. See the header.
|
||||
if (p_lost_peak > MINPK_MAX_MISSING_PEAK * p_peak) continue;
|
||||
if (overlap == OverlapMode::Reject && p_own < overlap_min_peak) continue;
|
||||
|
||||
const double B = std::max(rh.bkg, PIXEL_VARIANCE_FLOOR);
|
||||
double I = rh.I, den = 0.0, wsum = 0.0;
|
||||
for (int iter = 0; iter < 4; ++iter) {
|
||||
double num = 0.0;
|
||||
den = 0.0;
|
||||
wsum = 0.0;
|
||||
for (const auto &[Pp, signal] : fit_pixels) {
|
||||
const double v = std::max(B + I * Pp, WEIGHT_VARIANCE_MIN_FRACTION * B);
|
||||
num += Pp * signal / v;
|
||||
den += Pp * Pp / v;
|
||||
wsum += Pp / v;
|
||||
}
|
||||
if (den > 0.0) I = num / den; else break;
|
||||
}
|
||||
if (!(den > 0.0)) continue;
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
if (!setups[i].fit) continue;
|
||||
const auto &rh = rough[i];
|
||||
const BraggFitOut &fo = fits[i];
|
||||
if (!FitKept(fo) || !(fo.den > 0.0f)) continue;
|
||||
const float den = fo.den, wsum = fo.wsum;
|
||||
double I = fo.I;
|
||||
|
||||
// Guard against profile-fit runaways: on a weak / near-zero reflection the reweighted Kabsch
|
||||
// iteration has no real peak to lock onto and can manufacture intensity the box sum never sees.
|
||||
@@ -679,7 +822,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
// The seed is a sum over the disk the box sum actually read, so when pixels are missing from
|
||||
// both - excluded to a neighbour, or unreadable - the fit's full-profile intensity has to be
|
||||
// scaled down to that same disk before the two are comparable. Nothing dropped gives 1.
|
||||
const double guard_scale = m_all > 0.0 ? m_read / m_all : 1.0;
|
||||
const double guard_scale = fo.m_all > 0.0f ? fo.m_read / fo.m_all : 1.0;
|
||||
if (std::abs(I * guard_scale - rh.I) > PROFILE_SUMMATION_MAX_NSIGMA * rh.sigma) {
|
||||
I = rh.I;
|
||||
sigma = rh.sigma;
|
||||
@@ -697,6 +840,10 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
return Finalize(predicted, npredicted, results, image_number);
|
||||
}
|
||||
|
||||
template std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(
|
||||
const BraggIntegrationEngineCPU::BufferSampler &, const BraggIntegrationEngineCPU::FlatOwnerMap &,
|
||||
const std::vector<Reflection> &, size_t, BraggIntegrationEngineCPU::PassA &, int64_t);
|
||||
|
||||
std::vector<Reflection> BraggIntegrationEngineCPU::Run(const ImagePreprocessorBuffer &image,
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, int64_t image_number) {
|
||||
|
||||
Reference in new issue
Block a user