integration: add --reciprocal-profile dial (global reciprocal-space width)
Build Packages / Unit tests (push) Successful in 59m53s
Build Packages / build:windows:nocuda (push) Successful in 18m24s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m30s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m33s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m54s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m20s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m2s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m29s
Build Packages / build:rpm (rocky8) (push) Successful in 14m37s
Build Packages / build:rpm (rocky9) (push) Successful in 12m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m10s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m21s
Build Packages / DIALS test (push) Successful in 12m22s
Build Packages / XDS test (durin plugin) (push) Successful in 7m34s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m41s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m52s
Build Packages / Generate python client (push) Successful in 24s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 16m53s
Build Packages / Unit tests (push) Successful in 59m53s
Build Packages / build:windows:nocuda (push) Successful in 18m24s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m30s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m33s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m54s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m20s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m2s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m29s
Build Packages / build:rpm (rocky8) (push) Successful in 14m37s
Build Packages / build:rpm (rocky9) (push) Successful in 12m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m10s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m21s
Build Packages / DIALS test (push) Successful in 12m22s
Build Packages / XDS test (durin plugin) (push) Successful in 7m34s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m41s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m52s
Build Packages / Generate python client (push) Successful in 24s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 16m53s
The per-shell profile width is learned in pixels, so it varies ~4x with resolution (mostly the geometric projection of a near-constant reciprocal-space relrod) and must be binned per shell -> it starves at high resolution / on sparse data. The new --reciprocal-profile flag instead learns ONE global width in reciprocal space, sigma2_q,tan = A + B|q| + C|q|^2: the Jacobian g_tan=cos(2theta) removes the geometric projection, and C|q|^2 is the crystal mosaicity relrod (variance ~(eta|q|)^2). Applied per reflection as sigma2_tan,px = (A + B|q| + C|q|^2)/g_tan^2 (B,C clamped >=0; quadratic->linear->constant fallback). Off by default. On the sharp HEWL test crystal (mosaicity 0.091deg, so C fits to ~0 and it reduces to the validated linear form) it is metric-neutral: ISa 16.2->16.3, anomalous 0.92x unchanged, CCref band 90.0->89.9, CC1/2 a touch lower (per-shell isn't starved at 23k spots/shell, and a global fit is less flexible). So: simpler + more transferable at a small CC1/2 cost, ISa/anomalous held. Its payoff is on MOSAIC crystals (large C|q|^2), where per-shell starves on the wide weak high-res spots and 6 shells are too coarse; both lyso test crystals are sharp, so it ships as a dial to try on mosaic data elsewhere. A separate radial relrod fit was tried and dropped (no gain). See NEXTGEN_INTEGRATOR.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,64 @@ std::vector<Reflection> ProfileIntegrateInternal(const DiffractionExperiment &ex
|
||||
const double F_px = geom.GetDetectorDistance_mm() / std::max(1e-6f, geom.GetPixelSize_mm());
|
||||
const bool use_ellipse = !empirical && (bw_sigma > 0.0 || c_radial > 0.0);
|
||||
|
||||
// Reciprocal-space global profile width (--reciprocal-profile): one global model
|
||||
// sigma2_q,tan = A + B*|q| + C*|q|^2 over all strong spots replaces the per-shell pixel width. The
|
||||
// Jacobian g_tan = cos(2theta) maps the pixel tangential moment into reciprocal space, removing the
|
||||
// geometric projection that makes the pixel width grow ~4x with resolution. The C*|q|^2 term is the
|
||||
// crystal MOSAICITY (relrod variance ~ (eta*|q|)^2): ~0 for a sharp crystal (the fit collapses to
|
||||
// A + B*|q|, tying the per-shell model) but dominant for a mosaic one, where per-shell starves on the
|
||||
// wide, weak high-res spots. Applied per reflection as sigma2_tan,px = (A + B*|q| + C*|q|^2)/g_tan^2.
|
||||
const bool recip_on = settings.GetReciprocalProfile();
|
||||
double recip_A = 0.0, recip_B = 0.0, recip_C = 0.0;
|
||||
bool use_recip = false;
|
||||
if (recip_on && !empirical) {
|
||||
double s1 = 0, sq = 0, sq2 = 0, sq3 = 0, sq4 = 0, sy = 0, sqy = 0, sq2y = 0;
|
||||
for (size_t i = 0; i < npredicted; ++i) {
|
||||
const auto &rh = rough[i];
|
||||
if (!rh.ok || !rh.strong || rh.I <= 0.0 || !(predicted[i].d > 0.0f)) continue;
|
||||
const double rx = predicted[i].predicted_x - beam_x, ry = predicted[i].predicted_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry);
|
||||
if (Rpx < 1e-6) continue;
|
||||
const double ux = rx / Rpx, uy = ry / Rpx;
|
||||
double m2 = 0.0, m2w = 0.0;
|
||||
for (int dy = -R; dy <= R; ++dy)
|
||||
for (int dx = -R; dx <= R; ++dx) {
|
||||
if (dx * dx + dy * dy >= r1_sq) continue;
|
||||
const int64_t x = rh.cx + dx, y = rh.cy + dy;
|
||||
if (x < 0 || y < 0 || x >= static_cast<int64_t>(xpixel) || y >= static_cast<int64_t>(ypixel)) continue;
|
||||
const auto px = ptr[y * xpixel + x];
|
||||
if (px == special || px == special + 1 || px == saturation || px == saturation - 1) continue;
|
||||
const double w = std::max(0.0, (static_cast<double>(px) - rh.bkg) / rh.I);
|
||||
const double tn = -dx * uy + dy * ux;
|
||||
m2 += w * tn * tn; m2w += w;
|
||||
}
|
||||
if (m2w <= 0.0) continue;
|
||||
const double tan2t = Rpx / F_px, cos2t = 1.0 / std::sqrt(1.0 + tan2t * tan2t);
|
||||
const double q = 1.0 / predicted[i].d, yv = cos2t * cos2t * (m2 / m2w);
|
||||
s1 += 1; sq += q; sq2 += q * q; sq3 += q * q * q; sq4 += q * q * q * q;
|
||||
sy += yv; sqy += q * yv; sq2y += q * q * yv;
|
||||
}
|
||||
// Fit quadratic, falling back to linear then constant so B and C stay >= 0 (a relrod cannot shrink
|
||||
// with resolution; a sharp crystal's C comes out as noise/<0 and drops to the linear branch).
|
||||
auto det3 = [](double a, double b, double c, double d, double e, double f, double g, double h, double i) {
|
||||
return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
||||
};
|
||||
if (s1 >= 30) {
|
||||
const double D3 = det3(s1, sq, sq2, sq, sq2, sq3, sq2, sq3, sq4), D2 = s1 * sq2 - sq * sq;
|
||||
const double C3 = std::fabs(D3) > 1e-9 ? det3(s1, sq, sy, sq, sq2, sqy, sq2, sq3, sq2y) / D3 : -1.0;
|
||||
const double B3 = std::fabs(D3) > 1e-9 ? det3(s1, sy, sq2, sq, sqy, sq3, sq2, sq2y, sq4) / D3 : 0.0;
|
||||
const double B2 = std::fabs(D2) > 1e-9 ? (s1 * sqy - sq * sy) / D2 : -1.0;
|
||||
if (std::fabs(D3) > 1e-9 && C3 > 0.0 && B3 >= 0.0) {
|
||||
recip_A = det3(sy, sq, sq2, sqy, sq2, sq3, sq2y, sq3, sq4) / D3; recip_B = B3; recip_C = C3;
|
||||
} else if (std::fabs(D2) > 1e-9 && B2 >= 0.0) {
|
||||
recip_A = (sy * sq2 - sqy * sq) / D2; recip_B = B2;
|
||||
} else {
|
||||
recip_A = sy / s1; // constant width fallback
|
||||
}
|
||||
use_recip = recip_A > 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pass B: profile-fit each reflection (Kabsch, de-biased variance v = B + I*P; iterate). ---
|
||||
std::vector<Reflection> out;
|
||||
out.reserve(npredicted);
|
||||
@@ -296,13 +354,19 @@ std::vector<Reflection> ProfileIntegrateInternal(const DiffractionExperiment &ex
|
||||
|
||||
int Rf = R;
|
||||
const std::vector<double> *Pvec = &shell_P[sh]; // ProfileEmpirical uses the shared learned grid
|
||||
double s2t = shell_sigma2[sh], s2r = s2t;
|
||||
const double rx = predicted[i].predicted_x - beam_x, ry = predicted[i].predicted_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry);
|
||||
const double tan2t = Rpx / F_px;
|
||||
double s2t = shell_sigma2[sh];
|
||||
if (use_recip) { // global reciprocal width instead of the per-shell pixel width
|
||||
const double q = 1.0 / std::max(predicted[i].d, 1e-6f), cos2t = 1.0 / std::sqrt(1.0 + tan2t * tan2t);
|
||||
s2t = std::max(0.25, (recip_A + recip_B * q + recip_C * q * q) / (cos2t * cos2t));
|
||||
}
|
||||
double s2r = s2t;
|
||||
double ux = 1.0, uy = 0.0;
|
||||
bool elong = false;
|
||||
if (use_ellipse) {
|
||||
const double rx = predicted[i].predicted_x - beam_x, ry = predicted[i].predicted_y - beam_y;
|
||||
const double Rpx = std::hypot(rx, ry), sbw = bw_sigma * Rpx;
|
||||
const double tan2t = Rpx / F_px;
|
||||
const double sbw = bw_sigma * Rpx;
|
||||
const double radial_extra = sbw * sbw + c_radial * tan2t * tan2t; // bandwidth + parallax + capture
|
||||
// Only elongate where the radial streak adds a genuine fraction of a pixel of variance; at
|
||||
// low/mid resolution the smear is sub-pixel and elongating just adds noise.
|
||||
|
||||
Reference in New Issue
Block a user