Files
Jungfraujoch/image_analysis/scale_merge/CellPopulation.cpp
T
leonarski_f 78d3b7ef75 Stills geometry: fit only the given cell's crystal form; a stray frame is no cell population
RefineStillsGeometry fitted distance, beam and cell on the strongest sampled frames whatever their
cell, so a sample holding two crystal forms put the cell, and with it the distance, between them -
while the merge later keeps only the given cell's population (GivenCellPopulation). The sampled
frames are now filtered by the same population before the bundle and the module placement are
formed, the population told from the strong frames (>= 20 indexed spots), whose cells show the
valley between two forms; a weak frame's cell at the start geometry spreads across it. A run of
one form has no valley and keeps every frame.

GivenCellPopulation: a stray frame or two far out in a population's tail is a local maximum of the
histogram with no valley of its own, so its range was everything, both forms included, and its
mean - the mean of all frames - could sit nearer the given cell than the given form's own and win.
A population is now represented only by its highest peak. Test
CellPopulation_StrayFramesBeyondTheTail (fails before, passes after).

Still battery, against the same code without the change: the two runs of a two-form sample
-0.0019 and -0.0015 R-free (0-cycle REFMAC; the second repeated three times, -0.0013..-0.0021,
run noise ~0.001), R_split 10.6 -> 9.8 % on the first; single-form controls fit the identical
geometry (R-free within 0.0003). Rotation does not call either path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit ada1b08003fa9748175cce381fc8f28569f28296)
2026-10-25 07:20:25 +01:00

106 lines
4.8 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "CellPopulation.h"
#include <algorithm>
#include <cmath>
#include <limits>
std::pair<double, double> GivenCellPopulation(const std::vector<double> &relative_volume) {
constexpr double everything = std::numeric_limits<double>::infinity();
// Below this many frames a histogram has no valleys to speak of.
constexpr size_t MIN_FRAMES = 50;
// A valley separates two populations only where it dips below this fraction of the kept peak; a
// shallower dip is noise in one population's tail.
constexpr double VALLEY_FRACTION = 0.5;
// A population holding less than this share of the frames is a stray group, not a crystal form.
constexpr double MIN_SHARE = 0.1;
const size_t n = relative_volume.size();
if (n < MIN_FRAMES)
return {-everything, everything};
std::vector<double> v = relative_volume;
std::sort(v.begin(), v.end());
// The bin: twice a robust standard deviation over n^(1/3) (Freedman-Diaconis with the median
// absolute deviation in place of the interquartile range, which a second population inflates).
const double median = v[n / 2];
std::vector<double> dev(n);
for (size_t i = 0; i < n; i++)
dev[i] = std::fabs(v[i] - median);
std::nth_element(dev.begin(), dev.begin() + n / 2, dev.end());
const double bin = 2.0 * 1.4826 * dev[n / 2] / std::cbrt(static_cast<double>(n));
if (!(bin > 0))
return {-everything, everything};
const double lo = v.front();
const int nbins = static_cast<int>((v.back() - lo) / bin) + 1;
std::vector<double> count(nbins, 0.0);
for (double x : v)
count[std::min(nbins - 1, static_cast<int>((x - lo) / bin))] += 1.0;
// Three-bin running mean, so a single empty or full bin is not a valley or a peak.
std::vector<double> smooth(nbins, 0.0);
for (int i = 0; i < nbins; i++) {
const int a = std::max(0, i - 1), b = std::min(nbins - 1, i + 1);
double s = 0;
for (int j = a; j <= b; j++)
s += count[j];
smooth[i] = s / (b - a + 1);
}
// Out from a peak on each side: the valley is the lowest point, below VALLEY_FRACTION of the peak,
// before the histogram rises again to twice its height by more than counting noise - the next
// population, however small. None: the population runs out to the end of the data.
const auto valley = [&](int peak, int step) {
int lowest = peak;
for (int i = peak + step; i >= 0 && i < nbins; i += step) {
if (smooth[i] < smooth[lowest])
lowest = i;
const double low = smooth[lowest], here = smooth[i];
// A three-bin mean of Poisson counts has a variance of a third of its value.
const bool rises = here >= 2.0 * low && here - low > 3.0 * std::sqrt((here + low) / 3.0);
if (low < VALLEY_FRACTION * smooth[peak] && rises)
return lowest;
}
return -1;
};
// Every population: a local maximum of the histogram and the valleys about it. Of those that hold
// a real share of the frames, the one centred nearest the given cell is kept - not merely the peak
// nearest it, which can be a few stray frames when the given cell is a percent off the crystal.
double best_low = -everything, best_high = everything, best_distance = everything;
for (int i = 0; i < nbins; i++) {
if (smooth[i] <= 0 || (i > 0 && smooth[i - 1] > smooth[i]) || (i + 1 < nbins && smooth[i + 1] >= smooth[i]))
continue;
const int left = valley(i, -1), right = valley(i, +1);
// A population is its highest peak: a stray frame or two in its tail is a local maximum too, and
// with no valley of its own its range is the whole population - or, where the tail runs out
// before any valley, everything, two forms included.
bool highest = true;
for (int j = left < 0 ? 0 : left; j <= (right < 0 ? nbins - 1 : right); j++)
if (smooth[j] > smooth[i])
highest = false;
if (!highest)
continue;
const double low = left < 0 ? -everything : lo + (left + 0.5) * bin;
const double high = right < 0 ? everything : lo + (right + 0.5) * bin;
size_t count = 0;
double sum = 0;
for (double x : v)
if (x >= low && x <= high) {
count++;
sum += x;
}
if (count < MIN_SHARE * static_cast<double>(n))
continue;
const double distance = std::fabs(sum / static_cast<double>(count));
if (distance < best_distance) {
best_distance = distance;
best_low = low;
best_high = high;
}
}
return {best_low, best_high};
}