Skip to content

Add ProForma 2.0 peptide exporter (-epepproforma) - #80

Merged
Luxxii merged 4 commits into
mpc-bioinformatics:masterfrom
okohlbacher:proforma-peptide-export
Sep 7, 2026
Merged

Add ProForma 2.0 peptide exporter (-epepproforma)#80
Luxxii merged 4 commits into
mpc-bioinformatics:masterfrom
okohlbacher:proforma-peptide-export

Conversation

@okohlbacher

Copy link
Copy Markdown

Adds a peptide exporter emitting ProForma 2.0 strings — the HUPO-PSI standard notation for peptidoforms/proteoforms (LeDuc et al., J. Proteome Res. 2022, specification). It is the peptide-level sibling of the PEFF export discussed in #29: peptides leave ProtGraph in a notation any ProForma-aware tool can parse, instead of the FASTA-header qualifier syntax.

protgraph -d trypsin --pep_hops 3 -fm "C:57.021464" -vm "M:15.994915" \
          -epepproforma --pep_proforma_out peptides.proforma.tsv examples/e_coli.dat

producing one tab-separated line per peptide path:

P0A884	49	53	1	RC[+57.021464]HLR
P0A884	1	2	0	M[+15.994915]K

Design decisions (feedback welcome — hence the draft)

  • Mass-delta tags by default. ProtGraph knows modifications only by their delta mass (-fm/-vm), so the honest default is C[+57.021464] — valid ProForma, no guessed identities, the user's input precision preserved verbatim (exponent notation is rewritten to plain decimal, as the ProForma mass grammar requires).
  • Named tags are opt-in. A repeatable --pep_proforma_mod_names "15.994915=UNIMOD:35" maps a delta you passed to a declared name, emitting M[UNIMOD:35]. No mass lookup, no new dependencies.
  • Terminal modifications (NPEPTERM/CPEPTERM/NPROTERM/CPROTERM) use ProForma terminal syntax: [+42.010565]-PEPTIDE / PEPTIDE-[+79.966].
  • Placement is edge-based, not coordinate-based. FIXMOD/VARMOD features ride the in-edges of the (cloned) modified node, so the exporter places each tag on the first residue of the traversed edge's target node. This is deliberate: features on residues introduced by VARIANT/MUTAGEN/CONFLICT carry UnknownPosition (on TP53, 220 of 286 VARMOD features in a -ft VARIANT -vm M:… run), so location-based placement would silently drop them; edge-target placement handles them, along with nodes merged after annotation.
  • Collapsed-edge Or qualifiers are treated as alternatives, not a conjunction: only modifications present in every branch are exported for a traversal, with a one-time warning suggesting --no_collapsing_edges when branches differ (this does occur on real entries, e.g. P21549 with a variable mod).
  • Reruns overwrite (initial write mode "w" through the common-file queue), matching the flag's help text.
  • TSV columns accession, start, end, misscleavages, proforma mirror the FASTA header content. Happy to switch to bare ProForma-per-line or different columns.

Everything else — traversal, miscleavage/length/weight filtering, batching, the common-file write queue — is inherited from APeptideExporter/PepFasta; the exporter is one new file plus the usual registration lines.

Tests and validation

  • New fixture tests/fixtures/minimal.txt (8-residue entry with a VARIANT) pins exact placement: fixed-mod position with coordinates, a variable mod on a variant-introduced residue, unmodified counterparts, terminal syntax on both ends, and rerun-overwrite. Runs in seconds. A smoke test covers the shipped examples. Full suite: 52 passed.
  • 661,479 exported lines (full E. coli example set, P21549 with variable/terminal mods, the fixture) all parse cleanly with pyteomics.proforma (v4.7): 0 failures.
  • flake8/isort clean under the repo's .flake8.

Upstream observations found while testing (not addressed here; happy to file/PR separately)

  • annotate_ptms.py (_apply_varmod, NPRO/CPRO branch): a variable protein-terminal clone's new in-edge carries only the terminal VARMOD qualifier — a prior fixed mod on the same residue keeps its mass on the node but loses its qualifier, so no qualifier-based consumer (FASTA headers included) can see it.
  • annotate_ptms.py (variable NPEPTERM/CPEPTERM with several deltas): the helper nodes get distinct masses, but the edge features all record the last loop delta.
  • cli.py accepts fixed NPEP<AA>/CPEP<AA> keys, but _apply_fixmod skips them, so they silently do nothing (variable ones work).

Context: we are building a proteoform-graph analysis tool (okohlbacher/ProteoformGraph) and would like ProtGraph's exports to interoperate; a ProForma-emitting ProtGraph is directly consumable by anything speaking the PSI standard.

🤖 Generated with Claude Code

Exports each peptide path as a ProForma 2.0 string (HUPO-PSI standard) into a
TSV of accession, start, end, misscleavages, proforma. Modifications from
-fm/-vm are emitted as signed mass-delta tags by default, optionally as named
tags via --pep_proforma_mod_names; terminal modifications use ProForma
terminal syntax. Placement is edge-based (first residue of the traversed
edge's target node), which also covers modifications on residues introduced
by VARIANT/MUTAGEN/CONFLICT, whose features carry no reference location.
Collapsed-edge Or qualifiers export only modifications shared by all
branches, with a warning when they differ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@okohlbacher
okohlbacher marked this pull request as ready for review September 1, 2026 10:50
@Luxxii Luxxii self-assigned this Sep 4, 2026
Comment thread protgraph/export/peptides/pep_proforma.py Outdated
Comment thread protgraph/export/peptides/pep_proforma.py
Comment thread protgraph/export/peptides/pep_proforma.py Outdated
Comment thread protgraph/export/peptides/pep_proforma.py Outdated
Comment thread protgraph/export/peptides/pep_proforma.py Outdated
Comment thread protgraph/cli.py
Comment thread protgraph/export/peptides/pep_proforma.py Outdated
Comment thread README.md Outdated

@Luxxii Luxxii 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.

Great Work! I added some comments on lines, especially on those, where i saw some issues/possible optimizations. I agree on the warning as some generated peptides paths are ambiguous and a user should run with --no_collapsing_edges if desiring exact modifications.

There is also a bug, concerning merged nodes. In case of a FIXMOD, e.g. "EIN", where "I" is getting a fixed modification, the current implementation would provide the following ProForma: "EIN[+XXXX]".

I also created a patch, based on your branch: okohlbacher:proforma-peptide-export (which can be applied via git apply pr_proforma_dl.patch). Here, addition of the qualifier column, addition of a header column (which is written once), a flag to also export qualifier column if desired and some optimizations on the proforma generation and the bug fix was done.

Here is the patch
pr_proforma_dl.patch

I tested this patch on the whole swissprot database and found no issues so far, exporting ProForma entries.

Tested command:

protgraph --pep_hops 60 -nm --pep_miscleavages 2 --pep_min_pep_length 6 -ft SIGNAL -ft PROPEP -ft PEPTIDE -ft INIT_MET -epepproforma -vm "M:16.3" -fm "C:57.3" -vm "NPEPTERM:21.21" --pep_proforma_mod_names "57.3=UNIMOD:57" uniprot_sprot.dat

I also updated to the ProtGraph Version 0.3.13 in the provided patch. If you apply this patch and check on your end if everything is working, i am happy to merge this and to make it available on PyPI

Dominik Lux and others added 2 commits September 4, 2026 21:28
….3.13

Patch provided by the maintainer in the PR review: column order
accession/proforma/start/end/misscleavages with an optional qualifiers
column (--pep_proforma_write_qualifiers), a write-once TSV header, node-wise
proforma assembly fixing modification placement inside merged nodes
(EIN with modified I now yields EI[+x]N), README section moved after the
FASTA walkthrough, version 0.3.13.
With a terminal modification applied, nodes[1] / nodes[-2] can be the empty
helper node whose position 0 does not exist in 1-based protein coordinates;
an NPEPTERM peptide reported start=0. Positions now come from the first and
last node carrying sequence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@okohlbacher
okohlbacher force-pushed the proforma-peptide-export branch from f1debf8 to c853850 Compare September 4, 2026 19:29
@okohlbacher

Copy link
Copy Markdown
Author

Patch applied — thanks, and good catch on the merged-node placement. Verified on our side: 52/52 tests, flake8 clean, and 661,790 exported ProForma lines (test fixture, P21549 with your exact flag set incl. --pep_proforma_write_qualifiers, full E. coli examples) parse cleanly with pyteomics.proforma. One small commit on top: with a terminal modification, nodes[1] is the empty helper node, so NPEPTERM peptides reported start=0; start/end now come from the first/last sequence-bearing node. From our side this is good to merge — and 0.3.13 vs. 0.4.0 is entirely your call.

Full-scale end-to-end testing surfaced two defects in the previous state:

Merged-node placement pooled (letter, tag) pairs across ALL nodes of the
path and applied them with sequential str.replace, so a peptide spanning
several nodes that each carry the same-letter modification received the
accumulated tags on every matching residue (silently inflating the mass,
e.g. C[+57][+57] under fixed carbamidomethyl with one miscleavage), and a
residue letter occurring inside an already-written tag's text got tags
injected mid-tag (C[UNIM[+15.99]OD:4]). Placement now uses only the node's
own modifications, locating each one via the feature's position relative to
the node start (falling back to the first matching residue for features
without a usable position), and assembles the string per residue index —
no replace, each modification exactly once.

The qualifiers option wrote one tab-separated FIELD per qualifier, giving a
ragged TSV (5-13 columns under a 6-column header); the qualifier strings
are now comma-joined into the single declared column, present (possibly
empty) on every row.

New fixture tests/fixtures/merged.txt pins both: EI[+23.123]NK for an
interior modification of a merged node, single tags on a two-node
miscleaved peptide, no adjacent duplicate and no nested tags, and a
uniform 6-column table with the flag set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@okohlbacher

Copy link
Copy Markdown
Author

We ran a full end-to-end pass over the branch (fresh clone → clean non-editable install → wheel contents → feature matrix incl. digestion modes, isoforms, terminal mods, qualifiers → -np 1 vs -np 4 concurrency on the whole E. coli example set). Two issues surfaced, both fixed in 09a6a3f:

  1. Merged-node placement could stack and corrupt tags. Pooling (letter, tag) pairs across the whole path + sequential str.replace meant a peptide spanning two nodes that each carry a C fixmod came out as C[+57.021464][+57.021464]… (silent +57 Da per extra tag — triggered by nothing more exotic than fixed carbamidomethyl with one miscleavage), and a residue letter occurring inside an earlier tag's text got tags injected mid-tag (C[UNIM[+15.994915][+15.994915]OD:4] — ~1.9% of rows in an E. coli run with -vm M + a mapped name). Placement now uses only the node's own modifications, located via the feature position relative to the node start (first-matching-residue fallback for position-less features, e.g. on variant residues), assembled per residue index. Your EINEI[+23.123]N case is pinned in a new fixture test, along with no-duplicate/no-nested assertions.
  2. --pep_proforma_write_qualifiers wrote one field per qualifier, giving a ragged TSV (5–13 columns under the 6-column header). The qualifier strings are now comma-joined into the single declared column, present (possibly empty) on every row.

Validation after the fix: 54/54 tests; flake8 clean; -np 1 vs -np 4 on the full E. coli set produce identical sorted row sets with the header exactly once and rerun-overwrite confirmed; 661k+ rows re-checked with pyteomics plus a semantic scan (no adjacent duplicate tags, no brackets inside parsed tag values — worth noting that proforma.parse alone happily accepts the corrupted tags, so syntactic parsing is a weak validator here). One cosmetic note: the Or-branch warning prints once per worker process rather than once per run.

From our side this is now genuinely good to merge.

@Luxxii Luxxii 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!

Regarding the FIXMOD and setting it exactly at the amino acid location for merged nodes: We could probably use .replace(aa, aa + "[MODIFICATION]"), since we have this case only in merged nodes. FIXMODs are the only qualifiers, which do not modify the graph and simply append info to the edges via _append_feature_in_edge. This causes them to be inside merged nodes. Merged nodes put such qualifier info to the corresponding incoming edge. We can keep this implementation as it is more precise.

@Luxxii
Luxxii merged commit 0e27682 into mpc-bioinformatics:master Sep 7, 2026
7 of 8 checks passed
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