Skip to content

Ptm stoich#2475

Open
pcruzparri wants to merge 59 commits into
smith-chem-wisc:masterfrom
pcruzparri:ptm_stoich
Open

Ptm stoich#2475
pcruzparri wants to merge 59 commits into
smith-chem-wisc:masterfrom
pcruzparri:ptm_stoich

Conversation

@pcruzparri

@pcruzparri pcruzparri commented Feb 17, 2025

Copy link
Copy Markdown
Contributor

Adds occupancy calculation to MM using peptide intensities from FlashLFQ. Backend calculation currently implemented in the MzLib.MzLibUtil.PositionFrequencyAnalysis class. In MM, the updating of the ProteinGroup.ModInfo field with intensity-based stoichiometry is done in the PostSearchAnalysis.QuantificationAnalysis() method, and the ProteinGroup.ToString() takes the ModInfo data to produce the occupancy string.

Description

This PR introduces PTM stoichiometry/occupancy quantification to MetaMorpheus, refactors ProteinGroup to inherit from mzLib's BioPolymerGroup, and adds several quality-of-life improvements and bug fixes.

Key Changes

1. PTM Occupancy by Spectral Count and Intensity
Protein group TSV output now includes four new families of dynamic columns per sample group:

  • SpectralCount_<sample> — number of PSMs observing the protein
  • Intensity_<sample> — FlashLFQ intensity per sample
  • CountOccupancy_<sample> — modified spectral count / total spectral count
  • IntensityOccupancy_<sample> — modified intensity / total intensity
    These columns are generated via the new PopulateSampleGroupResults() method on BioPolymerGroup. Columns only appear when quantification state is available upstream, preserving backward compatibility for workflows that return early from quantification.

2. ProteinGroup → BioPolymerGroup Inheritance
ProteinGroup now extends Omics.BioPolymerGroup.BioPolymerGroup from mzLib, delegating quantification, sample grouping, and occupancy logic to the base class. Backward-compatible alias properties (Proteins, AllPeptides, ProteinGroupName, IntensitiesByFile, etc.) map to base class properties so existing consumers are unaffected.

3. ScanMetadata Record
Scan metadata (scan number, retention time, precursor charge/mass/intensity, ion mobility, etc.) is now captured in a lightweight immutable ScanMetadata record on Ms2ScanWithSpecificMass and SpectralMatch. This allows heavyweight scan objects to be released from memory after scoring while preserving metadata access via pass-through properties.

4. SpectralMatch → ISpectralMatch Interface
SpectralMatch now implements ISpectralMatch from mzLib, with explicit interface members for OneBasedScanNumber, Intensities, GetIdentifiedBioPolymersWithSetMods(), CompareTo(ISpectralMatch?), and Equals(ISpectralMatch?).

5. ThreadSafeObservableCollection Improvements
Updated to facilitate safe iteration through the collection without worrying about threading issues, with comprehensive test coverage.

6. Calling Code Adapted to New Property Semantics
IntensitiesByFile changed from a simple auto-property (getter returned the backing reference) to an adapter property whose getter returns a new copy via .ToDictionary(). Calling code in PostSearchAnalysisTask and SilacConversions was updated from the old mutate-the-getter pattern (.Add() on the returned dictionary) to the build-locally-then-assign pattern.

7. Bug Fixes

  • Fixed variant description access (variant.Description instead of variant.VariantCallFormatDataString.Description) with null-safety guard
  • Fixed possibleVariantPsms ordering result being discarded (now assigned back)
  • Fixed early return from quantification when experimental design is unavailable, while still writing count-based occupancy columns
  • Fixed test isolation issue where stale ExperimentalDesign.tsv in shared TestData/ directory affected other tests

@pcruzparri pcruzparri added the WIP Work in progress, not ready for review label Mar 10, 2025
@pcruzparri pcruzparri marked this pull request as ready for review October 2, 2025 18:54
@pcruzparri pcruzparri removed the WIP Work in progress, not ready for review label Oct 2, 2025
… branch of mzlib. Code is untestested. Have seen some discrepancies with the quantity of quantified mods and their indexing.
…upsOutput() was not consistent when run individually vs when run with all other tests. The ordering of the output was changing. Fixed to not look for the first data line but rather for the one with the right PG.
@pcruzparri pcruzparri added the WIP Work in progress, not ready for review label Oct 27, 2025
trishorts

This comment was marked as outdated.

Comment thread MetaMorpheus/TaskLayer/SearchTask/PostSearchAnalysisTask.cs
trishorts

This comment was marked as outdated.

trishorts

This comment was marked as outdated.

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

Automated code review - 4 finding(s) at severity >= Major, posted as inline comments.

Comment thread MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs
Comment thread MetaMorpheus/GuiFunctions/Util/ThreadSafeObservableCollection.cs Outdated
Comment thread MetaMorpheus/GuiFunctions/Util/ThreadSafeObservableCollection.cs Outdated
@pcruzparri pcruzparri requested a review from trishorts June 18, 2026 20:06

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

Review Summary

Approved findings from the dashboard were revalidated against the current PR head after a freshness sync. The head commit changed since initial analysis, but both approved findings still resolved to valid inline anchors on the updated patch.

Major (2)

  • MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs:99 Alias collection getters now return detached copies instead of live mutable collections, creating a silent behavioral break for callers that mutate getter results.
  • MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs:415 ProteinGroup's custom coverage calculation no longer keeps the inherited BioPolymerGroup.CoverageResult contract in sync, so inherited callers can observe coverage that diverges from MetaMorpheus output.

Grouping: 2 approved findings -> 2 inline comments | 0 summary-only.

Comment thread MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs
Comment thread MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs

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

Automated code review - 1 finding(s) at severity >= Major, posted as inline comments.

Comment thread MetaMorpheus/EngineLayer/ProteinParsimony/ProteinGroup.cs
@pcruzparri pcruzparri requested a review from trishorts June 23, 2026 00:02

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

Approving. I re-reviewed against the current head (ae97ad2) — not just the reply threads — and every Critical/Major finding from this round is resolved or acknowledged-and-deferred with a test:

  • Variant NRE — reverted to master's VariantCallFormatDataString.Description; the incomplete guard is gone. ✅
  • .OfType<SpectralMatch>() dropping PSMs — now a single Cast<SpectralMatch>().ToList() reused for Score/QValue/PEP, so all three are computed over the same population and an unexpected type throws loudly. ✅
  • ThreadSafeObservableCollection deadlock (Critical) + PropertyChanged marshalling — whole (out-of-scope) class reverted out of the PR. ✅
  • Alias getters silently detached — now IReadOnly with the snapshot semantics documented. ✅
  • Glyco PopulateSampleGroupResults on null LFQ — glyco now always assigns IntensitiesByFile (zeros when no LFQ) and populates uniformly, matching the search path. ✅

Nice, responsive turnaround — the two genuinely risky/out-of-scope items (the deadlock and the variant patch) were cleanly reverted rather than papered over.

Follow-up notes (non-blocking — for the deferred BioPolymerGroup migration PR)

Two of the findings were reasonably deferred with tests. Recording the residual debt so it isn't lost in the reply threads:

  1. Method-hiding is a trap for the very migration this defers to. GetTabSeparatedHeader() and CalculateSequenceCoverage() are now new-hidden on a type that is-a BioPolymerGroup. It's safe today only because every caller uses the concrete type — I confirmed WriteProteinGroupsToTsv (PostSearchAnalysisTask.cs:1554) holds List<ProteinGroup> and calls .First().GetTabSeparatedHeader(), so the MM override wins. But the follow-up that moves logic onto BioPolymerGroup must resolve this together with the CoverageResult contract finding (#3443992211) — the first base-typed caller would otherwise silently get empty columns / diverging coverage.

  2. Header/row column uniformity is enforced by producer discipline, not structure. The header is built from one representative group while each row re-derives its own columns from its own SampleGroupResults/HasIntensityData. It stays aligned only because the search and glyco paths now both assign IntensitiesByFile for every group (zeros for unmeasured), and TestMultipleProteinGroupsHeaderAndRowsHaveSameColumnCount locks that case. It remains latent TSV-corruption risk if any future path populates SampleGroupResults non-uniformly. The single-shared-schema fix (drive header + rows from one authoritative column shape) is the real resolution and should be the deferred PR — please track it as an issue, not just a reply.

  3. Trivial perf, optional: the IntensitiesByFile getter allocates a fresh dictionary on every call, and around ProteinGroup.cs:650 it's called twice to read a single value (null-check then GetValueOrDefault). Cache in a local. Not worth holding anything for.

None of these block the merge.

public IEnumerable<IBioPolymerWithSetMods> GetIdentifiedBioPolymersWithSetMods() =>
BestMatchingBioPolymersWithSetMods.Select(h => h.SpecificBioPolymer);

public int CompareTo(ISpectralMatch? other)

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.

Careful. We already have a carefully orchestrated compare method for spectral matches, we should unify them

string.Join("|", proteinsOrderedByAccession.Select(p => p.Organism).Distinct()));

foreach (var psm in proteinGroup.AllPsmsBelowOnePercentFDR.Where(v => v.FullSequence != null))
foreach (var psm in proteinGroup.AllPsmsBelowOnePercentFDR.OfType<SpectralMatch>()

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.

There may be issues here with the spectral matches being of the type GlycoSpectralMatch. A cast or something may be more appropriate here, unsure, but please verify this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants