v1.0.0-rc.179 (#90)
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast. * Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU). * Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors. * Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer. Reviewed-on: #90 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #90.
This commit is contained in:
1 parent
afbe844b20
commit
b00c668e3a
394 files changed
+57778
-2178
No files matched your search
@@ -7,6 +7,8 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
|
||||
@@ -23,6 +25,11 @@ namespace {
|
||||
// The engine reads pixels in the INT32_MIN(masked)/INT32_MAX(saturated) convention.
|
||||
inline bool valid(int32_t v) { return v != INT32_MIN && v != INT32_MAX; }
|
||||
|
||||
// A direction on the plane in a panel's pixel axes (MakeBraggStencil). Unchanged without panels.
|
||||
inline std::pair<double, double> to_panel_axes(const BraggPanel &pn, double ux, double uy) {
|
||||
return {ux * pn.fx + uy * pn.fy, ux * pn.sx + uy * pn.sy};
|
||||
}
|
||||
|
||||
// Ask for the cache lines from first to last ahead of their use. Only a hint: it changes no result.
|
||||
inline void prefetch(const void *p) {
|
||||
#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
|
||||
@@ -78,27 +85,33 @@ BraggIntegrationEngineCPU::BraggIntegrationEngineCPU(const DiffractionExperiment
|
||||
: BraggIntegrationEngine(experiment),
|
||||
refl_mask(static_cast<int>(xpixel), static_cast<int>(ypixel), 0),
|
||||
owner(static_cast<int>(xpixel), static_cast<int>(ypixel), BRAGG_OWNER_NONE),
|
||||
static_mask(mask.GetPackedMask()) {}
|
||||
static_mask(mask.GetPackedMask()) {
|
||||
if (mask.GetMask().size() != npixel)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"Pixel mask size doesn't match the image");
|
||||
}
|
||||
|
||||
void BraggIntegrationEngineCPU::MarkNeighbours(const std::vector<Reflection> &predicted,
|
||||
const std::vector<BraggPanel> &panels,
|
||||
const std::vector<uint32_t> &order) {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
// --- 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
|
||||
// - and MASK_TAIL where only the tail of a rocking curve does, which is counted for the density
|
||||
// guard but read as background. The region is the INNER stencil ellipse, taken in each
|
||||
// neighbour's OWN frame - an elongated neighbour whose streak is still marked as a disk would
|
||||
// leak its tails into this reflection's ring. ---
|
||||
// leak its tails into this reflection's ring. Only on the neighbour's own panel: beyond it the
|
||||
// image holds pixels that are elsewhere on the detector. ---
|
||||
refl_mask.Clear();
|
||||
for (const uint32_t i : order) {
|
||||
const auto &r = predicted[i];
|
||||
const BraggPanel &pn = panels[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);
|
||||
const int x0 = std::max(0, static_cast<int>(std::floor(r.predicted_x - st.ex_in - 1.0f)));
|
||||
const int x1 = std::min(W - 1, static_cast<int>(std::ceil(r.predicted_x + st.ex_in + 1.0f)));
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(r.predicted_y - st.ey_in - 1.0f)));
|
||||
const int y1 = std::min(H - 1, static_cast<int>(std::ceil(r.predicted_y + st.ey_in + 1.0f)));
|
||||
const BraggStencil st = MakeBraggStencil(pn, stencil);
|
||||
const int x0 = std::max(pn.x0, static_cast<int>(std::floor(r.predicted_x - st.ex_in - 1.0f)));
|
||||
const int x1 = std::min(pn.x1, static_cast<int>(std::ceil(r.predicted_x + st.ex_in + 1.0f)));
|
||||
const int y0 = std::max(pn.y0, static_cast<int>(std::floor(r.predicted_y - st.ey_in - 1.0f)));
|
||||
const int y1 = std::min(pn.y1, static_cast<int>(std::ceil(r.predicted_y + st.ey_in + 1.0f)));
|
||||
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);
|
||||
@@ -115,10 +128,12 @@ void BraggIntegrationEngineCPU::CountNeighbourCrowding(const std::vector<Reflect
|
||||
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);
|
||||
const std::vector<BraggPanel> panels = Panels(predicted, npredicted);
|
||||
MarkNeighbours(predicted, panels, order);
|
||||
for (const uint32_t i : order) {
|
||||
const auto &r = predicted[i];
|
||||
const BraggStencil st = MakeBraggStencil(r.predicted_x, r.predicted_y, stencil);
|
||||
const BraggPanel &pn = panels[i];
|
||||
const BraggStencil st = MakeBraggStencil(pn, 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)));
|
||||
@@ -131,7 +146,8 @@ void BraggIntegrationEngineCPU::CountNeighbourCrowding(const std::vector<Reflect
|
||||
|| 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))
|
||||
if (!BraggPanelHolds(pn, x, y)
|
||||
|| (idx / 32 < static_mask.size() && ((static_mask[idx / 32] >> (idx % 32)) & 1U)))
|
||||
continue;
|
||||
++n_ring;
|
||||
if (refl_mask.Get(x, y) == 0)
|
||||
@@ -147,7 +163,7 @@ 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);
|
||||
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);
|
||||
@@ -155,7 +171,8 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
|
||||
// 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);
|
||||
const std::vector<BraggPanel> panels = Panels(predicted, npredicted);
|
||||
MarkNeighbours(predicted, panels, order);
|
||||
owner.Clear();
|
||||
|
||||
// --- Signal-region ownership: a pixel inside two reflections' signal regions belongs to the
|
||||
@@ -169,10 +186,11 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
const float claim_sq = claim * claim;
|
||||
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)));
|
||||
const int y0 = std::max(0, static_cast<int>(std::floor(r.predicted_y - claim)));
|
||||
const int y1 = std::min(H - 1, static_cast<int>(std::ceil(r.predicted_y + claim)));
|
||||
const BraggPanel &pn = panels[i];
|
||||
const int x0 = std::max(pn.x0, static_cast<int>(std::floor(r.predicted_x - claim)));
|
||||
const int x1 = std::min(pn.x1, static_cast<int>(std::ceil(r.predicted_x + claim)));
|
||||
const int y0 = std::max(pn.y0, static_cast<int>(std::floor(r.predicted_y - claim)));
|
||||
const int y1 = std::min(pn.y1, static_cast<int>(std::ceil(r.predicted_y + claim)));
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
const float dx = x - r.predicted_x, dy = y - r.predicted_y;
|
||||
@@ -220,15 +238,16 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
// prefetch.)
|
||||
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));
|
||||
const Box b = box_of(n, MakeBraggStencil(panels[order[k + 1]], 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];
|
||||
const BraggPanel &pn = panels[i];
|
||||
// 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 BraggStencil st = MakeBraggStencil(pn, stencil);
|
||||
const Box box = box_of(r, st);
|
||||
const int x0 = box.x0, x1 = box.x1, y0 = box.y0, y1 = box.y1;
|
||||
|
||||
@@ -240,10 +259,17 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
BraggBoxSums s{};
|
||||
bkg_vals.clear();
|
||||
clip_ring.clear();
|
||||
// Where the frame's curve is in use: the reflection's place on it, and the curve summed over the
|
||||
// readable disk pixels and over the ring pixels the background is taken from
|
||||
// (BraggCurveCorrection, in FinishPassA).
|
||||
const bool curve = bkg_radial && !radial_curve.empty();
|
||||
const float rc = curve ? CurveRadius(pn) : st.r0;
|
||||
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];
|
||||
// Off the reflection's panel is a gap: unreadable, and not lost to the flux.
|
||||
const bool on = BraggPanelHolds(pn, x, y);
|
||||
const int32_t px = on ? img[y * W + x] : INT32_MIN;
|
||||
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)) {
|
||||
@@ -258,8 +284,10 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
}
|
||||
++s.n_inner;
|
||||
if (px == INT32_MAX
|
||||
|| (px == INT32_MIN && !static_masked(static_cast<size_t>(y) * W + x)))
|
||||
|| (px == INT32_MIN && on && !static_masked(static_cast<size_t>(y) * W + x)))
|
||||
s.overloaded = 1;
|
||||
if (px == INT32_MAX)
|
||||
++s.n_saturated;
|
||||
if (!valid(px)) continue;
|
||||
s.I_sum += px;
|
||||
s.I_sum_x += static_cast<int64_t>(x) * px;
|
||||
@@ -269,6 +297,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
s.x_sum += x;
|
||||
s.y_sum += y;
|
||||
++s.n_inner_valid;
|
||||
if (curve) s.curve_disk += CurveFixed(rc + d.rad);
|
||||
} else if (d.inner >= r2_sq && d.outer < r3_sq) {
|
||||
if (!valid(px)) continue;
|
||||
++s.n_ring;
|
||||
@@ -278,6 +307,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
s.bkg_sum += px;
|
||||
if (bkg_trim > 0.0f) bkg_vals.push_back(px);
|
||||
++s.n_bkg;
|
||||
if (curve) s.curve_ring += CurveFixed(rc + d.rad);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,10 +317,11 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
if (ClipsRing(s)) {
|
||||
const int32_t thr = ClipThreshold(s);
|
||||
for (const auto &[px, rad] : clip_ring)
|
||||
if (px <= thr) {
|
||||
if (static_cast<int64_t>(px) <= static_cast<int64_t>(thr) + RadialClipOffset(rc, rad)) {
|
||||
clip.sum += px;
|
||||
++clip.n;
|
||||
if (bkg_radial) {
|
||||
if (curve) clip.curve += CurveFixed(rc + rad);
|
||||
if (bkg_radial && radial_curve.empty()) {
|
||||
// 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);
|
||||
@@ -302,18 +333,19 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
FinishPassA(a, i, r, st, s, clip, bkg_vals);
|
||||
}
|
||||
|
||||
return ProfilePasses(img, owner, predicted, npredicted, a, image_number);
|
||||
return ProfilePasses(img, owner, predicted, npredicted, panels, 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) {
|
||||
// direction, so no per-pixel sqrt is needed. Out to the far corner of the plane the panels span.
|
||||
const float bx0 = static_cast<float>(plane_bounds.x0), bx1 = static_cast<float>(plane_bounds.x1);
|
||||
const float by0 = static_cast<float>(plane_bounds.y0), by1 = static_cast<float>(plane_bounds.y1);
|
||||
a.n_rad = static_cast<int>(std::ceil(std::hypot(std::max<double>(beam_x - bx0, bx1 - beam_x),
|
||||
std::max<double>(beam_y - by0, by1 - beam_y)))) + 2;
|
||||
if (bkg_radial && radial_curve.empty()) {
|
||||
a.rad_sum.assign(a.n_rad, 0);
|
||||
a.rad_cnt.assign(a.n_rad, 0);
|
||||
}
|
||||
@@ -330,10 +362,12 @@ bool BraggIntegrationEngineCPU::ClipsRing(const BraggBoxSums &s) const {
|
||||
|
||||
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.
|
||||
// the mean, the mean taken at no less than one photon. 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));
|
||||
const double g = pixel_gain;
|
||||
const double thr = bkg + bkg_clip_nsigma * std::sqrt(g * std::max(bkg, g));
|
||||
return static_cast<int32_t>(std::clamp(std::floor(thr), static_cast<double>(INT32_MIN),
|
||||
static_cast<double>(INT32_MAX)));
|
||||
}
|
||||
@@ -346,6 +380,7 @@ void BraggIntegrationEngineCPU::FinishPassA(PassA &a, size_t i, const Reflection
|
||||
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;
|
||||
out.saturated = s.n_saturated;
|
||||
|
||||
if (overlap != OverlapMode::Off && s.n_own < s.n_disk) {
|
||||
++counts.overlapped;
|
||||
@@ -403,17 +438,21 @@ void BraggIntegrationEngineCPU::FinishPassA(PassA &a, size_t i, const Reflection
|
||||
// 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.
|
||||
// bkg_var is in counted units, as the profile fit reads it; every variance formed from it is
|
||||
// taken at the gain, so a CCD's sigma - and the strong flag and the guard read from it - is in
|
||||
// photons rather than in ADU counted as photons.
|
||||
const double g = pixel_gain;
|
||||
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.var_bkg = g * (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;
|
||||
out.sigma = g; // a photon
|
||||
if (s.I_sum > 0) {
|
||||
out.sigma = std::max(out.sigma, std::sqrt(static_cast<double>(s.I_sum) + var_bkg_term));
|
||||
out.sigma = std::max(out.sigma, std::sqrt(g * (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
|
||||
@@ -449,6 +488,13 @@ void BraggIntegrationEngineCPU::FinishPassA(PassA &a, size_t i, const Reflection
|
||||
a.inv_d2_max = std::max(a.inv_d2_max, inv_d2);
|
||||
}
|
||||
}
|
||||
// With the frame's curve in use, the correction over the pixels pass A read: the ring pixels the
|
||||
// background above was taken from, and the readable disk pixels.
|
||||
if (bkg_radial && !radial_curve.empty()) {
|
||||
const bool kept = ClipsRing(s) && clip.n > 5;
|
||||
out.curve_corr = BraggCurveCorrection(kept ? clip.curve : s.curve_ring, kept ? clip.n : s.n_bkg,
|
||||
s.curve_disk, static_cast<int>(s.n_inner_valid));
|
||||
}
|
||||
a.rough[i] = out;
|
||||
}
|
||||
|
||||
@@ -465,7 +511,7 @@ bool BraggIntegrationEngineCPU::FitKept(const BraggFitOut &o) const {
|
||||
|
||||
template <class Sampler, class OwnerMap>
|
||||
BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const OwnerMap &owner_map,
|
||||
const BraggFitSetup &s, size_t i,
|
||||
const BraggFitSetup &s, const BraggPanel &pn, size_t i,
|
||||
const std::vector<float> &shell_profiles,
|
||||
std::vector<float> &cells,
|
||||
std::vector<FitPixel> &fit_pixels) const {
|
||||
@@ -476,9 +522,15 @@ BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const Owne
|
||||
BraggFitOut o{};
|
||||
|
||||
const float *P;
|
||||
if (s.shell >= 0)
|
||||
P = shell_profiles.data() + static_cast<size_t>(s.shell) * GG;
|
||||
else {
|
||||
if (s.shell >= 0) {
|
||||
// The shell's learned profile is in the plane's axes; laid out here in the panel's.
|
||||
const float *learned = shell_profiles.data() + static_cast<size_t>(s.shell) * GG;
|
||||
cells.resize(GG);
|
||||
for (int dy = -R; dy <= R; ++dy)
|
||||
for (int dx = -R; dx <= R; ++dx)
|
||||
cells[(dy + R) * G + (dx + R)] = learned[BraggProfileCell(pn, dx, dy, R)];
|
||||
P = cells.data();
|
||||
} 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);
|
||||
@@ -506,7 +558,7 @@ BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const Owne
|
||||
// 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);
|
||||
const BraggStencil st = MakeBraggStencil(pn, stencil);
|
||||
fit_pixels.clear();
|
||||
for (int dy = -Rf; dy <= Rf; ++dy)
|
||||
for (int dx = -Rf; dx <= Rf; ++dx) {
|
||||
@@ -523,7 +575,7 @@ BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const Owne
|
||||
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];
|
||||
const int32_t v = BraggPanelHolds(pn, x, y) ? img[y * W + x] : INT32_MIN;
|
||||
if (!valid(v)) {
|
||||
o.p_lost_peak = std::max(o.p_lost_peak, Pp);
|
||||
continue;
|
||||
@@ -557,7 +609,8 @@ BraggFitOut BraggIntegrationEngineCPU::FitProfile(const Sampler &img, const Owne
|
||||
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,
|
||||
size_t npredicted,
|
||||
const std::vector<BraggPanel> &panels, PassA &a,
|
||||
int64_t image_number) {
|
||||
const int W = static_cast<int>(xpixel), H = static_cast<int>(ypixel);
|
||||
std::vector<BraggFitResult> results(npredicted);
|
||||
@@ -568,27 +621,34 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
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
|
||||
// pixels: one short dot product per reflection. ---
|
||||
// correct it by mean_ring(B) - mean_disk(B). With the frame's curve that was taken in pass A over
|
||||
// the pixels themselves (BraggCurveCorrection); with the curve just accumulated from the
|
||||
// reflections it is the kernel, one short dot product per reflection. ---
|
||||
if (bkg_radial) {
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
auto &rh = rough[i];
|
||||
if (!rh.ok) continue;
|
||||
// An empty bin contributes the reflection's own background, so a fully empty
|
||||
// neighbourhood gives corr == 0 exactly (the kernel weights sum to zero).
|
||||
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, 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;
|
||||
double corr = rh.curve_corr;
|
||||
if (radial_curve.empty()) {
|
||||
const float *kern = k_diff.data() + static_cast<size_t>(rh.k_bin) * k_len;
|
||||
for (int k = 0; k < k_len; ++k) {
|
||||
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
|
||||
rh.I = static_cast<double>(rh.I_sum) - static_cast<double>(rh.n_inner) * rh.bkg;
|
||||
// The background under the disk is now the corrected one, and so is its Poisson share of
|
||||
// the non-signal variance. The estimate's own variance (bkg_var) stays: it is the noise of
|
||||
// the ring pixels, which the correction - a smooth curve - does not change; nor does sigma,
|
||||
// which is taken from the counts in the disk.
|
||||
rh.var_bkg -= pixel_gain * static_cast<double>(rh.n_inner) * corr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,9 +667,9 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
if (overlap == OverlapMode::Reject && rh.n_own < overlap_min_peak * rh.n_disk) continue;
|
||||
results[i] = {static_cast<float>(rh.I), static_cast<float>(rh.sigma), static_cast<float>(rh.bkg),
|
||||
static_cast<float>(rh.obs_x), static_cast<float>(rh.obs_y),
|
||||
static_cast<float>(rh.var_bkg), true, rh.has_obs, rh.overloaded};
|
||||
static_cast<float>(rh.var_bkg), true, rh.has_obs, rh.overloaded, rh.saturated};
|
||||
}
|
||||
return Finalize(predicted, npredicted, results, image_number);
|
||||
return Finalize(predicted, npredicted, panels, results, image_number);
|
||||
}
|
||||
|
||||
auto shell_of = [&](float d) {
|
||||
@@ -622,7 +682,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
|
||||
// --- Learn the profile per shell (+ global) from the strong spots. ---
|
||||
// Two things are learned. The empirical profile is the average grid in the DETECTOR frame, which is
|
||||
// where it is applied. The width is a pair of second moments taken in each spot's OWN radial /
|
||||
// where it is applied - in the plane's axes, which a turned panel's pixels are mapped onto. The width is a pair of second moments taken in each spot's OWN radial /
|
||||
// tangential frame: a grid stacked in the detector frame is azimuthally averaged, so its <r^2> is
|
||||
// sigma_r^2 + sigma_t^2 with no way back, and a radially smeared spot reads as a wide TANGENTIAL
|
||||
// one. Rotating each contribution into the spot's frame keeps the two apart.
|
||||
@@ -637,19 +697,22 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
const auto &rh = rough[i];
|
||||
if (!rh.ok || !rh.strong || rh.I <= 0.0) continue;
|
||||
const double rx = predicted[i].predicted_x - beam_x, ry = predicted[i].predicted_y - beam_y;
|
||||
const BraggPanel &pn = panels[i];
|
||||
const double rx = pn.plane_x - beam_x, ry = pn.plane_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry);
|
||||
const double ux = Rpx > 1e-6 ? rx / Rpx : 1.0, uy = Rpx > 1e-6 ? ry / Rpx : 0.0;
|
||||
for (int dy = -R; dy <= R; ++dy)
|
||||
const auto [ux, uy] = to_panel_axes(pn, Rpx > 1e-6 ? rx / Rpx : 1.0, Rpx > 1e-6 ? ry / Rpx : 0.0);
|
||||
for (int dy = -R; dy <= R; ++dy)
|
||||
for (int dx = -R; dx <= R; ++dx) {
|
||||
const int x = rh.cx + dx, y = rh.cy + dy;
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||
if (x < 0 || y < 0 || x >= W || y >= H || !BraggPanelHolds(pn, x, y)) continue;
|
||||
const int32_t px = img[y * W + x];
|
||||
if (!valid(px)) continue;
|
||||
if (exclude && !clean(x, y, i)) continue;
|
||||
const double v = (static_cast<double>(px) - rh.bkg) / rh.I;
|
||||
shell_grid[rh.shell][grid_idx(dx, dy)] += v;
|
||||
global_grid[grid_idx(dx, dy)] += v;
|
||||
// The grid in the plane's axes (BraggProfileCell); the moments below are axis-free.
|
||||
const int cell = BraggProfileCell(pn, dx, dy, R);
|
||||
shell_grid[rh.shell][cell] += v;
|
||||
global_grid[cell] += v;
|
||||
if (dx * dx + dy * dy >= r1_sq) continue;
|
||||
const double rad = dx * ux + dy * uy, tn = -dx * uy + dy * ux;
|
||||
shell_mom[rh.shell].rad += v * rad * rad;
|
||||
@@ -720,17 +783,19 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
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
|
||||
// event (Reflection::overloaded); the missing-peak cut below would otherwise drop just this
|
||||
// frame and leave the event to be extrapolated from its flanks.
|
||||
// missing. It is passed on with its box sum and the flag, for a rotation merge to drop its
|
||||
// rocking event (Reflection::overloaded) and for a still to be kept as a lower bound
|
||||
// (Finalize); the missing-peak cut below would otherwise drop just this frame and leave the
|
||||
// event to be extrapolated from its flanks.
|
||||
if (rh.overloaded) {
|
||||
results[i] = {static_cast<float>(rh.I), static_cast<float>(rh.sigma), static_cast<float>(rh.bkg),
|
||||
static_cast<float>(rh.obs_x), static_cast<float>(rh.obs_y),
|
||||
static_cast<float>(rh.var_bkg), true, false, true};
|
||||
static_cast<float>(rh.var_bkg), true, false, true, rh.saturated};
|
||||
continue;
|
||||
}
|
||||
const int sh = rh.shell < 0 ? 0 : rh.shell;
|
||||
const BraggStencil st = MakeBraggStencil(predicted[i].predicted_x, predicted[i].predicted_y, stencil);
|
||||
const BraggPanel &pn = panels[i];
|
||||
const BraggStencil st = MakeBraggStencil(pn, stencil);
|
||||
fs.fit = 1;
|
||||
fs.px = predicted[i].predicted_x;
|
||||
fs.py = predicted[i].predicted_y;
|
||||
@@ -740,7 +805,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
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 rx = pn.plane_x - beam_x, ry = pn.plane_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry);
|
||||
const double tan2t = Rpx / F_px;
|
||||
double s2t = shell_sigma2[sh].tan;
|
||||
@@ -754,7 +819,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
const double radial_extra = std::max(shell_sigma2[sh].rad - s2t,
|
||||
sbw * sbw + c_radial * tan2t * tan2t);
|
||||
if (Rpx > 1e-6 && radial_extra > 0.25) {
|
||||
ux = rx / Rpx; uy = ry / Rpx;
|
||||
std::tie(ux, uy) = to_panel_axes(pn, rx / Rpx, ry / Rpx);
|
||||
s2r = s2t + radial_extra;
|
||||
elong = true;
|
||||
}
|
||||
@@ -781,7 +846,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
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.B = static_cast<float>(std::max(rh.bkg, pixel_gain * PIXEL_VARIANCE_FLOOR));
|
||||
fs.v_min = static_cast<float>(WEIGHT_VARIANCE_MIN_FRACTION) * fs.B;
|
||||
fs.I0 = static_cast<float>(rh.I);
|
||||
}
|
||||
@@ -792,7 +857,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
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);
|
||||
fits[i] = FitProfile(img, owner_map, setups[i], panels[i], i, shell_profiles, cells_buf, fit_pixels);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
@@ -810,15 +875,17 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
// 1/den is the profile-fit variance with the background taken as exact. The fit is
|
||||
// I = sum(P*(px-bkg)/v) / sum(P^2/v), so dI/dbkg = -wsum/den and the background estimate's
|
||||
// own error adds (wsum/den)^2 * var(bkg) - the same term the box sum was missing.
|
||||
double sigma = std::sqrt(1.0 / den + (wsum / den) * (wsum / den) * rh.bkg_var);
|
||||
// Both variances at the gain (FinishPassA), so the guard below compares like with like.
|
||||
const double g = pixel_gain;
|
||||
double sigma = std::sqrt(g * (1.0 / den + (wsum / den) * (wsum / den) * rh.bkg_var));
|
||||
// var_bkg is the NON-SIGNAL part of that variance, and 1/den is the fit's variance taken at the
|
||||
// fitted intensity, so the signal part to remove is I itself - not max(0, I). Clamping it leaves
|
||||
// a down-fluctuated reflection's own (deflated) variance standing as its background variance,
|
||||
// which is 2-3x too small; the merge then weights exactly the down-fluctuated observations up.
|
||||
// The whole point of a separate var_bkg is that it does not move with the observation's own
|
||||
// fluctuation, and 1/den - I is what does not (1/den grows with I one for one).
|
||||
double var_bkg = std::max(0.0, 1.0 / den - I
|
||||
+ (wsum / den) * (wsum / den) * rh.bkg_var);
|
||||
double var_bkg = g * std::max(0.0, 1.0 / den - I
|
||||
+ (wsum / den) * (wsum / den) * rh.bkg_var);
|
||||
// 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.
|
||||
@@ -834,21 +901,22 @@ std::vector<Reflection> BraggIntegrationEngineCPU::ProfilePasses(const Sampler &
|
||||
results[i] = {static_cast<float>(I), static_cast<float>(sigma),
|
||||
static_cast<float>(rh.bkg),
|
||||
static_cast<float>(rh.obs_x), static_cast<float>(rh.obs_y),
|
||||
static_cast<float>(var_bkg), true, rh.has_obs, rh.overloaded};
|
||||
static_cast<float>(var_bkg), true, rh.has_obs, rh.overloaded, rh.saturated};
|
||||
}
|
||||
|
||||
return Finalize(predicted, npredicted, results, image_number);
|
||||
return Finalize(predicted, npredicted, panels, 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);
|
||||
const std::vector<Reflection> &, size_t, const std::vector<BraggPanel> &,
|
||||
BraggIntegrationEngineCPU::PassA &, int64_t);
|
||||
|
||||
std::vector<Reflection> BraggIntegrationEngineCPU::Run(const ImagePreprocessorBuffer &image,
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, int64_t image_number) {
|
||||
if (image.size() != npixel)
|
||||
return Finalize(predicted, npredicted, std::vector<BraggFitResult>(npredicted), image_number);
|
||||
return Finalize(predicted, npredicted, {}, std::vector<BraggFitResult>(npredicted), image_number);
|
||||
return RunImpl(BufferSampler{image.data()}, predicted, npredicted, image_number);
|
||||
}
|
||||
|
||||
@@ -856,7 +924,7 @@ std::vector<Reflection> BraggIntegrationEngineCPU::Run(const CompressedImage &im
|
||||
const std::vector<Reflection> &predicted,
|
||||
size_t npredicted, int64_t image_number) {
|
||||
if (image.GetWidth() * image.GetHeight() != npixel)
|
||||
return Finalize(predicted, npredicted, std::vector<BraggFitResult>(npredicted), image_number);
|
||||
return Finalize(predicted, npredicted, {}, std::vector<BraggFitResult>(npredicted), image_number);
|
||||
|
||||
std::vector<uint8_t> scratch;
|
||||
const auto *ptr = image.GetUncompressedPtr(scratch);
|
||||
|
||||
Reference in new issue
Block a user