Skip to content

Clamp experiment q-value to each entry's best run q-value (cross-impl parity with pwiz#4390) - #49

Merged
brendanx67 merged 1 commit into
reconciliation-v3-first-pass-base-idsfrom
q-clamp-parity
Jul 8, 2026
Merged

Clamp experiment q-value to each entry's best run q-value (cross-impl parity with pwiz#4390)#49
brendanx67 merged 1 commit into
reconciliation-v3-first-pass-base-idsfrom
q-clamp-parity

Conversation

@brendanx67

Copy link
Copy Markdown
Collaborator

Mirrors the C# Osprey experiment-q "best-of-runs" clamp (ProteoWizard/pwiz#4390) so the two implementations stay in cross-impl parity.

Why

Experiment-level FDR competes each precursor's single best observation against a de-duplicated (thinner) decoy null, so the raw experiment q-value can fall below every per-run q-value. That lets a precursor pass experiment-level FDR with no single run passing run-level FDR — producing reported peptides with no run-level ID (a blib ID-line artifact) and an anti-conservative experiment-wide FDP.

What

Adds clamp_experiment_q_to_best_run (in pipeline.rs), which floors each entry's experiment_precursor_qvalue / experiment_peptide_qvalue up to that entry's best (min-over-runs) combined run q-value (FdrLevel::Both = max(run_precursor, run_peptide)), i.e. a best-of-runs aggregate can never be more confident than its best single run. Keying:

  • precursor floor by the full entry_id (decoy high bit included),
  • peptide floor by (modified_sequence, is_decoy) — both target/decoy-specific, so a target never inherits its paired decoy's good run; an empty modified_sequence (a stub loaded without the column) is skipped.

Two call sites, mirroring the C#:

  • end of run_percolator_fdr (both the direct and streaming paths, covering first-pass and the reconciliation-aware second pass);
  • an unconditional re-clamp on the final post-reconciliation pool right before the blib output gate, since Stage-6 reconciliation resets the run q-value of moved / gap-fill peaks after the in-pass clamp.

The two HPC --join-at-pass=2 recompute paths are left unclamped for now, each marked with a // PARITY NOTE for a follow-up.

Validation

Compare-EndToEnd-Crossimpl shows the C# (pwiz#4390) and this branch remain at end-to-end bit-parity at 1e-9 on Stellar (56,534 precursors) and Astral (160,358) — Stage-7 protein FDR and full blib content match. cargo fmt / clippy -D warnings / cargo test all pass.

Companion to the C# PR; opened against reconciliation-v3-first-pass-base-ids (the current parity base) for a clean single-commit diff.

Experiment-level FDR competes each precursor's single best observation
against a de-duplicated (thinner) decoy null, so the raw experiment
q-value can fall below every per-run q-value. That lets a precursor pass
experiment-level FDR with no single run passing run-level FDR, producing
reported peptides with no run-level ID (the blib ID-line artifact) and an
anti-conservative experiment-wide FDP calibration.

Add clamp_experiment_q_to_best_run, which floors each entry's
experiment_precursor_qvalue / experiment_peptide_qvalue up to that
entry's best (min-over-runs) combined run q-value (FdrLevel::Both =
max(run_precursor, run_peptide)). The precursor floor keys on the full
entry_id (decoy high bit included) and the peptide floor on
(modified_sequence, is_decoy) - both target/decoy-specific, so a target
never inherits its paired decoy's good run. An empty modified_sequence
(a stub loaded without the column) has no peptide identity and is skipped.

The clamp runs at the end of run_percolator_fdr for both the direct and
streaming paths (covering first-pass and the reconciliation-aware second
pass), and again unconditionally on the final post-reconciliation pool
right before the blib output gate, since Stage-6 reconciliation resets
the run q-value of moved / gap-fill peaks after the in-pass clamp.

The two HPC --join-at-pass=2 recompute paths are left unclamped for now,
each marked with a PARITY NOTE for a follow-up.

This mirrors PercolatorEngine.ClampExperimentQToBestRun and its two call
sites in the C# implementation, for cross-impl parity.
brendanx67 added a commit to ProteoWizard/pwiz-ai that referenced this pull request Jul 8, 2026
…klog

* Q-value filtering TODO: Brendan accepted (model-diagnostics libdecoy re-check);
  Copilot addressed (94f8e80bc3); Rust parity PR maccoss/osprey#49 opened.
* Added backlog TODO for the gendecoy salvage-or-deadend decision.
@brendanx67
brendanx67 requested a review from maccoss July 8, 2026 15:51
@brendanx67
brendanx67 merged commit 0cfe78c into reconciliation-v3-first-pass-base-ids Jul 8, 2026
@brendanx67
brendanx67 deleted the q-clamp-parity branch July 8, 2026 18:20
maccoss added a commit that referenced this pull request Jul 13, 2026
…ned intensity features (#55)

* Carry the join-wide first-pass base_id set in reconciliation.json (v3)

The per-file <stem>.reconciliation.json boundary file now carries
first_pass_base_ids, the join-wide set of base_ids that survived
first-pass compaction (format_version 2 -> 3), written sorted ascending
from the first-pass passing set the pipeline already computes before
compaction.

On the OspreySharp side this set lets a per-file HPC rescore worker
compact to exactly the set the in-memory straight-through pipeline used,
instead of recomputing a per-file subset that drops cross-file entries.
This commit is the Rust half: it keeps the reconciliation.json format
byte-identical across impls so the parity tests stay copy-free. Wiring
the Rust per-file worker to consume the set (the analogous HPC
self-consistency fix) is a follow-up.

* first_pass_base_ids is required in v3 (no serde default): v2 envelopes
  are rejected at parse time rather than a worker silently recomputing a
  divergent subset. Mirrors the OspreySharp hard-fail.

Rust half of a cross-impl change with OspreySharp (ProteoWizard/pwiz
#4353). Verified: the round-trip byte-parity hook
(OSPREY_CROSS_IMPL_RECONCILIATION_OUT) produces a reconciliation.json
byte-identical to OspreySharp's IOTest.TestReconciliationFileRoundTrip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Clamp experiment q-value to each entry's best run q-value (#49)

Experiment-level FDR competes each precursor's single best observation
against a de-duplicated (thinner) decoy null, so the raw experiment
q-value can fall below every per-run q-value. That lets a precursor pass
experiment-level FDR with no single run passing run-level FDR, producing
reported peptides with no run-level ID (the blib ID-line artifact) and an
anti-conservative experiment-wide FDP calibration.

Add clamp_experiment_q_to_best_run, which floors each entry's
experiment_precursor_qvalue / experiment_peptide_qvalue up to that
entry's best (min-over-runs) combined run q-value (FdrLevel::Both =
max(run_precursor, run_peptide)). The precursor floor keys on the full
entry_id (decoy high bit included) and the peptide floor on
(modified_sequence, is_decoy) - both target/decoy-specific, so a target
never inherits its paired decoy's good run. An empty modified_sequence
(a stub loaded without the column) has no peptide identity and is skipped.

The clamp runs at the end of run_percolator_fdr for both the direct and
streaming paths (covering first-pass and the reconciliation-aware second
pass), and again unconditionally on the final post-reconciliation pool
right before the blib output gate, since Stage-6 reconciliation resets
the run q-value of moved / gap-fill peaks after the in-pass clamp.

The two HPC --join-at-pass=2 recompute paths are left unclamped for now,
each marked with a PARITY NOTE for a follow-up.

This mirrors PercolatorEngine.ClampExperimentQToBestRun and its two call
sites in the C# implementation, for cross-impl parity.

* Percolator: always stream, remove the direct/small-experiment path [WIP]

Unify on the streaming path regardless of experiment size: drop the
use_streaming dispatch and the run_percolator_fdr_direct function. The direct
path trained the SVM on ALL entries with a different standardizer-fit
population; streaming subsamples best-per-precursor. There is no reason to hold
every file's features resident, so removing the direct path lowers the memory
ceiling and leaves a single code path.

Mirrors the C# streaming-only change (ProteoWizard/pwiz branch
Skyline/work/20260708_osprey_percolator_streaming_only) to keep the two
implementations at cross-impl parity. Verified: end-to-end bit-parity with C#
at 1e-9 on Stellar (57112 precursors, Stage-7 protein FDR + blib content match).
cargo fmt / clippy -D warnings / cargo test all pass.

* Condition intensity features with log10 to prevent Percolator hijack (#53)

* Condition intensity features with log10 to prevent Percolator hijack

peak_apex, peak_area, and peak_sharpness were emitted as raw linear
intensities. The single experiment-wide Percolator standardizer maps a lone
high-intensity DIA interference to a z-score of 100-300 that dominates the
linear SVM discriminant, letting intensity outliers (a random mix of target /
decoy / entrapment) hijack the top of the score ranking (floored q-values, a
top-of-ranking FDP spike, and a full run collapsing to zero IDs at 1:1
entrapment).

Apply log10(x + 1) to all three in their feature computations, matching
Skyline mProphet's MQuestIntensityCalc. A linear SVM cannot learn a saturating
transform on its own, so it must be applied in the feature. Contained to these
PIN features; peak.area stays raw for quantification.

Mirrors the paired ProteoWizard/pwiz change to keep C#/Rust cross-impl parity;
verified with Compare-EndToEnd-Crossimpl at 1e-9 on Stellar and Astral. cargo
fmt / clippy -D warnings / cargo test all pass.

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

* Floor peak_sharpness at 0 before log10 to avoid a non-finite feature

The apex is an override/CWT lookup, not the recomputed reference-XIC max, so a
supplied apex below an edge yields a negative mean slope. (sharpness + 1.0).log10()
is then non-finite for sharpness <= -1; flooring the input with sharpness.max(0.0)
before the log keeps the feature well-defined. peak_apex and peak_area are >= 0 by
construction and unchanged.

Mirrors the paired ProteoWizard/pwiz change to keep C#/Rust cross-impl parity;
re-verified with Compare-EndToEnd-Crossimpl at 1e-9 on Stellar and Astral. cargo
fmt / clippy -D warnings / cargo test all pass.

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

* Correct the join-at-pass=2 clamp comments

The two PARITY NOTE comments at the --join-at-pass=2 recompute sites claimed
the HPC path does not apply clamp_experiment_q_to_best_run and that a follow-up
should add one. That overstates the gap. There is no early return between those
recompute sites and the authoritative clamp on the final post-Stage-6 pool, and
the only consumer of experiment q -- the blib output gate -- runs after it. (The
protein report is written just before the clamp but takes parsimony and
protein_fdr_result, not the entries' experiment q.) So the HPC path is clamped;
the notes predate the authoritative clamp added later on this branch.

Comment-only change; no behavior difference.

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

* Address Copilot review: fix the v3 envelope rejection in the rescore worker

hydrate_for_rescore hard-coded `format_version != 2` as a redundant second check.
read_reconciliation_file already validates format_version against
RECONCILIATION_FORMAT_VERSION, which this branch bumps to 3, so the literal check
rejected every envelope the pipeline can now write -- the per-file rescore worker
was dead on this branch and no test covers it, which is why CI stayed green.
Removed the check rather than re-pinning it to 3: a duplicate version gate that
must be kept in sync is exactly what drifted.

Also:
* Corrected the v2 changelog entry, which described `file_stems` as optional via
  serde(default) when it is required and v1 is rejected at deserialization.
* Extracted condition_intensity_feature (log10(max(x, 0) + 1)) and unit-tested the
  contract: 0 -> 0, decade scaling, monotonicity, and negative inputs flooring to a
  finite 0. Flooring apex/area is a no-op (both are >= 0 by construction), so PIN
  values stay bit-identical to the C# side.

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

* Fix clippy doc_lazy_continuation in condition_intensity_feature docs

A doc line began with ">= 0", which markdown parses as a block-quote marker, so
the following lines became lazy continuations and clippy::doc_lazy_continuation
fired under -D warnings. Reworded to spell out "non-negative" instead of opening
a line with the >= operator. Comment-only; no behavior change.

---------

Co-authored-by: brendanx67 <brendanx@proteinms.net>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brendan MacLean <brendanx67@users.noreply.github.com>
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.

1 participant