// Copyright 2019 Global Phasing Ltd. #include "gemmi/pdb.hpp" #include // for isalpha #include // for atoi, strtol #include // for memcpy, strstr, strchr, strcmp #include // for min, swap #include // for invalid_argument #include #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 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 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 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 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& 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, " 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, " 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(res1)->get(con.partner1.atom_name)) for (const Atom& a2 : const_cast(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& 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 conn_records; std::unordered_map 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(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& 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 read_remark_290(const std::vector& raw_remarks) { std::vector 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