The Bravais class is chosen by a walk that reads two axes as EQUAL when they agree to a fixed relative tolerance, and the class carrying that equality is then imposed on everything below: the cell is refined with a = b, reflections are predicted from it, and they are integrated at those predicted positions. On a small cell the tolerance is far wider than the spot positions resolve, so a genuinely orthorhombic crystal whose a and b differ by 2 % is integrated as tetragonal and every decision downstream is read off the wreckage. Measure the equality instead of assuming it. The indexer already refines each candidate a second time with nothing held, so the first pass now reports how far that free refinement leaves the two axes apart. A relative split of eps displaces a reflection at radius r by eps/2 * r pixels: where that displacement at the far corner of the detector stays inside the integration disc, imposing the equality moves nothing out of its own box and the higher symmetry is kept with no extra pass. Where it does not, both hypotheses are run as probe-only passes - the promoted class, and the class the same walk carries when it is granted no length equality at all - and whichever realises the lower held-out positional residual is kept, the promoted class on a tie. Two small-molecule sets whose axes differ by 2 % now index, refine and merge in their own orthorhombic lattice instead of a tetragonal mean: one goes from a cell 1 % wrong and P 1 at CC1/2 0.19 to the deposited cell within 0.2 % and P 2 2 2 at CC1/2 0.95, the other from a tetragonal mean to a cell matching its reference to 0.4 %. Protein sets whose symmetry is real (P41212, P4222, I23, P6422, F4132) are unchanged: their free refinements leave the axes 0.02-0.10 % apart, a few tenths of a pixel, so the question is never asked. Where it was asked on a weak sweep whose free refinement diverged, the arms decided for the higher symmetry and the output was identical. No new threshold: the comparison is the integration radius the run already integrates at, and the arms are judged by HeldOutResidualFell, as the geometry walk's rounds are. postrefine_probe_only_ returns for the arms' sake - a pass run only to measure stops before the scaling engine is built, so neither arm pays for a merge or a space-group search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
671 lines
28 KiB
C++
671 lines
28 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
#include "../common/CrystalLattice.h"
|
|
#include "../common/Coord.h"
|
|
#include "../common/UnitCell.h"
|
|
#include "../image_analysis/lattice_search/LatticeSearch.h"
|
|
#include "gemmi/symmetry.hpp"
|
|
#include <cmath>
|
|
|
|
// Helper: check near-equality of unit cell parameters
|
|
static void check_uc(const UnitCell& uc, double a, double b, double c,
|
|
double alpha, double beta, double gamma,
|
|
double eps_len = 1e-6, double eps_ang = 1e-4) {
|
|
CHECK(uc.a == Catch::Approx(a).margin(eps_len));
|
|
CHECK(uc.b == Catch::Approx(b).margin(eps_len));
|
|
CHECK(uc.c == Catch::Approx(c).margin(eps_len));
|
|
CHECK(uc.alpha == Catch::Approx(alpha).margin(eps_ang));
|
|
CHECK(uc.beta == Catch::Approx(beta ).margin(eps_ang));
|
|
CHECK(uc.gamma == Catch::Approx(gamma).margin(eps_ang));
|
|
}
|
|
|
|
|
|
TEST_CASE("LatticeSearch - cubic I") {
|
|
// Build a body-centered cubic cell with a=40:
|
|
// primitive basis vectors (conventional I cubic primitive):
|
|
// p1 = (0, a/2, a/2), p2 = (a/2, 0, a/2), p3 = (a/2, a/2, 0)
|
|
const double a = 40.0;
|
|
CrystalLattice L(
|
|
Coord(a, 0, 0),
|
|
Coord(0, a, 0),
|
|
Coord(0, 0, a)
|
|
);
|
|
L = L.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Cubic);
|
|
CHECK(res.centering == 'I');
|
|
|
|
// Conventional cubic I should have equal edges and 90° angles
|
|
auto uc = res.conventional.GetUnitCell();
|
|
CHECK(uc.a == Catch::Approx( a )); // In this construction, conventional a matches given a
|
|
CHECK(uc.b == Catch::Approx( a ));
|
|
CHECK(uc.c == Catch::Approx( a ));
|
|
CHECK(uc.alpha == Catch::Approx(90.0));
|
|
CHECK(uc.beta == Catch::Approx(90.0));
|
|
CHECK(uc.gamma == Catch::Approx(90.0));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - cubic F") {
|
|
// Build a body-centered cubic cell with a=40:
|
|
// primitive basis vectors (conventional I cubic primitive):
|
|
// p1 = (0, a/2, a/2), p2 = (a/2, 0, a/2), p3 = (a/2, a/2, 0)
|
|
const double a = 40.0;
|
|
CrystalLattice L(
|
|
Coord(a, 0, 0),
|
|
Coord(0, a, 0),
|
|
Coord(0, 0, a)
|
|
);
|
|
L = L.ToPrimitive('F');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Cubic);
|
|
CHECK(res.centering == 'F');
|
|
|
|
// Conventional cubic I should have equal edges and 90° angles
|
|
auto uc = res.conventional.GetUnitCell();
|
|
CHECK(uc.a == Catch::Approx( a )); // In this construction, conventional a matches given a
|
|
CHECK(uc.b == Catch::Approx( a ));
|
|
CHECK(uc.c == Catch::Approx( a ));
|
|
CHECK(uc.alpha == Catch::Approx(90.0));
|
|
CHECK(uc.beta == Catch::Approx(90.0));
|
|
CHECK(uc.gamma == Catch::Approx(90.0));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - cubic P") {
|
|
// Simple cubic P, a=30
|
|
const double a = 30.0;
|
|
CrystalLattice L(
|
|
Coord(a,0,0),
|
|
Coord(0,a,0),
|
|
Coord(0,0,a)
|
|
);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Cubic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, a, a, 90.0, 90.0, 90.0, 1e-6, 1e-4);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - tetragonal I") {
|
|
// Build a body-centered cubic cell with a=40:
|
|
// primitive basis vectors (conventional I cubic primitive):
|
|
// p1 = (0, a/2, a/2), p2 = (a/2, 0, a/2), p3 = (a/2, a/2, 0)
|
|
const double a = 40.0;
|
|
const double b = 34.0;
|
|
CrystalLattice L(
|
|
Coord(a, 0, 0),
|
|
Coord(0, a, 0),
|
|
Coord(0, 0, b)
|
|
);
|
|
L = L.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(res.centering == 'I');
|
|
|
|
// Conventional cubic I should have equal edges and 90° angles
|
|
auto uc = res.conventional.GetUnitCell();
|
|
CHECK(uc.a == Catch::Approx( a )); // In this construction, conventional a matches given a
|
|
CHECK(uc.b == Catch::Approx( a ));
|
|
CHECK(uc.c == Catch::Approx( b ));
|
|
CHECK(uc.alpha == Catch::Approx(90.0));
|
|
CHECK(uc.beta == Catch::Approx(90.0));
|
|
CHECK(uc.gamma == Catch::Approx(90.0));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - tetragonal I - v2") {
|
|
// Build a body-centered cubic cell with a=40:
|
|
// primitive basis vectors (conventional I cubic primitive):
|
|
// p1 = (0, a/2, a/2), p2 = (a/2, 0, a/2), p3 = (a/2, a/2, 0)
|
|
const double a = 40.0;
|
|
const double b = 54.0;
|
|
CrystalLattice L(
|
|
Coord(a, 0, 0),
|
|
Coord(0, a, 0),
|
|
Coord(0, 0, b)
|
|
);
|
|
L = L.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(res.centering == 'I');
|
|
|
|
// Conventional cubic I should have equal edges and 90° angles
|
|
auto uc = res.conventional.GetUnitCell();
|
|
CHECK(uc.a == Catch::Approx( a )); // In this construction, conventional a matches given a
|
|
CHECK(uc.b == Catch::Approx( a ));
|
|
CHECK(uc.c == Catch::Approx( b ));
|
|
CHECK(uc.alpha == Catch::Approx(90.0));
|
|
CHECK(uc.beta == Catch::Approx(90.0));
|
|
CHECK(uc.gamma == Catch::Approx(90.0));
|
|
}
|
|
|
|
// Tetragonal P: a=b!=c, all angles 90, P-centering
|
|
TEST_CASE("LatticeSearch - tetragonal P") {
|
|
const double a = 37.0, c = 59.0;
|
|
CrystalLattice L(
|
|
Coord(a,0,0),
|
|
Coord(0,a,0),
|
|
Coord(0,0,c)
|
|
);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, a, c, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
// Orthorhombic F: all angles 90, unequal edges, F-centering
|
|
TEST_CASE("LatticeSearch - orthorhombic F") {
|
|
const double a = 35.0, b = 41.0, c = 57.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
|
|
CrystalLattice L = conv.ToPrimitive('F');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'F');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-1, 1e-2);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - orthorhombic F - permutation 1") {
|
|
const double a = 41.0, b = 57.0, c = 35.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
|
|
CrystalLattice L = conv.ToPrimitive('F');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'F');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, c, a, b, 90.0, 90.0, 90.0, 1e-1, 1e-2);
|
|
}
|
|
|
|
// Orthorhombic C: all angles 90, unequal edges, C-centering
|
|
TEST_CASE("LatticeSearch - orthorhombic C") {
|
|
const double a = 35.0, b = 41.0, c = 57.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
CrystalLattice L = conv.ToPrimitive('C');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'C');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-1, 1e-2);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - orthorhombic I") {
|
|
const double a = 35.0, b = 41.0, c = 57.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
CrystalLattice L = conv.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'I');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
|
|
TEST_CASE("LatticeSearch - orthorhombic I - permutation1") {
|
|
const double a = 57.0, b = 41.0, c = 35.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
CrystalLattice L = conv.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'I');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, c, b, a, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - orthorhombic I - permutation2") {
|
|
const double a = 41.0, b = 57.0, c = 35.0;
|
|
CrystalLattice conv(a,b,c, 90.0,90.0,90.0);
|
|
CrystalLattice L = conv.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'I');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, c, a, b, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
// A character states its scalar products as fractions of A, B and C, and the three C-centred
|
|
// monoclinic ones (28, 29, 30) state one of them as 2*D or 2*E - twice a cosine. The cosine that
|
|
// implies leaves [-1,1] as soon as the cell's own angle is far enough from 90, and the character is
|
|
// then geometrically impossible for that metric. This cell is triclinic; character 28 asks it for a
|
|
// gamma whose cosine is 1.127.
|
|
TEST_CASE("LatticeSearch - an impossible character is not a match") {
|
|
CrystalLattice L(30.0, 35.0, 40.0, 65.0, 70.0, 70.0);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Triclinic);
|
|
}
|
|
|
|
// An exact I-centred orthorhombic lattice whose reduced cell comes out all-acute with gamma at 90 -
|
|
// ON the boundary between the two Niggli types, where the reduction may present either. Character 42
|
|
// is stated for the obtuse setting, and only the flip that keeps gamma reaches it. Both defects have
|
|
// to be gone: without the impossible-character fix this metric matches character 28 and never gets
|
|
// as far as the retry, and without the gamma flip the retry does not have the setting it needs.
|
|
TEST_CASE("LatticeSearch - orthorhombic I on the type boundary in gamma") {
|
|
const double a = 45.0, b = 50.0, c = 80.0;
|
|
CrystalLattice conv(a, b, c, 90.0, 90.0, 90.0);
|
|
CrystalLattice L = conv.ToPrimitive('I');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'I');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
// Orthorhombic P: all angles 90, unequal edges, P-centering
|
|
TEST_CASE("LatticeSearch - orthorhombic P") {
|
|
const double a = 35.0, b = 41.0, c = 57.0;
|
|
CrystalLattice L(a,b,c, 90.0,90.0,90.0);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-6, 1e-4);
|
|
}
|
|
|
|
// Hexagonal P: a=b!=c, alpha=beta=90, gamma=120, P-centering
|
|
TEST_CASE("LatticeSearch - hexagonal P") {
|
|
const double a = 30.0, c = 48.0;
|
|
CrystalLattice L(
|
|
Coord(a, 0, 0),
|
|
Coord(-a/2, a*std::sqrt(3)/2, 0),
|
|
Coord(0, 0, c)
|
|
);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Hexagonal);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
CHECK(uc.a == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-2));
|
|
CHECK(uc.alpha == Catch::Approx(90.0).margin(1e-2));
|
|
CHECK(uc.beta == Catch::Approx(90.0).margin(1e-2));
|
|
CHECK(uc.gamma == Catch::Approx(120.0).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - monoclinic C (unique b)") {
|
|
const double a = 50.0, b = 60.0, c = 70.0;
|
|
const double alpha = 90.0, beta = 96.0, gamma = 90.0;
|
|
CrystalLattice conv(a,b,c, alpha,beta,gamma);
|
|
auto L = conv.ToPrimitive('C');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'C');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
// Check right angles at alpha,gamma and non-90 beta; lengths comparable
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.beta - beta) < 1e-2);
|
|
// Lengths should match within small tolerance
|
|
CHECK(uc.a == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(b).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - monoclinic C (unique b) - v2") {
|
|
const double a = 71.0, b = 35.0, c = 90.0;
|
|
const double alpha = 90.0, beta = 96.0, gamma = 90.0;
|
|
CrystalLattice conv(a,b,c, alpha,beta,gamma);
|
|
auto L = conv.ToPrimitive('C');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'C');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
// Check right angles at alpha,gamma and non-90 beta; lengths comparable
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.beta - beta) < 1e-2);
|
|
// Lengths should match within small tolerance
|
|
CHECK(uc.a == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(b).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - monoclinic C (unique a)") {
|
|
const double a = 60.0, b = 50.0, c = 70.0;
|
|
const double alpha = 96.0, beta = 90.0, gamma = 90.0;
|
|
CrystalLattice conv(a,b,c, alpha,beta,gamma);
|
|
auto L = conv.ToPrimitive('C');
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'C');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
// Check right angles at alpha,gamma and non-90 beta; lengths comparable
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.beta - alpha) < 1e-2);
|
|
// Lengths should match within small tolerance
|
|
CHECK(uc.a == Catch::Approx(b).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - monoclinic P (unique b)") {
|
|
const double a = 50.0, b = 60.0, c = 70.0;
|
|
const double alpha = 90.0, beta = 96.0, gamma = 90.0;
|
|
CrystalLattice conv(a,b,c, alpha,beta,gamma);
|
|
|
|
auto res = LatticeSearch(conv, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
// Check right angles at alpha,gamma and non-90 beta; lengths comparable
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.beta - beta) < 1e-2);
|
|
// Lengths should match within small tolerance
|
|
CHECK(uc.a == Catch::Approx(a).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(b).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - monoclinic P (unique b) - v2") {
|
|
const double a = 90.0, b = 35.0, c = 71.0;
|
|
const double alpha = 90.0, beta = 96.0, gamma = 90.0;
|
|
CrystalLattice conv(a,b,c, alpha,beta,gamma);
|
|
|
|
auto res = LatticeSearch(conv, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
auto uc = res.conventional.GetUnitCell();
|
|
// Check right angles at alpha,gamma and non-90 beta; lengths comparable
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.beta - beta) < 1e-2);
|
|
// Lengths should match within small tolerance
|
|
CHECK(uc.a == Catch::Approx(c).margin(1e-2));
|
|
CHECK(uc.b == Catch::Approx(b).margin(1e-2));
|
|
CHECK(uc.c == Catch::Approx(a).margin(1e-2));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - triclinic P") {
|
|
// General triclinic primitive cell
|
|
CrystalLattice L(33.1, 41.7, 52.3, 89.1, 85.0, 76.3);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
// System should be triclinic, centering P, and conventional equals some standardized primitive
|
|
CHECK(res.system == gemmi::CrystalSystem::Triclinic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
// The conventional cell should be metric-equivalent to input. We verify only the system and centering here.
|
|
// Reduced primitive must be non-singular
|
|
auto uc_red = res.primitive_reduced.GetUnitCell();
|
|
CHECK(uc_red.a > 0);
|
|
CHECK(uc_red.b > 0);
|
|
CHECK(uc_red.c > 0);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - triclinic P - v2") {
|
|
// General triclinic primitive cell
|
|
CrystalLattice L(33.1, 41.7, 52.3, 100, 92, 115);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
// System should be triclinic, centering P, and conventional equals some standardized primitive
|
|
CHECK(res.system == gemmi::CrystalSystem::Triclinic);
|
|
CHECK(res.centering == 'P');
|
|
|
|
// The conventional cell should be metric-equivalent to input. We verify only the system and centering here.
|
|
// Reduced primitive must be non-singular
|
|
auto uc_red = res.primitive_reduced.GetUnitCell();
|
|
CHECK(uc_red.a > 0);
|
|
CHECK(uc_red.b > 0);
|
|
CHECK(uc_red.c > 0);
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - trigonal R") {
|
|
const double a = 32.0;
|
|
const double alpha = 80.0;
|
|
|
|
// Build rhombohedral in rhombohedral setting (primitive axes a=b=c, alpha=beta=gamma)
|
|
CrystalLattice L(a, a, a, alpha, alpha, alpha);
|
|
|
|
auto res = LatticeSearch(L, 1e-6);
|
|
|
|
CHECK(res.system == gemmi::CrystalSystem::Trigonal);
|
|
CHECK(res.centering == 'R');
|
|
|
|
auto uc_red = res.conventional.GetUnitCell();
|
|
CHECK(uc_red.alpha == Catch::Approx(90).margin(1e-2));
|
|
CHECK(uc_red.beta == Catch::Approx(90).margin(1e-2));
|
|
CHECK(uc_red.gamma == Catch::Approx(120).margin(1e-2));
|
|
|
|
auto uc_prim = res.primitive_reduced.GetUnitCell();
|
|
CHECK(uc_prim.alpha == Catch::Approx(alpha).margin(1e-2));
|
|
CHECK(uc_prim.beta == Catch::Approx(alpha).margin(1e-2));
|
|
CHECK(uc_prim.gamma == Catch::Approx(alpha).margin(1e-2));
|
|
}
|
|
|
|
// The class-filtered walk: the same table, restricted to one Bravais class. A tetragonal-P lattice is
|
|
// also a C-centred orthorhombic one (a_C = a+b, b_C = -a+b, c_C = c), and asking for that class has to
|
|
// return that setting even though the plain search rightly prefers the tetragonal one.
|
|
TEST_CASE("LatticeSearchForClass - tetragonal P also has a C-centred orthorhombic setting") {
|
|
const double a = 50.0, c = 120.0;
|
|
const CrystalLattice L(a, a, c, 90, 90, 90);
|
|
|
|
const auto plain = LatticeSearch(L, 1e-6);
|
|
CHECK(plain.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(plain.centering == 'P');
|
|
|
|
const auto ortho = LatticeSearchForClass(L, gemmi::CrystalSystem::Orthorhombic, 'C', 1e-6);
|
|
REQUIRE(ortho.has_value());
|
|
CHECK(ortho->system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(ortho->centering == 'C');
|
|
const auto uc = ortho->conventional.GetUnitCell();
|
|
// The C cell is the face diagonal on a and b, so twice the volume and a = b = a_tet * sqrt(2).
|
|
CHECK(uc.a == Catch::Approx(a * std::sqrt(2.0)).margin(1e-4));
|
|
CHECK(uc.b == Catch::Approx(a * std::sqrt(2.0)).margin(1e-4));
|
|
CHECK(uc.c == Catch::Approx(c).margin(1e-4));
|
|
CHECK(uc.alpha == Catch::Approx(90).margin(1e-4));
|
|
CHECK(uc.beta == Catch::Approx(90).margin(1e-4));
|
|
CHECK(uc.gamma == Catch::Approx(90).margin(1e-4));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearchForClass - a class the metric cannot carry is refused") {
|
|
// A general triclinic metric has no monoclinic-C setting, and an F-centred cubic lattice has no
|
|
// hexagonal-P one (its hexagonal description is R-centred).
|
|
const CrystalLattice tri(41.0, 47.0, 53.0, 71.0, 83.0, 97.0);
|
|
CHECK_FALSE(LatticeSearchForClass(tri, gemmi::CrystalSystem::Monoclinic, 'C').has_value());
|
|
|
|
const double a = 60.0;
|
|
const auto cubic_f = CrystalLattice(a, a, a, 90, 90, 90).ToPrimitive('F');
|
|
CHECK(LatticeSearch(cubic_f, 1e-6).centering == 'F');
|
|
CHECK_FALSE(LatticeSearchForClass(cubic_f, gemmi::CrystalSystem::Hexagonal, 'P').has_value());
|
|
// ... but its rhombohedral setting is there, which is what makes the refusal above a real answer
|
|
// rather than an artefact of the filter.
|
|
const auto rhomb = LatticeSearchForClass(cubic_f, gemmi::CrystalSystem::Trigonal, 'R');
|
|
REQUIRE(rhomb.has_value());
|
|
CHECK(rhomb->centering == 'R');
|
|
}
|
|
|
|
TEST_CASE("LatticeSearchForClass - asking for what the plain search found returns the same setting") {
|
|
const double a = 40.0;
|
|
const auto L = CrystalLattice(a, a, a, 90, 90, 90).ToPrimitive('I');
|
|
const auto plain = LatticeSearch(L, 1e-6);
|
|
const auto filtered = LatticeSearchForClass(L, plain.system, plain.centering, 1e-6);
|
|
REQUIRE(filtered.has_value());
|
|
CHECK(filtered->niggli_class == plain.niggli_class);
|
|
check_uc(filtered->conventional.GetUnitCell(), a, a, a, 90, 90, 90, 1e-4, 1e-4);
|
|
}
|
|
|
|
// The reduction epsilon. An exactly body-centred tetragonal lattice with c > a*sqrt(2) reduces to a
|
|
// character whose gamma is 90 EXACTLY, so the scalar product that decides the Niggli type is
|
|
// structurally zero and what a float lattice carries there is rounding. Axis-aligned that rounding
|
|
// happens to vanish - which is why the two tetragonal-I cases above pass - but every lattice the
|
|
// pipeline classifies is a refined, ROTATED one, and rotating this one about its own 4-fold is
|
|
// enough to lose the 4-fold on 38 of 60 rotations.
|
|
TEST_CASE("LatticeSearch - a body-centred tetragonal lattice keeps its 4-fold once it is rotated") {
|
|
CrystalLattice L(Coord(40, 0, 0), Coord(0, 40, 0), Coord(0, 0, 90));
|
|
L = L.ToPrimitive('I').Multiply(RotMatrix(0.3f, Coord(0, 0, 1)));
|
|
const auto res = LatticeSearch(L, 1e-6);
|
|
CHECK(res.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(res.centering == 'I');
|
|
}
|
|
|
|
// ITA character 43, the mI form. An ordinary centred-monoclinic crystal that happens to reduce into
|
|
// the form the table names mI - the same Bravais lattice in another setting, there is no fifteenth
|
|
// type. With that row absent the walk reaches character 44 and the centring is lost outright. The
|
|
// three monoclinic-C cases above reduce to characters 14, 39 and 14, so none of them samples it.
|
|
TEST_CASE("LatticeSearch - a centred monoclinic lattice that reduces to the mI form keeps its centring") {
|
|
const CrystalLattice L = CrystalLattice(35, 60, 30, 90, 120, 90).ToPrimitive('C');
|
|
const auto res = LatticeSearch(L, 1e-6);
|
|
CHECK(res.system == gemmi::CrystalSystem::Monoclinic);
|
|
CHECK(res.centering == 'I');
|
|
const auto uc = res.conventional.GetUnitCell();
|
|
CHECK(std::fabs(uc.alpha - 90.0) < 1e-3);
|
|
CHECK(std::fabs(uc.gamma - 90.0) < 1e-3);
|
|
CHECK(std::fabs(res.conventional.CalcVolume())
|
|
== Catch::Approx(2 * std::fabs(L.CalcVolume())).epsilon(1e-4));
|
|
}
|
|
|
|
// The change of basis to a primitive cell is stated by gemmi as an operator on COORDINATES, while
|
|
// CrystalLattice::Multiply combines BASIS VECTORS, so it has to be transposed. A, B, C, I and F are
|
|
// symmetric and never showed the omission; R and H are not. An R-centred lattice is the case that
|
|
// matters, because it is the centring whose setting most often has to be re-seated.
|
|
TEST_CASE("CrystalLattice::ToPrimitive gives an R-centred lattice its rhombohedral primitive cell") {
|
|
const double a = 50.0, c = 120.0;
|
|
const CrystalLattice hex(a, a, c, 90, 90, 120);
|
|
const auto prim = hex.ToPrimitive('R').GetUnitCell();
|
|
// A rhombohedral primitive cell: three equal edges, three equal angles, a third of the volume.
|
|
CHECK(prim.a == Catch::Approx(prim.b).epsilon(1e-5));
|
|
CHECK(prim.b == Catch::Approx(prim.c).epsilon(1e-5));
|
|
CHECK(prim.alpha == Catch::Approx(prim.beta).epsilon(1e-5));
|
|
CHECK(prim.beta == Catch::Approx(prim.gamma).epsilon(1e-5));
|
|
CHECK(std::fabs(hex.ToPrimitive('R').CalcVolume())
|
|
== Catch::Approx(std::fabs(hex.CalcVolume()) / 3.0).epsilon(1e-4));
|
|
// ...and it goes back to the hexagonal cell it came from.
|
|
const auto back = hex.ToPrimitive('R').FromPrimitive('R').GetUnitCell();
|
|
CHECK(back.a == Catch::Approx(a).epsilon(1e-4));
|
|
CHECK(back.c == Catch::Approx(c).epsilon(1e-4));
|
|
CHECK(back.gamma == Catch::Approx(120.0).epsilon(1e-4));
|
|
}
|
|
|
|
// SymmetrizeMetric is what a run falls back on when it has adopted a group on a cell no fit under
|
|
// that group produced. It has to do two things: leave the group's metric exactly satisfied, and move
|
|
// the cell as little as that requires - which means the orientation it arrives in is kept.
|
|
TEST_CASE("SymmetrizeMetric puts a cell onto the metric its group fixes") {
|
|
const auto &c2 = *gemmi::find_spacegroup_by_name("C 1 2 1");
|
|
|
|
// A C-centred monoclinic setting whose alpha is 1.5 deg off, as a freely refined metric promoted
|
|
// after integration arrives: the group cannot describe it.
|
|
const CrystalLattice off(160.0, 140.0, 90.0, 88.5, 119.9, 90.1);
|
|
const auto fixed = SymmetrizeMetric(off, c2).GetUnitCell();
|
|
CHECK(fixed.alpha == Catch::Approx(90.0).margin(1e-3));
|
|
CHECK(fixed.gamma == Catch::Approx(90.0).margin(1e-3));
|
|
// The free angle and the lengths stay where they were, to well inside the move it had to make.
|
|
CHECK(fixed.beta == Catch::Approx(119.9).margin(0.2));
|
|
CHECK(fixed.a == Catch::Approx(160.0).epsilon(2e-3));
|
|
CHECK(fixed.b == Catch::Approx(140.0).epsilon(2e-3));
|
|
CHECK(fixed.c == Catch::Approx(90.0).epsilon(2e-3));
|
|
|
|
// A cell the group already describes is not moved at all, whatever orientation it is in.
|
|
const CrystalLattice ok(80.0, 50.0, 60.0, 90.0, 105.0, 90.0);
|
|
const auto same = SymmetrizeMetric(ok, c2).GetUnitCell();
|
|
check_uc(same, 80.0, 50.0, 60.0, 90.0, 105.0, 90.0, 1e-3, 1e-3);
|
|
|
|
// ...including one that is not axis-aligned: the answer is a property of the lattice, not of the
|
|
// frame it is written in, and the orientation it came in is the orientation it goes out in.
|
|
const RotMatrix rot(0.7, Coord(1.0f, 2.0f, 3.0f).Normalize());
|
|
const CrystalLattice turned = off.Multiply(rot);
|
|
// A LEFT-handed basis keeps its hand and its angles: the three-Coord constructor would flip an
|
|
// axis and hand back the supplement of beta, which is a different cell from the one the
|
|
// reflections are indexed on - and a metric projection may not change the cell that much.
|
|
const CrystalLattice flipped = off.Multiply(gemmi::Mat33(-1, 0, 0, 0, 1, 0, 0, 0, 1));
|
|
REQUIRE(flipped.CalcVolume() < 0);
|
|
const auto left = SymmetrizeMetric(flipped, c2);
|
|
CHECK(left.CalcVolume() < 0);
|
|
CHECK(left.GetUnitCell().beta == Catch::Approx(flipped.GetUnitCell().beta).margin(1e-3));
|
|
CHECK(left.GetUnitCell().alpha == Catch::Approx(90.0).margin(1e-3));
|
|
|
|
const auto turned_fixed = SymmetrizeMetric(turned, c2);
|
|
check_uc(turned_fixed.GetUnitCell(), fixed.a, fixed.b, fixed.c, 90.0, fixed.beta, 90.0, 1e-3, 1e-3);
|
|
CHECK(turned_fixed.Vec0() * turned.Vec0()
|
|
== Catch::Approx(turned_fixed.Vec0().Length() * turned.Vec0().Length()).epsilon(1e-4));
|
|
}
|
|
|
|
TEST_CASE("LatticeSearch - a pseudo-tetragonal cell is tetragonal at the walk's tolerance and "
|
|
"orthorhombic with no length equality granted") {
|
|
// a and b 2.4 % apart: inside LATTICE_SEARCH_DIST_TOLERANCE, far outside what spot positions
|
|
// resolve. The two hypotheses rugnux carries forward are exactly these two answers.
|
|
const double a = 5.795, b = 5.933, c = 12.362;
|
|
CrystalLattice L(Coord(a, 0, 0), Coord(0, b, 0), Coord(0, 0, c));
|
|
|
|
const auto promoted = LatticeSearch(L);
|
|
CHECK(promoted.system == gemmi::CrystalSystem::Tetragonal);
|
|
CHECK(promoted.centering == 'P');
|
|
CHECK(ClassImposesLengthEquality(promoted.system));
|
|
|
|
const auto free_class = LatticeSearch(L, /*dist_tolerance=*/0.0);
|
|
CHECK(free_class.system == gemmi::CrystalSystem::Orthorhombic);
|
|
CHECK(free_class.centering == 'P');
|
|
const auto uc = free_class.conventional.GetUnitCell();
|
|
check_uc(uc, a, b, c, 90.0, 90.0, 90.0, 1e-2, 1e-2);
|
|
}
|
|
|
|
TEST_CASE("ClassImposesLengthEquality names the classes whose metric asserts a = b") {
|
|
CHECK(ClassImposesLengthEquality(gemmi::CrystalSystem::Tetragonal));
|
|
CHECK(ClassImposesLengthEquality(gemmi::CrystalSystem::Trigonal));
|
|
CHECK(ClassImposesLengthEquality(gemmi::CrystalSystem::Hexagonal));
|
|
CHECK(ClassImposesLengthEquality(gemmi::CrystalSystem::Cubic));
|
|
CHECK_FALSE(ClassImposesLengthEquality(gemmi::CrystalSystem::Orthorhombic));
|
|
CHECK_FALSE(ClassImposesLengthEquality(gemmi::CrystalSystem::Monoclinic));
|
|
CHECK_FALSE(ClassImposesLengthEquality(gemmi::CrystalSystem::Triclinic));
|
|
}
|