PixelRefine: checkpoint before cleanup (factored model + all diagnostic levers)
Snapshot of the messy state: factored likelihood Terms 1+2+3 behind PR_* env flags (PR_INTENSITY/PR_SHAPE/PR_RECENTER) alongside the old per-pixel ShoeboxResidual, plus diagnostic scaffolding (PR_R0/R1/COV/FIX_R0/FIX_R/ ADAPT_R1/CENTROID/RECENTER) and the FACTORED_MODEL.md spec. Next commit makes Terms 1+2 the model and strips all of this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <Eigen/Dense>
|
||||
#include <ceres/ceres.h>
|
||||
#include <ceres/covariance.h>
|
||||
#include <ceres/rotation.h>
|
||||
|
||||
#include "../geom_refinement/LatticeReduction.h"
|
||||
@@ -35,6 +36,8 @@ struct ReflGroup {
|
||||
double pol; // per-reflection polarization correction (raw = true * pol)
|
||||
double Ibkg; // local flat background (raw counts, constant over the shoebox)
|
||||
double predicted_x, predicted_y;
|
||||
double R1_eff = 0.0; // tangential profile width to use (Term 2; 0 => fall back to data.R[1])
|
||||
double dcx = 0.0, dcy = 0.0; // Term 3: profile recentre shift (observed centroid - predicted)
|
||||
std::vector<PixelObs> pixels;
|
||||
};
|
||||
|
||||
@@ -752,6 +755,30 @@ void PixelRefine::SweepOrientationCell(const T *image, BraggPrediction &predicti
|
||||
data.latt = best;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Term 1 of the factored likelihood (FACTORED_MODEL.md): the per-reflection
|
||||
// *intensity* (0th-moment) residual. The profile-fit amplitude J should equal the
|
||||
// scaled reference J_model = G * exp(-B/4d^2) * partiality * pol * I_ref. One scalar
|
||||
// residual per reflection, weighted by the model-expected (Fisher) sigma_J. This is
|
||||
// the scaling residual - integration and scaling become one objective, and the empty
|
||||
// pixels (which make no residual of their own) stop dominating the fit. With geometry
|
||||
// and R held fixed, J, partiality and sigma_J are constants, so only G and B are free.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct IntensityResidual {
|
||||
IntensityResidual(double J, double sigma_J, double partiality, double pol,
|
||||
double I_ref, double inv_4d2)
|
||||
: J(J), inv_sigma(1.0 / sigma_J), partiality(partiality), pol(pol),
|
||||
I_ref(I_ref), inv_4d2(inv_4d2) {}
|
||||
template<typename T>
|
||||
bool operator()(const T *const G, const T *const B, T *residual) const {
|
||||
const T B_term = ceres::exp(-B[0] * T(inv_4d2));
|
||||
const T J_model = G[0] * B_term * T(partiality) * T(pol) * T(I_ref);
|
||||
residual[0] = (J_model - T(J)) * T(inv_sigma);
|
||||
return true;
|
||||
}
|
||||
double J, inv_sigma, partiality, pol, I_ref, inv_4d2;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
void PixelRefine::Run(const T *image,
|
||||
BraggPrediction &prediction,
|
||||
@@ -929,12 +956,146 @@ void PixelRefine::Run(const T *image,
|
||||
for (int i = 0; i < 3; ++i)
|
||||
orient_prior[i] = latt_vec0[i];
|
||||
|
||||
// ---- Term 3: per-reflection recentre on the observed centroid ----------------
|
||||
// The geometry predicts the spot to ~0.4 px (per-reflection scatter a global fit
|
||||
// cannot remove); a tight Term-2 template centred on the prediction then sits off
|
||||
// the real spot. For confident spots, shift the profile centre to the observed
|
||||
// centroid (used consistently by Term 2, Term 1 and the extraction below). Weak
|
||||
// spots keep the prediction (recentring on a noise centroid would bias them).
|
||||
for (auto &g : groups) {
|
||||
g.dcx = 0.0;
|
||||
g.dcy = 0.0;
|
||||
}
|
||||
if (data.recenter_profile && !groups.empty()) {
|
||||
const double box_px = (2.0 * radius + 1.0) * (2.0 * radius + 1.0);
|
||||
for (auto &g : groups) {
|
||||
double sw = 0.0, swx = 0.0, swy = 0.0;
|
||||
for (const auto &px : g.pixels) {
|
||||
const double w = std::max(px.Iobs - g.Ibkg, 0.0);
|
||||
sw += w; swx += w * px.x; swy += w * px.y;
|
||||
}
|
||||
if (sw <= 0.0)
|
||||
continue;
|
||||
if (sw / std::sqrt(std::max(box_px * g.Ibkg, 1.0)) < data.recenter_min_signif)
|
||||
continue;
|
||||
double dcx = swx / sw - g.predicted_x, dcy = swy / sw - g.predicted_y;
|
||||
const double dl = std::sqrt(dcx * dcx + dcy * dcy);
|
||||
if (dl > 2.0) { dcx *= 2.0 / dl; dcy *= 2.0 / dl; } // clamp runaway centroids
|
||||
g.dcx = dcx;
|
||||
g.dcy = dcy;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Term 2: per-resolution tangential profile width R1 from spot shapes ------
|
||||
// Default: every reflection uses the global R1; with shape_R1 on, override it with
|
||||
// R1 = sqrt(2*<eps_t^2>) from the intensity-weighted second moment of the strong
|
||||
// spots, binned by resolution (low res small spots, high res larger). A shape
|
||||
// statistic - normalised by the total, so decoupled from the per-image scale.
|
||||
for (auto &g : groups)
|
||||
g.R1_eff = data.R[1];
|
||||
if (data.shape_R1 && !groups.empty()) {
|
||||
constexpr int n_bins = 6;
|
||||
const double box_px = (2.0 * radius + 1.0) * (2.0 * radius + 1.0);
|
||||
double s2min = 1e30, s2max = 0.0;
|
||||
for (const auto &g : groups) {
|
||||
const double s2 = 1.0 / (g.d * g.d);
|
||||
s2min = std::min(s2min, s2);
|
||||
s2max = std::max(s2max, s2);
|
||||
}
|
||||
const double span = std::max(s2max - s2min, 1e-12);
|
||||
auto bin_of = [&](double d) {
|
||||
return std::clamp(static_cast<int>((1.0 / (d * d) - s2min) / span * n_bins), 0, n_bins - 1);
|
||||
};
|
||||
std::vector<std::vector<double>> bin_M2(n_bins);
|
||||
for (const auto &g : groups) {
|
||||
double sw = 0.0, sw_et2 = 0.0;
|
||||
for (const auto &px : g.pixels) {
|
||||
PixelObs probe{px.x - g.dcx, px.y - g.dcy, 0.0, g.Ibkg, 1.0};
|
||||
PixelResidual pr(probe, 1.0, lambda, pixel_size, g.h, g.k, g.l,
|
||||
g.R_bw_sq, g.pol, data.crystal_system);
|
||||
double q_sq, eps_r, eps_t_sq;
|
||||
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot,
|
||||
latt_vec0, latt_vec1, latt_vec2, q_sq, eps_r, eps_t_sq))
|
||||
continue;
|
||||
const double w = std::max(px.Iobs - g.Ibkg, 0.0);
|
||||
sw += w;
|
||||
sw_et2 += w * eps_t_sq;
|
||||
}
|
||||
if (sw <= 0.0)
|
||||
continue;
|
||||
const double signif = sw / std::sqrt(std::max(box_px * g.Ibkg, 1.0));
|
||||
if (signif >= 5.0) // only well-measured spots define the shape
|
||||
bin_M2[bin_of(g.d)].push_back(sw_et2 / sw);
|
||||
}
|
||||
std::vector<double> bin_R1(n_bins, data.R[1]);
|
||||
for (int b = 0; b < n_bins; ++b)
|
||||
if (bin_M2[b].size() >= 5) {
|
||||
const double r1 = std::sqrt(2.0 * std::max(MedianInPlace(bin_M2[b]), 0.0));
|
||||
if (std::isfinite(r1) && r1 > 1e-4)
|
||||
bin_R1[b] = std::clamp(r1, 1e-4, 0.05);
|
||||
}
|
||||
for (auto &g : groups)
|
||||
g.R1_eff = bin_R1[bin_of(g.d)];
|
||||
data.shape_R1_lores = bin_R1[0]; // lowest resolution bin
|
||||
data.shape_R1_hires = bin_R1[n_bins - 1]; // highest resolution bin
|
||||
}
|
||||
|
||||
// ---- 4. Build the problem ---------------------------------------------
|
||||
// One residual block per shoebox (N residuals), so the expensive
|
||||
// per-reflection node geometry is evaluated once per reflection instead
|
||||
// of once per pixel.
|
||||
ceres::Problem problem;
|
||||
size_t residual_pixels = 0;
|
||||
if (data.intensity_residual) {
|
||||
// Term-1 path: one per-reflection intensity residual. Geometry & R fixed, so
|
||||
// J / partiality / sigma_J are computed here as constants and only G, B vary.
|
||||
const double R0 = data.R[0];
|
||||
for (const auto &g : groups) {
|
||||
const double R1 = g.R1_eff; // Term 2: per-resolution profile width
|
||||
double num = 0.0, den = 0.0, rad = 0.0;
|
||||
std::vector<std::pair<double, double>> pt_sig; // (P_t, Iobs-Bg) for Fisher pass
|
||||
pt_sig.reserve(g.pixels.size());
|
||||
for (const auto &px : g.pixels) {
|
||||
PixelObs probe{px.x - g.dcx, px.y - g.dcy, 0.0, g.Ibkg, 1.0}; // Term 3 recentre
|
||||
PixelResidual pr(probe, 1.0, lambda, pixel_size, g.h, g.k, g.l,
|
||||
g.R_bw_sq, g.pol, data.crystal_system);
|
||||
double q_sq, eps_r, eps_t_sq;
|
||||
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot,
|
||||
latt_vec0, latt_vec1, latt_vec2, q_sq, eps_r, eps_t_sq))
|
||||
continue;
|
||||
if (!(R1 > 0.0) || !(R0 > 0.0))
|
||||
continue;
|
||||
const double P_t = std::exp(-eps_t_sq / (R1 * R1)) / (M_PI * R1 * R1);
|
||||
const double R0_eff_sq = R0 * R0 + g.R_bw_sq;
|
||||
const double P_rad = std::exp(-eps_r * eps_r / R0_eff_sq);
|
||||
const double v = std::max(g.Ibkg, 1.0);
|
||||
const double sig = px.Iobs - g.Ibkg;
|
||||
num += P_t * sig / v;
|
||||
den += P_t * P_t / v;
|
||||
rad += P_rad * P_t * P_t / v;
|
||||
pt_sig.emplace_back(P_t, sig);
|
||||
}
|
||||
if (!(den > 0.0))
|
||||
continue;
|
||||
const double J = num / den;
|
||||
const double partiality = rad / den;
|
||||
// Model-expected (Fisher) variance: v_p = background + expected signal J*P_t,
|
||||
// not the per-pixel observed counts (which down-bias) - so the weight tracks
|
||||
// information, and an expected-strong reflection that is absent hurts.
|
||||
double den_f = 0.0;
|
||||
for (const auto &[P_t, sig] : pt_sig) {
|
||||
const double v_f = std::max(g.Ibkg + std::max(J, 0.0) * P_t, 1.0);
|
||||
den_f += P_t * P_t / v_f;
|
||||
}
|
||||
const double sigma_J = std::sqrt(1.0 / std::max(den_f, 1e-30));
|
||||
const double inv_4d2 = (g.d > 0.0) ? 1.0 / (4.0 * g.d * g.d) : 0.0;
|
||||
auto *cost = new ceres::AutoDiffCostFunction<IntensityResidual, 1, 1, 1>(
|
||||
new IntensityResidual(J, sigma_J, partiality, g.pol, g.Itrue, inv_4d2));
|
||||
problem.AddResidualBlock(cost, nullptr, &data.scale_factor, &data.B_factor);
|
||||
++residual_pixels;
|
||||
}
|
||||
data.residual_count = residual_pixels;
|
||||
} else {
|
||||
for (const auto &g : groups) {
|
||||
auto *cost = new ceres::DynamicAutoDiffCostFunction<ShoeboxResidual>(
|
||||
new ShoeboxResidual(g, lambda, pixel_size, data.crystal_system));
|
||||
@@ -959,8 +1120,23 @@ void PixelRefine::Run(const T *image,
|
||||
residual_pixels += g.pixels.size();
|
||||
}
|
||||
data.residual_count = residual_pixels;
|
||||
}
|
||||
|
||||
// ---- 5. Constrain / bound parameter blocks ----------------------------
|
||||
if (data.intensity_residual) {
|
||||
// Only G and B are in this problem; geometry/R are not parameters here.
|
||||
problem.SetParameterLowerBound(&data.scale_factor, 0, 0.0);
|
||||
if (!data.refine_B)
|
||||
problem.SetParameterBlockConstant(&data.B_factor);
|
||||
// Regularize G->1, weight sqrt(n_refl/sigma): commensurate because the data
|
||||
// term is now one residual per reflection (unlike the per-pixel path).
|
||||
if (data.scale_reg_sigma > 0.0 && !groups.empty()) {
|
||||
const double w = std::sqrt(static_cast<double>(groups.size()) / data.scale_reg_sigma);
|
||||
auto *reg = new ceres::AutoDiffCostFunction<ScalarRegularizer, 1, 1>(
|
||||
new ScalarRegularizer(w, 1.0));
|
||||
problem.AddResidualBlock(reg, nullptr, &data.scale_factor);
|
||||
}
|
||||
} else {
|
||||
if (!data.refine_orientation) {
|
||||
problem.SetParameterBlockConstant(latt_vec0);
|
||||
} else if (data.orient_reg_sigma_deg > 0.0) {
|
||||
@@ -1029,11 +1205,17 @@ void PixelRefine::Run(const T *image,
|
||||
problem.SetParameterBlockConstant(&data.B_factor);
|
||||
|
||||
if (data.refine_R) {
|
||||
problem.SetParameterLowerBound(data.R, 0, 1e-5);
|
||||
problem.SetParameterLowerBound(data.R, 1, 1e-5);
|
||||
if (data.fix_R0) {
|
||||
// Diagnostic: hold R0 constant, refine R1 only.
|
||||
problem.SetManifold(data.R, new ceres::SubsetManifold(2, {0}));
|
||||
} else {
|
||||
problem.SetParameterLowerBound(data.R, 0, 1e-5);
|
||||
problem.SetParameterLowerBound(data.R, 1, 1e-5);
|
||||
}
|
||||
} else {
|
||||
problem.SetParameterBlockConstant(data.R);
|
||||
}
|
||||
} // end per-pixel (non-intensity_residual) constraints
|
||||
|
||||
// ---- 6. Solve (or, for max_iterations<=0, just evaluate the cost) -----
|
||||
// Evaluate-only is the live-residual path: it reports the current cost
|
||||
@@ -1057,6 +1239,50 @@ void PixelRefine::Run(const T *image,
|
||||
|
||||
data.final_cost = summary.final_cost;
|
||||
data.solved = summary.IsSolutionUsable();
|
||||
|
||||
// Diagnostic: Pearson correlations on the final solve. Always needs G and B
|
||||
// refined for G-B; the R correlations are added only when R is also refined.
|
||||
if (data.compute_covariance && data.solved && iter == n_iter - 1 &&
|
||||
data.refine_scale && data.refine_B) {
|
||||
ceres::Covariance::Options copt;
|
||||
copt.algorithm_type = ceres::DENSE_SVD;
|
||||
copt.null_space_rank = -1; // tolerate (and reveal) degenerate directions
|
||||
ceres::Covariance cov(copt);
|
||||
std::vector<std::pair<const double *, const double *>> blocks = {
|
||||
{&data.scale_factor, &data.scale_factor}, {&data.B_factor, &data.B_factor},
|
||||
{&data.scale_factor, &data.B_factor}};
|
||||
if (data.refine_R) {
|
||||
blocks.push_back({data.R, data.R});
|
||||
blocks.push_back({&data.scale_factor, data.R});
|
||||
blocks.push_back({&data.B_factor, data.R});
|
||||
}
|
||||
if (cov.Compute(blocks, &problem)) {
|
||||
double cGG, cBB, cGB;
|
||||
cov.GetCovarianceBlock(&data.scale_factor, &data.scale_factor, &cGG);
|
||||
cov.GetCovarianceBlock(&data.B_factor, &data.B_factor, &cBB);
|
||||
cov.GetCovarianceBlock(&data.scale_factor, &data.B_factor, &cGB);
|
||||
const double sG = std::sqrt(std::max(cGG, 0.0));
|
||||
const double sB = std::sqrt(std::max(cBB, 0.0));
|
||||
auto rho = [](double c, double s1, double s2) {
|
||||
return (s1 > 0.0 && s2 > 0.0) ? c / (s1 * s2) : NAN;
|
||||
};
|
||||
data.corr_GB = rho(cGB, sG, sB);
|
||||
if (data.refine_R) {
|
||||
double cRR[4], cGR[2], cBR[2];
|
||||
cov.GetCovarianceBlock(data.R, data.R, cRR);
|
||||
cov.GetCovarianceBlock(&data.scale_factor, data.R, cGR);
|
||||
cov.GetCovarianceBlock(&data.B_factor, data.R, cBR);
|
||||
const double sR0 = std::sqrt(std::max(cRR[0], 0.0));
|
||||
const double sR1 = std::sqrt(std::max(cRR[3], 0.0));
|
||||
data.corr_GR0 = rho(cGR[0], sG, sR0);
|
||||
data.corr_GR1 = rho(cGR[1], sG, sR1);
|
||||
data.corr_BR0 = rho(cBR[0], sB, sR0);
|
||||
data.corr_BR1 = rho(cBR[1], sB, sR1);
|
||||
data.corr_R0R1 = rho(cRR[1], sR0, sR1);
|
||||
}
|
||||
data.covariance_valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 7. Write refined geometry + lattice back into data ---------------
|
||||
@@ -1096,6 +1322,139 @@ void PixelRefine::Run(const T *image,
|
||||
}
|
||||
} // predict<->refine iterations
|
||||
|
||||
// ---- Adaptive integration mask --------------------------------------------
|
||||
// Measure R1 (tangential profile width) from the intensity-weighted tangential
|
||||
// second moment of the strong spots, rather than fitting it. R1 is a *shape*
|
||||
// statistic: sigma_t^2 = sum_p (I_p-B) eps_t,p^2 / sum_p (I_p-B), normalised by the
|
||||
// total so it is independent of the per-image scale - which is exactly what breaks
|
||||
// the R1<->G degeneracy (a measured width cannot be traded against G). One value per
|
||||
// image here (from the strong, mostly low-res spots); a per-resolution version is the
|
||||
// natural next step for the high-res / DMM-streak shapes.
|
||||
if (data.adaptive_R1 && !groups.empty()) {
|
||||
std::vector<double> itrue;
|
||||
itrue.reserve(groups.size());
|
||||
for (const auto &g : groups)
|
||||
itrue.push_back(g.Itrue);
|
||||
const size_t cut_idx = itrue.size() * 7 / 10; // keep the strongest ~30%
|
||||
std::nth_element(itrue.begin(), itrue.begin() + cut_idx, itrue.end());
|
||||
const double itrue_cut = itrue[cut_idx];
|
||||
|
||||
std::vector<double> sigma_t2;
|
||||
for (const auto &g : groups) {
|
||||
if (g.Itrue < itrue_cut)
|
||||
continue;
|
||||
double sw = 0.0, sw_et2 = 0.0;
|
||||
for (const auto &px : g.pixels) {
|
||||
PixelObs probe{px.x, px.y, 0.0, g.Ibkg, 1.0};
|
||||
PixelResidual pr(probe, 1.0, lambda, pixel_size, g.h, g.k, g.l,
|
||||
g.R_bw_sq, g.pol, data.crystal_system);
|
||||
double q_sq, eps_r, eps_t_sq;
|
||||
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot,
|
||||
latt_vec0, latt_vec1, latt_vec2, q_sq, eps_r, eps_t_sq))
|
||||
continue;
|
||||
const double w = std::max(px.Iobs - g.Ibkg, 0.0);
|
||||
sw += w;
|
||||
sw_et2 += w * eps_t_sq;
|
||||
}
|
||||
if (sw > 0.0)
|
||||
sigma_t2.push_back(sw_et2 / sw);
|
||||
}
|
||||
if (sigma_t2.size() >= 5) {
|
||||
const double r1 = std::sqrt(2.0 * MedianInPlace(sigma_t2)); // R1^2 = 2 sigma_t^2
|
||||
if (std::isfinite(r1) && r1 > 1e-5)
|
||||
data.R[1] = r1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Centering diagnostic --------------------------------------------------
|
||||
// Observed-centroid vs predicted-position offset for the strong spots, after all
|
||||
// refinement. Large rms (relative to the spot size) means a tight profile mask
|
||||
// sits off the real spot - which is why a generous box can beat profile fitting.
|
||||
if (data.measure_centroid && !groups.empty()) {
|
||||
const double beam_x = data.geom.GetBeamX_pxl();
|
||||
const double beam_y = data.geom.GetBeamY_pxl();
|
||||
const double box_px = (2.0 * radius + 1.0) * (2.0 * radius + 1.0);
|
||||
// Raw pixel value (sentinel/bounds safe) for the parabolic peak fit. A constant
|
||||
// background cancels in the parabola, so no need to subtract it here.
|
||||
auto pix_val = [&](int x, int y) -> double {
|
||||
if (x < 0 || x >= static_cast<int>(xpixel) || y < 0 || y >= static_cast<int>(ypixel))
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
const size_t np = static_cast<size_t>(xpixel) * y + x;
|
||||
if (image[np] == std::numeric_limits<T>::max())
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
if (std::is_signed_v<T> && image[np] == std::numeric_limits<T>::min())
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
return static_cast<double>(image[np]);
|
||||
};
|
||||
struct Off { double signif, tang_c, tang_p, rad_c; };
|
||||
std::vector<Off> offs;
|
||||
double sdx = 0.0, sdy = 0.0, sd2 = 0.0;
|
||||
size_t nc = 0;
|
||||
for (const auto &g : groups) {
|
||||
double sw = 0.0, swx = 0.0, swy = 0.0, bmax = -1e30;
|
||||
int bx = 0, by = 0;
|
||||
for (const auto &px : g.pixels) {
|
||||
const double s = px.Iobs - g.Ibkg;
|
||||
const double w = std::max(s, 0.0);
|
||||
sw += w; swx += w * px.x; swy += w * px.y;
|
||||
if (s > bmax) { bmax = s; bx = static_cast<int>(std::lround(px.x)); by = static_cast<int>(std::lround(px.y)); }
|
||||
}
|
||||
if (sw <= 0.0)
|
||||
continue;
|
||||
const double signif = sw / std::sqrt(std::max(box_px * g.Ibkg, 1.0));
|
||||
if (signif < 5.0)
|
||||
continue; // a measurable spot at any resolution (not just the strong low-res ones)
|
||||
// Sub-pixel peak (mode): parabola through the brightest pixel and its two
|
||||
// neighbours per axis. The mode tracks the prediction even when an asymmetric
|
||||
// tail drags the centroid (mean) sideways - so peak vs centroid separates a
|
||||
// shape asymmetry from a true position error.
|
||||
double peak_x = bx, peak_y = by;
|
||||
{ const double l = pix_val(bx - 1, by), c = pix_val(bx, by), r = pix_val(bx + 1, by);
|
||||
const double den = l - 2.0 * c + r;
|
||||
if (std::isfinite(den) && den < -1e-9)
|
||||
peak_x = bx + std::clamp(0.5 * (l - r) / den, -1.0, 1.0); }
|
||||
{ const double l = pix_val(bx, by - 1), c = pix_val(bx, by), r = pix_val(bx, by + 1);
|
||||
const double den = l - 2.0 * c + r;
|
||||
if (std::isfinite(den) && den < -1e-9)
|
||||
peak_y = by + std::clamp(0.5 * (l - r) / den, -1.0, 1.0); }
|
||||
const double dcx = swx / sw - g.predicted_x, dcy = swy / sw - g.predicted_y;
|
||||
const double dpx = peak_x - g.predicted_x, dpy = peak_y - g.predicted_y;
|
||||
sdx += dcx; sdy += dcy; sd2 += dcx * dcx + dcy * dcy; ++nc;
|
||||
const double rx = g.predicted_x - beam_x, ry = g.predicted_y - beam_y;
|
||||
const double rr = std::sqrt(rx * rx + ry * ry);
|
||||
if (rr < 1.0)
|
||||
continue;
|
||||
const double rad_c = (dcx * rx + dcy * ry) / rr; // signed radial (outward +)
|
||||
const double tang_c = (dcx * -ry + dcy * rx) / rr; // signed tangential
|
||||
const double tang_p = (dpx * -ry + dpy * rx) / rr;
|
||||
offs.push_back({signif, std::fabs(tang_c), std::fabs(tang_p), rad_c});
|
||||
}
|
||||
if (nc >= 5) {
|
||||
data.centroid_bias_px = std::sqrt((sdx / nc) * (sdx / nc) + (sdy / nc) * (sdy / nc));
|
||||
data.centroid_rms_px = std::sqrt(sd2 / nc);
|
||||
}
|
||||
if (offs.size() >= 10) {
|
||||
std::vector<double> sig;
|
||||
sig.reserve(offs.size());
|
||||
for (const auto &o : offs)
|
||||
sig.push_back(o.signif);
|
||||
std::nth_element(sig.begin(), sig.begin() + sig.size() / 2, sig.end());
|
||||
const double smed = sig[sig.size() / 2];
|
||||
double slo = 0, shi = 0, tclo = 0, tchi = 0, tplo = 0, tphi = 0, rclo = 0, rchi = 0;
|
||||
int nlo = 0, nhi = 0;
|
||||
for (const auto &o : offs) {
|
||||
if (o.signif < smed) { slo += o.signif; tclo += o.tang_c; tplo += o.tang_p; rclo += o.rad_c; ++nlo; }
|
||||
else { shi += o.signif; tchi += o.tang_c; tphi += o.tang_p; rchi += o.rad_c; ++nhi; }
|
||||
}
|
||||
if (nlo > 0 && nhi > 0) {
|
||||
data.centroid_lo_signif = slo / nlo; data.centroid_hi_signif = shi / nhi;
|
||||
data.centroid_lo_tang_c = tclo / nlo; data.centroid_hi_tang_c = tchi / nhi;
|
||||
data.centroid_lo_tang_p = tplo / nlo; data.centroid_hi_tang_p = tphi / nhi;
|
||||
data.centroid_lo_rad_c = rclo / nlo; data.centroid_hi_rad_c = rchi / nhi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Extract integrated reflections ---------------------------------------
|
||||
// Profile fitting gives the recorded amplitude (fitting the tangential profile
|
||||
// P_t against the background-subtracted pixels):
|
||||
@@ -1138,26 +1497,32 @@ void PixelRefine::Run(const T *image,
|
||||
// Debye-Waller factor for this reflection (constant over its shoebox).
|
||||
const double B_term = std::exp(-data.B_factor / (4.0 * g.d * g.d));
|
||||
|
||||
// Term 3 recentre shift (precomputed in the pre-pass; 0 if off or the spot was not
|
||||
// confident). Profile evaluated at (x-dcx, y-dcy), data summed at (x,y).
|
||||
const double dcx = g.dcx, dcy = g.dcy;
|
||||
|
||||
double num = 0.0, den = 0.0, bkg_sum = 0.0, radial_sum = 0.0;
|
||||
double prof_live = 0.0, prof_full = 0.0; // tangential profile: captured / total
|
||||
size_t n = 0;
|
||||
|
||||
for (int y = cy - radius; y <= cy + radius; ++y) {
|
||||
for (int x = cx - radius; x <= cx + radius; ++x) {
|
||||
// Geometry/profile for this grid point (valid even off the detector).
|
||||
PixelObs probe{static_cast<double>(x), static_cast<double>(y), 0.0, g.Ibkg, 1.0};
|
||||
// Geometry/profile for this grid point (profile recentred by (dcx,dcy)).
|
||||
PixelObs probe{static_cast<double>(x) - dcx, static_cast<double>(y) - dcy,
|
||||
0.0, g.Ibkg, 1.0};
|
||||
PixelResidual pr(probe, 1.0, lambda, pixel_size, g.h, g.k, g.l,
|
||||
g.R_bw_sq, g.pol, data.crystal_system);
|
||||
double q_sq, eps_r, eps_t_sq;
|
||||
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot,
|
||||
latt_vec0, latt_vec1, latt_vec2, q_sq, eps_r, eps_t_sq))
|
||||
continue;
|
||||
if (!(data.R[0] > 0.0) || !(data.R[1] > 0.0))
|
||||
if (!(data.R[0] > 0.0) || !(g.R1_eff > 0.0))
|
||||
continue;
|
||||
|
||||
// Tangential profile shape (area-normalized) -> the fit template.
|
||||
const double P_t = std::exp(-eps_t_sq / (data.R[1] * data.R[1]))
|
||||
/ (M_PI * data.R[1] * data.R[1]);
|
||||
// Tangential profile shape (area-normalized) -> the fit template. Uses the
|
||||
// per-reflection R1_eff (Term 2), falling back to the global R1 by default.
|
||||
const double R1 = g.R1_eff;
|
||||
const double P_t = std::exp(-eps_t_sq / (R1 * R1)) / (M_PI * R1 * R1);
|
||||
prof_full += P_t; // whole shoebox, on- or off-detector
|
||||
|
||||
// Only real, unmasked detector pixels carry signal.
|
||||
|
||||
Reference in New Issue
Block a user