Skip to content

#35: surface patch features in every run and in the default feature set (10.0.0, breaking) - #36

Merged
teese merged 4 commits into
mainfrom
35-surface-patch-features
Oct 19, 2026
Merged

teese merged 4 commits into
mainfrom
35-surface-patch-features

Conversation

@teese

@teese teese commented Oct 15, 2026 •

Copy link
Copy Markdown

Problem

Prodes computes a table of protein surface features from a structure, for use in machine learning models of chromatography. It gives each point of the protein surface two values, a projected hydrophobicity (the molecular hydrophobicity potential, Mhp in column names) and an electrostatic potential (Ep), and summarises each over the whole surface by its sign. Every hydrophobic point is pooled whether the points form one region or twenty scattered ones, and no feature can be restricted to part of the molecule, such as the CDRs of an antibody. Hydrophobic interaction chromatography retention of antibodies has been reported to correlate with the area of contiguous hydrophobic surface on the CDRs, which no Prodes feature measures.

Closes #35

What changed

  • New step in every run, in src/prodes/calculations/patches.py. Its settings and residue sets are passed as CLI options or calculate(..., patches=PatchSettings(...)).
  • Each surface point's hydrophobicity and potential are averaged over a 5 Å neighbourhood; points past a threshold that touch on the 1 Å surface grid form a patch; patches under 100 Ų are dropped.
  • 11 new default columns, at the end of src/prodes/data/features_reduced.yaml: summed area, largest area and count of the hydrophobic patches (SurfMhpPatchArea, SurfMhpPatchMaxArea, NSurfMhpPatch), the same three for positive and for negative potential (SurfEpPos*, SurfEpNeg*), and the highest and the area-weighted positive sum of the averaged hydrophobicity (SurfMhpLocalMax, SurfMhpLocalPosSum).
  • --residue-set NAME=FILE repeats every column as <column>_NAME, restricted to the residues listed in the file.
  • The surface point table in the output zip carries each point's area (area_A2), owning residue (residue), neighbourhood averages (local_mhp, local_ep_volts) and hydrophobic patch (mhp_patch_id).
  • Breaking, version 10.0.0: the default feature table has 65 columns instead of 54, --full-features writes 116 instead of 105, --patches is gone, and --hydro mj needs --patch-hydrophobic-threshold. No earlier column changes value.
  • Method, settings and limitations in docs/surface_patches.md.
  • The command line enables faulthandler, so a crash in compiled code prints a Python stack instead of exiting silently. docs/parallelism_and_memory.md explains the return codes of a run killed by a signal.
  • docs/surface_patches.md records that the default patch settings were checked against antibody HIC data and kept: settings tuned on GDPa1 did worse on the held-out GDPa3 panel.
  • docs/structure_preparation.md states that hydrogens do not change any feature value, and why PROPKA leaves some groups without a pKa (a C-terminus without OXT; an N-terminal aspartate).

Results

Measured outside this repository, on version 9.1.0 of this branch, which wrote the same patch columns under a --patches option, with a random-forest model of HIC retention built on sequence-derived features, to which the patch columns were added. Patches were computed with two residue sets per antibody: the CDRs, and the whole variable region (Fv), both from AHo numbering. Scores are Spearman R between predicted and measured retention on the public GDPa1 panel (about 236 antibodies), using the five cross-validation folds its authors publish, the same folds in every row; and on the public GDPa3 panel (79 antibodies), predicted by a model fitted on all of GDPa1. Each figure is the mean over five random-forest seeds.

Scored on Structure source Sequence model Sequence model + patch columns Improved in
GDPa1, mean per fold ABodyBuilder3 Fv 0.533 0.573 5 of 5 folds
GDPa1, mean per fold Boltz-2 whole antibody 0.529 0.589 5 of 5 folds
GDPa3, held out ABodyBuilder3 Fv 0.527 0.620 5 of 5 seeds

Adding the 54 default Prodes columns to the sequence model instead changed the GDPa1 mean per-fold Spearman R by between -0.011 and -0.001, depending on the structure source. The patch columns changed anion exchange (pooled out-of-fold Spearman R 0.763 to 0.768) and cation exchange (0.822 to 0.814) by less than 0.01. On 423 structures (Fv models, whole antibodies, anion and cation exchange proteins) the 54 earlier default columns were identical with and without the patch step.

The patch step takes about 1 s for 57,000 surface points; the whole Prodes run on the largest test structure, PDB entry 1AO6, takes 67 s including it.

Checks on tests/data/ARH96693.pdb.zip, the single-chain structure the regression tests use:

Check Result
A run with a residue set against one without: every whole-surface column identical
Sum of the point areas against the Area feature (total solvent accessible surface area) equal
Patch features with --full-features (the full 105-feature legacy set) against without identical

CI not run yet. 488/488 tests pass locally.

Acceptance criteria still open

All acceptance criteria met.

Not verified

  • The default settings (radius 5 Å, minimum patch area 100 Ų, hydrophobic threshold 0, electrostatic threshold 0.5 V) were checked on antibody HIC data only. Tuned on GDPa1, the best setting raised the correlation of SurfMhpPatchArea_CDR with retention there from 0.34 to 0.46 but lowered it on GDPa3 from 0.355 to 0.328, and the GDPa3 score of the HIC model fell from 0.615 to 0.589, so the defaults were kept. They have not been checked for other proteins or other chromatography modes.
  • One of 26 runs of one whole antibody, at 12 workers, gave different ShellEp*Formal columns; every other column agreed. These columns summarise the electrostatic potential projected onto 120 planes placed around the protein, and in that run the summed potential of one plane changed sign. 25 further runs of the same input agreed: 8 complete runs, and 17 repeats of only the step that computes the 120 planes, at 1, 4 and 12 workers, which agreed to within 1e-15. The cause was not found.
  • The 11 patch columns were not part of the redundancy analysis behind the 54 earlier default columns (docs/redundant_feature_analysis.md).
  • The dense arrays of the neighbourhood average are not counted against --mem-limit: a few MB for an Fv, a few tens of MB for a whole antibody (estimated, not measured).
Detail
  • Patches are found on the whole surface, then restricted: a patch crossing the border of a residue set is one patch, the minimum area applies to the whole patch, and the restricted columns count only its share inside the set.
  • Each point's area is the summed area of the Shrake-Rupley dots in its 1 Å cell, weighted as Atom.surface_area weights them; its residue is the residue whose dots carry the most area in the cell, ties going to the alphabetically first label.
  • The 0.5 V electrostatic default lies between the medians of the 75th (0.39 V) and 90th (0.61 V) percentiles of the absolute local potential over the eight structures in tests/data, at 0.15 M.
  • Refused with an error: the default hydrophobic threshold of 0 on a hydrophobicity scale with no negative values (mj, the unscaled Miyazawa-Jernigan scale); a residue label the structure does not have, checked before the surface is built; a residue set that owns no surface point; a radius above 15 Å.
  • FeatureDictionary resolves <code>_NAME for patch codes only, so Area_CDR is not a feature code.

teese added 4 commits October 16, 2026 00:07
The default features summarise the surface by the sign of each point's value, so every hydrophobic point is pooled whether the points form one region or many, and every feature covers the whole molecule. Hydrophobic interaction chromatography retention of antibodies has been reported to correlate with the area of contiguous hydrophobic surface, in particular on the CDRs, which no existing feature measures.

A new, opt-in patch step (--patches, or calculate(patches=PatchSettings(...))) averages the projected hydrophobicity and the formal-charge potential of each surface point over a neighbourhood (default 5 A, at most 15 A), marks the points past a threshold, joins marked points that touch on the 1 A surface lattice into patches, and discards patches under a minimum area (default 100 A^2, about half the solvent accessible area of one exposed phenylalanine). It reports the summed area, largest area and count of the hydrophobic, positive and negative patches, plus the maximum and the area-weighted positive sum of the local hydrophobicity. --residue-set NAME=FILE reports every one of these again over the residues listed, with patches found on the whole surface and only their in-set share counted.

Each surface point now carries the solvent accessible area of its Shrake-Rupley dots and the residue owning most of it, computed while the surface cell still holds those dots. The point table always writes both, and read_surface_points reads the residue as text. The default hydrophobic threshold is refused on a scale with no negative values, residue labels and the threshold are checked before the surface is built, patch options given without --patches are refused, and the patch settings and residue sets are recorded in prodes_run.json. The patch columns are appended after the default ones, so without the flag the feature table is unchanged. The feature dictionary documents the patch columns in a separate file, features_patches.yaml, and resolves a residue-set suffix on patch codes only.

The README passages that said Prodes finds no discrete patches and that its hydrophobicity potential has the length scale of SAP are corrected: the exp(-d) weight has a 1 A decay length, which is why the neighbourhood average is needed. Version 9.1.0.
…s and silent exits

A Prodes run that ends with a non-zero exit code, no traceback and no bundle gives its caller nothing to act on. The command line now enables faulthandler, so a crash in compiled code prints the Python stack of every thread to stderr before the process exits; forked workers inherit it. A SIGKILL, typically from the out-of-memory killer, cannot be caught, so docs/parallelism_and_memory.md now gives the return codes of both cases under subprocess and where to look for an out-of-memory kill.

docs/structure_preparation.md now states that hydrogens do not change any feature value (stripping all hydrogens from an ABodyBuilder3 Fv model left all 77 columns of a --patches run unchanged), and names the two causes of the warning about titratable groups missing from the pKa file. Both are gaps in PROPKA's own output rather than values dropped by convert_propka: a C-terminus without OXT gets no C- group, and PROPKA 3.5.1 gives no side-chain pKa for an aspartate that is the first residue of a chain, as on most antibody light chains.
…data and kept

The patch settings were tuned against HIC retention time on the GDPa1 antibody panel (236 antibodies) over radius 3 to 12 A, minimum area 0 to 500 A^2, hydrophobic threshold -0.08 to 0.06 and electrostatic threshold 0.1 to 1.0 V. The best setting on GDPa1 raised the Spearman R of SurfMhpPatchArea_CDR there from 0.34 to 0.46, but lowered it on the held-out GDPa3 panel (79 antibodies) from 0.355 to 0.328, and an HIC model fitted on GDPa1 scored 0.589 on GDPa3 with the tuned setting against 0.615 with the defaults. The defaults therefore stay as they are. docs/surface_patches.md, the README and the comment on DEFAULT_PATCH_MIN_AREA now say so, in place of calling the defaults uncalibrated starting values, and state that any re-tuning needs an independent panel to check it on.
… the default set

Added to a random-forest model of antibody hydrophobic interaction chromatography retention built on sequence features, the 11 surface patch features raised the Spearman R on the held-out GDPa3 panel from 0.527 to 0.620 and improved every one of the five published GDPa1 folds, and they cost about a second per structure. They were nevertheless only written under --patches, so a caller had to know to ask for them.

The patch step now runs on every structure and --patches is removed. The 11 patch entries move from features_patches.yaml, which is deleted, to the end of features_reduced.yaml, so the default feature set is 65 columns and --full-features writes 116; each is marked per_residue_set, and SurfArea moves with them under residue_set_only_features. The patch options (--patch-radius, --patch-min-area, --patch-hydrophobic-threshold, --patch-ep-threshold, --residue-set) now always apply, and calculate(patches=None) means PatchSettings() rather than no patches. prodes_run.json always records the patch settings, and the surface point table always carries local_mhp, local_ep_volts and mhp_patch_id. FeatureDictionary.get_feature_codes takes residue_sets to list the columns of a run with residue sets, and get_patch_feature_codes returns the 11 patch codes.

This is a breaking change, so the version is 10.0.0: the default feature table has 11 more columns, a --full-features table no longer matches the original 105 columns, drop_redundant_features requires the patch columns, and a run with --hydro mj, whose values are all positive, is refused unless --patch-hydrophobic-threshold is given. No earlier column changes value. The tests that pinned the original 105 columns now name the patch codes as the one deliberate addition, and the README, docs/surface_patches.md and docs/redundant_feature_analysis.md describe the new default set, noting that the patch features were not part of the redundancy analysis.

A docstring in tests/test_pka.py no longer names a particular downstream caller.
@teese teese changed the title #35: add contiguous surface patch features with residue-set restriction #35: surface patch features in every run and in the default feature set (10.0.0, breaking) Oct 19, 2026
@teese
teese merged commit 513f7eb into main Oct 19, 2026
3 checks passed
@teese
teese deleted the 35-surface-patch-features branch October 19, 2026 09:13
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.

Contiguous surface patch features, restrictable to a residue set

1 participant