Rigid body: no JFJOCH_RIGID_BODY_CPU; GPU/CPU parity tests tightened

The GPU rigid body is chosen by whether a card is there, nothing else. A run
that must stay on the CPU says so at the process level (CUDA_VISIBLE_DEVICES=),
which RigidBodyGPUPool::Create already honours through get_gpu_count().

Parity tests ([ModelValidation][gpu], compiled into jfjoch_test on a CUDA
build):
- every GPU case now SKIPs without a card, instead of passing silently;
- ModelMaskGPU_MatchesGemmi also checks that the CPU path's PutMaskOnGrid is
  gemmi's mask bit for bit on the same grid, so the GPU mask is held against
  the CPU path itself;
- RigidBodyGPU_MatchesCPU: Jacobian columns 2e-3 -> 5e-4 relative (measured
  up to 5e-5; the floor is the 1e-4 the scale's near-tie can move every
  column by). Residuals stay at 2e-4 of <Fobs> (measured up to 3.9e-5);
- RigidBodyGPU_FitAgreesWithCPU: fit endpoint 2e-3 -> 1e-5 A rmsd (measured
  up to 5e-7 A).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-09-28 22:46:00 +02:00
1 parent b1f79ed27f
commit 23812b902c
4 files changed
+35 -33

No files matched your search

+18 -20
View File
@@ -1239,10 +1239,8 @@ TEST_CASE("ModelValidation_RigidBodyReportsTheRotationItApplied", "[ModelValidat
// give the same residuals and the same Jacobian to rounding (float distances on the device, cuFFT for
// FFTW), on every group of the composition tests, both zones, anisotropic atoms included.
TEST_CASE("RigidBodyGPU_MatchesCPU", "[ModelValidation][gpu]") {
if (get_gpu_count() == 0) {
SUCCEED("no GPU");
return;
}
if (get_gpu_count() == 0)
SKIP("No GPU");
Logger logger("RigidBodyGPU_MatchesCPU");
const double q0[6] = {0.12, -0.09, 0.07, 0.15, -0.10, 0.05};
for (const char *cryst : kRigidBodyCrysts) {
@@ -1288,7 +1286,11 @@ TEST_CASE("RigidBodyGPU_MatchesCPU", "[ModelValidation][gpu]") {
}
INFO(cryst << " at " << zone << " A, column " << j << ": relative difference "
<< std::sqrt(diff / norm));
CHECK(std::sqrt(diff) <= 2e-3 * std::sqrt(norm) + 1e-9);
// Measured at up to 5e-5 (the rotation columns, a forward difference of float-gridded
// density; the translation columns agree to about 1e-6). Every column is proportional to
// the overall scale, which the near-tie above can move by 1e-4, so that is the floor of
// an honest bound; 5e-4 leaves room for the difference's own rounding on another card.
CHECK(std::sqrt(diff) <= 5e-4 * std::sqrt(norm) + 1e-9);
}
}
}
@@ -1297,10 +1299,8 @@ TEST_CASE("RigidBodyGPU_MatchesCPU", "[ModelValidation][gpu]") {
// The whole Jacobian on the GPU against the central difference of the GPU's own residuals, with the
// bounds of ModelValidation_RigidBodyJacobianMatchesNumericScaleRefit.
TEST_CASE("RigidBodyGPU_JacobianMatchesNumericScaleRefit", "[ModelValidation][gpu]") {
if (get_gpu_count() == 0) {
SUCCEED("no GPU");
return;
}
if (get_gpu_count() == 0)
SKIP("No GPU");
const double close[6] = {0.0, 0.0, 0.0, 0.05, -0.04, 0.03};
const double rotated[6] = {0.12, -0.09, 0.07, 0.15, -0.10, 0.05};
for (const char *cryst : kRigidBodyCrysts) {
@@ -1330,13 +1330,10 @@ namespace {
}
}
// The GPU fit walks where the CPU fit walks: from the same displaced start, to the same placement
// within a few thousandths of an angstrom.
// The GPU fit walks where the CPU fit walks: from the same displaced start, to the same placement.
TEST_CASE("RigidBodyGPU_FitAgreesWithCPU", "[ModelValidation][gpu]") {
if (get_gpu_count() == 0) {
SUCCEED("no GPU");
return;
}
if (get_gpu_count() == 0)
SKIP("No GPU");
Logger logger("RigidBodyGPU_FitAgreesWithCPU");
for (const char *cryst : {kCryst, kPolarCryst, kRigidBodyCrysts[3]}) {
gemmi::Structure cpu_st = AnisoCluster(cryst), gpu_st = AnisoCluster(cryst);
@@ -1352,17 +1349,18 @@ TEST_CASE("RigidBodyGPU_FitAgreesWithCPU", "[ModelValidation][gpu]") {
rmsd += pc[i].dist_sq(pg[i]) / static_cast<double>(pc.size());
INFO(cryst << ": CPU " << cpu.angle_deg << " deg " << cpu.shift_A << " A, GPU " << gpu.angle_deg << " deg "
<< gpu.shift_A << " A, " << std::sqrt(rmsd) << " A apart");
CHECK(std::sqrt(rmsd) < 2e-3);
// Measured at up to 5e-7 A: the endpoint moves only by what the residuals differ by (a few
// 1e-6 of <Fobs>) over the target's curvature. The bound leaves a factor 20 for another card's
// cuFFT and float rounding, and is still four orders under anything a placement is judged at.
CHECK(std::sqrt(rmsd) < 1e-5);
}
}
// Deterministic: the same fit twice, and on a pool of one engine and of four, gives the same placement
// bit for bit.
TEST_CASE("RigidBodyGPU_Deterministic", "[ModelValidation][gpu]") {
if (get_gpu_count() == 0) {
SUCCEED("no GPU");
return;
}
if (get_gpu_count() == 0)
SKIP("No GPU");
Logger logger("RigidBodyGPU_Deterministic");
const char *cryst = kRigidBodyCrysts[4];
std::vector<std::vector<gemmi::Position>> placed;