Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4b9df2a
Read the per-file FDR sidecars in bounded chunks instead of whole
brendanx67 Sep 3, 2026
9a1eb51
Added the per-file survivor source Stage 7 needs to fold without hold…
brendanx67 Sep 6, 2026
187c0a2
Updated the workflow page and the boundary lists for what #4633 landed
brendanx67 Sep 7, 2026
ad4ef8d
Removed the second-pass retrain toggle and transfer-compete, leaving …
brendanx67 Sep 7, 2026
d969570
Extracted transfer's per-run body so its caller can move to the fan-out
brendanx67 Sep 7, 2026
73dda2e
Added a gate leg for the two non-default pass-2 arms
brendanx67 Sep 7, 2026
4d0bae8
Made SecondPassFDR fold over the runs instead of holding them
brendanx67 Sep 7, 2026
5ed90f3
Finished the Stage 7 fold: the last pool reader, and the silence it left
brendanx67 Sep 7, 2026
1cc6fcc
Pinned the per-file overlay chain's order and its refusal
brendanx67 Sep 7, 2026
cb1d673
Fixed what the review found: three ways the streamed join wrote or th…
brendanx67 Sep 7, 2026
ded6472
Made the gate able to see which Stage 7 shape ran, and stopped a part…
brendanx67 Sep 7, 2026
68cbcff
Cut mode 10 to one arm, and made it assert that the arm engaged
brendanx67 Sep 7, 2026
a9f5190
Changed pass-2 model diagnostics to fold run by run
brendanx67 Sep 7, 2026
2a8c198
Moved the pass-1 product check ahead of the pass-2 diagnostics fold
brendanx67 Sep 8, 2026
89069ca
Addressed code review findings on the pass-2 diagnostics fold
brendanx67 Sep 8, 2026
20fb5c2
Fixed the two Copilot findings this branch actually introduces
brendanx67 Sep 8, 2026
293ade8
Added P16: a report is a derived view, not a phase's private output
brendanx67 Sep 8, 2026
1d58878
Reframed P16 around the streamlined-then-diagnostics resume
brendanx67 Sep 8, 2026
34ea446
Removed the pass-2 retrain and made the report a derived view
brendanx67 Sep 8, 2026
7de1774
Added the pay-later diagnostics gate and fixed the resident pass-2 fold
brendanx67 Sep 8, 2026
9254b83
Merge remote-tracking branch 'origin/master' into Skyline/work/202609…
brendanx67 Sep 8, 2026
652043b
Declared the pass-2 diagnostics product so its fold can be reached
brendanx67 Sep 8, 2026
22f16b2
Corrected the Stage-7 resident-gap disclosure the gate prints
brendanx67 Sep 8, 2026
32ea5f6
Tokened the Stage-7 resident join and gave the ordinary run a voice
brendanx67 Sep 8, 2026
40997e2
Made the straight-through Stage-7 join fold run by run
brendanx67 Sep 8, 2026
b07bd30
Merge remote-tracking branch 'origin/master' into Skyline/work/202609…
brendanx67 Sep 8, 2026
f7920db
Retired --input-scores, the Rust-era input-kind seam
brendanx67 Sep 8, 2026
1a12090
Addressed Copilot review feedback on PR #4646
brendanx67 Sep 9, 2026
c0da9e5
Retired the scores-parquet fallback and fixed the review blockers
brendanx67 Sep 9, 2026
d49b755
Moved BuildRescoredPool's summary back off the per-run source
brendanx67 Sep 9, 2026
07a5650
Stopped the parallel gate counting its own warnings as failures
brendanx67 Sep 9, 2026
3cc4fa7
Recorded why ResolveSidecarBasePath's fallback is not safe to delete
brendanx67 Sep 9, 2026
d78bd9c
Merge branch 'master' into Skyline/work/20260908_osprey_stage7_stream…
brendanx67 Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions pwiz_tools/Osprey/Documentation/Help/en/CommandLine.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ <h1>Osprey command-line usage</h1>
<table>
<tr><th>Argument</th><th>Description</th></tr>
<tr><td><nobr>--task</nobr> &lt;SpectraCache | PerFileScoring | FirstPassFDR | PerFileRescoring | SecondPassFDR | ModelDiagnostics&gt;</td><td>HPC: run exactly one pipeline task (one node = one task). Omit for the full pipeline. SpectraCache stages the .spectra.bin caches; ModelDiagnostics regenerates only the <nobr>--model-diagnostics</nobr> report for a COMPLETED run, writing no other artifact.</td></tr>
<tr><td><nobr>--input-scores</nobr> &lt;paths|dir&gt;</td><td>HPC: one or more .scores.parquet files, or a single directory (<nobr>non-recursive</nobr>). Mutex with <nobr>--input</nobr>.</td></tr>
</table>
<div class="RowType">Logging</div>
<table>
Expand All @@ -87,22 +86,22 @@ <h1>Osprey command-line usage</h1>
<tr><td><nobr>-h</nobr>,&nbsp;<nobr>--help</nobr></td><td>Show this help message ([ascii|unicode|sections|html|&lt;Section&gt;])</td></tr>
<tr><td><nobr>-v</nobr>,&nbsp;<nobr>--version</nobr></td><td>Show version</td></tr>
</table>
<p>EXAMPLES:</p><p> osprey -i sample.mzML -l library.tsv -o results.blib</p><p> osprey -i *.mzML -l library.tsv -o results.blib --resolution hram</p><p>HPC SPLIT (one node = one --task): see --task / --input-scores above.</p><div class="RowType">Distributed execution (HPC)</div>
<p>EXAMPLES:</p><p> osprey -i sample.mzML -l library.tsv -o results.blib</p><p> osprey -i *.mzML -l library.tsv -o results.blib --resolution hram</p><p>HPC SPLIT (one node = one --task): see --task above.</p><div class="RowType">Distributed execution (HPC)</div>
<p>Run with no <code>--task</code> for the whole pipeline in one process. For distributed (HPC / workflow-engine) execution the pipeline splits at its join / fan-out boundaries into four single-task workers &mdash; one node = one <code>--task</code>: <code>PerFileScoring</code> (split, per file) &rarr; <code>FirstPassFDR</code> (join, all files) &rarr; <code>PerFileRescoring</code> (split, per file) &rarr; <code>SecondPassFDR</code> (join, all files). Pass the same <code>--library</code> and search options to every task; the parquet integrity check rejects inputs whose search/library hash does not match.</p>
<pre>
# split 1 - one process per mzML (writes &lt;stem&gt;.scores.parquet, &lt;stem&gt;.calibration.json beside each input)
Osprey --task PerFileScoring -i s1.mzML -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01

# join 1 - one process over ALL parquets (pass a directory so the order is deterministic)
Osprey --task FirstPassFDR --input-scores ./scores_dir -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
# join 1 - one process over ALL runs (pass a sorted list so the order is deterministic)
Osprey --task FirstPassFDR --input-list runs.txt -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
# writes beside each parquet: &lt;stem&gt;.1st-pass.fdr_scores.bin, &lt;stem&gt;.reconciliation.json

# split 2 - one process per file (parquet + its two sidecars co-located)
Osprey --task PerFileRescoring --input-scores s1.scores.parquet -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
Osprey --task PerFileRescoring -i s1.mzML -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
# writes: &lt;stem&gt;.scores-reconciled.parquet

# join 2 - one process over ALL reconciled parquets (writes out.blib)
Osprey --task SecondPassFDR --input-scores ./reconciled_dir -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
# join 2 - one process over ALL runs, reading their reconciled parquets (writes out.blib)
Osprey --task SecondPassFDR --input-list runs.txt -l hela.tsv -o out.blib --resolution unit --protein-fdr 0.01
</pre>
<p><code>--input-scores</code> takes a directory (globbed and sorted internally) or an explicit file list (used in the order given). FirstPassFDR reconciliation is order-sensitive, so for <code>FirstPassFDR</code> and <code>SecondPassFDR</code> pass a directory or a deterministically sorted list. The rehydration sidecars must travel with their parquet into each worker's working directory. Let the scheduler do the fan-out (one file per split process) rather than <code>--parallel-files</code>, which is the single-node multi-file mode.</p>
<p>EVERY task takes <code>-i</code>, naming the DATA files - the same names the first split was given. A join task derives each run's parquet and sidecars from the input stem, so the data file itself need not still exist: what has to be in the worker's working directory (or under <code>--output-dir</code>) is that run's artifacts. FirstPassFDR reconciliation is order-sensitive, so pass a deterministically sorted list - <code>--input-list</code> takes one path per line and is what a cohort past a few hundred runs needs, since <code>-i</code> spends the command line at O(files). Let the scheduler do the fan-out (one file per split process) rather than <code>--parallel-files</code>, which is the single-node multi-file mode.</p>
</body></html>
27 changes: 10 additions & 17 deletions pwiz_tools/Osprey/Osprey.Core/OspreyConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,15 @@ public class OspreyConfig
/// Pipeline-membership flag (read by each task's <c>IsIncluded</c>):
/// include only the per-file fan-out, not the joining tasks. Set by both
/// <c>--task PerFileScoring</c> and <c>--task PerFileRescoring</c>; the
/// concrete behavior depends on the input type. With <c>-i</c> mzML it
/// is the Stage 1-4 worker — each input produces a
/// <c>{stem}.scores.parquet</c> next to it, no FDR, no blib. With
/// <see cref="InputScores"/> it is the Stage 6 rescore worker. The two
/// are told apart by input type (see <see cref="SelectedTask"/>).
/// concrete behavior depends on which of the two selected it.
/// <c>PerFileScoring</c> is the Stage 1-4 worker - each input produces a
/// <c>{stem}.scores.parquet</c> next to it, no FDR, no blib;
/// <c>PerFileRescoring</c> is the Stage 6 rescore worker. The two are told apart
/// by <see cref="SelectedTask"/>, which is the only thing that ever decided it -
/// they used to be told apart by input KIND as well, and that second seam is gone.
/// </summary>
public bool NoJoin { get; set; }

/// <summary>
/// HPC scoring split: when set (non-null, non-empty), skip Stages 1-4
/// entirely and load these per-file scoring caches as the starting
/// point for Stage 5+. Set by <c>--input-scores</c>. When set,
/// <see cref="InputFiles"/> is ignored.
/// </summary>
public List<string> InputScores { get; set; }

/// <summary>
/// HPC: when true, exit after Stage 5 + reconciliation planning,
/// having written the boundary files
Expand Down Expand Up @@ -371,10 +364,10 @@ public class OspreyConfig
/// membership flags above (<see cref="NoJoin"/>,
/// <see cref="StopAfterStage5"/>, <see cref="ExpectReconciledInput"/>)
/// are derived from this and drive each task's <c>IsIncluded</c>; this
/// property additionally lets argument validation enforce the
/// task&#8596;input-type contract (e.g. PerFileScoring takes mzML,
/// PerFileRescore takes <see cref="InputScores"/>) and name the task the
/// user actually typed in error messages.
/// property additionally lets argument validation name the task the user actually
/// typed in error messages. It no longer has an input-KIND contract to enforce:
/// every task takes the same data files, and the second seam that said "you handed
/// me parquets, so Stage 1-4 is done" has retired into these flags.
/// </summary>
public HpcTask? SelectedTask { get; set; }

Expand Down
14 changes: 9 additions & 5 deletions pwiz_tools/Osprey/Osprey.Core/ResidentPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,15 @@ public static class ResidentPaths
/// disclosure table had to be invented to see it at all.</para>
///
/// <para>It admits ONLY the chosen case. A run that takes the resident join because no
/// streamed one was admissible - a straight-through run, where
/// <c>ExpectReconciledInput</c> is false - is not refused by this token, because there
/// is nothing for the operator to choose. That case goes when
/// <c>CanStreamStage7Join</c>'s admission stops being expressed as a CLI flag; then this
/// token is the only way to be resident, and it can be deleted with the switch.</para>
/// streamed one was admissible is not refused by this token, because there is nothing
/// for the operator to choose. That set has SHRUNK to one: the straight-through run
/// used to be in it - <c>CanStreamStage7Join</c>'s admission was the CLI flag
/// <c>ExpectReconciledInput</c>, so the ordinary run could not stream by construction -
/// and it is now derived from the reconciled parquets on disk, which every route
/// satisfies. What is left is a pass-2 mode whose per-file half has no worker
/// (<c>OSPREY_PASS2_QVALUE=transfer</c>), and when that half moves to
/// <c>Pass2PerFileWorker</c> this exemption has no subject, the guard can refuse
/// unconditionally, and this token becomes the only way to be resident.</para>
/// </summary>
public static readonly string STAGE7_STREAM_OFF = @"stage7-stream-off";

Expand Down
29 changes: 29 additions & 0 deletions pwiz_tools/Osprey/Osprey.IO/ParquetScoreCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ public static bool IsSubsetWithoutScoreIndex(string path)
return !HasColumn(path, FIELD_SCORE_INDEX.Name);
}

/// <summary>
/// True when <paramref name="path"/> is a reconciled parquet THIS build can read in the
/// survivor-subset shape: it exists, carries the current
/// <see cref="RECONCILED_SURVIVORS"/> marker, and carries the <c>score_index</c> column
/// that ties each survivor row back to its Stage 4 ordinal.
///
/// <para>The positive form of <see cref="IsSubsetWithoutScoreIndex"/> plus the marker
/// test, in one open, because two callers ask the same question about the same file and
/// asking it twice is what let them drift. One is the Stage 7 refusal that names the
/// stale files; the other is the admission a per-run fold consults BEFORE it commits to
/// rebuilding each run from these parquets - and that admission has to be the SAME
/// question the refusal asks, or a run is admitted to a fold it then aborts.</para>
///
/// <para>Says nothing about whether Stage 6 did any rescore WORK on the file - that is
/// the <c>osprey.rescored</c> footer key and a different question, deciding whether a
/// second Percolator pass is owed. This one asks only whether the rows are readable in
/// the shape a survivor rebuild needs.</para>
/// </summary>
public static bool IsCurrentReconciledSurvivorSubset(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
return false;
var footer = LoadFooterMetadata(path);
footer.TryGetValue(@"osprey.reconciled", out string marker);
if (!string.Equals(marker, RECONCILED_SURVIVORS, StringComparison.Ordinal))
return false;
return HasColumn(path, FIELD_SCORE_INDEX.Name);
}

/// <summary>Whether a parquet's schema carries a column by this name.</summary>
public static bool HasColumn(string path, string columnName)
{
Expand Down
7 changes: 4 additions & 3 deletions pwiz_tools/Osprey/Osprey.Tasks/BlibOutputWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ private static Dictionary<string, long> CreateSourceFiles(
IReadOnlyList<string> fileNames, double fdrThreshold)
{
string libraryIdName = Path.GetFileName(config.LibrarySource.Path);
var inputs = config.InputScores != null && config.InputScores.Count > 0
? config.InputScores.ConvertAll(RescoreHydration.SyntheticInputFromParquet)
: config.InputFiles;
// The data files, on every route. A Stage 7 node used to be handed parquets and
// had to convert them back into data-file names right here, to write the
// SpectrumSourceFiles rows Skyline reads; it is handed the names themselves now.
var inputs = config.InputFiles;
var sourcePathByName = new Dictionary<string, string>();
if (inputs != null)
{
Expand Down
20 changes: 8 additions & 12 deletions pwiz_tools/Osprey/Osprey.Tasks/FirstPassFdrTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,14 @@ internal sealed class FirstPassFdrTask : OspreyTask
/// </summary>
internal static bool IsIncludedFor(OspreyConfig c)
{
bool inputs = c.InputScores != null && c.InputScores.Count > 0;
// The (inputs && StopAfterStage5) clause leans on a CLI-enforced
// invariant: StopAfterStage5 is set by --task FirstPassFDR, which
// requires --input-scores, so StopAfterStage5 implies inputs at
// parse time -- a --task FirstPassFDR run can never reach here without
// InputScores.
// ProgramTests.TestValidateFirstPassFdrRequiresInputScores pins that
// rejection, since the membership truth table (PipelineMembershipTest)
// does not encode the cross-flag dependency on its own.
return (!inputs && !c.NoJoin)
|| (inputs && c.StopAfterStage5)
|| (inputs && !c.NoJoin && !c.ExpectReconciledInput);
// Three clauses over two seams collapsed to one over the task flags. The
// retired term was `inputs` - were parquets supplied - which the truth table
// above shows was never doing independent work: it tracked exactly the tasks
// whose flags already say so. Excluded for the two per-file workers (NoJoin)
// and for the Stage 7 node (ExpectReconciledInput); included for the full
// pipeline, for --task FirstPassFDR itself, and for --task ModelDiagnostics,
// which needs first-pass state to render.
return !c.NoJoin && !c.ExpectReconciledInput;
}

// Stage 5/6 planning byproducts this task publishes. The same four types
Expand Down
Loading