Files
Jungfraujoch/gemmi_gph/gemmi/cif.hpp
T
leonarski_f dd0bffb283
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 11m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m27s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m54s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m25s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m33s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m19s
Build Packages / build:rpm (rocky8) (push) Successful in 12m23s
Build Packages / build:rpm (rocky9) (push) Successful in 13m21s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m30s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m55s
Build Packages / DIALS test (push) Successful in 13m42s
Build Packages / XDS test (durin plugin) (push) Successful in 9m26s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m41s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m12s
Build Packages / Generate python client (push) Successful in 19s
Build Packages / Build documentation (push) Successful in 52s
Build Packages / Create release (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 5m29s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m12s
Build Packages / build:windows:cuda (push) Successful in 18m36s
v1.0.0-rc.159 (#69)
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* rugnux: Add `--model model.pdb` - score the merged data against an atomic model and compute initial maps. It reports R-work/R-free (scaling the model to the observed amplitudes with an overall scale, an anisotropic B and a flat bulk solvent - the standard few-parameter model, so a batch of maps stays directly comparable) and writes 2Fo-Fc / Fo-Fc electron-density maps (CCP4) plus a map-coefficient MTZ. The structure itself is not refined; the model is only re-fractionalised into the data cell.
* rugnux: The merged reflection output now carries French-Wilson amplitudes (|F| and its sigma) next to the intensities - MTZ `F`/`SIGF`, mmCIF `_refln.F_meas_au`, and the text HKL - computed with the correct centric/acentric Wilson prior and epsilon multiplicity, so a downstream program (e.g. phenix.refine) can refine against amplitudes. The intensity columns are unchanged.
* rugnux: R-free test-set flags are now assigned deterministically and consistently across symmetry - a Bijvoet pair I(+)/I(-) is never split between the work and free sets, and the assignment is a reproducible per-hkl hash that depends only on the reflection index, so every dataset of one crystal form gets the same ~5% free set (what a multi-dataset campaign such as PanDDA needs). On small data the fraction is floored so the test set stays large enough for a stable R-free (~500 reflections, capped at 10%); it stays flat at 5% on ordinary data. When a reference MTZ carries a `FreeR_flag` column its test set is imported instead, letting a whole campaign inherit one shared free set.
* rugnux: A reference MTZ (`--reference-mtz`) can now fix the space group and cell for rotation data too (previously rejected), without being used to scale - the rotation merge stays self-consistent. When the crystal has an indexing (merohedral) ambiguity - a lattice symmetry higher than its Laue symmetry, e.g. P3/P4/P6/C2 - the reference also resolves it: each candidate reindexing (identity plus the twin-law cosets of the metric symmetry) is scored by its intensity correlation against the reference and the data are re-merged in the best-correlating one. This is a metric-preserving relabelling of hkl (the cell is unchanged) and a no-op for a holohedral crystal such as lysozyme.
* rugnux: `--model` validation now aligns the data to the model before scoring - the observed reflections are reindexed into the model's enantiomorph when the two differ only by hand (indistinguishable from merged intensities). A merohedral indexing ambiguity is resolved against the reference MTZ when one is given (so a whole campaign shares one indexing convention); only with a model and no reference does validation fall back to fitting each candidate reindexing and keeping the lowest R-free.
* rugnux: De-novo symmetry - recover a genuine high-symmetry group whose data are imperfectly scaled. Such a merge's within-orbit chi² lands just past the self-consistency bound (each real symmetry step adds a little systematic scatter), right where a merohedral twin also lands, so the chi² ratio alone cannot separate them. The candidate is now rescued when the extra intensity-proportional systematic error it invokes stays small relative to the confirmed subgroup - a genuine symmetry step gains multiplicity without inflating the merge error model's b, whereas a twin forces non-equivalent reflections together and b balloons. Fixes cubic insulin (I23 instead of I222) with no change to any other crystal in the test battery, including the twins that must stay in their lower symmetry.
* Docs: Document the French-Wilson amplitude estimation, R-free flagging, reference-based space-group/ambiguity resolution, and model-based validation/maps in CPU_DATA_ANALYSIS.md.
* Frontend: The status-bar pill now shows a progress bar during detector calibration (previously only during measurement), and the calibration state and its button are labelled "Calibration"/"CALIBRATE" (the internal `Pedestal` state name is unchanged for back-compatibility).Reviewed-on: #69

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-13 13:54:03 +02:00

386 lines
14 KiB
C++

// Copyright 2017 Global Phasing Ltd.
//
// CIF parser (based on PEGTL) with pluggable actions,
// and a set of actions that prepare Document.
// To just read the CIF format, include read_cif.hpp instead.
#ifndef GEMMI_CIF_HPP_
#define GEMMI_CIF_HPP_
#include <cassert>
#include <cstdio> // for FILE
#include <iosfwd> // for size_t, istream
#include <string>
#include "third_party/tao/pegtl.hpp" // IWYU pragma: keep
//#include "third_party/tao/pegtl/contrib/tracer.hpp" // for debugging
#include "cifdoc.hpp" // for Document, etc
#include "fileutil.hpp" // for CharArray, file_open
#if defined(_MSC_VER)
#pragma warning(push)
// warning C4244: an integer type is converted to a smaller integer type
#pragma warning(disable: 4244)
// warning C4267: conversion from 'size_t' to 'type', possible loss of data
#pragma warning(disable: 4267)
#endif
namespace gemmi {
namespace cif {
using std::size_t;
namespace pegtl = tao::pegtl;
// **** grammar rules, named similarly as in the CIF 1.1 spec ****
namespace rules {
template<int TableVal> struct lookup_char {
using analyze_t = pegtl::analysis::generic<pegtl::analysis::rule_type::ANY>;
template<typename Input> static bool match(Input& in) {
if (!in.empty() && cif::char_table(in.peek_char()) == TableVal) {
if (TableVal == 2) // this set includes new-line
in.bump(1);
else
in.bump_in_this_line(1);
return true;
}
return false;
}
};
// (letter) refers to sections in Table 2.2.7.1 in Vol.G of ITfC (2006).
// (g) Character sets.
// OrdinaryCharacter: ! % & ()*+,-./0-9: <=>?@A-Z[] \ ^ `a-z{|}~
using ordinary_char = lookup_char<1>;
using ws_char = lookup_char<2>;
// !"#$%&'()*+,-./0-9:;<=>?@A-Z[\]^_`a-z{|}~
struct nonblank_ch : pegtl::range<'!', '~'> {};
// ascii space is just before '!'
struct anyprint_ch : pegtl::ranges<' ', '~', '\t'> {};
// (f) White space and comments.
struct comment : pegtl::if_must<pegtl::one<'#'>, pegtl::until<pegtl::eolf>>{};
struct whitespace : pegtl::plus<pegtl::sor<ws_char, comment>> {};
struct ws_or_eof : pegtl::sor<whitespace, pegtl::eof> {};
// (b) Reserved words.
struct str_data : TAOCPP_PEGTL_ISTRING("data_") {};
struct str_loop : pegtl::seq<TAOCPP_PEGTL_ISTRING("loop_"), pegtl::at<ws_or_eof>> {};
struct str_global : pegtl::seq<TAOCPP_PEGTL_ISTRING("global_"), pegtl::at<ws_or_eof>> {};
struct str_save : TAOCPP_PEGTL_ISTRING("save_") {};
struct str_stop : pegtl::seq<TAOCPP_PEGTL_ISTRING("stop_"), pegtl::at<ws_or_eof>> {};
struct keyword : pegtl::sor<str_data, str_loop, str_global,
str_save, str_stop> {};
// (e) Character strings and text fields.
template<typename Q>
struct endq : pegtl::seq<Q, pegtl::at<pegtl::sor<
pegtl::one<' ','\n','\r','\t','#'>,
pegtl::eof>>> {};
// strict rule would be:
// template <typename Q> struct quoted_tail : until<endq<Q>, anyprint_ch> {};
// but it was relaxed after PDB accepted 5q1h with non-ascii character
template<typename Q>
struct quoted_tail : pegtl::until<endq<Q>, pegtl::not_one<'\n'>> {};
template<typename Q>
struct quoted : pegtl::if_must<Q, quoted_tail<Q>> {};
struct singlequoted : quoted<pegtl::one<'\''>> {};
struct doublequoted : quoted<pegtl::one<'"'>> {};
struct field_sep : pegtl::seq<pegtl::bol, pegtl::one<';'>> {};
// CIF 2.0 requires whitespace after text field, so it'd be:
// until<endq<field_sep>> instead of until<field_sep>.
struct textfield : pegtl::if_must<field_sep, pegtl::until<field_sep>> {};
struct unquoted : pegtl::seq<pegtl::not_at<keyword>,
pegtl::not_at<pegtl::one<'_','$','#'>>,
pegtl::plus<nonblank_ch>> {};
// (a) Basic structure of CIF. (c) Tags and values.
// datablockname in STAR/CIF should not be empty, but we made an exception
// for RELION which writes blocks starting with bare data_
struct datablockname : pegtl::star<nonblank_ch> {};
struct datablockheading : pegtl::sor<pegtl::seq<str_data, datablockname>, str_global> {};
struct tag : pegtl::seq<pegtl::one<'_'>, pegtl::plus<nonblank_ch>> {};
// unquoted value made of ordinary characters only - for a typical mmCIF file
// it is faster to check it first even if we backtrack on some values_.
struct simunq : pegtl::seq<pegtl::plus<ordinary_char>, pegtl::at<ws_char>> {};
struct value : pegtl::sor<simunq, singlequoted, doublequoted,
textfield, unquoted> {};
struct item_tag : tag {};
struct item_value : value {};
struct loop_tag : tag {};
struct loop_value : value {};
struct loop_end : pegtl::opt<str_stop, ws_or_eof> {};
struct loop : pegtl::if_must<str_loop,
whitespace,
pegtl::plus<pegtl::seq<loop_tag, whitespace, pegtl::discard>>,
pegtl::sor<pegtl::plus<pegtl::seq<loop_value, ws_or_eof,
pegtl::discard>>,
// handle incorrect CIF with empty loop
pegtl::at<pegtl::sor<keyword, pegtl::eof>>>,
loop_end> {};
struct missing_value : pegtl::bol {};
struct dataitem : pegtl::if_must<item_tag, whitespace,
pegtl::if_then_else<item_value, ws_or_eof,
missing_value>,
pegtl::discard> {};
struct framename : pegtl::plus<nonblank_ch> {};
struct endframe : str_save {};
struct frame : pegtl::if_must<str_save, framename, whitespace,
pegtl::star<pegtl::sor<dataitem, loop>>,
endframe, ws_or_eof> {};
struct datablock : pegtl::seq<datablockheading, ws_or_eof,
pegtl::star<pegtl::sor<dataitem, loop, frame>>> {};
struct content : pegtl::plus<datablock> {};
struct file : pegtl::seq<pegtl::opt<whitespace>,
pegtl::if_must<pegtl::not_at<pegtl::eof>,
content, pegtl::eof>> {};
struct one_block : pegtl::seq<pegtl::opt<whitespace>,
pegtl::if_must<pegtl::not_at<pegtl::eof>, datablock>> {};
} // namespace rules
// **** error messages ****
template<typename Rule> const std::string& error_message() {
static const std::string s = "parse error";
return s;
}
#define error_msg(rule, msg) \
template<> inline const std::string& error_message<rule>() { \
static const std::string s = msg; \
return s; \
}
error_msg(rules::quoted_tail<pegtl::one<'\''>>, "unterminated 'string'")
error_msg(rules::quoted_tail<pegtl::one<'"'>>, "unterminated \"string\"")
error_msg(pegtl::until<rules::field_sep>, "unterminated text field")
error_msg(rules::framename, "unnamed save_ frame")
error_msg(rules::content, "expected block header (data_)")
#undef error_msg
template<typename Rule> struct Errors : public pegtl::normal<Rule> {
template<typename Input, typename ... States>
static void raise(const Input& in, States&& ...) {
throw pegtl::parse_error(error_message<Rule>()
//+ " matching " + pegtl::internal::demangle<Rule>()
, in);
}
};
// **** parsing actions that fill the storage ****
template<typename Rule> struct Action : pegtl::nothing<Rule> {};
// We don't store comments here. We don't have a proper storage for comments.
// They can be stored as Items, but this leaves out comments before
// the first block, comments inside loops, or between tag and value.
// Additionally, a comment after a loop cannot be processed immediately
// b/c at that point we don't know if the loop is finished yet.
// If we were to store (a subset of) comments, we'd need to check first
// how it affects performance.
//template<> struct Action<rules::comment> {
// template<typename Input> static void apply(const Input& in, Document& out) {
// }
//};
template<> struct Action<rules::datablockname> {
template<typename Input> static void apply(const Input& in, Document& out) {
out.blocks.emplace_back(in.string());
Block& block = out.blocks.back();
// Empty block name (just data_ ) is not STAR/CIF conformant,
// but it's written by RELION and buccaneer; we must support it.
if (block.name.empty())
block.name += ' ';
out.items_ = &block.items;
}
};
template<> struct Action<rules::str_global> {
template<typename Input> static void apply(const Input&, Document& out) {
out.blocks.emplace_back();
out.items_ = &out.blocks.back().items;
}
};
template<> struct Action<rules::framename> {
template<typename Input> static void apply(const Input& in, Document& out) {
out.items_->emplace_back(FrameArg{in.string()});
out.items_->back().line_number = in.iterator().line;
out.items_ = &out.items_->back().frame.items;
}
};
template<> struct Action<rules::endframe> {
template<typename Input> static void apply(const Input&, Document& out) {
out.items_ = &out.blocks.back().items;
}
};
template<> struct Action<rules::item_tag> {
template<typename Input> static void apply(const Input& in, Document& out) {
out.items_->emplace_back(in.string());
out.items_->back().line_number = in.iterator().line;
}
};
template<> struct Action<rules::item_value> {
template<typename Input> static void apply(const Input& in, Document& out) {
Item& last_item = out.items_->back();
assert(last_item.type == ItemType::Pair);
last_item.pair[1] = in.string();
}
};
template<> struct Action<rules::str_loop> {
template<typename Input> static void apply(const Input& in, Document& out) {
out.items_->emplace_back(LoopArg{});
out.items_->back().line_number = in.iterator().line;
}
};
template<> struct Action<rules::loop_tag> {
template<typename Input> static void apply(const Input& in, Document& out) {
Item& last_item = out.items_->back();
assert(last_item.type == ItemType::Loop);
last_item.loop.tags.emplace_back(in.string());
}
};
template<> struct Action<rules::loop_value> {
template<typename Input> static void apply(const Input& in, Document& out) {
Item& last_item = out.items_->back();
assert(last_item.type == ItemType::Loop);
last_item.loop.values.emplace_back(in.string());
}
};
template<> struct Action<rules::loop> {
template<typename Input> static void apply(const Input& in, Document& out) {
Item& last_item = out.items_->back();
assert(last_item.type == ItemType::Loop);
const Loop& loop = last_item.loop;
if (loop.values.size() % loop.tags.size() != 0)
throw pegtl::parse_error(
"Wrong number of values in loop " + loop.common_prefix() + "*",
in);
}
};
template<typename Input> void parse_input(Document& d, Input&& in) {
pegtl::parse<rules::file, Action, Errors>(in, d);
}
template<typename Input> Document read_input(Input&& in, int check_level=1) {
Document doc;
doc.source = in.source();
parse_input(doc, in);
if (check_level > 0) {
check_for_missing_values(doc);
check_for_duplicates(doc);
if (check_level > 1) {
for (const cif::Block& block : doc.blocks) {
if (block.name == " ")
fail(doc.source + ": missing block name (bare data_)");
check_empty_loops(block, doc.source);
}
}
}
return doc;
}
template<typename Input>
size_t parse_one_block(Document& d, Input&& in) {
pegtl::parse<rules::one_block, Action, Errors>(in, d);
return in.byte();
}
// pegtl::file_input may use mmap and be faster, but does not work
// on Windows with Unicode filenames.
#if defined(_WIN32)
#define GEMMI_CIF_FILE_INPUT(in, path) \
tao::pegtl::read_input<> in(gemmi::file_open(path.c_str(), "rb").release(), path)
#else
#define GEMMI_CIF_FILE_INPUT(in, path) \
tao::pegtl::file_input<> in(path)
#endif
inline Document read_file(const std::string& filename, int check_level=1) {
GEMMI_CIF_FILE_INPUT(in, filename);
return read_input(in, check_level);
}
inline Document read_memory(const char* data, size_t size, const char* name, int check_level=1) {
pegtl::memory_input<> in(data, size, name);
return read_input(in, check_level);
}
inline Document read_cstream(std::FILE *f, size_t bufsize, const char* name, int check_level=1) {
pegtl::cstream_input<> in(f, bufsize, name);
return read_input(in, check_level);
}
inline Document read_istream(std::istream &is, size_t bufsize, const char* name,
int check_level=1) {
pegtl::istream_input<> in(is, bufsize, name);
return read_input(in, check_level);
}
template<typename Rule> struct CheckAction : pegtl::nothing<Rule> {};
template<> struct CheckAction<rules::missing_value> {
template<typename Input> static void apply(const Input& in) {
throw pegtl::parse_error("tag without value", in);
}
};
template<typename Input> bool try_parse(Input&& in, std::string* msg) {
try {
return pegtl::parse<rules::file, CheckAction, Errors>(in);
} catch (pegtl::parse_error& e) {
if (msg)
*msg = e.what();
return false;
}
}
// A function for transparent reading of normal and compressed files.
// T should have the same traits as BasicInput and MaybeGzipped.
template<typename T>
Document read(T&& input, int check_level=1) {
if (CharArray mem = input.uncompress_into_buffer())
return read_memory(mem.data(), mem.size(), input.path().c_str(), check_level);
if (input.is_stdin())
return read_cstream(stdin, 16*1024, "stdin", check_level);
return read_file(input.path(), check_level);
}
template<typename T>
bool check_syntax(T&& input, std::string* msg) {
if (CharArray mem = input.uncompress_into_buffer()) {
pegtl::memory_input<> in(mem.data(), mem.size(), input.path());
return try_parse(in, msg);
}
GEMMI_CIF_FILE_INPUT(in, input.path());
return try_parse(in, msg);
}
template<typename T>
size_t read_one_block(Document& d, T&& input, size_t limit) {
if (input.is_compressed()) {
CharArray mem = input.uncompress_into_buffer(limit);
return parse_one_block(d, pegtl::memory_input<>(mem.data(), mem.size(),
input.path().c_str()));
}
if (input.is_stdin())
return parse_one_block(d, pegtl::cstream_input<>(stdin, 16*1024, "stdin"));
GEMMI_CIF_FILE_INPUT(in, input.path());
return parse_one_block(d, std::move(in));
}
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
} // namespace cif
} // namespace gemmi
#endif