Files
Jungfraujoch/image_analysis/indexing/SpindleCuspLoss.cpp
T
leonarski_fandClaude Opus 5.5 0c1c963379 Move SpotWidth to bragg_integration/, SpindleCuspLoss and LeftoverLattices to indexing/
A pure move of three method files out of rugnux/: SpotWidth (spot width and
bandwidth, which set the integration radius) joins JFJochBraggIntegration, which now
links JFJochImagePreprocessing for the ImagePreprocessorBuffer it reads;
SpindleCuspLoss and LeftoverLattices join JFJochIndexing. Only include paths and CMake
lists change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
2026-10-07 14:05:37 +02:00

112 lines
5.7 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <array>
#include <cmath>
#include <vector>
#include "SpindleCuspLoss.h"
#include "../../common/JFJochMath.h"
std::optional<SpindleCuspLoss> SpindleUnrepairedFraction(const gemmi::SpaceGroup &sg,
const CrystalLattice &lattice,
const Coord &spindle,
double wavelength_A,
double d_min_A) {
const double spindle_len = spindle.Length();
if (spindle_len < 1e-9 || wavelength_A <= 0 || d_min_A <= 0)
return {};
const Coord axis = spindle * static_cast<float>(1.0 / spindle_len);
const double sin_tm = std::min(1.0, wavelength_A / (2.0 * d_min_A));
const double cos_tm = std::sqrt(std::max(0.0, 1.0 - sin_tm * sin_tm));
if (sin_tm <= 0)
return {};
// Proper rotations only, identity included. Friedel and the improper operators need no separate
// treatment: the blind double cone and the measured region are both inversion-symmetric, so -R
// lands a point inside or outside exactly as R does.
std::vector<std::array<std::array<double, 3>, 3>> rot;
for (const auto &op : sg.operations().derive_symmorphic().sym_ops) {
const auto &w = op.rot;
const double det =
static_cast<double>(w[0][0]) * (w[1][1] * w[2][2] - w[1][2] * w[2][1])
- static_cast<double>(w[0][1]) * (w[1][0] * w[2][2] - w[1][2] * w[2][0])
+ static_cast<double>(w[0][2]) * (w[1][0] * w[2][1] - w[1][1] * w[2][0]);
if (det <= 0)
continue;
if (op.rot == gemmi::Op::identity().rot)
continue; // handled exactly below, without the float round trip through the basis
std::array<std::array<double, 3>, 3> m{};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
m[i][j] = static_cast<double>(w[i][j]) / gemmi::Op::DEN;
rot.push_back(m);
}
const Coord a = lattice.Vec0(), b = lattice.Vec1(), c = lattice.Vec2();
const Coord as = lattice.Astar(), bs = lattice.Bstar(), cs = lattice.Cstar();
// Deterministic spiral over ONE lobe of the blind cone; the other lobe contributes identically
// (the orbit of -p is the negated orbit of p, and only |cos| to the spindle enters). Only cone
// directions can be lost - the identity is in every orbit - so nothing outside is sampled.
constexpr int N_DIRECTIONS = 8192;
const double golden_angle = PI * (3.0 - std::sqrt(5.0));
// Any unit vector perpendicular to the spindle, to open the cap around it.
const Coord seed = std::fabs(axis.x) < 0.9f ? Coord(1, 0, 0) : Coord(0, 1, 0);
const Coord e1 = (axis % seed).Normalize();
const Coord e2 = axis % e1;
double lost_sum = 0, p1_sum = 0;
for (int i = 0; i < N_DIRECTIONS; i++) {
const double z = cos_tm + (1.0 - cos_tm) * (static_cast<double>(i) + 0.5) / N_DIRECTIONS;
const double r = std::sqrt(std::max(0.0, 1.0 - z * z));
const double phi = golden_angle * i;
const Coord p = axis * static_cast<float>(z)
+ e1 * static_cast<float>(r * std::cos(phi))
+ e2 * static_cast<float>(r * std::sin(phi));
// h = (a.p, b.p, c.p) are continuous Miller coordinates; an operator sends the reflection
// h to h' = h W (gemmi's row convention) and h' returns to Cartesian through the reciprocal
// basis. No matrix inversion: direct and reciprocal bases are mutually inverse.
const double h0 = a * p, h1 = b * p, h2 = c * p;
// The orbit's largest folded angle to the spindle, as its sine (the fold to [0, 90] makes
// the sine monotone). A point at direction p and radius q is unmeasured iff every image of
// its orbit is inside the cone of ITS OWN shell, i.e. iff q > 2 sin(a_max) / lambda - so
// the direction loses the radial fraction 1 - (sin a_max / sin theta_max)^3 of its unique
// reflections, reciprocal volume being what unique reflections are proportional to.
// The identity's own angle comes straight from z, exactly; only the non-trivial
// operators go through the basis.
const double sin_id = std::sqrt(std::max(0.0, 1.0 - z * z));
double max_sin = sin_id;
for (const auto &m : rot) {
const double g0 = h0 * m[0][0] + h1 * m[1][0] + h2 * m[2][0];
const double g1 = h0 * m[0][1] + h1 * m[1][1] + h2 * m[2][1];
const double g2 = h0 * m[0][2] + h1 * m[1][2] + h2 * m[2][2];
const Coord s = as * static_cast<float>(g0) + bs * static_cast<float>(g1)
+ cs * static_cast<float>(g2);
const double len = s.Length();
if (len < 1e-12)
continue;
const double cos_a = std::min(1.0, std::fabs(s * axis) / len);
max_sin = std::max(max_sin, std::sqrt(std::max(0.0, 1.0 - cos_a * cos_a)));
}
if (max_sin < sin_tm) {
const double x = max_sin / sin_tm;
lost_sum += 1.0 - x * x * x;
}
const double y = sin_id / sin_tm;
p1_sum += 1.0 - y * y * y;
}
SpindleCuspLoss ret;
ret.theta_max_deg = std::asin(sin_tm) * 180.0 / PI;
// The double cone is (1 - cos theta_max) of the sphere, and the sample covers exactly the cone,
// so the cone average rescales by that solid angle to a fraction of ALL unique reflections.
ret.lost_unique_fraction = (1.0 - cos_tm) * lost_sum / N_DIRECTIONS;
ret.cone_fraction = p1_sum > 0 ? lost_sum / p1_sum : 0.0;
return ret;
}