Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
723 lines
28 KiB
C++
723 lines
28 KiB
C++
// Copyright 2017-2023 Global Phasing Ltd.
|
|
|
|
#include <gemmi/to_pdb.hpp>
|
|
|
|
#include <cassert>
|
|
#include <cctype> // for isdigit
|
|
#include <cstring> // for memset, memcpy
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <sstream> // for ostringstream
|
|
|
|
#include <gemmi/fail.hpp> // for fail
|
|
#include <gemmi/sprintf.hpp>
|
|
#include <gemmi/resinfo.hpp> // for find_tabulated_residue
|
|
#include <gemmi/util.hpp>
|
|
|
|
namespace gemmi {
|
|
|
|
#define WRITE(...) do { \
|
|
snprintf_z(buf, 82, __VA_ARGS__); \
|
|
buf[80] = '\n'; \
|
|
os.write(buf, 81); \
|
|
} while(0)
|
|
|
|
#define WRITEU(...) do { \
|
|
snprintf_z(buf, 82, __VA_ARGS__); \
|
|
buf[80] = '\n'; \
|
|
for (int i_ = 0; i_ != 80; i_++) \
|
|
if (buf[i_] >= 'a' && buf[i_] <= 'z') buf[i_] -= 0x20; \
|
|
os.write(buf, 81); \
|
|
} while(0)
|
|
|
|
#define WRITELN(...) do { \
|
|
int length__ = snprintf_z(buf, 82, __VA_ARGS__); \
|
|
if (length__ < 80) \
|
|
std::memset(buf + length__, ' ', 80 - length__); \
|
|
buf[80] = '\n'; \
|
|
os.write(buf, 81); \
|
|
} while(0)
|
|
|
|
bool use_hetatm(const Residue& res) {
|
|
if (res.het_flag == 'H')
|
|
return true;
|
|
if (res.het_flag == 'A')
|
|
return false;
|
|
if (res.entity_type == EntityType::Branched ||
|
|
res.entity_type == EntityType::NonPolymer ||
|
|
res.entity_type == EntityType::Water)
|
|
return true;
|
|
return !find_tabulated_residue(res.name).is_standard();
|
|
}
|
|
|
|
namespace {
|
|
|
|
// works for non-negative values only
|
|
void base36_encode(char* buffer, int width, int value) {
|
|
const char base36[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
buffer[width] = '\0';
|
|
do {
|
|
buffer[--width] = base36[value % 36];
|
|
value /= 36;
|
|
} while (value != 0 && width != 0);
|
|
while (width != 0)
|
|
buffer[--width] = ' ';
|
|
}
|
|
|
|
// based on http://cci.lbl.gov/hybrid_36/
|
|
std::array<char,8> encode_serial_in_hybrid36(int serial) {
|
|
std::array<char,8> str;
|
|
assert(serial >= 0);
|
|
if (serial < 100000)
|
|
to_chars_z(str.data(), str.data() + 8, serial);
|
|
else
|
|
base36_encode(str.data(), 5, serial + (10 * 36 * 36 * 36 * 36 - 100000));
|
|
return str;
|
|
}
|
|
|
|
std::array<char,8> write_seq_id(const SeqId& seqid) {
|
|
std::array<char,8> str;
|
|
char* ptr = str.data();
|
|
if (*seqid.num > -1000 && *seqid.num < 10000) {
|
|
ptr = to_chars_z(ptr, ptr + 5, *seqid.num);
|
|
} else if (seqid.num.has_value()) {
|
|
base36_encode(ptr, 4, *seqid.num + (10 * 36 * 36 * 36 - 10000));
|
|
ptr += 4;
|
|
}
|
|
*ptr++ = seqid.icode;
|
|
*ptr = '\0';
|
|
return str;
|
|
}
|
|
|
|
const char* find_last_break(const char *str, int max_len) {
|
|
int last_break = 0;
|
|
for (int i = 0; i < max_len; i++) {
|
|
if (str[i] == '\0')
|
|
return str + i;
|
|
if (str[i] == ' ' || str[i] == '-')
|
|
last_break = i + 1;
|
|
}
|
|
return str + (last_break != 0 ? last_break : max_len);
|
|
}
|
|
|
|
// Write record with possible continuation lines, with the format:
|
|
// 1-6 record name, 8-10 continuation, 11-lastcol string.
|
|
void write_multiline(std::ostream& os, const char* record_name,
|
|
const std::string& text, int lastcol) {
|
|
if (text.empty())
|
|
return;
|
|
char buf[88]; // a few bytes extra, just in case
|
|
const char *start = text.c_str();
|
|
const char *end = find_last_break(start, lastcol-10);
|
|
WRITEU("%-6s %-70.*s", record_name, static_cast<int>(end-start), start);
|
|
for (int n = 2; n < 1000 && *end != '\0'; ++n) {
|
|
start = end;
|
|
end = find_last_break(start, lastcol-11);
|
|
int len = int(end - start);
|
|
WRITEU("%-6s %3d %-69.*s", record_name, n, len, start);
|
|
}
|
|
}
|
|
|
|
void write_ncs_op(const NcsOp& op, std::ostream& os) {
|
|
char buf[88];
|
|
for (int i = 0; i < 3; ++i) {
|
|
WRITE("MTRIX%d %3.3s%10.6f%10.6f%10.6f %14.5f %-21c", i+1,
|
|
op.id.c_str(), op.tr.mat[i][0], op.tr.mat[i][1], op.tr.mat[i][2],
|
|
op.tr.vec.at(i), op.given ? '1' : ' ');
|
|
}
|
|
}
|
|
|
|
void write_remarks(const Structure& st, std::ostream& os) {
|
|
char buf[88];
|
|
if (st.resolution > 0) {
|
|
WRITE("%-80s", "REMARK 2");
|
|
WRITE("REMARK 2 RESOLUTION. %7.2f %-49s", st.resolution, "ANGSTROMS.");
|
|
}
|
|
if (!st.assemblies.empty()) {
|
|
const char* preface[] = {
|
|
"REMARK 350",
|
|
"REMARK 350 COORDINATES FOR A COMPLETE MULTIMER REPRESENTING THE KNOWN",
|
|
"REMARK 350 BIOLOGICALLY SIGNIFICANT OLIGOMERIZATION STATE OF THE",
|
|
"REMARK 350 MOLECULE CAN BE GENERATED BY APPLYING BIOMT TRANSFORMATIONS",
|
|
"REMARK 350 GIVEN BELOW. BOTH NON-CRYSTALLOGRAPHIC AND",
|
|
"REMARK 350 CRYSTALLOGRAPHIC OPERATIONS ARE GIVEN."
|
|
};
|
|
for (const char* line : preface)
|
|
WRITE("%-80s", line);
|
|
int counter = 0;
|
|
for (const Assembly& assem : st.assemblies) {
|
|
WRITE("%-80s", "REMARK 350");
|
|
WRITE("REMARK 350 BIOMOLECULE: %-56d", ++counter);
|
|
if (assem.author_determined)
|
|
WRITEU("REMARK 350 AUTHOR DETERMINED BIOLOGICAL UNIT: %-34s",
|
|
assem.oligomeric_details.c_str());
|
|
if (assem.software_determined) {
|
|
WRITEU("REMARK 350 SOFTWARE DETERMINED QUATERNARY STRUCTURE: %-27s",
|
|
assem.oligomeric_details.c_str());
|
|
if (!assem.software_name.empty())
|
|
WRITEU("REMARK 350 SOFTWARE USED: %-54s",
|
|
assem.software_name.c_str());
|
|
if (!std::isnan(assem.absa))
|
|
WRITELN("REMARK 350 TOTAL BURIED SURFACE AREA: %.0f ANGSTROM**2",
|
|
assem.absa);
|
|
if (!std::isnan(assem.ssa))
|
|
WRITELN("REMARK 350 SURFACE AREA OF THE COMPLEX: %.0f ANGSTROM**2",
|
|
assem.ssa);
|
|
if (!std::isnan(assem.more))
|
|
WRITELN("REMARK 350 CHANGE IN SOLVENT FREE ENERGY: %.1f KCAL/MOL",
|
|
assem.more);
|
|
}
|
|
int oper_cnt = 0;
|
|
for (const Assembly::Gen& gen : assem.generators) {
|
|
std::string chains_str;
|
|
if (!gen.chains.empty()) {
|
|
chains_str = join_str(gen.chains, ", ");
|
|
} else {
|
|
// subchains -> chains
|
|
std::vector<std::string> chains;
|
|
for (const Chain& ch : st.first_model().chains)
|
|
if (!ch.residues.empty() && !in_vector(ch.name, chains) &&
|
|
in_vector(ch.residues[0].subchain, gen.subchains))
|
|
chains.push_back(ch.name);
|
|
chains_str = join_str(chains, ", ");
|
|
}
|
|
size_t end = chains_str.length();
|
|
if (end >= 30)
|
|
end = chains_str.rfind(' ', 29);
|
|
WRITELN("REMARK 350 APPLY THE FOLLOWING TO CHAINS: %s",
|
|
chains_str.substr(0, end).c_str());
|
|
while (end < chains_str.length()) {
|
|
size_t begin = end + 1;
|
|
end = chains_str.length();
|
|
if (end - begin >= 30)
|
|
end = chains_str.rfind(' ', begin + 29);
|
|
WRITELN("REMARK 350 AND CHAINS: %s",
|
|
chains_str.substr(begin, end - begin).c_str());
|
|
}
|
|
for (const Assembly::Operator& oper : gen.operators) {
|
|
++oper_cnt;
|
|
const Transform& tr = oper.transform;
|
|
for (int i = 0; i < 3; ++i)
|
|
WRITE("REMARK 350 "
|
|
"BIOMT%d %3d%10.6f%10.6f%10.6f %14.5f ",
|
|
i+1, oper_cnt,
|
|
tr.mat[i][0], tr.mat[i][1], tr.mat[i][2], tr.vec.at(i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void write_chain_atoms(const Chain& chain, std::ostream& os,
|
|
int& serial, PdbWriteOptions opt) {
|
|
char buf[88];
|
|
buf[0] = '\0';
|
|
if (chain.name.length() > 2)
|
|
fail("long chain name: " + chain.name);
|
|
for (const Residue& res : chain.residues) {
|
|
bool as_het = use_hetatm(res);
|
|
for (const Atom& a : res.atoms) {
|
|
serial = opt.preserve_serial ? a.serial : serial + 1;
|
|
// 1- 6 6s record name
|
|
// 7-11 5d integer serial
|
|
// 12 1 -
|
|
// 13-16 4s atom name (from 13 only if 4-char or 2-char symbol)
|
|
// 17 1c altloc
|
|
// 18-20 3s residue name
|
|
// 21 1 -
|
|
// 22 1s chain
|
|
// 23-26 4d integer residue sequence number
|
|
// 27 1c insertion code
|
|
// 28-30 3 -
|
|
// 31-38 8f x (8.3)
|
|
// 39-46 8f y
|
|
// 47-54 8f z
|
|
// 55-60 6f occupancy (6.2)
|
|
// 61-66 6f temperature factor (6.2)
|
|
// 67-76 6 -
|
|
// 73-76 segment identifier, left-justified (non-standard)
|
|
// 77-78 2s element symbol, right-justified
|
|
// 79-80 2s charge
|
|
int written_bytes = snprintf_z(buf, 82,
|
|
"%-6s%5s %-4.4s%c%3.3s"
|
|
"%2s%5s %8.3f%8.3f%8.3f",
|
|
as_het ? "HETATM" : "ATOM",
|
|
encode_serial_in_hybrid36(serial).data(),
|
|
a.padded_name().c_str(),
|
|
a.altloc ? std::toupper(a.altloc) : ' ',
|
|
res.name.c_str(),
|
|
chain.name.c_str(),
|
|
write_seq_id(res.seqid).data(),
|
|
// We want to avoid negative zero and round the numbers up
|
|
// if they originally had one digit more and that digit was 5.
|
|
a.pos.x > -5e-4 && a.pos.x < 0 ? 0 : a.pos.x + 1e-10,
|
|
a.pos.y > -5e-4 && a.pos.y < 0 ? 0 : a.pos.y + 1e-10,
|
|
a.pos.z > -5e-4 && a.pos.z < 0 ? 0 : a.pos.z + 1e-10);
|
|
if GEMMI_UNLIKELY(written_bytes > 54) {
|
|
// The only items expected to overflow above are the coordinates,
|
|
// if the integer part of the number exceeds 5 characters.
|
|
// This happens when something goes wrong and the model is far from
|
|
// the origin. Such a model should be shifted; it can't be written it
|
|
// in a spec-conforming format: Real(8.3). Here we overwrite the last
|
|
// digits - trimming is better than overflowing the line.
|
|
snprintf_z(buf+38, 82-38, "%8.3f", a.pos.y);
|
|
snprintf_z(buf+46, 82-46, "%8.3f", a.pos.z);
|
|
}
|
|
snprintf_z(buf+54, 82-54,
|
|
"%6.2f%6.2f %-4.4s%2s%c%c",
|
|
// Occupancy is stored as single prec, but we know it's <= 1,
|
|
// so no precision is lost even if it had 6 digits after dot.
|
|
a.occ + 1e-6,
|
|
// B is harder to get rounded right. It is stored as float,
|
|
// and may be given with more than single precision in mmCIF
|
|
// If it was originally %.5f (5TIS) we need to add 0.5 * 10^-5.
|
|
std::min(a.b_iso + 0.5e-5, 999.99),
|
|
res.segment.c_str(),
|
|
a.element.uname(),
|
|
// Charge is written as 1+ or 2-, etc, or just empty space.
|
|
// Sometimes PDB files have explicit 0s (5M05); we ignore them.
|
|
a.charge ? a.charge > 0 ? '0'+a.charge : '0'-a.charge : ' ',
|
|
a.charge ? a.charge > 0 ? '+' : '-' : ' ');
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
if (a.aniso.nonzero()) {
|
|
// re-using part of the buffer
|
|
std::memcpy(buf, "ANISOU", 6);
|
|
const double eps = 1e-6;
|
|
snprintf_z(buf+28, 43, "%7.0f%7.0f%7.0f%7.0f%7.0f%7.0f",
|
|
a.aniso.u11*1e4 + eps, a.aniso.u22*1e4 + eps,
|
|
a.aniso.u33*1e4 + eps, a.aniso.u12*1e4 + eps,
|
|
a.aniso.u13*1e4 + eps, a.aniso.u23*1e4 + eps);
|
|
buf[28+42] = ' ';
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
}
|
|
}
|
|
if (opt.ter_records && buf[0] != '\0' &&
|
|
(opt.ter_ignores_type ? &res == &chain.residues.back()
|
|
: (res.entity_type == EntityType::Polymer &&
|
|
(&res == &chain.residues.back() ||
|
|
(&res + 1)->entity_type != EntityType::Polymer)))) {
|
|
if (opt.numbered_ter) {
|
|
// re-using part of the buffer in the middle, e.g.:
|
|
// TER 4153 LYS B 286
|
|
snprintf_z(buf, 82, "TER %5s",
|
|
encode_serial_in_hybrid36(++serial).data());
|
|
std::memset(buf+11, ' ', 6);
|
|
std::memset(buf+28, ' ', 52);
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
} else {
|
|
WRITE("%-80s", "TER");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
void write_pdb(const Structure& st, std::ostream& os, PdbWriteOptions opt) {
|
|
// check if structure can be written as pdb
|
|
for (const gemmi::Model& model : st.models)
|
|
for (const gemmi::Chain& chain : model.chains)
|
|
if (chain.name.size() > 2)
|
|
gemmi::fail("chain name too long for the PDB format: " + chain.name);
|
|
|
|
const std::string& entry_id = st.get_info("_entry.id");
|
|
const char* entry_id_4 = entry_id.size() <= 4 ? entry_id.c_str() : "";
|
|
char buf[88];
|
|
|
|
// HEADER
|
|
if (!opt.minimal_file) {
|
|
const char* months = "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC???";
|
|
const std::string& date =
|
|
st.get_info("_pdbx_database_status.recvd_initial_deposition_date");
|
|
std::string pdb_date;
|
|
if (date.size() == 10) {
|
|
unsigned month_idx = 10 * (date[5] - '0') + date[6] - '0' - 1;
|
|
std::string month(months + 3 * std::min(month_idx, 13u), 3);
|
|
pdb_date = date.substr(8, 2) + "-" + month + "-" + date.substr(2, 2);
|
|
}
|
|
// "classification" in PDB == _struct_keywords.pdbx_keywords in mmCIF
|
|
const std::string& keywords = st.get_info("_struct_keywords.pdbx_keywords");
|
|
if (!pdb_date.empty() || !keywords.empty() || !entry_id.empty())
|
|
WRITEU("HEADER %-40.40s%-9s %-18.18s",
|
|
keywords.c_str(), pdb_date.c_str(), entry_id.c_str());
|
|
}
|
|
|
|
// TITLE, KEYWDS, EXPDTA, NUMMDL, REMARK
|
|
if (!opt.minimal_file) {
|
|
write_multiline(os, "TITLE", st.get_info("_struct.title"), 80);
|
|
write_multiline(os, "KEYWDS", st.get_info("_struct_keywords.text"), 79);
|
|
std::string expdta = st.get_info("_exptl.method");
|
|
if (expdta.empty())
|
|
expdta = join_str(st.meta.experiments, "; ",
|
|
[](const ExperimentInfo& e) { return e.method; });
|
|
write_multiline(os, "EXPDTA", expdta, 79);
|
|
if (st.models.size() > 1)
|
|
WRITE("NUMMDL %-6zu %63s", st.models.size(), "");
|
|
|
|
if (!st.raw_remarks.empty()) {
|
|
for (const std::string& line : st.raw_remarks) {
|
|
os << line;
|
|
if (line.empty() || line.back() != '\n')
|
|
os << '\n';
|
|
}
|
|
} else {
|
|
write_remarks(st, os);
|
|
}
|
|
}
|
|
|
|
// DBREF[12], SEQRES
|
|
if (opt.seqres_records && !st.models.empty()) {
|
|
std::vector<const Entity*> entity_list;
|
|
entity_list.reserve(st.models[0].chains.size());
|
|
for (const Chain& ch : st.models[0].chains) {
|
|
const Entity* entity = st.get_entity_of(ch.get_polymer());
|
|
// If the input pdb file has no TER records the subchains and entities
|
|
// are not setup automatically. In such case it is possible to call
|
|
// setup_entities() to use heuristic to split polymer and ligands
|
|
// and assign entities. But if it was not called, we may still find
|
|
// the original SEQRES in the entity named after the chain name.
|
|
if (!entity && st.input_format == CoorFormat::Pdb &&
|
|
!ch.residues.empty() && ch.residues[0].subchain.empty()) {
|
|
entity = st.get_entity(ch.name);
|
|
if (entity && !entity->subchains.empty())
|
|
entity = nullptr;
|
|
}
|
|
entity_list.push_back(entity);
|
|
}
|
|
// DBREF / DBREF1 / DBREF2
|
|
for (size_t i = 0; i != entity_list.size(); ++i)
|
|
if (const Entity* entity = entity_list[i]) {
|
|
const Chain& ch = st.models[0].chains[i];
|
|
for (const Entity::DbRef& dbref : entity->dbrefs) {
|
|
bool short_record = *dbref.db_end.num < 100000 &&
|
|
dbref.accession_code.size() < 9 &&
|
|
dbref.id_code.size() < 13;
|
|
SeqId begin = dbref.seq_begin;
|
|
SeqId end = dbref.seq_end;
|
|
if (!begin.num || !end.num)
|
|
if (ConstResidueGroup polymer = ch.get_polymer()) {
|
|
begin = polymer.label_seq_id_to_auth(dbref.label_seq_begin);
|
|
end = polymer.label_seq_id_to_auth(dbref.label_seq_end);
|
|
}
|
|
snprintf_z(buf, 82, "DBREF %4s%2s %5s %5s %-6s ",
|
|
entry_id_4, ch.name.c_str(),
|
|
write_seq_id(begin).data(),
|
|
write_seq_id(end).data(),
|
|
dbref.db_name.c_str());
|
|
if (dbref.db_name == "PDB" && dbref.id_code == entry_id) {
|
|
// PDB uses self-reference for fragments that don't have real
|
|
// reference. No idea why. In such case the same begin/end is used.
|
|
} else {
|
|
begin = dbref.db_begin;
|
|
end = dbref.db_end;
|
|
}
|
|
if (short_record) {
|
|
snprintf_z(buf+33, 82-33, "%-8s %-12s %5d%c %5d%c \n",
|
|
dbref.accession_code.c_str(), dbref.id_code.c_str(),
|
|
*begin.num, begin.icode, *end.num, end.icode);
|
|
} else {
|
|
buf[5] = '1'; // -> DBREF1
|
|
snprintf_z(buf+33, 82-33, " %-33s\n",
|
|
dbref.id_code.c_str());
|
|
}
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
if (!short_record)
|
|
WRITE("DBREF2 %4s%2s %-22s %10d %10d ",
|
|
entry_id_4, ch.name.c_str(),
|
|
dbref.accession_code.c_str(), *begin.num, *end.num);
|
|
}
|
|
}
|
|
// SEQRES
|
|
for (size_t i = 0; i != entity_list.size(); ++i)
|
|
if (const Entity* entity = entity_list[i]) {
|
|
const Chain& ch = st.models[0].chains[i];
|
|
int row = 0;
|
|
int col = 0;
|
|
for (const std::string& monomers : entity->full_sequence) {
|
|
if (col == 0)
|
|
snprintf_z(buf, 82, "SEQRES%4d%2s%5zu %62s\n",
|
|
++row, ch.name.c_str(),
|
|
entity->full_sequence.size(), "");
|
|
size_t end = std::min(monomers.find(','), std::min(monomers.length(), (size_t)3));
|
|
std::memcpy(buf + 18 + 4*col + 4-end, monomers.c_str(), end);
|
|
if (++col == 13) {
|
|
os.write(buf, 81);
|
|
col = 0;
|
|
}
|
|
}
|
|
if (col != 0) {
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!opt.minimal_file) {
|
|
// MODRES
|
|
for (const ModRes& modres : st.mod_residues) {
|
|
snprintf_z(buf, 82, "MODRES %4s %3.3s%2s %5s %3s %-41.41s %-8.8s\n",
|
|
entry_id_4,
|
|
modres.res_id.name.c_str(),
|
|
modres.chain_name.c_str(),
|
|
write_seq_id(modres.res_id.seqid).data(),
|
|
modres.parent_comp_id.c_str(),
|
|
modres.details.c_str(),
|
|
modres.mod_id.c_str());
|
|
buf[80] = '\n';
|
|
for (int i_ = 29; i_ != 80; i_++)
|
|
if (buf[i_] >= 'a' && buf[i_] <= 'z') buf[i_] -= 0x20;
|
|
os.write(buf, 81);
|
|
}
|
|
|
|
// HET
|
|
if (!st.models.empty()) {
|
|
auto full_ccd_code = [&](const std::string& name) -> std::string {
|
|
for (const auto& mapping : st.shortened_ccd_codes)
|
|
if (mapping.second == name)
|
|
return "real CCD code: " + mapping.first;
|
|
return "";
|
|
};
|
|
for (const Chain& chain : st.models[0].chains)
|
|
for (const Residue& res : chain.residues)
|
|
if (use_hetatm(res) && !find_tabulated_residue(res.name).is_water()) {
|
|
WRITE("HET %3s %2s%5s %6zu %-51s\n",
|
|
res.name.c_str(),
|
|
chain.name.c_str(),
|
|
write_seq_id(res.seqid).data(),
|
|
res.atoms.size(),
|
|
res.name[0] == '~' ? full_ccd_code(res.name).c_str() : "");
|
|
}
|
|
}
|
|
}
|
|
|
|
// HETNAM - but it's used only for tilde-hetnam extension
|
|
for (const auto& mapping : st.shortened_ccd_codes)
|
|
WRITE("HETNAM %3s %55s %-9s\n", mapping.second.c_str(), "", mapping.first.c_str());
|
|
|
|
// HELIX
|
|
if (!opt.minimal_file && !st.helices.empty()) {
|
|
int counter = 0;
|
|
for (const Helix& helix : st.helices) {
|
|
if (++counter == 10000)
|
|
counter = 0;
|
|
// According to the PDB spec serial number can be from 1 to 999.
|
|
// Here, we allow for up to 9999 helices by using columns 7 and 11.
|
|
snprintf_z(buf, 82, "HELIX %4d%4d %3.3s%2s %5s %3.3s%2s %5s%2d %35d \n",
|
|
counter, counter,
|
|
helix.start.res_id.name.c_str(), helix.start.chain_name.c_str(),
|
|
write_seq_id(helix.start.res_id.seqid).data(),
|
|
helix.end.res_id.name.c_str(), helix.end.chain_name.c_str(),
|
|
write_seq_id(helix.end.res_id.seqid).data(),
|
|
(int) helix.pdb_helix_class, helix.length);
|
|
if (helix.length < 0) // make 72-76 blank if the length is not given
|
|
std::memset(buf+71, ' ', 5);
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
}
|
|
}
|
|
|
|
// SHEET
|
|
if (!opt.minimal_file && !st.sheets.empty()) {
|
|
for (const Sheet& sheet : st.sheets) {
|
|
int strand_counter = 0;
|
|
for (const Sheet::Strand& strand : sheet.strands) {
|
|
const AtomAddress& a2 = strand.hbond_atom2;
|
|
const AtomAddress& a1 = strand.hbond_atom1;
|
|
// H-bond atom names are expected to be O and N
|
|
WRITE("SHEET%5d %3.3s%2zu %3.3s%2s%5s %3.3s%2s%5s%2d %-3s%3.3s%2s%5s "
|
|
" %-3s%3.3s%2s%5s ",
|
|
++strand_counter, sheet.name.c_str(), sheet.strands.size(),
|
|
strand.start.res_id.name.c_str(), strand.start.chain_name.c_str(),
|
|
write_seq_id(strand.start.res_id.seqid).data(),
|
|
strand.end.res_id.name.c_str(), strand.end.chain_name.c_str(),
|
|
write_seq_id(strand.end.res_id.seqid).data(), strand.sense,
|
|
a2.atom_name.c_str(), a2.res_id.name.c_str(),
|
|
a2.chain_name.c_str(),
|
|
a2.res_id.seqid.num ? write_seq_id(a2.res_id.seqid).data() : "",
|
|
a1.atom_name.c_str(), a1.res_id.name.c_str(),
|
|
a1.chain_name.c_str(),
|
|
a1.res_id.seqid.num ? write_seq_id(a1.res_id.seqid).data() : "");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!st.models.empty()) {
|
|
// SSBOND (note: uses only the first model and primary conformation)
|
|
if (opt.ssbond_records) {
|
|
int counter = 0;
|
|
for (const Connection& con : st.connections)
|
|
if (con.type == Connection::Disulf) {
|
|
const_CRA cra1 = st.models[0].find_cra(con.partner1, true);
|
|
const_CRA cra2 = st.models[0].find_cra(con.partner2, true);
|
|
if (!cra1.atom || !cra2.atom)
|
|
continue;
|
|
NearestImage im = st.cell.find_nearest_image(cra1.atom->pos,
|
|
cra2.atom->pos, con.asu);
|
|
if (++counter == 10000)
|
|
counter = 0;
|
|
WRITE("SSBOND%4d %3.3s%2s %5s %3.3s%2s %5s %28s %6s %5.2f ",
|
|
counter,
|
|
cra1.residue->name.c_str(), cra1.chain->name.c_str(),
|
|
write_seq_id(cra1.residue->seqid).data(),
|
|
cra2.residue->name.c_str(), cra2.chain->name.c_str(),
|
|
write_seq_id(cra2.residue->seqid).data(),
|
|
"1555", im.symmetry_code(false).c_str(), im.dist());
|
|
}
|
|
}
|
|
|
|
// LINK (note: uses only the first model and primary conformation)
|
|
if (opt.link_records) {
|
|
for (const Connection& con : st.connections)
|
|
if (con.type == Connection::Covale || con.type == Connection::MetalC ||
|
|
con.type == Connection::Unknown) {
|
|
const_CRA cra1 = st.models[0].find_cra(con.partner1, true);
|
|
const_CRA cra2 = st.models[0].find_cra(con.partner2, true);
|
|
// In special cases (LINKR gap) atoms are not there.
|
|
if (!cra1.residue || !cra2.residue)
|
|
continue;
|
|
std::string im_pdb_symbol, im_dist_str;
|
|
bool im_same_asu = true;
|
|
if (cra1.atom && cra2.atom) {
|
|
NearestImage im = st.cell.find_nearest_image(cra1.atom->pos,
|
|
cra2.atom->pos, con.asu);
|
|
im_pdb_symbol = im.symmetry_code(false);
|
|
im_dist_str = to_str_prec<2>(im.dist());
|
|
im_same_asu = im.same_asu();
|
|
}
|
|
// Pdb spec: "sym1 and sym2 are right justified and are given as
|
|
// blank when the identity operator (and no cell translation) is
|
|
// to be applied to the atom." But all files from wwPDB have
|
|
// 1555 not blank, so here we also write 1555,
|
|
// except for LINKR (Refmac variant of LINK).
|
|
snprintf_z(buf, 82, "LINK %-4s%c%3.3s%2s%5s "
|
|
" %-4s%c%3.3s%2s%5s %6s %6s %5s \n",
|
|
cra1.atom ? cra1.atom->padded_name().c_str() : "",
|
|
cra1.atom && cra1.atom->altloc ? std::toupper(cra1.atom->altloc) : ' ',
|
|
cra1.residue->name.c_str(),
|
|
con.partner1.chain_name.c_str(),
|
|
write_seq_id(cra1.residue->seqid).data(),
|
|
cra2.atom ? cra2.atom->padded_name().c_str() : "",
|
|
cra2.atom && cra2.atom->altloc ? std::toupper(cra2.atom->altloc) : ' ',
|
|
cra2.residue->name.c_str(),
|
|
con.partner2.chain_name.c_str(),
|
|
write_seq_id(cra2.residue->seqid).data(),
|
|
"1555", im_pdb_symbol.c_str(), im_dist_str.c_str());
|
|
if (im_same_asu && !con.link_id.empty())
|
|
std::memset(buf+58, ' ', 14); // erase symmetry
|
|
if ((opt.use_linkr ||opt.use_link_id) && !con.link_id.empty()) {
|
|
if (opt.use_linkr)
|
|
buf[4] = 'R'; // LINK -> LINKR
|
|
// overwrite distance with link_id
|
|
if (opt.use_linkr || opt.use_link_id)
|
|
snprintf_z(buf+72, 82-72, "%-8s\n", con.link_id.c_str());
|
|
}
|
|
buf[80] = '\n';
|
|
os.write(buf, 81);
|
|
}
|
|
}
|
|
|
|
// CISPEP
|
|
if (opt.cispep_records) {
|
|
int counter = 0;
|
|
for (const CisPep& cispep : st.cispeps) {
|
|
WRITE("CISPEP%4d %3.3s%2s %5s %3.3s%2s %5s %9d %12.2f %20s",
|
|
++counter,
|
|
cispep.partner_c.res_id.name.c_str(),
|
|
cispep.partner_c.chain_name.c_str(),
|
|
write_seq_id(cispep.partner_c.res_id.seqid).data(),
|
|
cispep.partner_n.res_id.name.c_str(),
|
|
cispep.partner_n.chain_name.c_str(),
|
|
write_seq_id(cispep.partner_n.res_id.seqid).data(),
|
|
st.models.size() > 1 ? cispep.model_num : 0,
|
|
std::isnan(cispep.reported_angle) ? 0. : cispep.reported_angle,
|
|
"");
|
|
if (counter == 9999)
|
|
counter = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CRYST1
|
|
if (opt.cryst1_record) {
|
|
WRITE("CRYST1%9.3f%9.3f%9.3f%7.2f%7.2f%7.2f %-11s%4s ",
|
|
st.cell.a, st.cell.b, st.cell.c, st.cell.alpha, st.cell.beta, st.cell.gamma,
|
|
st.spacegroup_hm.empty() ? "P 1" : st.spacegroup_hm.c_str(),
|
|
st.get_info("_cell.Z_PDB").c_str());
|
|
}
|
|
|
|
// ORIGX1/2/3
|
|
if (!opt.minimal_file && st.has_origx && !st.origx.is_identity()) {
|
|
for (int i = 0; i < 3; ++i)
|
|
WRITE("ORIGX%d %13.6f%10.6f%10.6f %14.5f %24s", i+1,
|
|
st.origx.mat[i][0], st.origx.mat[i][1], st.origx.mat[i][2],
|
|
st.origx.vec.at(i), "");
|
|
}
|
|
|
|
// SCALE1/2/3
|
|
if (!opt.minimal_file && st.cell.explicit_matrices) {
|
|
for (int i = 0; i < 3; ++i)
|
|
// We add a small number to avoid negative 0.
|
|
WRITE("SCALE%d %13.6f%10.6f%10.6f %14.5f %24s", i+1,
|
|
st.cell.frac.mat[i][0] + 1e-15, st.cell.frac.mat[i][1] + 1e-15,
|
|
st.cell.frac.mat[i][2] + 1e-15, st.cell.frac.vec.at(i) + 1e-15, "");
|
|
}
|
|
|
|
// MTRIX1/2/3
|
|
if (!st.ncs.empty()) {
|
|
auto identity = st.info.find("_struct_ncs_oper.id");
|
|
if (identity != st.info.end() &&
|
|
!in_vector_f([&](const NcsOp& op) { return op.id == identity->second; }, st.ncs))
|
|
write_ncs_op(NcsOp{identity->second, true, {}}, os);
|
|
for (const NcsOp& op : st.ncs)
|
|
write_ncs_op(op, os);
|
|
}
|
|
|
|
// MODEL, ATOM, HETATM, TER, ENDMDL
|
|
if (opt.atom_records) {
|
|
for (const Model& model : st.models) {
|
|
int serial = 0;
|
|
if (st.models.size() > 1)
|
|
WRITE("MODEL %8d %65s", model.num, "");
|
|
for (const Chain& chain : model.chains)
|
|
write_chain_atoms(chain, os, serial, opt);
|
|
if (st.models.size() > 1)
|
|
WRITE("%-80s", "ENDMDL");
|
|
}
|
|
}
|
|
|
|
// CONECT
|
|
if (opt.conect_records) {
|
|
auto num_str = [](const std::vector<int>& nums, size_t i) {
|
|
return i < nums.size() ? encode_serial_in_hybrid36(nums[i]) : std::array<char,8>{};
|
|
};
|
|
for (const auto& num_pair : st.conect_map)
|
|
for (size_t i = 0; i < num_pair.second.size(); i += 4)
|
|
WRITE("CONECT%5s%5s%5s%5s%5s%50s",
|
|
encode_serial_in_hybrid36(num_pair.first).data(),
|
|
encode_serial_in_hybrid36(num_pair.second[i]).data(),
|
|
num_str(num_pair.second, i+1).data(),
|
|
num_str(num_pair.second, i+2).data(),
|
|
num_str(num_pair.second, i+3).data(),
|
|
"");
|
|
}
|
|
|
|
// END
|
|
if (opt.end_record)
|
|
WRITE("%-80s", "END");
|
|
}
|
|
|
|
std::string make_pdb_string(const Structure& st, PdbWriteOptions opt) {
|
|
std::ostringstream os;
|
|
write_pdb(st, os, opt);
|
|
return os.str();
|
|
}
|
|
|
|
#undef WRITE
|
|
#undef WRITEU
|
|
|
|
} // namespace gemmi
|