Skip to content

CaviTracer: mmCIF output, a truer lining measurement, and multi-frame fixes - #2278

Merged
karolamik13 merged 12 commits into
prody:mainfrom
briza81:CaviTracer-extra-options
Sep 13, 2026
Merged

karolamik13 merged 12 commits into
prody:mainfrom
briza81:CaviTracer-extra-options

Conversation

@briza81

@briza81 briza81 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

1. Optional mmCIF output in the sb-ncbr tunnels schema

calcChannels and calcPoresFromChannels gain output_format — 'pqr' (default, unchanged behaviour) or 'mmcif'.
The schema is the one MOLEonline and ChannelsDB publish tunnels under (sb-ncbr/tunnels-schema v1.0). It puts the profile, the layers and the lining residues of every channel in one file with
keys joining them
, where this module otherwise spreads them over a PQR and two text tables joined only by filename convention.

Seven of the schema's eight categories are written. sb_ncbr_channel_annotation is
omitted: it records ChannelsDB literature annotation, not a property of a de-novo
trace.

Notes on the mapping, since the schema fixes no vocabulary for them:

  • .type follows MOLE — a channel is a Tunnel, a pore a Pore, a chamber link a Path. Links share the channels file, told apart by this column, unlike the PQR output which keeps them separate.
  • Physicochemical properties use the scales MOLE's method page names — Kyte & Doolittle, Cid et al. 1992, Zimmerman et al. 1968, Jones/Taylor/Thornton 1992, and MOLE's own logP/logD/logS fragment tables. Every item carries a value except bRadius, which needs B-factors.
  • Properties are averaged over standard residues only, as the schema defines them ("a sum of charged amino acid residues") and as MOLE computes them. Where a lining holds anything else, the covered fraction and the uncovered residue names go to the log and into the file as a comment above the properties — the schema has nowhere to record them
  • separate has no meaning for mmCIF (the value of the format is the joins between its categories) and is ignored.
  • Multi-frame writes one .cif per frame, as the PQR path already does; the schema describes one structure.

Cost: +0–6% over the trace, ~11 ms per channel, scaling with channel count rather than structure size. File size is ~2–3× the PQR, dominated by the layer_residue join. Nothing is computed on the PQR path.

Viewer: one vis_channels.py now serves both formats, with the same group names and the same colour per rank, so a channel is the same colour whichever way the run was written.


2. The lining is measured to the atom surface, not its centre

distA becomes a clearance between van der Waals surfaces rather than a centre-to-centre distance, and its default moves from 2.5 to 1.5 Å.

This changes which residues are reported as lining a channel. Both radii belong in the criterion: without the probe radius a fixed distA reaches a different depth depending on how wide the channel is, and without the atom radius the reach past an atom's own surface varies by element — widest at hydrogen, negative for the alkali and alkaline-earth ions, which then had to overlap the probe before being reported at all.

Residues lying inside the route rather than beside it are now reported separately. They are usually an ion or cofactor the tessellation never saw — a real result — but can also indicate that the structure passed to the report is not the one the channels were traced on.

3. VDW_RADII overhauled and every lookup hardened

The table mixed scales without recording any of them; Bondi's transition metals are effectively metallic radii, so ZN sat a full Ångström below every modern determination. Each block now names its source (Bondi family where it agrees with
Alvarez, Alvarez for the metals, alkali/alkaline earths and f-block). 42 elements added, 27 values changed, none of them protein-forming.

Lookups are case-insensitive and fall back on a named UNKNOWN entry, announcing the substitution once rather than raising KeyError. Previously a metalloprotein handed over whole failed at the report rather than the trace — the wrong end to fail at.

4. prune_narrow_edges, off by default

CAVER-style pruning of sub-probe Voronoi edges. Without it the search routes through gates narrower than the probe and the channel is discarded afterwards by the bottleneck filter, which loses corridors: a cheap pinched route wins the exit, is
dropped, and the wider route to the same mouth is never reported. MOLE filters, CAVER prunes, and the option picks. Off by default — 4 of 22 test cases gain channels, none loses any.


Bug fixes in the multi-frame path

stop_frame was off by one in calcChannelsMultipleFrames's multi-model branch.
The sentinel -1 was passed straight into a Python slice, where it means "one before the end" rather than "all", so the last model was silently dropped; positive values were taken exclusively where the docstring promises inclusively. The
function's own trajectory branch and the surface-cavity equivalent were correct, so one function disagreed with itself depending on how frames were supplied. All call sites now go through one _frameBounds() helper, so the branches cannot drift apart again. Out-of-range values are clamped rather than raising, since callers had written stop_frame=numCoordsets() to work around the exclusive behaviour. getSurfaceCavityResidueNamesMultipleFrames gains the stop_frame its docstring had always documented but which was never read.

trajectory= rejected every in-memory ensemble. The frame cursor _nfi exists only on a file-backed DCD, and the branch above it converts an Atomic argument into an Ensemble — precisely the type that could not survive the next line. AtomGroup, Ensemble and PDBEnsemble now work alongside DCDFile and Trajectory; a DCD's position is still saved and restored.

Empty results left an empty file. A run finding nothing now writes no file at all

…dges, the graph keeps every Voronoi edge, so the search routes through gates narrower than the probe and the channel is discarded afterwards by the bottleneck filter. That loses corridors: a cheap pinched route wins the exit, is dropped, and the wider route to the same mouth is never reported, since only one path per exit comes back. prune_narrow_edges leaves those edges out of the graph instead, at the floor the filter uses - MOLE filters, CAVER prunes, and the option picks. Off by default as it sometimes changes what channels are reported 4/22 cases gain a few channels and none loses any.
…tA becomes a clearance between surfaces, default 1.5. Unknown elements fall back on a named UNKNOWN entry of 2.0 A and say so, rather than raising KeyError as they would have

+ residues lying inside the route are reported - likely indicating difference between structure used for channel calc and residues detection.
…nown atoms + dropping the deep-atom report from the cavity path as unreachable
…eCavityResidueNames raised KeyError on an element outside VDW_RADII before reaching the lining that had just been taught to fall back, and calcChannels still did on any structure traced with its hetero atoms kept - only water is dropped, so a metalloprotein handed over whole stopped at the trace rather than the report, which is the wrong end to fail at. getVdwRadii is now case-insensitive, falls back on the table's UNKNOWN entry and names what it substituted; _atomRadii adapts it to an Atomic and covers the one case it cannot, a file with no element column. The announcement lives there alone, passed down rather than restated, so a cavity report that reads the radii twice says it once.
…ata - previously the table mixed scales without recording any of them. Bondi's few transition metals are effectively metallic radii, so ZN 1.39 sat a full Angstrom below every modern determination. Each block now names its source. Bondi family where it agrees with Alvarez, Alvarez for the metals, alkali and alkaline earths, and the

f-block. Overall, 42 elements added, so MN and CO no longer fall through to UNKNOWN, and 27 values changed, none of them protein-forming.
…chema), selected by output_format arguemnt - either pqr by defualut or mmcif/cif
…rame functions, via _frameBounds: stop_frame is documented as the last frame to analyse, with -1 meaning all of them, but each multi-frame function spelled that out for itself and they did not agree. Where the sentinel was passed straight into a Python slice, -1 was read as an ordinary negative index and the last frame was silently dropped, and every other value was taken exclusively rather than inclusively. Any negative value now means to the end: treating one negative as a sentinel and another as an index is the ambiguity that caused this. getSurfaceCavityResidueNamesMultipleFrames also gains the stop_frame its docstring has always documented but which was never read, and its trajectory branch no longer runs past the results it was given.
… the multi-frame functions now fetch it with getattr and rewind and restore it only for a file-backed trajectory, the form the residue report already used. An in-memory ensemble - Ensemble, PDBEnsemble or atom group - is therefore accepted as trajectory= alongside a DCD, whose position is saved and restored

exactly as before.
…one is still there. saveChannelsToPdb, saveCavitiesToPdb and writeChannelsCIF do not write any file when no results exist. Since an earlier run's file then survives and goes on looking current, so these are warned about.
…operties, and say how much of each lining they cover, so every item of schema carries a value. The properties are averaged over standard residues alone, as the schema defines them. A lining with nothing scorable in it. Since the residues left out are rarely a random sample of the wall, any channel whose lining holds them is named with its covered fraction and what was left out, in the log and as a comment above the properties.
…d colours were chosen in CIE-Lab, where they looked well separated, but visually rank 6 was the same cyan as rank 3; offset and cycle are now tuned in OKLab against lit and shadowed versions of each colour, with the CAVER primaries unchanged. + cleaning of couple docstrings details.
@briza81

briza81 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@karolamik13 please have a look, these include all we discussed, with several minor fixes. After this is resolved, no code changes are expected, unless bugs are to be fixed :)

@karolamik13
karolamik13 self-requested a review September 13, 2026 17:23

@karolamik13 karolamik13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tested the code on multiple examples (single PDBs, multi-model PDBs, trajectories, etc.) -- looks good. The CIF file saved from channels seems to be fine. I don't have experience with this format. I was not able to check it with some visualization programs, but I have checked the CIF file with gemmi (see below; the number of channels is correct, and the CIF passes validation).

CIF validation: PASS
Number of channels: 9
Number of profile points: 720
channel0 points = 40 length = 5.129
channel1 points = 50 length = 5.282
channel2 points = 70 length = 5.082
channel3 points = 65 length = 5.489
channel4 points = 65 length = 5.646
channel5 points = 110 length = 15.127
channel6 points = 160 length = 24.036
channel7 points = 90 length = 16.683
channel8 points = 70 length = 9.312

@karolamik13
karolamik13 merged commit 5ffabac into prody:main Sep 13, 2026
6 checks passed
@karolamik13

Copy link
Copy Markdown
Contributor

For those who would like to visualize channels in PyMol:

from prody import *
p = parsePDB('1tqn')
atoms = p.select("protein")
channels, surface = calcChannels(atoms, output_path='.', output_format='mmcif', separate=True)

and then except channels also "vis_channels.py" is created.

Next (after gunzip 1tqn.tar.gz or downloading uncompressed pdb file):
pymol vis_channels.py -- 1tqn.pdb channels.cif

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.

2 participants