From 6abc44710df542c80ae1a287f7c91096473ee367 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 21 Oct 2026 20:30:45 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit c4d6ad74c706181cbb183cfcaa2481653cd7cad6) --- broker/JFJochBrokerHttp.cpp | 10 ++-- broker/OpenAPIConvert.cpp | 2 + broker/gen/model/Rugnux_live_status.cpp | 16 +++++- broker/gen/model/Rugnux_live_status.h | 7 +++ broker/jfjoch_api.yaml | 17 +++++-- broker/redoc-static.html | 12 +++-- docs/CPU_DATA_ANALYSIS_DECISIONS.md | 2 +- docs/python_client/docs/DefaultApi.md | 8 +-- docs/python_client/docs/RugnuxLiveStatus.md | 1 + .../src/client/@tanstack/react-query.gen.ts | 2 +- frontend/src/client/sdk.gen.ts | 2 +- frontend/src/client/types.gen.ts | 15 ++++-- frontend/src/client/zod.gen.ts | 7 ++- frontend/src/components/RugnuxPage.tsx | 2 +- .../src/components/RugnuxStructureViewer.tsx | 50 +++++++++++++++---- .../structure_refinement/ModelValidation.cpp | 23 ++++++++- receiver/RugnuxStructure.cpp | 23 +++++++-- receiver/RugnuxStructure.h | 9 ++-- tests/BrokerHttpAuthTest.cpp | 1 + tests/ModelValidationTest.cpp | 45 +++++++++++++++++ tests/RugnuxStructureTest.cpp | 22 ++++++-- 21 files changed, 224 insertions(+), 52 deletions(-) diff --git a/broker/JFJochBrokerHttp.cpp b/broker/JFJochBrokerHttp.cpp index 0215495e2..a6e7255a6 100644 --- a/broker/JFJochBrokerHttp.cpp +++ b/broker/JFJochBrokerHttp.cpp @@ -1047,18 +1047,20 @@ void JFJochBrokerHttp::rugnux_model_cif_get(httplib::Response &response) { void JFJochBrokerHttp::rugnux_map_ccp4_get(const std::optional &type, httplib::Response &response) { const std::string t = type.value_or("2fofc"); - if (t != "2fofc" && t != "fofc") { + if (t != "2fofc" && t != "fofc" && t != "anom") { response.status = 400; response.set_content("Unknown map type " + t, "text/plain"); return; } const auto structure = state_machine.GetRugnuxStatus().structure; - if (!structure || !structure->HasMaps()) { + const RugnuxMapType map_type = t == "2fofc" ? RugnuxMapType::TwoFoFc + : t == "fofc" ? RugnuxMapType::FoFc : RugnuxMapType::Anomalous; + const std::string map = structure ? structure->Map(map_type) : std::string(); + if (map.empty()) { response.status = 404; return; } - response.set_content(structure->Map(t == "2fofc" ? RugnuxMapType::TwoFoFc : RugnuxMapType::FoFc), - "application/octet-stream"); + response.set_content(map, "application/octet-stream"); } void JFJochBrokerHttp::config_bragg_integration_get(httplib::Response &response) { diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index ffd43b274..53168776a 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -3,6 +3,7 @@ #include "../common/JFJochMath.h" #include "OpenAPIConvert.h" +#include "../receiver/RugnuxStructure.h" // From https://en.cppreference.com/w/cpp/string/byte/tolower std::string str_tolower(std::string s) { @@ -1277,6 +1278,7 @@ org::openapitools::server::model::Rugnux_live_status Convert(const RugnuxLiveSna ret.setSummary(summary); ret.setWarnings(input.warnings); ret.setPlacedModel(input.structure != nullptr); + ret.setAnomalousMap(input.structure != nullptr && input.structure->HasAnomalousMap()); return ret; } diff --git a/broker/gen/model/Rugnux_live_status.cpp b/broker/gen/model/Rugnux_live_status.cpp index 7a4c1059d..b7abe100b 100644 --- a/broker/gen/model/Rugnux_live_status.cpp +++ b/broker/gen/model/Rugnux_live_status.cpp @@ -23,6 +23,7 @@ Rugnux_live_status::Rugnux_live_status() { m_State = ""; m_Placed_model = false; + m_Anomalous_map = false; m_Probe = ""; m_Probe_s = 0.0; m_Probe_sIsSet = false; @@ -68,7 +69,7 @@ bool Rugnux_live_status::validate(std::stringstream& msg, const std::string& pat bool success = true; const std::string _pathPrefix = pathPrefix.empty() ? "Rugnux_live_status" : pathPrefix; - + if (stepProgressIsSet()) { const float& value = m_Step_progress; @@ -165,6 +166,9 @@ bool Rugnux_live_status::operator==(const Rugnux_live_status& rhs) const (isPlacedModel() == rhs.isPlacedModel()) && + (isAnomalousMap() == rhs.isAnomalousMap()) + && + (getProbe() == rhs.getProbe()) && @@ -232,6 +236,7 @@ void to_json(nlohmann::json& j, const Rugnux_live_status& o) j = nlohmann::json::object(); j["state"] = o.m_State; j["placed_model"] = o.m_Placed_model; + j["anomalous_map"] = o.m_Anomalous_map; j["probe"] = o.m_Probe; if(o.probeSIsSet()) j["probe_s"] = o.m_Probe_s; @@ -265,6 +270,7 @@ void from_json(const nlohmann::json& j, Rugnux_live_status& o) { j.at("state").get_to(o.m_State); j.at("placed_model").get_to(o.m_Placed_model); + j.at("anomalous_map").get_to(o.m_Anomalous_map); j.at("probe").get_to(o.m_Probe); if(j.find("probe_s") != j.end()) { @@ -334,6 +340,14 @@ void Rugnux_live_status::setPlacedModel(bool const value) { m_Placed_model = value; } +bool Rugnux_live_status::isAnomalousMap() const +{ + return m_Anomalous_map; +} +void Rugnux_live_status::setAnomalousMap(bool const value) +{ + m_Anomalous_map = value; +} std::string Rugnux_live_status::getProbe() const { return m_Probe; diff --git a/broker/gen/model/Rugnux_live_status.h b/broker/gen/model/Rugnux_live_status.h index 20932abd8..a1d8ba05e 100644 --- a/broker/gen/model/Rugnux_live_status.h +++ b/broker/gen/model/Rugnux_live_status.h @@ -72,6 +72,11 @@ public: bool isPlacedModel() const; void setPlacedModel(bool const value); /// + /// The merge kept the Bijvoet pairs, so with the placed model there is an anomalous difference map (`/rugnux/map.ccp4?type=anom`). + /// + bool isAnomalousMap() const; + void setAnomalousMap(bool const value); + /// /// Answer of the early lattice probe on the first 60 degrees; empty until it answers /// std::string getProbe() const; @@ -185,6 +190,8 @@ protected: bool m_Placed_model; + bool m_Anomalous_map; + std::string m_Probe; double m_Probe_s; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index 265206dba..305834ae3 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -2786,6 +2786,7 @@ components: - warnings - probe - placed_model + - anomalous_map properties: state: type: string @@ -2795,6 +2796,11 @@ components: description: | The run placed the model: it can be fetched from `/rugnux/model.cif` and its maps from `/rugnux/map.ccp4`. + anomalous_map: + type: boolean + description: | + The merge kept the Bijvoet pairs, so with the placed model there is an anomalous difference map + (`/rugnux/map.ccp4?type=anom`). probe: type: string description: Answer of the early lattice probe on the first 60 degrees; empty until it answers @@ -3641,7 +3647,7 @@ paths: summary: Get an electron-density map of the last rugnux live run description: | The map of the placed model (`/rugnux/model.cif`) in CCP4 format (mode 2, float), made from the map - coefficients written to `_maps.mtz`. It covers one unit cell centred on the model - along an axis where + coefficients written to `_maps.mtz` (FWT/PHWT, DELFWT/PHDELWT, ANOM/PHANOM). It covers one unit cell centred on the model - along an axis where the model reaches further than the cell, the model plus 5 A instead - sampled every d_min/3; the mean and RMS in its header are those of the unit cell, so contour levels in sigma are the usual ones. parameters: @@ -3650,9 +3656,12 @@ paths: required: false schema: type: string - enum: [2fofc, fofc] + enum: [2fofc, fofc, anom] default: 2fofc - description: "`2fofc`: the 2mFo-DFc map; `fofc`: the mFo-DFc difference map" + description: | + `2fofc`: the 2mFo-DFc map; `fofc`: the mFo-DFc difference map; `anom`: the anomalous difference map, + (F(+) - F(-)) on the model phases less 90 degrees, where the merge kept the Bijvoet pairs (`anomalous_map` + in `/rugnux/status`) security: - bearerAuth: [] responses: @@ -3676,7 +3685,7 @@ paths: schema: type: string "404": - description: The last rugnux live run placed no model or made no maps + description: The last rugnux live run placed no model or made no maps, or (`anom`) has no Bijvoet pairs /config/zeromq_preview: put: summary: Set ZeroMQ preview settings diff --git a/broker/redoc-static.html b/broker/redoc-static.html index d346ae438..7e9320b0e 100644 --- a/broker/redoc-static.html +++ b/broker/redoc-static.html @@ -864,7 +864,7 @@ and validation of every following rugnux live collection, until it is removed wi
Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "state": "Idle",
  • "placed_model": true,
  • "probe": "string",
  • "probe_s": 0.1,
  • "start_to_first_image_s": 0.1,
  • "message": "string",
  • "images_expected": 0,
  • "expected_collection_s": 0.1,
  • "images_received": 0,
  • "elapsed_s": 0.1,
  • "step_progress": 1,
  • "steps": [
    ],
  • "last_image_s": 0.1,
  • "cut": "string",
  • "cut_s": 0.1,
  • "last_image_to_written_s": 0.1,
  • "verdict": "string",
  • "verdict_text": "string",
  • "summary": [
    ],
  • "warnings": [
    ]
}

Get the model as placed by the last rugnux live run

The model placed against the data by molecular replacement or validation, as mmCIF - the same text as the +

http://localhost:5232/rugnux/status

Response samples

Content type
application/json
{
  • "state": "Idle",
  • "placed_model": true,
  • "anomalous_map": true,
  • "probe": "string",
  • "probe_s": 0.1,
  • "start_to_first_image_s": 0.1,
  • "message": "string",
  • "images_expected": 0,
  • "expected_collection_s": 0.1,
  • "images_received": 0,
  • "elapsed_s": 0.1,
  • "step_progress": 1,
  • "steps": [
    ],
  • "last_image_s": 0.1,
  • "cut": "string",
  • "cut_s": 0.1,
  • "last_image_to_written_s": 0.1,
  • "verdict": "string",
  • "verdict_text": "string",
  • "summary": [
    ],
  • "warnings": [
    ]
}

Get the model as placed by the last rugnux live run

The model placed against the data by molecular replacement or validation, as mmCIF - the same text as the _model.cif file written beside the data: the data's cell and space group, with the input model's own metadata (entities, sequences, references, authors, software) kept. Kept as long as /rugnux/status.

Authorizations:
bearerAuth

Responses

Get an electron-density map of the last rugnux live run

The map of the placed model (/rugnux/model.cif) in CCP4 format (mode 2, float), made from the map -coefficients written to _maps.mtz. It covers one unit cell centred on the model - along an axis where +coefficients written to _maps.mtz (FWT/PHWT, DELFWT/PHDELWT, ANOM/PHANOM). It covers one unit cell centred on the model - along an axis where the model reaches further than the cell, the model plus 5 A instead - sampled every d_min/3; the mean and RMS in its header are those of the unit cell, so contour levels in sigma are the usual ones.

-
Authorizations:
bearerAuth
query Parameters
type
string
Default: "2fofc"
Enum: "2fofc" "fofc"

2fofc: the 2mFo-DFc map; fofc: the mFo-DFc difference map

+
Authorizations:
bearerAuth
query Parameters
type
string
Default: "2fofc"
Enum: "2fofc" "fofc" "anom"

2fofc: the 2mFo-DFc map; fofc: the mFo-DFc difference map; anom: the anomalous difference map, +(F(+) - F(-)) on the model phases less 90 degrees, where the merge kept the Bijvoet pairs (anomalous_map +in /rugnux/status)

Responses

Set ZeroMQ preview settings

Jungfraujoch can generate preview message stream on ZeroMQ SUB socket. Here settings of the socket can be adjusted. @@ -1113,7 +1115,7 @@ then image might be replaced in the buffer between calling /images and /image.cb