integration: the sensor efficiency is carried as its own quantity, not folded into the Lorentz-polarization factor

It was multiplied into the per-reflection factor at prediction, so that factor held
Lorentz, polarization and efficiency at once and the two spellings that reach a file
- the wire key and the reflection dataset - meant something different from what
they had meant the day before. The unmerged MTZ had to divide the two apart again
at write time to fill its own columns, which is a good sign the wrong thing was
being carried.

Carry them separately. The prescaling factor is Lorentz and polarization again, what
its name and both reference implementations mean by it, and the efficiency is its own
field through prediction, integration, serialization and storage. Fifteen sites that
want the total now multiply the two - once per reflection, not once per pixel.

The efficiency is stored rather than recomputed on read, because the writer has no
geometry to recompute it from, and because a file written before the correction
existed would have had a radial trend invented for it. Sixty stored files were
checked for the one combination that would be ambiguous - the old meaning of the
factor beside a stored efficiency - and none carries it.

Output does not move. Re-scaling a file written before the efficiency existed is
byte-identical, which is a proof rather than a sample, since the stored factor is
exactly one there. Where the efficiency is live, one product is reassociated -
(L*Q)/P becomes (L/P)*Q - and about a third of the values differ in the last bit or
two: every structural column is identical, so no reflection is gained, lost or
reindexed, and no intensity in 1.4 million observations moves by as much as 1e-4 of
its own sigma.

The parity tests now compare the efficiency as well, and their non-vacuity guard
watches it rather than the factor it left - which is the same guard that went blind
when the efficiency was added to a field it was not watching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-09-05 16:28:15 +02:00
co-authored by Claude Opus 5
parent e44ce069a2
commit 7c10d62dab
20 changed files with 90 additions and 75 deletions
+6 -6
View File
@@ -82,7 +82,7 @@ TEST_CASE("Unmerged MTZ: LP is Lorentz-polarization and QE carries the sensor ef
IntegrationOutcome outcome;
const float raw[3] = {1000.0f, 250.0f, 40.0f};
const float corr[3] = {1.75f, 2.50f, 0.90f}; // the whole prescaling product
const float lp[3] = {1.75f, 2.50f, 0.90f}; // Lorentz x polarization, and nothing else
const float qe[3] = {0.9375f, 0.8125f, 1.0f}; // 1.0 = the sensor said nothing to correct
for (int i = 0; i < 3; ++i) {
Reflection r{};
@@ -91,7 +91,7 @@ TEST_CASE("Unmerged MTZ: LP is Lorentz-polarization and QE carries the sensor ef
r.d = 5.0f + i;
r.I = raw[i]; // the writer is what applies the factor
r.sigma = std::sqrt(raw[i]);
r.prescaling_corr = corr[i];
r.prescaling_corr = lp[i];
r.qe_corr = qe[i];
r.partiality = 1.0f;
r.predicted_x = 100.0f + i; r.predicted_y = 200.0f + i;
@@ -117,14 +117,14 @@ TEST_CASE("Unmerged MTZ: LP is Lorentz-polarization and QE carries the sensor ef
const float LP = mtz.data[i * mtz.columns.size() + c_lp->idx];
const float QE = mtz.data[i * mtz.columns.size() + c_qe->idx];
INFO("row " << i);
// LP holds Lorentz x polarization alone: the sensor term has been divided out of it.
CHECK(LP == Catch::Approx(corr[i] / qe[i]).epsilon(1e-5));
// LP holds Lorentz x polarization alone: the sensor term was never inside it.
CHECK(LP == Catch::Approx(lp[i]).epsilon(1e-5));
// QE is a divisor normalised to 1 at normal incidence, so it never drops below 1.
CHECK(QE == Catch::Approx(1.0f / qe[i]).epsilon(1e-5));
CHECK(QE >= 1.0f);
// ... and the two together put the raw counts back.
CHECK(I / LP * QE == Catch::Approx(raw[i]).epsilon(1e-4));
// The intensity itself did not move: it is still the fully corrected value.
CHECK(I == Catch::Approx(raw[i] * corr[i]).epsilon(1e-5));
// The intensity itself is the fully corrected value - both halves applied.
CHECK(I == Catch::Approx(raw[i] * lp[i] * qe[i]).epsilon(1e-5));
}
}