Skip to content

to_pdb: write REMARK 3 / 200 / 230 / 240 from Metadata#2

Open
CV-GPhL wants to merge 1 commit into
masterfrom
to-pdb-write-remark-3-200
Open

to_pdb: write REMARK 3 / 200 / 230 / 240 from Metadata#2
CV-GPhL wants to merge 1 commit into
masterfrom
to-pdb-write-remark-3-200

Conversation

@CV-GPhL

@CV-GPhL CV-GPhL commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Problem

gemmi's PDB reader (src/pdb.cpp: read_remark3_line, read_remark_200_230_240)
parses REMARK 3 (refinement) and REMARK 200 / 230 / 240 (data collection)
into Structure::meta, and to_mmcif writes the corresponding
_refine / _reflns / _diffrn* / _exptl_crystal / _software categories.
But src/to_pdb.cpp's write_remarks() emits only REMARK 2 and REMARK 350, so
any * → PDB conversion silently drops all refinement and data-collection
provenance gemmi is already holding. This is a read-but-don't-write asymmetry,
not a format limitation — the PDB format expresses these records fine.

Change

Adds two functions to the anonymous namespace in src/to_pdb.cpp, called from
write_remarks() between the REMARK 2 and REMARK 350 blocks:

  • write_remark_3(const Metadata&, std::ostream&) — resolution range,
    completeness, reflection count, R / Rfree / Rall, free-set count,
    cross-validation method, mean B, refinement program.
  • write_remark_200_230_240(const Metadata&, std::ostream&) — experiment type,
    temperature, source / synchrotron / beamline / generator, monochromator,
    wavelength, detector, data-reduction/scaling/phasing software, method used to
    determine the structure, starting model, and reflection statistics.

write_remarks() runs only when st.raw_remarks is empty — i.e. the
mmCIF-sourced case where the metadata would otherwise be lost; PDB-sourced
structures still re-emit their verbatim raw remarks unchanged. Both functions
no-op when the relevant metadata is absent, so output is unchanged for models
without it. REMARK 200 / 230 / 240 is used for x-ray / neutron / electron
respectively (the parser keys scattering type on the record number).

They are inverses of read_remark3_line() / read_remark_200_230_240() in
pdb.cpp, and every emitted key string matches the parser's expected key
exactly (same internal spacing), so the output round-trips through gemmi's own
reader.

Verification

mmCIF → Structure → make_pdb_string → read_pdb recovers meta.refinement,
meta.experiments and meta.crystals with correct values — e.g. 3nkj:
res 1.6, Rwork 0.1817, Rfree 0.2186, 7740 unique reflns, ROTATING ANODE, 93 K.
Builds against current master (0.7.6-dev).

Scope

This is the core of the reader's coverage. Detail fields that
read_remark3_line also parses — geometry restraints, per-bin/shell statistics,
anisotropic B, DPI/Luzzati, correlation coefficients and TLS groups — are
intentionally left for a follow-up to keep the diff reviewable.

🤖 Generated with Claude Code

gemmi's PDB reader parses REMARK 3 (refinement) and REMARK 200/230/240 (data
collection) into Structure::meta, and to_mmcif writes the corresponding
_refine / _reflns / _diffrn* / _exptl_crystal / _software categories. But
to_pdb's write_remarks() only emitted REMARK 2 and REMARK 350, so any * -> PDB
conversion silently dropped all refinement and data-collection provenance that
gemmi was already holding. This is a read-but-don't-write asymmetry, not a
format limitation.

Add write_remark_3() and write_remark_200_230_240() (inverses of
read_remark3_line / read_remark_200_230_240 in pdb.cpp), called from
write_remarks() between the REMARK 2 and REMARK 350 blocks. write_remarks()
runs only when st.raw_remarks is empty -- the mmCIF-sourced case where the
metadata would otherwise be lost; PDB-sourced structures still re-emit their
verbatim raw remarks unchanged. The functions no-op when the relevant metadata
is absent, so output is unchanged for models without it.

REMARK 200/230/240 is chosen for x-ray/neutron/electron respectively (the
parser keys scattering type on the record number). Every emitted key string
matches the parser's expected key exactly (same internal spacing), so the
output round-trips through gemmi's own reader -- verified on 3nkj: res 1.6,
Rwork 0.1817, Rfree 0.2186, 7740 unique reflns, ROTATING ANODE, 93 K.

This is the core of the reader's coverage. Detail fields that read_remark3_line
also parses -- geometry restraints, per-shell statistics, anisotropic B,
DPI/Luzzati, correlation coefficients and TLS groups -- are intentionally left
for a follow-up to keep the diff reviewable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces 'write_remark_3' and 'write_remark_200_230_240' in 'src/to_pdb.cpp' to emit refinement and data-collection REMARKs from metadata when writing PDB files. The feedback highlights two key issues: first, 'write_remark_3' only writes the first refinement block, which drops details for joint refinement structures; second, the index-based loop in 'write_remark_200_230_240' assumes alignment between experiments and crystals, which can lead to mismatched data or out-of-bounds access. Refactoring both functions to loop over all refinement blocks and safely map experiments to crystals using IDs is recommended.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/to_pdb.cpp
Comment on lines +203 to +245
void write_remark_3(const Metadata& meta, std::ostream& os) {
if (meta.refinement.empty())
return;
char buf[88];
const RefinementInfo& ref = meta.refinement[0];
WRITELN("REMARK 3");
WRITELN("REMARK 3 REFINEMENT.");
for (const SoftwareItem& s : meta.software)
if (s.classification == SoftwareItem::Refinement) {
std::string v = s.name;
if (!s.version.empty()) { v += ' '; v += s.version; }
WRITELN("REMARK 3 PROGRAM : %s", v.c_str());
}
WRITELN("REMARK 3");
WRITELN("REMARK 3 DATA USED IN REFINEMENT.");
if (!std::isnan(ref.resolution_high))
WRITELN("REMARK 3 RESOLUTION RANGE HIGH (ANGSTROMS) : %.2f", ref.resolution_high);
if (!std::isnan(ref.resolution_low))
WRITELN("REMARK 3 RESOLUTION RANGE LOW (ANGSTROMS) : %.2f", ref.resolution_low);
if (!std::isnan(ref.completeness))
WRITELN("REMARK 3 COMPLETENESS FOR RANGE (%%) : %.1f", ref.completeness);
if (ref.reflection_count != -1)
WRITELN("REMARK 3 NUMBER OF REFLECTIONS : %d", ref.reflection_count);
WRITELN("REMARK 3");
WRITELN("REMARK 3 FIT TO DATA USED IN REFINEMENT.");
if (!ref.cross_validation_method.empty())
WRITELN("REMARK 3 CROSS-VALIDATION METHOD : %s", ref.cross_validation_method.c_str());
if (!ref.rfree_selection_method.empty())
WRITELN("REMARK 3 FREE R VALUE TEST SET SELECTION : %s", ref.rfree_selection_method.c_str());
if (!std::isnan(ref.r_all))
WRITELN("REMARK 3 R VALUE (WORKING + TEST SET) : %.4f", ref.r_all);
if (!std::isnan(ref.r_work))
WRITELN("REMARK 3 R VALUE (WORKING SET) : %.4f", ref.r_work);
if (!std::isnan(ref.r_free))
WRITELN("REMARK 3 FREE R VALUE : %.4f", ref.r_free);
if (ref.rfree_set_count != -1)
WRITELN("REMARK 3 FREE R VALUE TEST SET COUNT : %d", ref.rfree_set_count);
if (!std::isnan(ref.mean_b)) {
WRITELN("REMARK 3");
WRITELN("REMARK 3 B VALUES.");
WRITELN("REMARK 3 MEAN B VALUE (OVERALL, A**2) : %.2f", ref.mean_b);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In joint refinement structures (such as X-ray + neutron, e.g., 5moo), there can be multiple refinement blocks in meta.refinement. Currently, write_remark_3 only writes the first refinement block (meta.refinement[0]), which silently drops the refinement details of any subsequent blocks. Looping over meta.refinement for the data-collection and fit sections resolves this issue while keeping the program header at the top.

void write_remark_3(const Metadata& meta, std::ostream& os) {
  if (meta.refinement.empty())
    return;
  char buf[88];
  WRITELN("REMARK   3");
  WRITELN("REMARK   3  REFINEMENT.");
  for (const SoftwareItem& s : meta.software)
    if (s.classification == SoftwareItem::Refinement) {
      std::string v = s.name;
      if (!s.version.empty()) { v += ' '; v += s.version; }
      WRITELN("REMARK   3   PROGRAM     : %s", v.c_str());
    }
  for (const RefinementInfo& ref : meta.refinement) {
    WRITELN("REMARK   3");
    WRITELN("REMARK   3  DATA USED IN REFINEMENT.");
    if (!std::isnan(ref.resolution_high))
      WRITELN("REMARK   3   RESOLUTION RANGE HIGH (ANGSTROMS) : %.2f", ref.resolution_high);
    if (!std::isnan(ref.resolution_low))
      WRITELN("REMARK   3   RESOLUTION RANGE LOW  (ANGSTROMS) : %.2f", ref.resolution_low);
    if (!std::isnan(ref.completeness))
      WRITELN("REMARK   3   COMPLETENESS FOR RANGE        (%%) : %.1f", ref.completeness);
    if (ref.reflection_count != -1)
      WRITELN("REMARK   3   NUMBER OF REFLECTIONS : %d", ref.reflection_count);
    WRITELN("REMARK   3");
    WRITELN("REMARK   3  FIT TO DATA USED IN REFINEMENT.");
    if (!ref.cross_validation_method.empty())
      WRITELN("REMARK   3   CROSS-VALIDATION METHOD : %s", ref.cross_validation_method.c_str());
    if (!ref.rfree_selection_method.empty())
      WRITELN("REMARK   3   FREE R VALUE TEST SET SELECTION : %s", ref.rfree_selection_method.c_str());
    if (!std::isnan(ref.r_all))
      WRITELN("REMARK   3   R VALUE     (WORKING + TEST SET) : %.4f", ref.r_all);
    if (!std::isnan(ref.r_work))
      WRITELN("REMARK   3   R VALUE            (WORKING SET) : %.4f", ref.r_work);
    if (!std::isnan(ref.r_free))
      WRITELN("REMARK   3   FREE R VALUE : %.4f", ref.r_free);
    if (ref.rfree_set_count != -1)
      WRITELN("REMARK   3   FREE R VALUE TEST SET COUNT : %d", ref.rfree_set_count);
    if (!std::isnan(ref.mean_b)) {
      WRITELN("REMARK   3");
      WRITELN("REMARK   3  B VALUES.");
      WRITELN("REMARK   3   MEAN B VALUE      (OVERALL, A**2) : %.2f", ref.mean_b);
    }
  }
}

Comment thread src/to_pdb.cpp
Comment on lines +250 to +257
void write_remark_200_230_240(const Metadata& meta, std::ostream& os) {
char buf[88];
for (size_t i = 0; i < meta.experiments.size() && i < meta.crystals.size(); ++i) {
const ExperimentInfo& e = meta.experiments[i];
const CrystalInfo& c = meta.crystals[i];
if (c.diffractions.empty())
continue;
const DiffractionInfo& d = c.diffractions[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In structures sourced from mmCIF, meta.experiments and meta.crystals are not guaranteed to be aligned by index or have the same size. Using a simple index-based loop (meta.experiments[i] and meta.crystals[i]) can lead to mismatched data or out-of-bounds access. Instead, map the experiment to the correct crystal and diffraction info using diffraction_ids and id matching, with a safe fallback to the first available crystal if no match is found.

void write_remark_200_230_240(const Metadata& meta, std::ostream& os) {
  char buf[88];
  for (const ExperimentInfo& e : meta.experiments) {
    const CrystalInfo* matched_c = nullptr;
    const DiffractionInfo* matched_d = nullptr;
    for (const std::string& diff_id : e.diffraction_ids) {
      for (const CrystalInfo& c : meta.crystals) {
        for (const DiffractionInfo& d : c.diffractions) {
          if (d.id == diff_id) {
            matched_c = &c;
            matched_d = &d;
            break;
          }
        }
        if (matched_d) break;
      }
      if (matched_d) break;
    }
    if (!matched_d && !meta.crystals.empty()) {
      for (const CrystalInfo& c : meta.crystals) {
        if (!c.diffractions.empty()) {
          matched_c = &c;
          matched_d = &c.diffractions[0];
          break;
        }
      }
    }
    if (!matched_d)
      continue;
    const CrystalInfo& c = *matched_c;
    const DiffractionInfo& d = *matched_d;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant