Skip to content

Recommend repairing the structure with PDB2PQR before PROPKA and Prodes - #33

Merged
teese merged 8 commits into
mainfrom
structure-preparation-with-pdb2pqr
Sep 12, 2026
Merged

teese merged 8 commits into
mainfrom
structure-preparation-with-pdb2pqr

Conversation

@teese

@teese teese commented Sep 11, 2026 •

Copy link
Copy Markdown

Problem

Prodes describes the structure it is given and does not correct one. When a residue's side chain was not modelled in the deposited file, which is ordinary for exposed charged residues at moderate resolution, that residue carries no charge, and nothing in the output records that it happened. Every charge-derived feature is then wrong with no signal to the user, and the README told users nothing about it.

No ticket. The work follows from a comparison of Prodes against PDB2PQR and APBS.

What changed

  • New Preparing your structure section in README.rst, plus docs/structure_preparation.md. Quick start and the worked loop in Using the features in a model repair first, then run PROPKA on the repaired file.
  • PDB2PQR goes in its own conda environment, not environment.yml, so the two projects' pins cannot constrain each other.
  • Two advisory notes: multi-chain pKa warnings, and feature selection inside cross-validation.

Results

Why the advice is needed. PDB entry 4HKZ is a Fab with two lysines modelled only as far as CB. The columns below differ only in whether those two side chains are present:

side chains unmodelled side chains rebuilt
Formal charge at pH 7.4 -2 0
Isoelectric point 6.46 7.61
Mean surface electrostatic potential -0.013 V +0.011 V
Surface points with positive potential (NSurfPosEpFormal) 12,976 13,684

31 of the 54 default features move, and a pI of 6.46 and a pI of 7.61 point at different resins.

Whether the advice works. PDB2PQR was installed with the exact command this README gives, then run over 855 structures: 826 monomers predicted by Boltz2 and 29 crystal structures.

structure set structures repaired failed
Boltz2 predicted monomers 826 826 0
crystal structures 29 28 1
total 855 854 1

173 seconds of wall clock on six worker processes; a median of 1.02 seconds per predicted structure. The one failure is PDB2PQR refusing a structure it should refuse, described below.

No test was added or changed; the full existing suite of 403 tests passes in an environment built fresh from the updated environment.yml. All pre-commit hooks pass, and every command added to the README was run end to end.

Not verified

  • No code changed, so no feature value moves because of this pull request.
  • The 4HKZ effect size depends on resolution and on how many charged residues are disordered; many deposited structures are complete.
  • PDB2PQR refuses some structures. The README documents that with crambin, where the refusal is a PDB2PQR defect; 5CHA above is the better example, where the refusal is correct. The README has not been updated to use it.
  • --ff=PARSE is documented without comparing the six force fields PDB2PQR ships. Only geometry reaches Prodes, so it should not matter, but that was reasoned, not measured.
  • The before-and-after feature comparison covers 40 of the 826 predicted structures and no crystal structures. Multimers and AlphaFold database structures were not run at all.
  • The feature selection note is advice about statistics rather than about this package, and is arguably outside a README's scope. It is a separate commit so it can be dropped on its own.
  • Property_point.set_lipo scores the C terminal OXT at the maximum of the hydrophobicity scale, which is inverted for a carboxylate oxygen. The constant is documented here but deliberately not changed, because changing it moves feature values and belongs in its own release.
Detail

Running PROPKA after the repair, not before

A residue whose side chain is missing has no pKa to predict, so PROPKA run on the deposited file silently omits those groups. On 4HKZ, PROPKA finds 145 titratable groups in the deposited file and 149 in the repaired one. The four extra are the two rebuilt lysine side chains and the two C-termini that only exist once OXT has been added. Among the 145 groups both runs share, six shift by more than 0.5 pKa units.

PDB2PQR does not replace PROPKA

PDB2PQR can run PROPKA internally with --titration-state-method=propka, but it writes no .pka file for prodes.io.pka_converter to read, and it fixes a single pH where Prodes deliberately separates prediction from calculation so that one prediction serves many pH values. The propka3 step stays. What changes is the file it is pointed at.

Why the repaired file keeps its original name

Prodes takes the ID column of the output from the file name it was given. A repaired file called 1GDW_prep.pdb labels that row 1GDW_prep, which is tedious to undo across a dataset, so the documented commands write the repaired file into a prepared/ subdirectory under its original name. PROPKA writes its .pka file into the working directory rather than beside its input, which the documented commands also account for.

Traps documented

--ffout renames residues into a force field's own scheme, putting ASH, GLH, LYN and HID into the file, and Prodes raises KeyError on any of those. PDB2PQR 3.6.1 accepts a .cif but produced an empty .pqr from a valid one in testing. PDB2PQR applies terminal patches at chain breaks, which Prodes discards because it re-titrates from scratch.

Why a separate environment

Prodes never imports PDB2PQR; it only reads the repaired PDB file PDB2PQR writes, so the two never have to be importable at the same time. Installing them together buys nothing and costs a shared solve: two independently maintained projects on separate release cycles, and the first time one moves a shared dependency the other has not caught up with, the environment stops solving and takes a working Prodes install with it. pip install pdb2pqr into one environment is documented as the alternative, with the note that the PyPI package pins docutils<0.18 while the conda-forge build does not.

References

PDB2PQR asks for two citations and prints both on every run. Both were checked against CrossRef and PubMed. The Protein Science article is volume 27, issue 1, pages 112-128, January 2018 issue, published online October 2017; the DOI is 10.1002/pro.3280. The PDB2PQR paper itself is Nucleic Acids Research 35(Web Server), W522-W525 (2007), DOI 10.1093/nar/gkm276.

The one structure PDB2PQR refused

PDB entry 5CHA, alpha-chymotrypsin. Residue VAL A 9 is modelled with its backbone nitrogen and no other atom: no CA, C, O, CB, CG1, CG2 or OXT, and the same for VAL E 9. Alpha-chymotrypsin is produced by cleaving chymotrypsinogen into three chains, and these are the ragged ends at a cleavage site. A residue with one atom cannot be rebuilt from its neighbours, so PDB2PQR stops with Too few atoms present to reconstruct or cap residue. Prodes reads the same file without a word and describes those residues as a lone nitrogen each.

What the repair does to a predicted structure

Every one of the 826 Boltz2 models needed exactly one atom, and in every case it was OXT, the second oxygen of the C terminal carboxylate, which Boltz2 does not write. No gaps, no alternate conformations, no non-standard residues. Crystal structures are the opposite: 10 of the 29 needed atoms rebuilt, up to 30 atoms across 8 residues, and 29 distinct non-standard residues appear across the two crystal sets.

Structure sources disagree about whether to write OXT at all: every AlphaFold database model sampled has one, no Boltz2 monomer has one, and 21 of 29 crystal structures have one. Prodes gives every OXT a fixed hydrophobicity of 1.0, the maximum on the default mj_scaled scale. Adding the atom therefore moves a median of 9 of the 54 features on a Boltz2 model, and two hydrophobicity features move in one direction.

That artefact is not created by this pull request; it already applies to every AlphaFold structure and most crystal structures. What repairing every structure does is make it uniform, so it becomes a constant rather than a difference between structure sources. docs/structure_preparation.md records this in full.

Checks

README.rst parses under docutils with no warnings other than a missing Pygments package. The four Quick start commands were run on 1GDW, and the Python loop under Using the features in a model was run over two structures, in an environment created from the updated environment.yml.

…and Prodes

Prodes describes the structure it is given and does not correct one. A residue
whose side chain was not modelled carries no charge, and nothing in the output
records that this happened, so every charge-derived feature is wrong without
any signal to the user.

PDB2PQR repairs the structure: it rebuilds missing heavy atoms, adds hydrogens,
optimises the hydrogen bond network, resolves the resulting steric clashes, adds
a missing OXT, and reports backbone chain breaks and gaps it cannot repair. It
is BSD-3-Clause, pure Python, and available from conda-forge.

Measured on PDB entry 4HKZ, a Fab with two lysines modelled only as far as CB:
rebuilding those two side chains alone moves 31 of the 54 default features, takes
the formal charge at pH 7.4 from -2 to 0, moves the isoelectric point from 6.46
to 7.61, and flips the sign of the mean surface electrostatic potential. For
choosing between an anion and a cation exchanger at a given buffer pH, those are
different recommendations.

Changes:

- A "Preparing your structure" section in the README covering why the step is
  needed, what it fixes, what Prodes keeps doing itself, and the traps.
- The quick start and the worked QSPR loop now repair the structure first and
  run every later step, PROPKA included, on the repaired file. Running PROPKA
  before the repair loses the titratable groups that the repair restores: on
  4HKZ, 145 groups against 149.
- docs/structure_preparation.md holds the detail, including what Prodes discards
  from the repaired file and why.
- pdb2pqr added to environment.yml, pinned, alongside propka and for the same
  reason.

The repaired file is written into a subdirectory under its original name rather
than being renamed, because the ID column of the output is taken from the file
name. PROPKA writes its .pka file into the working directory rather than beside
its input, which the documented commands account for.

PDB2PQR can run PROPKA itself, but that does not replace the propka3 step: it
writes no .pka file for the converter to read, and it fixes a single pH where
Prodes deliberately separates prediction from calculation.
…outside cross-validation

Two notes in the README for users building a dataset, both about failure modes
that are quiet rather than loud.

The pKa step prints hundreds of warnings on any structure with more than one
chain, because pKa values are keyed by residue number with no chain, so every
chain's predictions are offered to every chain's residue with that number and
most are rejected. A user meeting this for the first time on an antibody cannot
tell whether their file is wrong. The note says the warnings are expected, says
what the underlying defect can actually do to a value, and points at the issue
that tracks it.

The second note covers feature selection. Screening the 54 columns against a
measurement and then cross-validating a model built from the survivors reports a
score that is not out of sample, because every held-out protein helped choose the
features. At the sample sizes typical of this field, tens of proteins rather than
thousands, that produces impressive numbers from data with no signal in it. The
note gives the correction, which is to refit the selection on each training fold.

Neither note changes any behaviour.
Prodes never imports PDB2PQR. It only reads the repaired PDB file that PDB2PQR
writes, so the two programs never have to be importable at the same time and
installing them together buys nothing. What it costs is that each one's pins
constrain the other's for as long as both are installed. They are independently
maintained projects on separate release cycles, and the first time one moves a
shared dependency the other has not caught up with, a single environment stops
solving and takes a working Prodes install down with it.

pdb2pqr is therefore removed from environment.yml, and the documented workflow
creates a separate environment for it:

    conda create -n pdb2pqr -c conda-forge pdb2pqr

The quick start now activates that environment for the repair step and the Prodes
environment for everything after it. The worked loop under "Using the features in
a model" calls conda run -n pdb2pqr instead, so a script runs start to finish
without switching environments by hand.

Installing into one environment with pip is documented as the alternative for
anyone who prefers it, with the note that the PyPI package pins docutils below
0.18 while the conda-forge build does not.

Also adds the second reference that PDB2PQR asks for and prints on every run,
the 2007 Nucleic Acids Research paper describing PDB2PQR itself, alongside the
2018 Protein Science paper on the wider software suite. Both were checked against
CrossRef and PubMed: the Protein Science article is volume 27, issue 1, pages
112-128, in the January 2018 issue, published online in October 2017.
.envrc holds the per-repo GitHub token routing and is machine-local, so it was
already untracked in practice but not ignored.

*_notrack.py matches the existing *_notrack.md convention: a script written to
answer one question, superseded as soon as the answer is recorded, and not
something a reader in five years should have to work out the purpose of.
…l oxygen is scored

Running PDB2PQR over 855 structures, 826 Boltz2 predicted monomers and 29
crystal structures, repaired 854 and refused 1, at a median of about one second
per predicted structure. The refusal was correct: 5CHA models a residue with its
backbone nitrogen and no other atom, which cannot be rebuilt from its neighbours.
Prodes reads that same file without a message.

Predicted structures turn out to need exactly one atom each, the C terminal OXT,
which Boltz2 does not write. That is worth documenting for two reasons.

Structure sources disagree about whether to write OXT at all. Over the structures
measured, every AlphaFold database model has one, no Boltz2 monomer has one, and
21 of 29 crystal structures have one. So the same protein arrives with a
different atom count depending on where its structure came from.

Prodes assigns hydrophobicity per residue and gives every heavy atom the value of
its residue, with one exception: OXT is given a fixed 1.0, which on the default
mj_scaled scale is the maximum, the same as phenylalanine. A carboxylate oxygen
is strongly hydrophilic, so the value is inverted. The constant has been present
since the first commit and this fork preserves it along with the rest of the
original algorithm.

Measured on 40 Boltz2 models, adding the OXT moves a median of 9 of the 54
default features. Most of that is the ordinary effect of one more atom on the
surface. Two hydrophobicity features move in one direction: NSurfPosMhp rises on
33 of the 39 structures where it moves, and SurfMhpMean rises on 12 of 13.

The size of the shift is small. The problem is that it currently applies to every
AlphaFold structure, no Boltz2 structure and most crystal structures, so a model
trained across sources partly learns which predictor produced each file.
Repairing every structure removes that, because every structure then carries
exactly one OXT per chain and the artefact becomes a constant rather than a
per-source difference.

No code changes. The constant itself is left alone here.
The paragraph explaining where the fixed hydrophobicity value came from was
longer than the fact it conveyed. One sentence naming the upstream repository
says the same thing.
… it affects

Prodes needs chain identifiers. It groups residues by chain, decides which
cysteines are bonded into a disulfide from the chain and residue number together,
and applies per-residue pKa values by residue number.

PDB2PQR writes two files and they behave differently. The file written by
--pdb-output, which is the one Prodes reads, always carries the chain identifier.
The .pqr file, which is PDB2PQR's normal output, leaves the chain column blank
unless --keep-chain is given. The cause is in io.print_biomolecule_atoms: the PDB
branch calls atom.get_pdb_string(), which takes no chain flag, while the PQR
branch calls atom.get_pqr_string(chainflag=chainflag).

So the flag is not required for this pipeline, and that was verified rather than
assumed. On structures of two, four, six and eight chains the --pdb-output file
carries every chain identifier, and running with and without --keep-chain
produces byte-for-byte identical PDB output. On the six chain structure 1F6R,
Prodes reads the same six chains, the same 727 residues and the same 24 disulfide
bonds from the repaired file as from the original.

The flag is added to the documented commands anyway. The .pqr file is what APBS
and the tools built on it consume, so a reader who uses it needs the chain
column, and nobody should have to remember which of two output files preserves
what.

A new section in the README and in docs/structure_preparation.md records the
difference between the two outputs, so this does not have to be rediscovered.
This branch was written against 7.1. Version 9.0 keys a predicted pKa by chain
as well as residue number, which makes one note added here wrong and another
understated.

Removes the note warning that the pKa step prints hundreds of warnings on a
multi-chain structure because predictions are keyed by residue number alone.
That is no longer true, and the note sat a few lines above the section that now
describes the fixed behaviour.

The section on chain identifiers said Prodes applies pKa values by residue
number. It looks them up by chain and residue number from 9.0, which is a
stronger reason to keep chain identifiers through a structure repair, not a
weaker one: a file whose chains were flattened would silently fall back to the
behaviour 9.0 exists to remove.

No change to the advice itself. The survey behind it still holds, because the
line it measured in src/prodes/core/point.py is unchanged in 9.0.
@teese
teese force-pushed the structure-preparation-with-pdb2pqr branch from b6ac264 to 4fe8fed Compare September 12, 2026 19:57
@teese
teese merged commit 579c9eb into main Sep 12, 2026
3 checks passed
@teese
teese deleted the structure-preparation-with-pdb2pqr branch September 13, 2026 19:43
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