RESULT-CHANGING. A tetragonal crystal in 4 twinned ~0.3 by k,h,-l (3r6o, I4_1) was merged in I4_122. Since the beam-stop fix both search arms passed 422 (H ratio 1.51 < 1.85); before it the Lorentz-filtered arm refused at 2.13x and was overridden by the all-observation arm. Either way the twin-immune zone - the one reading a twin cannot fake - was never asked: the Stage A zone gate skipped any candidate that holds every rotation of its lattice, on the argument that no twin law exists for such a group. But the subgroup's twin law is one of the candidate's own operators, so the holohedral promotion is exactly the commonest merohedral-twin case (4 -> 422, 3 -> 32 on a rhombohedral lattice, 321 -> 622), and the L-test-under-merge gate leaves such a twin in the higher group by design. The gate now runs whenever the P1 merge reads twinned (<|L|> in [0.375, 0.44)), holohedral or not. 3r6o reads 422 over 4 at -58 nats (acentric). With 422 refused, the all-observation arm took 222 over 4: the same-order tie-break credits a group's axial two-folds with the screw evidence of their rows, and on a body-centred crystal the I centring kills the odd class of every principal row (30 nats each), so a 222 of the real 2-fold plus two twin laws beat the crystal's own 4. Rows whose odd class a centring extinguishes are no longer counted where the merge reads that centring on its general reflections (the centring class at < 0.25 of the rest's mean E^2; measured 0.11-0.13 for I on 3r6o, 0.97-1.00 for the absent centrings). Changed (CPU screen of 53 sets against the kept rc175 run 20261016-0057, plus 3r6o): - 3r6o: I4_122 -> I4_1 (deposited I4_1), TWINNING INDICATED, twin law h,-k,-l ~0.32; ISa 4.15 -> 7.77, R_meas 0.218 -> 0.146, CC1/2 0.989 -> 0.993. - 8c3e: P622 -> P321 with P3_121 | P3_221 as alternatives, screw on c undetermined (deposited P3_121, twinned): 622 refused at -70.8 nats; ISa 5.69 -> 6.91. Unchanged: 3mc4, 5j23, 6toc, 6oww, 6iu5/6/9, 2wnn, 2xfw, 4bwl, 5ebi, 2wnq, 6vww, 7k1l, 6pxb, 8xte, 8xtg, 9i80, 5uth, 6h5t, 8k1g, 6moj, 7kcn, 5vml, 3p85, 7rji, 8xtf, 9h0q, 7l84, 6v2r, 7t5t, 5m17, 6p8p, 3meb, 5cc8, 9lxl, 8v4j, 6zqr, 9fcf, 5src, 11if, cuhf2, lyso_x06da_ref, lyso_x10sa_90deg_1, thau_x10sa_16keV, thau_bl1a_6p5keV, insu_H_x06da_twin/notwin, insu_I_x06da_ref, cytc_x06da_1, myob_x06da_split, kdp_x10sa_20keV (space group identical, ISa within run noise; 7l84/5src/11if differ only by the model-adopted enantiomorph label). Test: a body-centred 4 twinned at 0.45, given its cell, stays in 4 (222 without the centring rule). [SearchSpaceGroup], [twinning], [twin] pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
386 lines
21 KiB
C++
386 lines
21 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
|
|
#include "../image_analysis/scale_merge/SearchSpaceGroup.h"
|
|
#include "SyntheticMergedReflections.h"
|
|
#include "gemmi/symmetry.hpp"
|
|
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// Point-group decision on MEROHEDRALLY TWINNED data.
|
|
//
|
|
// SearchSpaceGroupTest.cpp exercises Stage B (systematic absences) on noise-free, exactly-symmetric
|
|
// intensities. The decision that actually goes wrong on real crystals is Stage A: whether the extra
|
|
// operator of the metric holohedry is a real symmetry or a twin law. That decision runs through the
|
|
// merge chi^2 gate, its systematic-b rescue and the operator gates - none of which the noise-free
|
|
// set can reach, because it has no errors for a chi^2 to be reduced by.
|
|
//
|
|
// Three crystals are modelled per lattice, all with the SAME metric symmetry (a merohedral twin has
|
|
// the supergroup's metric, so the lattice cannot arbitrate - only the intensities can):
|
|
// * genuine supergroup - structure factors invariant under the supergroup; must be promoted;
|
|
// * untwinned subgroup - structure factors invariant under the subgroup only; must NOT be;
|
|
// * twinned subgroup - the same crystal at twin fraction alpha; must NOT be promoted for any
|
|
// 0 <= alpha < 0.5, because promoting averages the two twin domains into
|
|
// one intensity and the twin is then unrecoverable downstream.
|
|
// At alpha = 0.5 the twin is physically indistinguishable from real symmetry, so only "terminates
|
|
// and returns one of the two" is asserted.
|
|
//
|
|
// Each is measured through two merge-quality regimes and five merge multiplicities. Neither changes
|
|
// any physics - they change only how well the same crystal was measured and how honest its sigmas
|
|
// are - so no decision above may move with them. That is the property the harness exists to pin
|
|
// down; every case below asserts it outright.
|
|
|
|
namespace {
|
|
using jfjoch_test::SyntheticMergeParams;
|
|
|
|
struct TwinCrystal {
|
|
std::string name;
|
|
std::string sub; // the crystal's true space group when twinned
|
|
std::string super; // supergroup of index 2; its extra operator is the twin law
|
|
gemmi::CrystalSystem system; // metric (lattice) symmetry, as rugnux passes it from indexing
|
|
// The OTHER maximal subgroup of the supergroup of the same order as `sub`, when one exists.
|
|
// 422 has two - 4 and 222 - and only one of them is the crystal. Which one a parent-normalised
|
|
// statistic divides by decides the promotion, so the harness reports both; empty when the
|
|
// supergroup has only one maximal subgroup of that order (32 over 3) and the choice cannot arise.
|
|
std::string rival_parent;
|
|
};
|
|
|
|
const std::vector<TwinCrystal> crystals = {
|
|
{"trigonal 3 -> 32 (R3 / R32, twin law k,h,-l)", "R 3 :H", "R 32 :H",
|
|
gemmi::CrystalSystem::Trigonal, ""},
|
|
{"tetragonal 4 -> 422 (P4 / P422, twin law h,-k,-l)", "P 4", "P 4 2 2",
|
|
gemmi::CrystalSystem::Tetragonal, "P 2 2 2"},
|
|
};
|
|
|
|
// How honest the merged sigmas are. Both regimes are ways a fitted error model misses in
|
|
// practice, and neither is a property of the crystal's symmetry.
|
|
struct MergeQuality {
|
|
std::string name;
|
|
double sigma_miscalibration;
|
|
double error_model_b;
|
|
std::optional<double> true_systematic_b;
|
|
};
|
|
|
|
const std::vector<MergeQuality> merge_quality = {
|
|
// The usual case: merged sigmas come out ~1.7x too small across the board, with the error
|
|
// model's b matching the systematic scatter that is actually there.
|
|
{"sigmas 1.7x too small", 1.7, 0.05, std::nullopt},
|
|
// The other way a fitted error model misses: the statistical sigmas come out somewhat too
|
|
// LARGE while b - the asymptotic per-observation I/sigma, ISa = 1/b - is fitted 3x too
|
|
// optimistic, so the systematic scatter present is 3x what the merged sigmas admit.
|
|
{"ISa 3x too optimistic", 0.6, 0.02, 0.06},
|
|
};
|
|
|
|
const std::vector<int> multiplicities = {2, 3, 6, 9, 18};
|
|
const std::vector<double> twin_fractions = {0.0, 0.05, 0.10, 0.20, 0.35, 0.50};
|
|
|
|
struct Decision {
|
|
std::string point_group;
|
|
std::string space_group;
|
|
size_t n_merged = 0;
|
|
std::vector<SpaceGroupOperatorScore> operators;
|
|
std::string report;
|
|
};
|
|
|
|
// true_group is the symmetry the structure factors have: the subgroup for the twin series, the
|
|
// supergroup for the genuine-high-symmetry control (where the twin law is a real symmetry
|
|
// operator, so the twin fraction has no effect).
|
|
Decision Decide(const TwinCrystal& c, const MergeQuality& q,
|
|
const std::string& true_group, double alpha, int multiplicity) {
|
|
SyntheticMergeParams p;
|
|
p.true_space_group = true_group;
|
|
p.twin_supergroup = c.super;
|
|
p.twin_fraction = alpha;
|
|
p.multiplicity = multiplicity;
|
|
p.sigma_miscalibration = q.sigma_miscalibration;
|
|
p.error_model_b = q.error_model_b;
|
|
p.true_systematic_b = q.true_systematic_b;
|
|
// One fixed seed for the whole harness: every case draws the same unit-normal stream, so two
|
|
// cases differ only in the knob under test and every test is reproducible.
|
|
p.seed = 20260727;
|
|
|
|
const auto merged = jfjoch_test::GenerateSyntheticMerged(p);
|
|
REQUIRE(merged.size() > 5000); // a realistic dataset, not a handful of reflections
|
|
|
|
SearchSpaceGroupOptions opt; // as rugnux/Rugnux.cpp sets it
|
|
opt.merge_friedel = true;
|
|
opt.lattice_system = c.system;
|
|
|
|
const auto result = SearchSpaceGroup(merged, opt);
|
|
|
|
Decision d;
|
|
d.point_group = result.point_group_hm;
|
|
d.space_group = result.best_space_group.has_value()
|
|
? result.best_space_group->short_name() : "none";
|
|
d.n_merged = merged.size();
|
|
d.operators = result.operator_scores;
|
|
d.report = SearchSpaceGroupResultToText(result);
|
|
return d;
|
|
}
|
|
|
|
std::string PointGroupOf(const std::string& space_group_name) {
|
|
return gemmi::get_spacegroup_by_name(space_group_name).point_group_hm();
|
|
}
|
|
|
|
std::string ShortNameOf(const std::string& space_group_name) {
|
|
return gemmi::get_spacegroup_by_name(space_group_name).short_name();
|
|
}
|
|
|
|
// The hkl triplets SearchSpaceGroup labels a space group's own rotations with. Lets a test tell the
|
|
// crystal's real symmetry operators from the twin law among result.operator_scores.
|
|
std::set<std::string> OperatorTripletsOf(const std::string& space_group_name) {
|
|
std::set<std::string> out;
|
|
const auto& sg = gemmi::get_spacegroup_by_name(space_group_name);
|
|
for (const auto& op : sg.operations().derive_symmorphic().sym_ops) {
|
|
if (op.rot == gemmi::Op::identity().rot)
|
|
continue;
|
|
out.insert(gemmi::Op{op.rot, {0, 0, 0}, op.notation}.as_hkl().triplet('h'));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// The statistic the promotion is actually decided on: the operator disagreement
|
|
// H = median|I1-I2|/(I1+I2) over the operators the promotion ADDS, divided by the same over the
|
|
// parent group's own operators, measured on the same reflections. Mirrors what SearchSpaceGroup computes
|
|
// for the sub -> super step, so a test can report the margin the max_operator_h_ratio bound has.
|
|
//
|
|
// The parent normalisation is the whole design, not a detail. An ABSOLUTE per-operator bound cannot
|
|
// work: two reflections related by a real symmetry operator still disagree, because they carry
|
|
// DIFFERENT systematic error - absorption, illumination, partiality - and how much of that a
|
|
// crystal has is a property of the measurement, not of its symmetry. So a genuine operator's own
|
|
// disagreement ranges over whatever the data quality happens to be, and any fixed bound placed on
|
|
// it rejects good crystals at one end or waves twins through at the other. Dividing by the parent
|
|
// operators - already confirmed, measured on the same reflections, carrying the same systematic
|
|
// floor - cancels the data quality and leaves only the question being asked: does the ADDED
|
|
// operator relate intensities as equal as the parent's do (real symmetry), or systematically less
|
|
// equal (a twin law mixing non-equivalent reflections)?
|
|
double HRatioOfPromotion(const std::vector<SpaceGroupOperatorScore>& operators,
|
|
const std::string& parent_group) {
|
|
const auto parent_ops = OperatorTripletsOf(parent_group);
|
|
double h_added = 0.0, h_parent = 0.0;
|
|
int n_added = 0, n_parent = 0;
|
|
for (const auto& s : operators) {
|
|
if (s.n_pairs < 200) // SearchSpaceGroupOptions::min_pairs_for_h
|
|
continue;
|
|
// The median, which is what the promotion is gated on (SearchSpaceGroup).
|
|
if (parent_ops.count(s.op_triplet_hkl) > 0) { h_parent += s.h_stat; ++n_parent; }
|
|
else { h_added += s.h_stat; ++n_added; }
|
|
}
|
|
if (n_added == 0 || n_parent == 0 || h_parent <= 0.0)
|
|
return std::numeric_limits<double>::quiet_NaN();
|
|
return (h_added / n_added) / (h_parent / n_parent);
|
|
}
|
|
|
|
std::string Describe(const TwinCrystal& c, const MergeQuality& q, double alpha, int multiplicity) {
|
|
std::ostringstream os;
|
|
os << c.name << ", " << q.name << ", twin fraction " << std::fixed << std::setprecision(2)
|
|
<< alpha << ", multiplicity " << multiplicity;
|
|
return os.str();
|
|
}
|
|
}
|
|
|
|
// Positive control: a crystal whose structure factors really do have the higher symmetry must be
|
|
// promoted to it. Guards the twin tests below against a criterion that simply never promotes.
|
|
TEST_CASE("SearchSpaceGroup promotes a genuinely high-symmetry crystal",
|
|
"[SearchSpaceGroup][twin]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
for (int mult : multiplicities) {
|
|
DYNAMIC_SECTION(c.name << ", " << q.name << ", genuine supergroup, multiplicity " << mult) {
|
|
const auto d = Decide(c, q, c.super, 0.0, mult);
|
|
INFO(d.report);
|
|
CHECK(d.point_group == PointGroupOf(c.super));
|
|
CHECK(d.space_group == ShortNameOf(c.super));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Negative control: an UNTWINNED crystal of the true subgroup (alpha = 0) must not be promoted - its
|
|
// extra metric operator relates reflections that are simply not equivalent.
|
|
TEST_CASE("SearchSpaceGroup keeps an untwinned low-symmetry crystal in its subgroup",
|
|
"[SearchSpaceGroup][twin]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
for (int mult : multiplicities) {
|
|
DYNAMIC_SECTION(Describe(c, q, 0.0, mult)) {
|
|
const auto d = Decide(c, q, c.sub, 0.0, mult);
|
|
INFO(d.report);
|
|
CHECK(d.point_group == PointGroupOf(c.sub));
|
|
CHECK(d.space_group == ShortNameOf(c.sub));
|
|
}
|
|
}
|
|
}
|
|
|
|
// A partial merohedral twin must stay in its true subgroup. Promoting it averages the two twin
|
|
// domains into one intensity, which no later step can undo: the twin fraction is not recoverable and
|
|
// the merged data are simply wrong.
|
|
TEST_CASE("SearchSpaceGroup keeps a partially twinned crystal in its true subgroup",
|
|
"[SearchSpaceGroup][twin]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
for (double alpha : {0.05, 0.10, 0.20, 0.35})
|
|
for (int mult : multiplicities) {
|
|
DYNAMIC_SECTION(Describe(c, q, alpha, mult)) {
|
|
const auto d = Decide(c, q, c.sub, alpha, mult);
|
|
INFO(d.report);
|
|
CHECK(d.point_group == PointGroupOf(c.sub));
|
|
CHECK(d.space_group == ShortNameOf(c.sub));
|
|
}
|
|
}
|
|
}
|
|
|
|
// A body-centred crystal in 4, twinned near-perfectly by a 2-fold of 422, given its cell as rugnux
|
|
// gives it. The agreement gates pass 422 at this fraction and only the twin-immune zone refuses it;
|
|
// what remains is a tie of order 4 between 4 and a 222 made of the crystal's 2-fold and two twin laws.
|
|
// The I centring extinguishes the odd class of every principal row, and that must not be read as
|
|
// three screw axes for the 222.
|
|
TEST_CASE("SearchSpaceGroup keeps a near-perfectly twinned body-centred 4 in 4", "[SearchSpaceGroup][twin]") {
|
|
SyntheticMergeParams p;
|
|
p.true_space_group = "I 4";
|
|
p.twin_supergroup = "I 4 2 2";
|
|
p.twin_fraction = 0.45;
|
|
p.multiplicity = 6;
|
|
p.seed = 20260727;
|
|
const auto merged = jfjoch_test::GenerateSyntheticMerged(p);
|
|
|
|
SearchSpaceGroupOptions opt;
|
|
opt.merge_friedel = true;
|
|
opt.lattice_system = gemmi::CrystalSystem::Tetragonal;
|
|
opt.cell = jfjoch_test::SyntheticCellFor(gemmi::get_spacegroup_by_name("I 4"));
|
|
const auto result = SearchSpaceGroup(merged, opt);
|
|
INFO(SearchSpaceGroupResultToText(result));
|
|
CHECK(result.point_group_hm == "4");
|
|
}
|
|
|
|
// A PERFECT (alpha = 0.5) merohedral twin produces intensities that are exactly invariant under the
|
|
// twin law: I_obs(h) = I_obs(twin h) for every reflection. No intensity statistic can tell it from a
|
|
// crystal that genuinely has the higher symmetry - the information is not in the data (it takes a
|
|
// different measurement, e.g. the |E| distribution's second moment, to even suspect it). So the only
|
|
// thing asserted here is that the search terminates and returns one of the two.
|
|
TEST_CASE("SearchSpaceGroup on a perfect merohedral twin returns one of the two symmetries",
|
|
"[SearchSpaceGroup][twin]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
for (int mult : multiplicities) {
|
|
DYNAMIC_SECTION(Describe(c, q, 0.5, mult)) {
|
|
const auto d = Decide(c, q, c.sub, 0.5, mult);
|
|
INFO(d.report);
|
|
REQUIRE(d.space_group != "none");
|
|
CHECK((d.point_group == PointGroupOf(c.sub) ||
|
|
d.point_group == PointGroupOf(c.super)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// THE property this harness exists for. Multiplicity changes only the sigmas - a merged sigma averages
|
|
// down as 1/sqrt(n) while the systematic error the crystal carries does not - so it changes
|
|
// how well the SAME crystal is measured, never what its symmetry is. A symmetry decision that moves
|
|
// when the same crystal is merged 2x instead of 18x is a defect of the criterion, not a property of
|
|
// the data.
|
|
//
|
|
// This is what a criterion thresholded on merge chi^2 or on a systematic-b RATIO cannot deliver: both
|
|
// are ratios to an error model that multiplicity and the sigma calibration move, so the tetragonal
|
|
// 4 -> 422 twin at alpha 0.20 / 0.35 used to flip - promoted at multiplicity 2 and kept at 18 with
|
|
// under-calibrated sigmas, and the other way round with an over-optimistic ISa. The operator
|
|
// disagreement ratio H_added/H_parent holds instead because there is no sigma in it at all: it
|
|
// compares intensities with intensities, and normalising against the parent group's own operators on
|
|
// the same reflections divides out both the data quality and the systematic floor that multiplicity
|
|
// and the error model move. An absolute bound on a single operator's H would not survive this - see
|
|
// HRatioOfPromotion.
|
|
TEST_CASE("SearchSpaceGroup point-group decision does not depend on merge multiplicity",
|
|
"[SearchSpaceGroup][twin]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
for (double alpha : twin_fractions) {
|
|
DYNAMIC_SECTION(Describe(c, q, alpha, 2) + " vs multiplicity 18") {
|
|
const auto low = Decide(c, q, c.sub, alpha, 2);
|
|
const auto high = Decide(c, q, c.sub, alpha, 18);
|
|
INFO("multiplicity 2:\n" << low.report << "\nmultiplicity 18:\n" << high.report);
|
|
CHECK(low.point_group == high.point_group);
|
|
CHECK(low.space_group == high.space_group);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Diagnostic, not run by default: ./jfjoch_test "[twin-h]"
|
|
// Prints the operator-disagreement ratio H_added/H_parent that the promotion is decided on, for the
|
|
// genuine high-symmetry crystal and for each twin fraction, across both merge-quality regimes and
|
|
// every multiplicity - i.e. how much margin the max_operator_h_ratio bound actually has, and whether
|
|
// either side of it drifts with data quality or data amount.
|
|
TEST_CASE("SearchSpaceGroup operator H ratio margins", "[.][twin-h]") {
|
|
SearchSpaceGroupOptions defaults;
|
|
std::cout << "H_added / H_parent for the sub -> super promotion; bound "
|
|
<< defaults.max_operator_h_ratio << " (above = refused as a twin)\n";
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality)
|
|
// Both normalisations where the supergroup has two maximal subgroups of the same order:
|
|
// against the crystal's true parent, and against its rival.
|
|
for (const auto& parent : c.rival_parent.empty()
|
|
? std::vector<std::string>{c.sub}
|
|
: std::vector<std::string>{c.sub, c.rival_parent}) {
|
|
std::cout << "\n" << c.name << "\n merge quality: " << q.name
|
|
<< " normalised against " << ShortNameOf(parent)
|
|
<< (parent == c.sub ? " (the crystal's own parent)" : " (the RIVAL parent)")
|
|
<< "\n";
|
|
std::cout << " " << std::setw(22) << std::left << "true symmetry / alpha" << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << ("mult " + std::to_string(mult));
|
|
std::cout << "\n " << std::setw(22) << std::left << "genuine supergroup" << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << std::fixed << std::setprecision(3)
|
|
<< HRatioOfPromotion(Decide(c, q, c.super, 0.0, mult).operators, parent);
|
|
std::cout << "\n";
|
|
for (double alpha : twin_fractions) {
|
|
std::ostringstream label;
|
|
label << "subgroup, alpha " << std::fixed << std::setprecision(2) << alpha;
|
|
std::cout << " " << std::setw(22) << std::left << label.str() << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << std::fixed << std::setprecision(3)
|
|
<< HRatioOfPromotion(Decide(c, q, c.sub, alpha, mult).operators, parent);
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
SUCCEED();
|
|
}
|
|
|
|
// Diagnostic, not run by default (hidden by the [.] tag):
|
|
// ./jfjoch_test "[twin-table]"
|
|
// prints the decision for every (true symmetry, twin fraction, multiplicity) combination in both
|
|
// merge-quality regimes - the table a redesign of the point-group criterion should be judged against.
|
|
TEST_CASE("SearchSpaceGroup twin decision table", "[.][twin-table]") {
|
|
for (const auto& c : crystals)
|
|
for (const auto& q : merge_quality) {
|
|
const auto reference = Decide(c, q, c.super, 0.0, 6);
|
|
std::cout << "\n" << c.name << "\n merge quality: " << q.name
|
|
<< " (true subgroup " << ShortNameOf(c.sub)
|
|
<< ", supergroup " << ShortNameOf(c.super) << ", "
|
|
<< reference.n_merged << " merged reflections)\n";
|
|
std::cout << " " << std::setw(22) << std::left << "true symmetry / alpha" << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << ("mult " + std::to_string(mult));
|
|
std::cout << "\n " << std::setw(22) << std::left << "genuine supergroup" << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << Decide(c, q, c.super, 0.0, mult).space_group;
|
|
std::cout << "\n";
|
|
for (double alpha : twin_fractions) {
|
|
std::ostringstream label;
|
|
label << "subgroup, alpha " << std::fixed << std::setprecision(2) << alpha;
|
|
std::cout << " " << std::setw(22) << std::left << label.str() << std::right;
|
|
for (int mult : multiplicities)
|
|
std::cout << std::setw(12) << Decide(c, q, c.sub, alpha, mult).space_group;
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
SUCCEED();
|
|
}
|