Anomalous difference map: ANOM/PHANOM in _maps.mtz, /rugnux/map.ccp4?type=anom, a magenta mesh in the viewer

Model validation already made the anomalous difference map's coefficients ((F(+) - F(-)) on the model
phases less 90 degrees, where the merge kept the Bijvoet pairs) to read the anomalous sites, then threw
them away. They now go into the map coefficients as ANOM/PHANOM (phenix.maps' labels), missing on the
reflections without a pair, so _maps.mtz opens as an anomalous map in Coot. Nothing in scaling, merging
or the validation's decisions changes; a merge without the Bijvoet split writes the twelve columns as before.

The broker serves the map as /rugnux/map.ccp4?type=anom (404 without Bijvoet pairs), and
rugnux_live_status.anomalous_map says it is there. The Mol* viewer loads it only then, as a magenta mesh
at +4 sigma with its own level slider and switch.

Tests: ANOM/PHANOM appear with Bijvoet pairs (as many as the reported pairs) and not without; the served
anomalous map of a point peaks at it; the auth test covers type=anom. End to end (myoglobin 2M replay):
anomalous_map true, the served map equals gemmi's FFT of ANOM/PHANOM in the written _maps.mtz to 3.4e-6 rms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit c4d6ad74c706181cbb183cfcaa2481653cd7cad6)
This commit is contained in:
leonarski_f committed 2026-10-22 20:18:51 +02:00
1 parent ee016d3d13
commit 6abc44710d
21 files changed
+224 -52

No files matched your search

@@ -21,6 +21,7 @@
#include <string>
#include <utility>
#include <vector>
#include <limits>
#include <unordered_map>
#include <gemmi/assembly.hpp> // expand_ncs
@@ -1725,6 +1726,8 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
// thrown away unread: they are not made at all.
const bool maps_superseded = schedule.maps_superseded && schedule.maps_superseded(result);
std::shared_ptr<ModelMaps> maps;
// The anomalous difference map's coefficients, where the merge kept the Bijvoet split: for the maps file too.
gemmi::AsuData<std::complex<float>> mapanom;
if (!maps_superseded) {
maps = std::make_shared<ModelMaps>();
@@ -1739,8 +1742,7 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
// rather than leaving a list of coordinates for someone to look up.
// Following ANODE, Thorn & Sheldrick (2011) J. Appl. Cryst. 44, 1285-1287
std::future<void> anomalous_map = std::async(std::launch::async, [&] {
gemmi::AsuData<std::complex<float>> mapanom =
anomalous_coefficients(merged, data_space_group != nullptr ? data_space_group : sg,
mapanom = anomalous_coefficients(merged, data_space_group != nullptr ? data_space_group : sg,
result.indexing_op, fmodel);
result.anomalous_pairs = static_cast<int>(mapanom.v.size());
if (!mapanom.v.empty()) {
@@ -1809,6 +1811,11 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
{"FREE", 'I'}};
for (const auto &[label, type] : columns)
mtz.columns.push_back({label, type, type == 'H' ? 0 : 1, {}});
// The anomalous difference map's coefficients (ANOM/PHANOM, as phenix.maps names them), on the reflections
// that have a Bijvoet pair; missing (NaN) on the others.
std::unordered_map<long, std::complex<float>> anom_by_hkl;
for (const auto &hv : mapanom.v)
anom_by_hkl[hkl_key(hv.hkl)] = hv.value;
for (size_t i = 0; i < terms.size(); i++) {
// [0, 360), the convention the MTZ 'P' columns carried before sigma_A weighting: std::arg
// returns (-180, 180] and a P column is not supposed to.
@@ -1825,6 +1832,18 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
for (size_t c = 0; c < mtz.columns.size(); c++)
mtz.columns[c].values.push_back(row[c]);
}
if (!anom_by_hkl.empty()) {
MtzTable::Column anom{"ANOM", 'F', 1, {}}, phanom{"PHANOM", 'P', 1, {}};
for (const auto &t : terms) {
const auto it = anom_by_hkl.find(hkl_key(t.hkl));
const float nan = std::numeric_limits<float>::quiet_NaN();
const double phi = it == anom_by_hkl.end() ? 0.0 : std::arg(it->second) * 180.0 / PI;
anom.values.push_back(it == anom_by_hkl.end() ? nan : std::abs(it->second));
phanom.values.push_back(it == anom_by_hkl.end() ? nan : static_cast<float>(phi < 0 ? phi + 360.0 : phi));
}
mtz.columns.push_back(std::move(anom));
mtz.columns.push_back(std::move(phanom));
}
}
result.ok = true;