Skip to content

Write CONECT records for bonds between residues differing in insertion code - #946

Merged
padix-key merged 4 commits into
biotite-dev:mainfrom
haeganm:fix-conect-ins-code
Nov 7, 2026
Merged

padix-key merged 4 commits into
biotite-dev:mainfrom
haeganm:fix-conect-ins-code

Conversation

@haeganm

@haeganm haeganm commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #945

_remove_non_conect_bonds() found inter-residue bonds by comparing res_id and chain_id only. Two residues that differ just in ins_code (e.g. 52 and 52A) looked like one residue, so a bond between them got no CONECT record and was lost when writing a PDB file.

This now compares residue starts via get_residue_starts_for(), the same way _filter_bonds() does for the PDBx writer.

Added one small test with a disulfide between 52 and 52A. It fails on main and passes here. tests/structure/io/test_pdb.py, test_pdbx.py and tests/structure/test_bonds.py pass, ruff 0.9.7 and pyright are clean on the changed file.

@padix-key padix-key left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch! Thank you for the fix 👍

Comment thread tests/structure/io/test_pdb.py Outdated
np.testing.assert_array_equal(actual_bonds, expected_bonds)


def test_inter_residue_bond_with_ins_code():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be possible to generalize this test? For example we could check that PDBFile.set_structure() adds the same CONECT records as there are in the PDB file downloaded from RCSB. The test assets do contain structures with insertion codes, so this should catch it. If for some reason complete CONECT equivalence is not possible, we could at least compare the number of CONECT records.

@codspeed

codspeed Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 16.1%

2 warnings

⚠️ 2 benchmarks with a significant change ran on different hardware

Their base and head results were measured on machines with a different CPU, core count or memory, so the changes might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚠️ 2 benchmarks with a significant change ran in a different software environment

Their base and head results were measured with a different OS or toolchain, such as compiler, runtime or library versions, so the changes might come from this difference rather than from the code. Open an affected benchmark to see what changed.

See affected benchmarks

❌ 2 (👁 2) regressed benchmarks
✅ 107 untouched benchmarks
⏩ 14 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
👁 benchmark_match_kmer_selection[KmerTable-11*11*1*1***111] 523.4 µs 641 µs -18.35%
👁 benchmark_match[KmerTable-11*11*1*1***111] 710.7 µs 824.4 µs -13.79%

Comparing haeganm:fix-conect-ins-code (aad7e2e) with main (81e1a2e)

Open in CodSpeed

Footnotes

  1. 14 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@haeganm

haeganm commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Good idea. I replaced the synthetic test with one over every PDB test asset. It checks that the written CONECT records include all pairs from the original file's CONECT records (matched by chain, residue, insertion code and atom name, since serials get renumbered) and all inter-residue bonds of the parsed structure.

One thing I found while doing this: the RCSB comparison alone doesn't catch the bug. RCSB only writes CONECT for hetero groups and disulfides, and hetero bonds are kept by the separate hetero check. What the fix changes in 1igy are 8 peptide bonds between Kabat-numbered residues like 82/82A, which RCSB doesn't record. So the inter-residue check is the half that fails on main (for 1igy only) and passes with the fix. The RCSB check holds on both.

The failing "databases" job is the biotite.database.rcsb doctest, which is also failing on main.

Comment thread tests/structure/io/test_pdb.py Resolved
Comment thread tests/structure/io/test_pdb.py Outdated
test_file.set_structure(atoms)
test_pairs = _conect_pairs(test_file)

assert _conect_pairs(ref_file) <= test_pairs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't these two sets be equal?

Suggested change
assert _conect_pairs(ref_file) <= test_pairs
assert _conect_pairs(ref_file) = test_pairs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the slow reply on this one.

I tried it with == and it fails on all 19 PDB test assets, but only one way. None of the original CONECT pairs are missing from the written file. The extra ones are all peptide C-N or O3'-P bonds (7279 pairs across the 19 files, e.g. LYS1.C - VAL2.N in 1aki). The original files have no records for backbone links, while set_structure() writes CONECT for every inter-residue bond. main does that too, so it's not from this PR.

So I made it an exact check instead:

assert test_pairs == _conect_pairs(ref_file) | inter_residue_pairs

This replaces both subset asserts. It passes on all 19 files and still fails on main only for 1igy (the 8 bonds like 82 - 82A).

Also rebased on main, the only conflict was the import block.

About CodSpeed: the 3 flagged benchmarks are the k-mer matching ones, which this PR doesn't touch, and benchmark_match[KmerTable-None] flips between ~294 and ~328 µs on unrelated PRs too, in both directions. Locally the residue start lookup adds ~30 µs to _remove_non_conect_bonds() on 1aki, which doesn't show up in set_structure() timings.

…n code

_remove_non_conect_bonds() compared only res_id and chain_id to find
inter-residue bonds, so a bond between e.g. residues 52 and 52A was
treated as intra-residue and not written. Compare residue starts
instead, like the PDBx writer does.
Compare the written CONECT records with the CONECT records of the
original file and with the inter-residue bonds of the structure,
matched by atom identity since serials are renumbered.
Keeping all altlocs and the original atom IDs makes the identity
matching unnecessary.
The written records are exactly the original CONECT records plus the
inter-residue bonds. The original files have no records for peptide and
phosphodiester bonds, so plain equality with them cannot hold.
@haeganm
haeganm force-pushed the fix-conect-ins-code branch from 9777950 to aad7e2e Compare November 5, 2026 21:18

@padix-key padix-key left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

@padix-key
padix-key merged commit 81df912 into biotite-dev:main Nov 7, 2026
29 checks passed

This branch was previously deployed

1 inactive deployment
publish — aad7e2ea Deployed Nov 7, 2026 by haeganm via Upload package to GitHub Releases & PyPI #1446
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.

PDB writer drops bonds between residues that differ only in insertion code

2 participants