Files
Jungfraujoch/gemmi_gph/json.cpp
T
leonarski_fandClaude Opus 5 18d3f55325 rugnux: read mmCIF models with --model, and say when a model could not be used
--model called gemmi::read_pdb unconditionally, so a coordinate file in mmCIF -
which is how the PDB serves coordinates by default - was refused outright:

  Model validation: cannot read model 6G8A.cif:
  Incorrect file format (perhaps it is cif not pdb?)

Worse than the refusal was what followed it. ValidateAgainstModel logged,
returned an empty result, and the run went on to finish with exit code 0, no
R-free, no maps and nothing in the report - indistinguishable from a run that was
never given --model at all. A script that passed a .cif, checked the exit code
and grepped for R-free simply got nothing back.

Both read sites now go through read_structure_gz with CoorFormat::Detect, so PDB,
mmCIF and mmJSON are all read, gzipped or not, and the format comes from the
file's content rather than from its name. Detect is passed explicitly: GEMMI
otherwise takes the extension and only falls back to the content when it does not
recognise one, and a model arrives named however whoever produced it named it.

That needed a part of GEMMI the vendored subset had trimmed away. mmread.hpp was
already here but its CIF parser was not, so this restores what upstream GEMMI
ships: read_cif/mmcif/json/mmread_gz, plus GEMMI's own copies of PEGTL (MIT,
Colin Hirsch and Daniel Frey; 155 headers, 672 kB) and sajson (MIT, Chad Austin;
one header). Both are listed in THIRD_PARTY_NOTICES.md with their own licence
texts, PEGTL's collected by COLLECT.sh and sajson's kept by hand, its terms being
a comment block rather than a file. json.cpp carries a one-line change: upstream
keeps sajson at the root of its repository, outside the include tree this subset
copies, so the include points at where the other bundled headers live.

ModelValidationResult gains failure_reason, and a model that was asked for and
could not be used now reaches the results report as

  WARNING: Model validation did not run: model bogus.pdb has no atoms or no unit cell

in --mode mx and --mode scale alike. The run still succeeds - a merge that is good
is not made bad by a model that was not - but it no longer does so quietly.

One log line was making a claim it could not support: the indexing-ambiguity
message said the ambiguity was "resolved against the supplied model" while being
printed before the model is read, so it announced a resolution that a failed model
never performed. It now says the model will be used; the reference branch, where
the work really has already happened, keeps the past tense.

Verified on the rotation test dataset with a deposited mmCIF, the same file
gzipped, an mmCIF under a .pdb name, a PDB, and an unreadable file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WmryXe8ASbNi632sUMfsa
2026-08-27 22:09:51 +02:00

133 lines
5.1 KiB
C++

// Copyright Global Phasing Ltd.
#include <gemmi/json.hpp>
#include <utility> // for move
#define SAJSON_UNSORTED_OBJECT_KEYS
#define SAJSON_NUMBERS_AS_STRINGS
// Upstream GEMMI keeps sajson at the root of its repository, outside the include tree; this
// vendored subset has only the include tree, so it sits beside GEMMI's other bundled third-party
// headers instead. The only change to this file.
// https://github.com/project-gemmi/gemmi/blob/v0.7.5/src/json.cpp
#include "gemmi/third_party/sajson.h"
namespace gemmi {
namespace cif {
using std::size_t;
static std::string json_type_as_string(sajson::type t) {
switch (t) {
case sajson::TYPE_INTEGER: return "<integer>";
case sajson::TYPE_DOUBLE: return "<double>";
case sajson::TYPE_NULL: return "<null>";
case sajson::TYPE_FALSE: return "<false>";
case sajson::TYPE_TRUE: return "<true>";
case sajson::TYPE_STRING: return "<string>";
case sajson::TYPE_ARRAY: return "<array>";
case sajson::TYPE_OBJECT: return "<object>";
default: return "<unknown type>";
}
}
static std::string as_cif_value(const sajson::value& val) {
switch (val.get_type()) {
case sajson::TYPE_DOUBLE:
return val.as_string();
case sajson::TYPE_NULL:
return "?";
// mmJSON files from PDBj (this format has no spec) have special support
// for boolean YES|NO, which is used only in category _em_specimen.
// IMO it's a bad idea, but we must handle it if we want to read mmJSON.
case sajson::TYPE_FALSE:
return "NO"; // "." in CIF-JSON
case sajson::TYPE_TRUE:
return "YES";
case sajson::TYPE_STRING:
return quote(val.as_string());
// Another undocumented feature of mmJSON: arrays as values.
// It seems that obscure types int-range and float-range are converted to
// 2-element arrays. But not only. link_entity_pdbjplus.db_accession has
// arrays with strings.
case sajson::TYPE_ARRAY: {
std::string s;
for (size_t i = 0; i < val.get_length(); ++i) {
if (i != 0)
s += ' ';
s += val.get_array_element(0).as_string();
}
return quote(s);
}
default:
fail("Unexpected ", json_type_as_string(val.get_type()), " as value in JSON.");
return "";
}
}
static void fill_document_from_sajson(Document& d, const sajson::document& s) {
// assuming mmJSON here, we'll add handling of CIF-JSON later on
sajson::value root = s.get_root();
if (root.get_type() != sajson::TYPE_OBJECT)
fail("not mmJSON - the root is not of type object");
for (size_t block_index = 0; block_index < root.get_length(); ++block_index) {
std::string block_name = root.get_object_key(block_index).as_string();
if (!starts_with(block_name, "data_"))
fail("not mmJSON - top level key should start with data_\n"
"(if you use gemmi-cif2json to write JSON, use -m for mmJSON)");
d.blocks.emplace_back(block_name.substr(5));
std::vector<Item>& items = d.blocks[block_index].items;
sajson::value top = root.get_object_value(block_index);
if (top.get_type() != sajson::TYPE_OBJECT)
fail("");
for (size_t i = 0; i != top.get_length(); ++i) {
std::string category_name = "_" + top.get_object_key(i).as_string() + ".";
sajson::value category = top.get_object_value(i);
if (category.get_type() != sajson::TYPE_OBJECT ||
category.get_length() == 0 ||
category.get_object_value(0).get_type() != sajson::TYPE_ARRAY)
fail("");
size_t cif_cols = category.get_length();
size_t cif_rows = category.get_object_value(0).get_length();
if (cif_rows > 1) {
items.emplace_back(LoopArg{});
Loop& loop = items.back().loop;
loop.tags.reserve(cif_cols);
loop.values.resize(cif_cols * cif_rows);
}
for (size_t j = 0; j != cif_cols; ++j) {
std::string tag = category_name + category.get_object_key(j).as_string();
sajson::value arr = category.get_object_value(j);
if (arr.get_type() != sajson::TYPE_ARRAY)
fail("Expected array, got " + json_type_as_string(arr.get_type()));
if (arr.get_length() != cif_rows)
fail("Expected array of length ", std::to_string(cif_rows), " not ",
std::to_string(arr.get_length()));
if (cif_rows == 1) {
items.emplace_back(tag, as_cif_value(arr.get_array_element(0)));
} else if (cif_rows != 0) {
Loop& loop = items.back().loop;
loop.tags.emplace_back(std::move(tag));
for (size_t k = 0; k != cif_rows; ++k)
loop.values[j + k*cif_cols] = as_cif_value(arr.get_array_element(k));
}
}
}
}
}
Document read_mmjson_insitu(char* buffer, size_t size, const std::string& name) {
Document doc;
sajson::document json = sajson::parse(sajson::dynamic_allocation(),
sajson::mutable_string_view(size, buffer));
if (!json.is_valid())
fail(name + ":", std::to_string(json.get_error_line()), " error: ",
json.get_error_message_as_string());
fill_document_from_sajson(doc, json);
doc.source = name;
return doc;
}
} // namespace cif
} // namespace gemmi