diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6f7d7cb65..8ce67ec3f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,7 @@ * Rugnux drops a rotation reflection whose spot held a saturated (overloaded) pixel, as XDS does, and reports the count as `OBSERVATIONS_REJECTED_OVERLOAD=`. * Rugnux fits a rotation sweep's per-frame scale on the partial reflections, even with few reflections per frame, when scaling from the fulls alone leaves no measurable error model. * Rugnux no longer refuses a genuine high-symmetry point group because one of its operators agrees with the data unusually well; it refuses a group whose operators split into an agreeing subgroup and a disagreeing rest. +* Rugnux no longer adopts an intermediate space group (e.g. C222_1 for a twinned P2_1 on a hexagonal metric) when the higher group was refused and the twin-immune reflections show the added two-folds to be a twin law. * Rugnux names glide planes in groups without a centre of symmetry (e.g. I-42d, Fdd2), reports a group with the same absences (C2/c and Cc) as an alternative, and states how the centre was decided (`SPACE_GROUP_CENTRE`, `CENTRE_STATISTICS_*`). ### 1.0.0-rc.173 diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 21131003f..098ce7596 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -7828,8 +7828,53 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b if (alt.centring_type() == cand.centering) { chosen = alt; break; } const bool point_group_higher = s2.best_space_group.has_value() && s2.point_group_order > sg_search.point_group_order; - const bool higher = point_group_higher - && chosen->centring_type() == cand.centering; + bool higher = point_group_higher + && chosen->centring_type() == cand.centering; + // A promotion over a refusal is arbitrated by the twin-immune zone of the added + // operators (TwinZoneVerdict), here as where the two arms disagree: where a twin + // gate of the search in hand refused a higher group, this ask reopens the question + // that gate answered, for whichever of the refused group's operators the new + // lattice holds, and its own search cannot answer it - the gates there are relative + // too, and a twin law or a pseudo-symmetry the refused group's other operators + // diluted passes them alone. Measured on a monoclinic crystal on a hexagonal metric, + // a pseudo-merohedral twin: 622 refused on the added-operator R, the C-centred + // orthorhombic sub-lattice then asked, its two in-plane two-folds at R 0.061 against + // 0.045 for the genuine screw, and C 2 2 21 adopted with the zone of those two-folds + // reading acentric. The group in hand is carried into the candidate's setting for + // it: h -> Q h with Q the reindex from the basis it was found in (read where GEMMI's + // table names the result, as every setting the search enumerates is). + std::string zone_refusal; + if (higher && sg_search.refused_by_twin_gate && sg_search.point_group_representative) { + gemmi::Mat33 q = reindex; + if (commit_reindex) + q = reindex.multiply(commit_reindex->inverse()); + const gemmi::Mat33 q_inv = q.inverse(); + gemmi::GroupOps lower_ops; + lower_ops.cen_ops = gemmi::centring_vectors(cand.centering); + for (const auto &op : sg_search.point_group_representative->operations().sym_ops) { + // apply_to_hkl uses rot^T, so the Miller-index matrix of op is rot^T / DEN. + gemmi::Mat33 m; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + m.a[i][j] = static_cast(op.rot[j][i]) / gemmi::Op::DEN; + const gemmi::Mat33 m_new = q.multiply(m).multiply(q_inv); + gemmi::Op op_new = gemmi::Op::identity(); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + op_new.rot[i][j] = static_cast(std::lround(m_new[j][i])) * gemmi::Op::DEN; + lower_ops.sym_ops.push_back(op_new); + } + if (const gemmi::SpaceGroup *lower_sg = gemmi::find_spacegroup_by_ops(lower_ops)) { + SearchSpaceGroupResult lower = sg_search; + lower.point_group_representative = *lower_sg; + if (TwinZoneVerdict(merged_all, *o2.cell, s2, lower, result) < 0) { + zone_refusal = sg_search.refused_point_group_hm + " was refused (" + + sg_search.refused_reason + "), and the twin-immune zone of the " + "operators this group adds reads acentric"; + higher = false; + } + } + } const auto &uc = cand.conventional.GetUnitCell(); // The two asks are told apart by what they were asked against: the class a search // has already been given, or the rotations the intensities have actually confirmed. @@ -7853,6 +7898,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b s2.best_space_group.has_value() ? s2.best_space_group->xhm() : "nothing", s2.point_group_order, sg_search.point_group_order, higher ? " - adopted" + : !zone_refusal.empty() ? " - keeping the group already found; " + zone_refusal : (point_group_higher ? " - keeping the group already found; the point group IS " "higher, but no candidate of it carries the centring the "