Add ProForma 2.0 peptide exporter (-epepproforma) - #80
Conversation
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>
Luxxii
left a comment
There was a problem hiding this comment.
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
….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>
f1debf8 to
c853850
Compare
|
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. |
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>
|
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 →
Validation after the fix: 54/54 tests; flake8 clean; From our side this is now genuinely good to merge. |
Luxxii
left a comment
There was a problem hiding this comment.
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.
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.
producing one tab-separated line per peptide path:
Design decisions (feedback welcome — hence the draft)
-fm/-vm), so the honest default isC[+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).--pep_proforma_mod_names "15.994915=UNIMOD:35"maps a delta you passed to a declared name, emittingM[UNIMOD:35]. No mass lookup, no new dependencies.NPEPTERM/CPEPTERM/NPROTERM/CPROTERM) use ProForma terminal syntax:[+42.010565]-PEPTIDE/PEPTIDE-[+79.966].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.Orqualifiers 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_edgeswhen branches differ (this does occur on real entries, e.g. P21549 with a variable mod)."w"through the common-file queue), matching the flag's help text.accession, start, end, misscleavages, proformamirror 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
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.pyteomics.proforma(v4.7): 0 failures.flake8/isortclean 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(variableNPEPTERM/CPEPTERMwith several deltas): the helper nodes get distinct masses, but the edge features all record the last loop delta.cli.pyaccepts fixedNPEP<AA>/CPEP<AA>keys, but_apply_fixmodskips 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