Files
Jungfraujoch/gemmi_gph/pdb.cpp
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

1270 lines
52 KiB
C++

// Copyright 2019 Global Phasing Ltd.
#include "gemmi/pdb.hpp"
#include <cctype> // for isalpha
#include <cstdlib> // for atoi, strtol
#include <cstring> // for memcpy, strstr, strchr, strcmp
#include <algorithm> // for min, swap
#include <stdexcept> // for invalid_argument
#include <unordered_map>
#include "gemmi/atof.hpp" // for fast_from_chars
#include "gemmi/atox.hpp" // for is_space, is_digit
#include "gemmi/input.hpp"
#include "gemmi/metadata.hpp" // for Metadata
#include "gemmi/model.hpp" // for Structure, impl::find_or_add
#include "gemmi/polyheur.hpp" // for assign_subchains
#include "gemmi/util.hpp" // for trim_str, alpha_up, istarts_with
namespace gemmi {
namespace {
int read_int(const char* p, int field_length) {
return string_to_int(p, false, field_length);
}
double read_double(const char* p, int field_length) {
double d = 0.;
// we don't check for errors here
fast_from_chars(p, p + field_length, d);
return d;
}
std::string read_string(const char* p, int field_length) {
// left trim
while (field_length != 0 && is_space(*p)) {
++p;
--field_length;
}
// EOL/EOF ends the string
for (int i = 0; i < field_length; ++i)
if (p[i] == '\n' || p[i] == '\r' || p[i] == '\0') {
field_length = i;
break;
}
// right trim
while (field_length != 0 && is_space(p[field_length-1]))
--field_length;
return std::string(p, field_length);
}
template<int N> int read_base36(const char* p) {
char zstr[N+1] = {0};
std::memcpy(zstr, p, N);
return std::strtol(zstr, nullptr, 36);
}
// The standard charge format is 2+, but some files have +2.
signed char read_charge(char digit, char sign) {
if (sign == ' ' && digit == ' ') // by far the most common case
return 0;
if (sign >= '0' && sign <= '9')
std::swap(digit, sign);
if (digit >= '0' && digit <= '9') {
if (sign != '+' && sign != '-' && sign != '\0' && !is_space(sign))
fail("Wrong format for charge: " +
std::string(1, digit) + std::string(1, sign));
return (digit - '0') * (sign == '-' ? -1 : 1);
}
// if we are here the field should be blank, but maybe better not to check
return 0;
}
int read_matrix(Transform& t, const char* line, size_t len) {
if (len < 46)
return 0;
char n = line[5] - '0';
if (n >= 1 && n <= 3) {
t.mat[n-1][0] = read_double(line+10, 10);
t.mat[n-1][1] = read_double(line+20, 10);
t.mat[n-1][2] = read_double(line+30, 10);
t.vec.at(n-1) = read_double(line+45, 10);
}
return n;
}
SeqId read_seq_id(const char* str) {
SeqId seqid;
if (str[4] != '\r' && str[4] != '\n')
seqid.icode = str[4];
// We support hybrid-36 extension, although it is never used in practice
// as 9999 residues per chain are enough.
if (str[0] < 'A') {
for (int i = 4; i != 0; --i, ++str)
if (!is_space(*str)) {
seqid.num = read_int(str, i);
break;
}
} else {
seqid.num = read_base36<4>(str) - 466560 + 10000;
}
return seqid;
}
ResidueId read_res_id(const char* seq_id, const char* name) {
return {read_seq_id(seq_id), {}, read_string(name, 3)};
}
char read_altloc(char c) { return c == ' ' ? '\0' : c; }
int read_serial(const char* ptr) {
return ptr[0] < 'A' ? read_int(ptr, 5)
: read_base36<5>(ptr) - 16796160 + 100000;
}
El infer_element_from_padded_name(const char* name) {
// Old versions of the PDB format had hydrogen names such as "1HB ".
// Some MD files use similar names for other elements ("1C4A" -> C).
if (name[0] == ' ' || is_digit(name[0]))
return impl::find_single_letter_element(name[1]);
// ... or it can be "C210"
if (is_digit(name[1]))
return impl::find_single_letter_element(name[0]);
if (name[3] != ' ') {
// Atom names HXXX are ambiguous, but Hg, He, Hf, Ho and Hs (almost)
// never have 4-character names, so H is assumed.
if (alpha_up(name[0]) == 'H')
return El::H;
// Similarly Deuterium (DXXX), but here alternatives are Dy, Db and Ds.
// Only Dysprosium is present in the PDB - in a single entry as of 2022.
if (alpha_up(name[0]) == 'D')
return El::D;
// Don't try harder for now. We don't recognize names such as CG11 as C
// (which we could; there is no Cg in the periodic table), but
// a name such as CL20 can be either Cl (in WGW) or C (in WQH) ¯\_(ツ)_/¯
}
return find_element(name);
}
bool element_from_padded_name_is_ambiguous(const char* name) {
return name[0] != ' ' && name[3] != ' ' && !is_digit(name[0]) && !is_digit(name[1]);
}
// "28-MAR-07" -> "2007-03-28"
// (we also accept less standard format "28-Mar-2007" as used by BUSTER)
// We do not check if the date is correct.
// The returned value is one of:
// DDDD-DD-DD - possibly correct date,
// DDDD-xx-DD - unrecognized month,
// empty string - the digits were not there.
std::string pdb_date_format_to_iso(const std::string& date) {
const char months[] = "JAN01FEB02MAR03APR04MAY05JUN06"
"JUL07AUG08SEP09OCT10NOV11DEC122222";
if (date.size() < 9 || !is_digit(date[0]) || !is_digit(date[1]) ||
!is_digit(date[7]) || !is_digit(date[8]))
return std::string();
std::string iso = "xxxx-xx-xx";
if (date.size() >= 11 && is_digit(date[9]) && is_digit(date[10])) {
std::memcpy(&iso[0], &date[7], 4);
} else {
std::memcpy(&iso[0], (date[7] > '6' ? "19" : "20"), 2);
std::memcpy(&iso[2], &date[7], 2);
}
char month[4] = {alpha_up(date[3]), alpha_up(date[4]), alpha_up(date[5]), '\0'};
if (const char* m = std::strstr(months, month))
std::memcpy(&iso[5], m + 3, 2);
std::memcpy(&iso[8], &date[0], 2);
return iso;
}
bool is_double(const char* p) {
while (is_space(*p)) ++p;
if (*p == '-' || *p == '+') ++p;
while (is_digit(*p)) ++p;
if (*p == '.') {
++p;
while (is_digit(*++p)) ++p;
}
while (is_space(*p)) ++p;
return *p == '\0';
}
template<size_t N>
bool same_str(const std::string& s, const char (&literal)[N]) {
return s.size() == N - 1 && std::strcmp(s.c_str(), literal) == 0;
}
bool is_tls_item(const std::string& key) {
return key.size() == 3 &&
(key[0] == 'T' || key[0] == 'L' || key[0] == 'S') &&
(key[1] == '1' || key[1] == '2' || key[1] == '3') &&
(key[2] == '1' || key[2] == '2' || key[2] == '3');
}
// Usually we have one program per line:
// XDS
// XDS VERSION NOVEMBER 3, 2014
// AIMLESS 0.5.17
// but it can also be a list of programs:
// autoPROC (Version 1.3.0), AIMLESS, STARANISO
// autoPROC, XDS (VERSION Jan 26, 2018)
// We assume that:
// - the name has only one word (apologies to Queen of Spades,
// Force Field X, APEX 2 and Insight II).
// - comma not followed by a digit separates programs
// - brackets and the word VERSION are to be removed from version
// Additionally, if version has format: "something (DATE)" where
// the DATE format is either 28-MAR-07 or 28-Mar-2007, then DATE
// is put into _software.date.
void add_software(Metadata& meta, SoftwareItem::Classification type, const std::string& name) {
for (size_t start = 0, end = 0; end != std::string::npos; start = end + 1) {
end = name.find(',', start);
while (end != std::string::npos &&
name[end+1] == ' ' && is_digit(name[end+2]))
end = name.find(',', end + 1);
meta.software.emplace_back();
SoftwareItem& item = meta.software.back();
item.name = trim_str(name.substr(start, end - start));
size_t sep = item.name.find(' ');
if (sep != std::string::npos) {
size_t ver_start = item.name.find_first_not_of(" (", sep + 1);
if (ver_start == std::string::npos) {
item.name.resize(sep);
item.classification = type;
continue;
}
item.version = item.name.substr(ver_start);
item.name.resize(sep);
if (!item.version.empty() && item.version.back() == ')') {
size_t open_br = item.version.find('(');
if (open_br == std::string::npos) {
item.version.pop_back();
} else if (open_br + 11 == item.version.size() ||
open_br + 13 == item.version.size()) {
item.date = pdb_date_format_to_iso(item.version.substr(open_br + 1));
if (item.date.size() == 10 && item.date[5] != 'x') {
size_t last = item.version.find_last_not_of(' ', open_br - 1);
item.version.resize(last + 1);
} else {
item.date.clear();
}
}
}
if (istarts_with(item.version, "version "))
item.version.erase(0, 8);
}
item.classification = type;
}
}
// REMARK 3 TERM COUNT WEIGHT FUNCTION.
// REMARK 3 BOND LENGTHS : 5760 ; 2.000 ; HARMONIC
void add_restraint_count_weight(RefinementInfo& ref_info, const char* key, const char* value) {
if (*value == 'N') // NULL instead of number
return;
ref_info.restr_stats.emplace_back(key);
RefinementInfo::Restr& restr = ref_info.restr_stats.back();
const char* endptr;
restr.count = no_sign_atoi(value, &endptr);
if (const char* sep = std::strchr(endptr, ';'))
restr.weight = fast_atof(sep + 1, &endptr);
if (const char* sep = std::strchr(endptr, ';'))
restr.function = read_string(sep+1, 50);
}
void read_remark3_line(const char* line, Metadata& meta,
std::string*& possibly_unfinished_remark3) {
// Based on:
// www.wwpdb.org/documentation/file-format-content/format23/remark3.html
// and analysis of PDB files.
// In special cases, such as joint X-ray and neutron refinement 5MOO,
// PDB file can have two REMARK 3 blocks.
// Generally, after "REMARK 3" we have either a header-like sentence
// or a key:value pair with a colon, or a continuation of text from the
// previous line.
const char* key_start = skip_blank(line + 10);
const char* colon = std::strchr(key_start, ':');
const char* key_end = rtrim_cstr(key_start, colon);
std::string key(key_start, key_end);
// multi-line continuation requires special handling
if (possibly_unfinished_remark3) {
if (key_start > line + 17) {
*possibly_unfinished_remark3 += ' ';
possibly_unfinished_remark3->append(key);
return;
}
possibly_unfinished_remark3 = nullptr;
}
if (colon) {
const char* value = skip_blank(colon + 1);
const char* end = rtrim_cstr(value);
if (end - value == 4 && std::strncmp(value, "NULL", 4) == 0)
return;
if (same_str(key, "PROGRAM"))
add_software(meta, SoftwareItem::Refinement, std::string(value, end));
if (meta.refinement.empty())
return;
RefinementInfo& ref_info = meta.refinement.back();
if (same_str(key, "RESOLUTION RANGE HIGH (ANGSTROMS)")) {
ref_info.resolution_high = fast_atof(value);
} else if (same_str(key, "RESOLUTION RANGE LOW (ANGSTROMS)")) {
ref_info.resolution_low = fast_atof(value);
} else if (same_str(key, "COMPLETENESS FOR RANGE (%)")) {
ref_info.completeness = fast_atof(value);
} else if (same_str(key, "NUMBER OF REFLECTIONS")) {
ref_info.reflection_count = std::atoi(value);
} else if (same_str(key, "CROSS-VALIDATION METHOD")) {
ref_info.cross_validation_method = std::string(value, end);
} else if (same_str(key, "FREE R VALUE TEST SET SELECTION")) {
ref_info.rfree_selection_method = std::string(value, end);
} else if (same_str(key, "R VALUE (WORKING + TEST SET)")) {
ref_info.r_all = fast_atof(value);
} else if (same_str(key, "R VALUE (WORKING SET)")) {
ref_info.r_work = fast_atof(value);
} else if (same_str(key, "FREE R VALUE")) {
ref_info.r_free = fast_atof(value);
} else if (same_str(key, "FREE R VALUE TEST SET COUNT")) {
ref_info.rfree_set_count = atoi(value);
} else if (same_str(key, "TOTAL NUMBER OF BINS USED")) {
ref_info.bin_count = std::atoi(value);
} else if (same_str(key, "BIN RESOLUTION RANGE HIGH (A)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().resolution_high = fast_atof(value);
} else if (same_str(key, "BIN RESOLUTION RANGE LOW (A)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().resolution_low = fast_atof(value);
} else if (same_str(key, "BIN COMPLETENESS (WORKING+TEST) (%)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().completeness = fast_atof(value);
} else if (same_str(key, "REFLECTIONS IN BIN (WORKING+TEST)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().reflection_count = std::atoi(value);
} else if (same_str(key, "BIN R VALUE (WORKING+TEST)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().r_all = fast_atof(value);
} else if (same_str(key, "REFLECTIONS IN BIN (WORKING SET)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().work_set_count = std::atoi(value);
} else if (same_str(key, "BIN R VALUE (WORKING SET)")) {
if (!ref_info.bins.empty())
ref_info.bins.back().r_work = fast_atof(value);
} else if (same_str(key, "BIN FREE R VALUE")) {
if (!ref_info.bins.empty())
ref_info.bins.back().r_free = fast_atof(value);
} else if (same_str(key, "BIN FREE R VALUE TEST SET COUNT")) {
if (!ref_info.bins.empty())
ref_info.bins.back().rfree_set_count = std::atoi(value);
} else if (same_str(key, "FROM WILSON PLOT (A**2)")) {
// TODO
// exper.b_wilson = fast_atof(value);
} else if (same_str(key, "MEAN B VALUE (OVERALL, A**2)")) {
ref_info.mean_b = fast_atof(value);
} else if (same_str(key, "B11 (A**2)")) {
ref_info.aniso_b.u11 = fast_atof(value);
} else if (same_str(key, "B22 (A**2)")) {
ref_info.aniso_b.u22 = fast_atof(value);
} else if (same_str(key, "B33 (A**2)")) {
ref_info.aniso_b.u33 = fast_atof(value);
} else if (same_str(key, "B12 (A**2)")) {
ref_info.aniso_b.u12 = fast_atof(value);
} else if (same_str(key, "B13 (A**2)")) {
ref_info.aniso_b.u13 = fast_atof(value);
} else if (same_str(key, "B23 (A**2)")) {
ref_info.aniso_b.u23 = fast_atof(value);
} else if (same_str(key, "ESD FROM LUZZATI PLOT (A)")) {
ref_info.luzzati_error = fast_atof(value);
} else if (same_str(key, "DPI (BLOW EQ-10) BASED ON R VALUE (A)")) {
ref_info.dpi_blow_r = fast_atof(value);
} else if (same_str(key, "DPI (BLOW EQ-9) BASED ON FREE R VALUE (A)")) {
ref_info.dpi_blow_rfree = fast_atof(value);
} else if (same_str(key, "DPI (CRUICKSHANK) BASED ON R VALUE (A)")) {
ref_info.dpi_cruickshank_r = fast_atof(value);
} else if (same_str(key, "DPI (CRUICKSHANK) BASED ON FREE R VALUE (A)")) {
ref_info.dpi_cruickshank_rfree = fast_atof(value);
} else if (same_str(key, "CORRELATION COEFFICIENT FO-FC")) {
ref_info.cc_fo_fc_work = fast_atof(value);
} else if (same_str(key, "CORRELATION COEFFICIENT FO-FC FREE")) {
ref_info.cc_fo_fc_free = fast_atof(value);
} else if (same_str(key, "BOND LENGTHS")) {
add_restraint_count_weight(ref_info, "t_bond_d", value);
} else if (same_str(key, "BOND ANGLES")) {
add_restraint_count_weight(ref_info, "t_angle_deg", value);
} else if (same_str(key, "TORSION ANGLES")) {
add_restraint_count_weight(ref_info, "t_dihedral_angle_d", value);
} else if (same_str(key, "TRIGONAL CARBON PLANES")) {
add_restraint_count_weight(ref_info, "t_trig_c_planes", value);
} else if (same_str(key, "GENERAL PLANES")) {
add_restraint_count_weight(ref_info, "t_gen_planes", value);
} else if (same_str(key, "ISOTROPIC THERMAL FACTORS")) {
add_restraint_count_weight(ref_info, "t_it", value);
} else if (same_str(key, "BAD NON-BONDED CONTACTS")) {
add_restraint_count_weight(ref_info, "t_nbd", value);
} else if (same_str(key, "IMPROPER TORSIONS")) {
add_restraint_count_weight(ref_info, "t_improper_torsion", value);
} else if (same_str(key, "CHIRAL IMPROPER TORSION")) {
add_restraint_count_weight(ref_info, "t_chiral_improper_torsion", value);
} else if (same_str(key, "SUM OF OCCUPANCIES")) {
add_restraint_count_weight(ref_info, "t_sum_occupancies", value);
} else if (same_str(key, "UTILITY DISTANCES")) {
add_restraint_count_weight(ref_info, "t_utility_distance", value);
} else if (same_str(key, "UTILITY ANGLES")) {
add_restraint_count_weight(ref_info, "t_utility_angle", value);
} else if (same_str(key, "UTILITY TORSION")) {
add_restraint_count_weight(ref_info, "t_utility_torsion", value);
} else if (same_str(key, "IDEAL-DIST CONTACT TERM")) {
add_restraint_count_weight(ref_info, "t_ideal_dist_contact", value);
} else if (same_str(key, "BOND LENGTHS (A)")) {
impl::find_or_add(ref_info.restr_stats, "t_bond_d").dev_ideal
= read_double(value, 50);
} else if (same_str(key, "BOND ANGLES (DEGREES)")) {
impl::find_or_add(ref_info.restr_stats, "t_angle_deg").dev_ideal
= read_double(value, 50);
} else if (same_str(key, "PEPTIDE OMEGA TORSION ANGLES (DEGREES)")) {
impl::find_or_add(ref_info.restr_stats, "t_omega_torsion").dev_ideal
= read_double(value, 50);
} else if (same_str(key, "OTHER TORSION ANGLES (DEGREES)")) {
impl::find_or_add(ref_info.restr_stats, "t_other_torsion").dev_ideal
= read_double(value, 50);
} else if (same_str(key, "TLS GROUP")) {
ref_info.tls_groups.emplace_back();
TlsGroup& tls_group = ref_info.tls_groups.back();
tls_group.id = std::string(value, end);
tls_group.num_id = (short) no_sign_atoi(tls_group.id.c_str());
} else if (same_str(key, "SET") ||
// "REMARK 3 SELECTION:" -> TLS
// "REMARK 3 SELECTION :" -> NCS
(same_str(key, "SELECTION") && colon == line + 23)) {
if (!ref_info.tls_groups.empty()) {
TlsGroup& group = ref_info.tls_groups.back();
group.selections.emplace_back();
group.selections.back().details = std::string(value, end);
possibly_unfinished_remark3 = &group.selections.back().details;
}
} else if (same_str(key, "RESIDUE RANGE")) {
if (!ref_info.tls_groups.empty() && end > colon+21) {
TlsGroup& group = ref_info.tls_groups.back();
group.selections.emplace_back();
TlsGroup::Selection& sel = group.selections.back();
sel.chain = read_string(colon+1, 5);
if (sel.chain == read_string(colon+16, 5)) {
try {
sel.res_begin = SeqId(read_string(colon+6, 6));
sel.res_end = SeqId(read_string(colon+21, 6));
} catch (std::invalid_argument&) {
group.selections.pop_back();
}
} else { // unexpected -- TLS group should be in one chain
group.selections.pop_back();
}
}
} else if (same_str(key, "ORIGIN FOR THE GROUP (A)")) {
std::vector<std::string> xyz = split_str_multi(std::string(value, end));
if (ref_info.tls_groups.empty() || xyz.size() != 3)
return;
Position& origin = ref_info.tls_groups.back().origin;
origin.x = fast_atof(xyz[0].c_str());
origin.y = fast_atof(xyz[1].c_str());
origin.z = fast_atof(xyz[2].c_str());
} else if (is_tls_item(key)) {
if (ref_info.tls_groups.empty())
return;
TlsGroup& tls = ref_info.tls_groups.back();
std::vector<std::string> tokens = split_str_multi(key_start);
for (size_t i = 0; i + 1 < tokens.size(); i += 2) {
std::string& k = tokens[i];
if (k.size() == 4 && k[3] == ':')
k.resize(3);
if (is_tls_item(k)) {
int x = k[1] - '1';
int y = k[2] - '1';
double v = fast_atof(tokens[i+1].c_str());
if (k[0] == 'S') {
tls.S[x][y] = v;
} else {
SMat33<double>& tensor = k[0] == 'T' ? tls.T : tls.L;
tensor.unchecked_ref(x, y) = v;
}
}
}
}
} else {
if (same_str(key, "DATA USED IN REFINEMENT.")) {
meta.refinement.emplace_back();
meta.refinement.back().id = std::to_string(meta.refinement.size());
} else if (same_str(key, "FIT IN THE HIGHEST RESOLUTION BIN.")) {
if (!meta.refinement.empty())
meta.refinement.back().bins.emplace_back();
}
}
}
void read_remark_200_230_240(const char* line, Metadata& meta, std::string*& cryst_desc) {
// multi-line continuation requires special handling
if (cryst_desc) {
if (line[10] == ' ' && line[11] == ' ') {
const char* start = line + 11;
cryst_desc->append(start, rtrim_cstr(start) - start);
return;
}
cryst_desc = nullptr;
}
const char* key_start = skip_blank(line + 10);
const char* colon = std::strchr(key_start, ':');
const char* key_end = rtrim_cstr(key_start, colon);
std::string key(key_start, key_end);
if (colon) {
const char* value = skip_blank(colon + 1);
const char* end = rtrim_cstr(value);
if (end - value == 4 && std::strncmp(value, "NULL", 4) == 0)
return;
if (same_str(key, "INTENSITY-INTEGRATION SOFTWARE")) {
add_software(meta, SoftwareItem::DataReduction, std::string(value, end));
} else if (same_str(key, "DATA SCALING SOFTWARE")) {
add_software(meta, SoftwareItem::DataScaling, std::string(value, end));
} else if (same_str(key, "SOFTWARE USED")) {
add_software(meta, SoftwareItem::Phasing, std::string(value, end));
} else if (same_str(key, "METHOD USED TO DETERMINE THE STRUCTURE")) {
meta.solved_by = std::string(value, end);
} else if (same_str(key, "STARTING MODEL")) {
meta.starting_model = std::string(value, end);
} else if (!meta.experiments.empty()) {
ExperimentInfo& exper = meta.experiments.back();
DiffractionInfo& diffr = meta.crystals.back().diffractions[0];
if (same_str(key, "EXPERIMENT TYPE")) {
exper.method = std::string(value, end);
} else if (same_str(key, "NUMBER OF CRYSTALS USED")) {
exper.number_of_crystals = std::atoi(value);
} else if (same_str(key, "PH")) {
if (is_double(value))
meta.crystals.back().ph = fast_atof(value);
else
meta.crystals.back().ph_range = std::string(value, end);
} else if (same_str(key, "DATE OF DATA COLLECTION")) {
diffr.collection_date = pdb_date_format_to_iso(std::string(value, end));
} else if (same_str(key, "TEMPERATURE (KELVIN)")) {
diffr.temperature = fast_atof(value);
} else if (same_str(key, "SYNCHROTRON (Y/N)")) {
if (*value == 'Y')
diffr.source = "SYNCHROTRON";
} else if (same_str(key, "RADIATION SOURCE")) {
if (same_str(diffr.source, "SYNCHROTRON"))
diffr.synchrotron = std::string(value, end);
else
diffr.source = std::string(value, end);
} else if (same_str(key, "NEUTRON SOURCE")) {
diffr.source = std::string(value, end);
} else if (same_str(key, "BEAMLINE")) {
diffr.beamline = std::string(value, end);
if (!diffr.synchrotron.empty() && diffr.source_type.empty())
diffr.source_type = diffr.synchrotron + " BEAMLINE " + diffr.beamline;
} else if (same_str(key, "X-RAY GENERATOR MODEL")) {
diffr.source_type = std::string(value, end);
} else if (same_str(key, "MONOCHROMATIC OR LAUE (M/L)")) {
diffr.mono_or_laue = *value;
} else if (same_str(key, "WAVELENGTH OR RANGE (A)")) {
diffr.wavelengths = std::string(value, end);
} else if (same_str(key, "MONOCHROMATOR")) {
diffr.monochromator = std::string(value, end);
} else if (same_str(key, "OPTICS")) {
diffr.optics = std::string(value, end);
} else if (same_str(key, "DETECTOR TYPE")) {
diffr.detector = std::string(value, end);
} else if (same_str(key, "DETECTOR MANUFACTURER")) {
diffr.detector_make = std::string(value, end);
} else if (same_str(key, "NUMBER OF UNIQUE REFLECTIONS")) {
exper.unique_reflections = std::atoi(value);
} else if (same_str(key, "RESOLUTION RANGE HIGH (A)")) {
exper.reflections.resolution_high = fast_atof(value);
} else if (same_str(key, "RESOLUTION RANGE LOW (A)")) {
exper.reflections.resolution_low = fast_atof(value);
} else if (same_str(key, "COMPLETENESS FOR RANGE (%)")) {
exper.reflections.completeness = fast_atof(value);
} else if (same_str(key, "DATA REDUNDANCY")) {
exper.reflections.redundancy = fast_atof(value);
} else if (same_str(key, "R MERGE (I)")) {
exper.reflections.r_merge = fast_atof(value);
} else if (same_str(key, "R SYM (I)")) {
exper.reflections.r_sym = fast_atof(value);
} else if (same_str(key, "<I/SIGMA(I)> FOR THE DATA SET")) {
exper.reflections.mean_I_over_sigma = fast_atof(value);
} else if (same_str(key, "REMARK")) {
cryst_desc = &meta.crystals.back().description;
*cryst_desc = std::string(value, end);
} else if (!exper.shells.empty()) {
if (same_str(key, "HIGHEST RESOLUTION SHELL, RANGE HIGH (A)")) {
exper.shells.back().resolution_high = fast_atof(value);
} else if (same_str(key, "HIGHEST RESOLUTION SHELL, RANGE LOW (A)")) {
exper.shells.back().resolution_low = fast_atof(value);
} else if (same_str(key, "COMPLETENESS FOR SHELL (%)")) {
exper.shells.back().completeness = fast_atof(value);
} else if (same_str(key, "DATA REDUNDANCY IN SHELL")) {
exper.shells.back().redundancy = fast_atof(value);
} else if (same_str(key, "R MERGE FOR SHELL (I)")) {
exper.shells.back().r_merge = fast_atof(value);
} else if (same_str(key, "R SYM FOR SHELL (I)")) {
exper.shells.back().r_sym = fast_atof(value);
} else if (same_str(key, "<I/SIGMA(I)> FOR SHELL")) {
exper.shells.back().mean_I_over_sigma = fast_atof(value);
}
}
}
} else {
if (same_str(key, "EXPERIMENTAL DETAILS")) {
meta.crystals.emplace_back();
CrystalInfo& c = meta.crystals.back();
c.id = std::to_string(meta.crystals.size());
c.diffractions.emplace_back();
c.diffractions[0].id = c.id;
meta.experiments.emplace_back();
meta.experiments.back().diffraction_ids.push_back(c.id);
if (line[8] == '0' && line[9] == '0')
c.diffractions[0].scattering_type = "x-ray";
else if (line[8] == '3' && line[9] == '0')
c.diffractions[0].scattering_type = "neutron";
else if (line[8] == '4' && line[9] == '0')
c.diffractions[0].scattering_type = "electron";
}
if (same_str(key, "IN THE HIGHEST RESOLUTION SHELL.")) {
if (!meta.experiments.empty())
meta.experiments.back().shells.emplace_back();
}
}
}
// Atom name and altloc are not provided in the SSBOND record.
// Usually it is SG (cysteine), but other disulfide bonds are also possible.
// If it's not SG, we pick the first sulfur atom in the residue.
const Residue* complete_ssbond_atom(AtomAddress& ad, const Model& mdl) {
ad.atom_name = "SG";
const_CRA cra = mdl.find_cra(ad);
if (cra.residue && (!cra.atom || cra.atom->element != El::S))
if (const Atom* a = cra.residue->find_by_element(El::S)) {
ad.atom_name = a->name;
ad.altloc = a->altloc;
}
return cra.residue;
}
void complete_ssbond(Connection& con, const Model& mdl, const UnitCell& cell) {
const Residue* res1 = complete_ssbond_atom(con.partner1, mdl);
const Residue* res2 = complete_ssbond_atom(con.partner2, mdl);
if (res1 && res2 && (con.partner1.altloc != '\0' || con.partner2.altloc != '\0')) {
// pick a pair of atoms in the shortest distance
double min_dist_sq = INFINITY;
for (const Atom& a1 : const_cast<Residue*>(res1)->get(con.partner1.atom_name))
for (const Atom& a2 : const_cast<Residue*>(res2)->get(con.partner2.atom_name))
if (a2.same_conformer(a1)) {
double dist_sq = cell.find_nearest_image(a1.pos, a2.pos, con.asu).dist_sq;
if (dist_sq < min_dist_sq) {
con.partner1.altloc = a1.altloc;
con.partner2.altloc = a2.altloc;
min_dist_sq = dist_sq;
}
}
}
}
Asu compare_link_symops(const std::string& record, short* reported_sym) {
if (record.size() < 72)
return Asu::Any; // it could be interpreted as Same
std::string s1 = read_string(&record[59], 6);
std::string s2 = read_string(&record[66], 6);
if (s1 == s2)
return Asu::Same;
size_t len1 = s1.length();
size_t len2 = s2.length();
if (len1 >= 4 && len1 < 6 && len2 >= 4 && len2 < 6) {
// for 5 digits, we assume here that two digits are for sym_idx
if (s1[0] == '1' && len1 == 4) // symop1 is usually 1555
reported_sym[0] = (short) read_int(s2.c_str(), len2 - 3);
else
reported_sym[0] = 99;
for (size_t i = 1; i <= 3; ++i)
reported_sym[i] = s2[len2 - 4 + i] - s1[len1 - 4 + i];
}
return Asu::Different;
}
void process_conn(Structure& st, const std::vector<std::string>& conn_records) {
int disulf_count = 0;
int covale_count = 0;
int metalc_count = 0;
for (const std::string& record : conn_records) {
if (record[0] == 'S' || record[0] == 's') { // SSBOND
if (record.length() < 32)
continue;
Connection c;
c.name = "disulf" + std::to_string(++disulf_count);
c.type = Connection::Disulf;
const char* r = record.c_str();
c.partner1.chain_name = read_string(r + 14, 2);
c.partner1.res_id = read_res_id(r + 17, r + 11);
c.partner2.chain_name = read_string(r + 28, 2);
char res_id2[5] = {' ', ' ', ' ', ' ', ' '};
std::memcpy(res_id2, r + 31, std::min((size_t)5, record.length() - 31));
c.partner2.res_id = read_res_id(res_id2, r + 25);
c.asu = compare_link_symops(record, c.reported_sym);
if (record.length() > 73)
c.reported_distance = read_double(r + 73, 5);
complete_ssbond(c, st.first_model(), st.cell);
st.connections.emplace_back(c);
} else if (record[0] == 'L' || record[0] == 'l') { // LINK
if (record.length() < 57)
continue;
Connection c;
for (int i : {0, 1}) {
const char* t = record.c_str() + 30 * i;
AtomAddress& ad = (i == 0 ? c.partner1 : c.partner2);
ad.chain_name = read_string(t + 20, 2);
ad.res_id = read_res_id(t + 22, t + 17);
ad.atom_name = read_string(t + 12, 4);
ad.altloc = read_altloc(t[16]);
}
auto get_elem = [&](const char* name, const AtomAddress& ad) {
if (element_from_padded_name_is_ambiguous(name)) {
const_CRA cra = st.first_model().find_cra(ad);
if (cra.atom)
return cra.atom->element.elem;
}
return infer_element_from_padded_name(name);
};
// emulating names used in wwPDB mmCIFs (covaleN and metalcN)
if (is_metal(get_elem(&record[12], c.partner1)) ||
is_metal(get_elem(&record[42], c.partner2))) {
c.name = "metalc" + std::to_string(++metalc_count);
c.type = Connection::MetalC;
} else {
c.name = "covale" + std::to_string(++covale_count);
c.type = Connection::Covale;
}
c.asu = compare_link_symops(record, c.reported_sym);
if (record.length() > 73) {
if (record[4] == 'R')
c.link_id = read_string(&record[72], 8);
else
c.reported_distance = read_double(&record[73], 5);
}
st.connections.emplace_back(c);
} else if (record[0] == 'C' || record[0] == 'c') { // CISPEP
if (record.length() < 22)
continue;
const char* r = record.c_str();
CisPep cispep;
cispep.partner_c.chain_name = read_string(r + 14, 2);
cispep.partner_c.res_id = read_res_id(r + 17, r + 11);
cispep.partner_n.chain_name = read_string(r + 28, 2);
cispep.partner_n.res_id = read_res_id(r + 31, r + 25);
// In files with a single model in the PDB CISPEP modNum is 0,
// but _struct_mon_prot_cis.pdbx_PDB_model_num is 1.
cispep.model_num = st.models.size() == 1 ? st.models[0].num : read_int(r + 43, 3);
cispep.reported_angle = read_double(r + 53, 6);
st.cispeps.push_back(cispep);
}
}
}
// move initials after comma, as in mmCIF (A.-B.DOE -> DOE, A.-B.), see
// https://www.wwpdb.org/documentation/file-format-content/format33/sect2.html#AUTHOR
void change_author_name_format_to_mmcif(std::string& name) {
// If the AUTHOR record has comma followed by space we get leading space here
while (name[0] == ' ')
name.erase(name.begin());
size_t pos = 0;
// Initials may have multiple letters (e.g. JU. or PON.)
// but should not have space after dot.
for (size_t i = 1; i < pos+4 && i+1 < name.size(); ++i)
if (name[i] == '.' && name[i+1] != ' ')
pos = i+1;
if (pos > 0)
name = name.substr(pos) + ", " + name.substr(0, pos);
}
// interprets subset of REMARKs from raw_remarks, filling in Metadata.
void read_metadata_from_remarks(Structure& st) {
std::string* possibly_unfinished_remark3 = nullptr;
std::string* cr_desc = nullptr;
Transform matrix;
for (const std::string& remark : st.raw_remarks) {
if (remark.size() <= 11)
continue;
const char* line = remark.c_str();
int num = read_int(line + 7, 3);
switch (num) {
case 2:
if (st.resolution == 0.0 && std::strstr(line, "ANGSTROM"))
st.resolution = read_double(line + 23, 7);
break;
case 3:
read_remark3_line(line, st.meta, possibly_unfinished_remark3);
break;
case 200:
case 230:
case 240:
read_remark_200_230_240(line, st.meta, cr_desc);
break;
case 300:
if (!st.meta.remark_300_detail.empty()) {
st.meta.remark_300_detail += '\n';
st.meta.remark_300_detail += rtrim_str(remark.substr(11));
} else if (remark.compare(11, 7, "REMARK:") == 0) {
st.meta.remark_300_detail = trim_str(remark.substr(18));
}
break;
case 350: {
const char* colon = std::strchr(line+11, ':');
if (colon == line+22 && starts_with(line+11, "BIOMOLECULE")) {
st.assemblies.emplace_back(read_string(line+23, 20));
continue;
}
if (st.assemblies.empty())
continue;
Assembly& assembly = st.assemblies.back();
auto r350_key = [&](int cpos, const char* text) {
return colon == line + cpos && starts_with(line+11, text);
};
if (starts_with(line+11, " BIOMT")) {
if (read_matrix(matrix, line+13, remark.size()-13) == 3)
if (!assembly.generators.empty()) {
auto& opers = assembly.generators.back().operators;
opers.emplace_back();
opers.back().name = read_string(line+20, 3);
opers.back().transform = matrix;
matrix.set_identity();
}
} else if (r350_key(44, "AUTHOR DETERMINED")) {
assembly.author_determined = true;
assembly.oligomeric_details = read_string(line+45, 35);
} else if (r350_key(51, "SOFTWARE DETERMINED")) {
assembly.software_determined = true;
assembly.oligomeric_details = read_string(line+52, 28);
} else if (r350_key(24, "SOFTWARE USED")) {
assembly.software_name = read_string(line+25, 55);
} else if (r350_key(36, "TOTAL BURIED SURFACE AREA")) {
assembly.absa = read_double(line+37, 12);
} else if (r350_key(38, "SURFACE AREA OF THE COMPLEX")) {
assembly.ssa = read_double(line+39, 12);
} else if (r350_key(40, "CHANGE IN SOLVENT FREE ENERGY")) {
assembly.more = read_double(line+41, 12);
} else if (r350_key(40, "APPLY THE FOLLOWING TO CHAINS") ||
r350_key(40, " AND CHAINS")) {
if (line[11] == 'A') // first line - APPLY ...
assembly.generators.emplace_back();
else if (assembly.generators.empty())
continue;
split_str_into_multi(read_string(line+41, 39), ", ",
assembly.generators.back().chains);
}
}
}
// if REMARK 2 was missing, try resolution from REMARK 3
if (st.resolution == 0.0) {
for (const RefinementInfo& ref_info : st.meta.refinement)
if (!std::isnan(ref_info.resolution_high) && ref_info.resolution_high != 0.) {
st.resolution = ref_info.resolution_high;
break;
}
}
}
}
} // anonymous namespace
void populate_structure_from_pdb_stream(AnyStream& line_reader, const std::string& source,
Structure& st,
PdbReadOptions options) {
if (options.max_line_length <= 0 || options.max_line_length > 120)
options.max_line_length = 120;
st.input_format = CoorFormat::Pdb;
st.name = path_basename(source, {".gz", ".pdb"});
Transform matrix;
std::vector<std::string> conn_records;
std::unordered_map<ResidueId, int> resmap;
Model *model = nullptr;
Chain *chain = nullptr;
Residue *resi = nullptr;
char line[122] = {0};
int line_num = 0;
bool after_ter = false;
auto wrong = [&line_num](const std::string& msg) {
fail("Problem in line ", std::to_string(line_num), ": ", msg);
};
while (size_t len = line_reader.copy_line(line, options.max_line_length+1)) {
++line_num;
if (options.check_non_ascii && st.non_ascii_line == 0)
for (size_t i = 0; i < len; ++i)
if (static_cast<unsigned char>(line[i]) >= 0x80) {
st.non_ascii_line = line_num;
break;
}
if (is_record_type4(line, "ATOM") || is_record_type4(line, "HETATM")) {
if (len < 55)
wrong("The line is too short to be correct:\n" + std::string(line));
std::string chain_name = read_string(line+20, 2);
ResidueId rid = read_res_id(line+22, line+17);
if (!chain || chain_name != chain->name) {
if (!model) {
// A single model usually doesn't have the MODEL record. Also,
// MD trajectories may have frames separated by ENDMDL without MODEL.
int num = (int) st.models.size() + 1;
if (st.find_model(num))
wrong("ATOM/HETATM between models");
st.models.emplace_back(num);
model = &st.models.back();
}
const Chain* prev_part = model->find_chain(chain_name);
after_ter = prev_part &&
prev_part->residues[0].entity_type == EntityType::Polymer;
model->chains.emplace_back(chain_name);
chain = &model->chains.back();
resmap.clear();
resi = nullptr;
}
// Non-standard but widely used 4-character segment identifier.
// Left-justified, and may include a space in the middle.
// The segment may be a portion of a chain or a complete chain.
if (len > 72)
rid.segment = read_string(line+72, 4);
if (!resi || !resi->matches(rid)) {
auto it = resmap.find(rid);
// In normal PDB files it is fast enough to use
// resi = chain->find_residue(rid);
// but in pseudo-PDB files (such as MD files where millions
// of residues are in the same "chain") it is too slow.
if (it == resmap.end()) {
resmap.emplace(rid, (int) chain->residues.size());
chain->residues.emplace_back(rid);
resi = &chain->residues.back();
resi->het_flag = line[0] & ~0x20;
if (after_ter)
resi->entity_type = resi->is_water() ? EntityType::Water
: EntityType::NonPolymer;
} else {
resi = &chain->residues[it->second];
}
}
Atom atom;
atom.serial = read_serial(line+6);
atom.name = read_string(line+12, 4);
atom.altloc = read_altloc(line[16]);
atom.pos.x = read_double(line+30, 8);
atom.pos.y = read_double(line+38, 8);
atom.pos.z = read_double(line+46, 8);
if (len > 58)
atom.occ = (float) read_double(line+54, 6);
if (len > 64)
atom.b_iso = (float) read_double(line+60, 6);
if (len > 76 && (std::isalpha(line[76]) || std::isalpha(line[77])))
atom.element = Element(line + 76);
else
atom.element = infer_element_from_padded_name(line+12);
atom.charge = (len > 78 ? read_charge(line[78], line[79]) : 0);
resi->atoms.emplace_back(atom);
} else if (is_record_type4(line, "ANISOU")) {
if (!model || !chain || !resi || resi->atoms.empty())
wrong("ANISOU record not directly after ATOM/HETATM.");
// We assume that ANISOU refers to the last atom.
// Can it not be the case?
Atom &atom = resi->atoms.back();
if (atom.aniso.u11 != 0.)
wrong("Duplicated ANISOU record or not directly after ATOM/HETATM.");
atom.aniso.u11 = read_int(line+28, 7) * 1e-4f;
atom.aniso.u22 = read_int(line+35, 7) * 1e-4f;
atom.aniso.u33 = read_int(line+42, 7) * 1e-4f;
atom.aniso.u12 = read_int(line+49, 7) * 1e-4f;
atom.aniso.u13 = read_int(line+56, 7) * 1e-4f;
atom.aniso.u23 = read_int(line+63, 7) * 1e-4f;
} else if (is_record_type4(line, "REMARK")) {
if (line[len-1] == '\n')
--len;
if (line[len-1] == '\r')
--len;
st.raw_remarks.emplace_back(line, line+len);
} else if (is_record_type4(line, "CONECT")) {
int serial = read_serial(line+6);
if (len >= 11 && serial != 0) {
std::vector<int>& bonded_atoms = st.conect_map[serial];
int limit = std::min(27, (int)len - 1);
for (int offset = 11; offset <= limit; offset += 5) {
int n = read_serial(line+offset);
if (n != 0)
bonded_atoms.push_back(n);
}
}
} else if (is_record_type4(line, "SEQRES")) {
std::string chain_name = read_string(line+10, 2);
Entity& ent = impl::find_or_add(st.entities, chain_name);
ent.entity_type = EntityType::Polymer;
for (int i = 19; i < 68 && i < (int)len; i += 4) {
std::string res_name = read_string(line+i, 3);
if (!res_name.empty())
ent.full_sequence.emplace_back(res_name);
}
} else if (is_record_type4(line, "HELIX")) {
if (len < 40)
continue;
Helix helix;
helix.start.chain_name = read_string(line+18, 2);
helix.start.res_id = read_res_id(line+21, line+15);
helix.end.chain_name = read_string(line+30, 2);
helix.end.res_id = read_res_id(line+33, line+27);
helix.set_helix_class_as_int(read_int(line+38, 2));
if (len > 72)
helix.length = read_int(line+72, 5);
st.helices.emplace_back(helix);
} else if (is_record_type4(line, "SHEET")) {
if (len < 40)
continue;
std::string sheet_id = read_string(line+11, 3);
Sheet& sheet = impl::find_or_add(st.sheets, sheet_id);
sheet.strands.emplace_back();
Sheet::Strand& strand = sheet.strands.back();
strand.start.chain_name = read_string(line+20, 2);
strand.start.res_id = read_res_id(line+22, line+17);
strand.end.chain_name = read_string(line+31, 2);
strand.end.res_id = read_res_id(line+33, line+28);
strand.sense = read_int(line+38, 2);
if (len > 67) {
// the SHEET record has no altloc for atoms of hydrogen bond
strand.hbond_atom2.atom_name = read_string(line+41, 4);
strand.hbond_atom2.chain_name = read_string(line+48, 2);
strand.hbond_atom2.res_id = read_res_id(line+50, line+45);
strand.hbond_atom1.atom_name = read_string(line+56, 4);
strand.hbond_atom1.chain_name = read_string(line+63, 2);
strand.hbond_atom1.res_id = read_res_id(line+65, line+60);
}
} else if (is_record_type4(line, "SSBOND") ||
is_record_type4(line, "LINK") ||
is_record_type4(line, "CISPEP")) {
conn_records.emplace_back(line);
} else if (is_record_type3(line, "TER") && !options.ignore_ter) { // finishes polymer chains
if (!chain || st.ter_status == 'e')
continue;
st.ter_status = 'y';
if (options.split_chain_on_ter) {
chain = nullptr;
// split_chain_on_ter is used for AMBER files that can have TER records
// in various places. So in such case TER doesn't imply entity_type.
continue;
}
// If we have 2+ TER records in one chain, they are used in non-standard
// way and should be better ignored (in all the chains).
if (after_ter) {
st.ter_status = 'e'; // all entity_types will be later set to Unknown
continue;
}
for (Residue& res : chain->residues) {
res.entity_type = EntityType::Polymer;
// Sanity check: water should not be marked as a polymer.
if GEMMI_UNLIKELY(res.is_water())
st.ter_status = 'e'; // all entity_types will be later set to Unknown
}
after_ter = true;
} else if (is_record_type4(line, "MODRES")) {
ModRes modres;
modres.chain_name = read_string(line + 15, 2);
modres.res_id = read_res_id(line + 18, line + 12);
modres.parent_comp_id = read_string(line + 24, 3);
if (len >= 30)
// this field is named comment in PDB spec, but details in mmCIF
modres.details = read_string(line + 29, 41);
// Refmac's extension: 73-80 mod_id
// Check for spaces to make sure it's not an overflowed comment
if (len >= 73 && line[70] == ' ' && line[71] == ' ')
modres.mod_id = read_string(line + 72, 8);
st.mod_residues.push_back(modres);
} else if (is_record_type4(line, "HETNAM")) {
if (len > 71 && line[70] == ' ') {
std::string full_code = read_string(line + 71, 8);
if (!full_code.empty())
st.shortened_ccd_codes.emplace_back(full_code, read_string(line + 11, 3));
}
} else if (is_record_type4(line, "DBREF")) { // DBREF or DBREF1 or DBREF2
std::string chain_name = read_string(line+11, 2);
Entity& ent = impl::find_or_add(st.entities, chain_name);
ent.entity_type = EntityType::Polymer;
if (line[5] == ' ' || line[5] == '1')
ent.dbrefs.emplace_back();
else if (ent.dbrefs.empty()) // DBREF2 without DBREF1?
break;
Entity::DbRef& dbref = ent.dbrefs.back();
if (line[5] == ' ' || line[5] == '1') {
dbref.seq_begin = read_seq_id(line+14);
dbref.seq_end = read_seq_id(line+20);
dbref.db_name = read_string(line+26, 6);
if (line[5] == ' ') {
dbref.accession_code = read_string(line+33, 8);
dbref.id_code = read_string(line+42, 12);
dbref.db_begin.num = read_int(line+55, 5);
dbref.db_begin.icode = line[60];
dbref.db_end.num = read_int(line+62, 5);
dbref.db_end.icode = line[67];
} else { // line[5] == '1'
dbref.id_code = read_string(line+47, 20);
}
} else if (line[5] == '2') {
dbref.accession_code = read_string(line+18, 22);
dbref.db_begin.num = read_int(line+45, 10);
dbref.db_end.num = read_int(line+57, 10);
}
} else if (is_record_type4(line, "HEADER")) {
if (len > 50)
st.info["_struct_keywords.pdbx_keywords"] = rtrim_str(std::string(line+10, 40));
if (len > 59) { // date in PDB has format 28-MAR-07
std::string date = pdb_date_format_to_iso(std::string(line+50, 9));
if (!date.empty())
st.info["_pdbx_database_status.recvd_initial_deposition_date"] = date;
}
if (len > 66) {
std::string entry_id = rtrim_str(std::string(line+62, 4));
if (!entry_id.empty())
st.info["_entry.id"] = entry_id;
}
} else if (is_record_type4(line, "TITLE")) {
if (len > 10)
st.info["_struct.title"] += rtrim_str(std::string(line+10, len-10-1));
} else if (is_record_type4(line, "KEYWDS")) {
if (len > 10)
st.info["_struct_keywords.text"] += rtrim_str(std::string(line+10, len-10-1));
} else if (is_record_type4(line, "EXPDTA")) {
if (len > 10)
st.info["_exptl.method"] += trim_str(std::string(line+10, len-10-1));
} else if (is_record_type4(line, "AUTHOR") && len > 10) {
std::string last;
if (!st.meta.authors.empty()) {
last = st.meta.authors.back();
st.meta.authors.pop_back();
}
size_t prev_size = st.meta.authors.size();
const char* start = skip_blank(line+10);
const char* end = rtrim_cstr(start, line+len);
split_str_into(std::string(start, end), ',', st.meta.authors);
if (!last.empty() && st.meta.authors.size() > prev_size) {
// the spaces were trimmed, we may need a space between words
if (last.back() != '-' && last.back() != '.')
last += ' ';
st.meta.authors[prev_size].insert(0, last);
}
} else if (is_record_type4(line, "SCALEn")) {
if (read_matrix(matrix, line, len) == 3) {
st.cell.set_matrices_from_fract(matrix);
matrix.set_identity();
}
} else if (is_record_type4(line, "ORIGX")) {
st.has_origx = true;
read_matrix(st.origx, line, len);
} else if (is_record_type4(line, "CRYST1")) {
if (len > 54)
st.cell.set(read_double(line+6, 9),
read_double(line+15, 9),
read_double(line+24, 9),
read_double(line+33, 7),
read_double(line+40, 7),
read_double(line+47, 7));
if (len > 56)
st.spacegroup_hm = read_string(line+55, 11);
if (len > 67) {
std::string z = read_string(line+66, 4);
if (!z.empty())
st.info["_cell.Z_PDB"] = z;
}
} else if (is_record_type4(line, "MTRIXn")) {
if (read_matrix(matrix, line, len) == 3) {
std::string id = read_string(line+7, 3);
if (matrix.is_identity()) {
// store only ID that will be used when writing to file
st.info["_struct_ncs_oper.id"] = id;
} else {
bool given = len > 59 && line[59] == '1';
st.ncs.push_back({id, given, matrix});
matrix.set_identity();
}
}
} else if (is_record_type4(line, "MODEL")) {
if (model && chain)
wrong("MODEL without ENDMDL?");
int num = read_int(line+6, 8);
model = &st.find_or_add_model(num);
if (!model->chains.empty())
wrong("duplicate MODEL number: " + std::to_string(num));
chain = nullptr;
} else if (is_record_type4(line, "ENDMDL")) {
model = nullptr;
chain = nullptr;
} else if (is_record_type3(line, "END")) {
break;
} else if (is_record_type4(line, "data")) {
if (line[4] == '_' && !model)
fail("Incorrect file format (perhaps it is cif not pdb?): " + source);
} else if (is_record_type4(line, "{\"da")) {
if (ialpha3_id(line+4) == ialpha3_id("ta_") && !model)
fail("Incorrect file format (perhaps it is mmJSON not pdb?): " + source);
}
}
// If we read a PDB header (they can be downloaded from RSCB) we have no
// models. User's code may not expect this. Usually, empty model will be
// handled more gracefully than no models.
if (st.models.empty())
st.models.emplace_back(1);
if (st.ter_status == 'e')
remove_entity_types(st);
// Here we assign Residue::subchain, but only for chains with all
// Residue::entity_type assigned, i.e. for chains with TER.
assign_subchains(st, /*force=*/false, /*fail_if_unknown=*/false);
for (Chain& ch : st.models[0].chains)
if (Entity* entity = st.get_entity(ch.name))
if (auto polymer = ch.get_polymer())
entity->subchains.emplace_back(polymer.subchain_id());
st.setup_cell_images();
process_conn(st, conn_records);
for (std::string& name : st.meta.authors)
change_author_name_format_to_mmcif(name);
if (!options.skip_remarks)
read_metadata_from_remarks(st);
restore_full_ccd_codes(st);
}
std::vector<Op> read_remark_290(const std::vector<std::string>& raw_remarks) {
std::vector<Op> ops;
// we only check triplet notation:
// REMARK 290 NNNMMM OPERATOR
// REMARK 290 1555 X,Y,Z
for (const std::string& remark : raw_remarks)
if (remark.size() > 25 && std::memcmp(&remark[7], "290", 3) == 0 &&
std::memcmp(&remark[10], " ", 5) == 0 &&
std::memcmp(&remark[18], "555 ", 6) == 0) {
if (read_int(remark.c_str() + 15, 3) != (int)ops.size() + 1)
fail("Symmetry operators not in order?: " + remark);
Op op = parse_triplet(read_string(remark.c_str() + 24, 56));
ops.push_back(op);
}
return ops;
}
} // namespace gemmi