Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
a6b1639
correct Within calculation
Nov 18, 2021
fa4da8b
update unit tests
Nov 18, 2021
3246567
conflicts resolved back to upstream
Feb 4, 2022
a018d4d
Merge remote-tracking branch 'upstream/master'
Feb 15, 2022
15a37d0
Merge remote-tracking branch 'upstream/master'
Feb 17, 2022
892fa45
this is the spot
Feb 18, 2022
211013c
Merge remote-tracking branch 'upstream/master'
Feb 25, 2022
68104ee
Merge branch 'master' of https://github.com/trishorts/mzLib
trishorts Mar 9, 2022
d715a08
Merge remote-tracking branch 'upstream/master'
Mar 16, 2022
3565522
Merge remote-tracking branch 'upstream/master'
Mar 23, 2022
72e7b53
Merge remote-tracking branch 'upstream/master'
Mar 29, 2022
593872a
Merge remote-tracking branch 'upstream/master'
trishorts Apr 13, 2022
42dd034
Merge branch 'master' of https://github.com/trishorts/mzLib
trishorts Apr 13, 2022
fbeaec0
Merge remote-tracking branch 'upstream/master'
trishorts Jun 1, 2022
614ded7
Merge remote-tracking branch 'upstream/master'
Jun 14, 2022
47307c8
Merge branch 'master' of https://github.com/trishorts/mzLib
Jun 14, 2022
28e05ae
Merge remote-tracking branch 'upstream/master'
Jul 6, 2022
0a7c609
Merge remote-tracking branch 'upstream/master'
Jul 26, 2022
630d8c7
Merge remote-tracking branch 'upstream/master'
trishorts Jul 27, 2022
f6a386b
Merge branch 'master' of https://github.com/trishorts/mzLib
trishorts Jul 27, 2022
d673800
Merge remote-tracking branch 'upstream/master'
Sep 11, 2022
675a0ae
Merge branch 'master' of https://github.com/trishorts/mzLib
Sep 11, 2022
15d4baf
Merge remote-tracking branch 'upstream/master'
Sep 27, 2022
03ca9f7
Merge remote-tracking branch 'upstream/master'
Oct 4, 2022
d0a4c79
Merge remote-tracking branch 'upstream/master'
Jan 30, 2023
894b998
Merge remote-tracking branch 'upstream/master'
Mar 15, 2023
dd0c8c9
Merge remote-tracking branch 'upstream/master'
Sep 26, 2024
c6310ae
Merge remote-tracking branch 'upstream/master'
Mar 4, 2026
34063e7
Merge remote-tracking branch 'upstream/master' into mzlib_speclib
Mar 14, 2026
f403791
msl foundation
Mar 14, 2026
3c34038
msl writer
Mar 15, 2026
c85bd81
reader
Mar 15, 2026
800b102
some more tests
Mar 15, 2026
f21a1ab
more tests
Mar 15, 2026
d9f0ce6
benchmark complete
Mar 15, 2026
3144511
custom neutral loss
Mar 15, 2026
dd924ff
compression
Mar 15, 2026
3c2fa47
compression complete
Mar 15, 2026
5cff2cd
streaming
Mar 15, 2026
60ec12a
koina connections
Mar 15, 2026
db7efc8
proteoforms who would have thought
Mar 15, 2026
7b1f46b
proteoform scoring
Mar 16, 2026
15c5709
msl merger
Mar 16, 2026
2e76bc1
python and rust readers for msl
Mar 16, 2026
c31d833
msl test coverage
Mar 16, 2026
eba2b4e
fix test
Mar 16, 2026
aa8b74b
coverage gaps
Mar 16, 2026
f432c0a
polish prompts
Mar 17, 2026
ffe6dac
koina
Mar 17, 2026
0d2366f
mslmerger
Mar 17, 2026
75de486
chore: ignore BenchmarkDotNet.Artifacts output directory
Mar 18, 2026
1aa4d8d
a little help from copilot
Mar 18, 2026
4434d9d
only happens in github
Mar 18, 2026
220b8d7
write streaming
Mar 18, 2026
c8ec65b
wrap up lurking bugs
Mar 18, 2026
ef9b66e
remove unused class
Mar 19, 2026
bc0bdd7
more unused code
Mar 19, 2026
43524d5
test mslstructs
Mar 19, 2026
e8ed559
eliminate more unused code
Mar 19, 2026
bcf57c9
spectrallibrary tests
Mar 19, 2026
8270d64
mslreader coverage
Mar 19, 2026
d3a1b20
write msp or msl depends on file extension in koina
Mar 20, 2026
fd9e0db
msp loaded for bear as msl
Mar 20, 2026
f0e1aa9
tests for stripped modification
Mar 21, 2026
35aecdb
null sequence guard
Mar 21, 2026
b80f7aa
be gentle
Mar 21, 2026
46c6424
normalize retention time of incoming spectra to existing library
Mar 22, 2026
8542188
variable naming consistency
Mar 22, 2026
88244a1
nics review respnse
trishorts Mar 26, 2026
9bd1e0b
internal review
Mar 28, 2026
c383e74
merge upstream
Mar 28, 2026
e909066
review fixes
Mar 28, 2026
202e6ac
Merge remote-tracking branch 'smith/master' into mzlib_speclib
trishorts Jun 8, 2026
0f8caa6
fix: qualify SpectralLibrary type in merged Prosit2020 test (namespac…
trishorts Jun 8, 2026
eeec6a0
perf(msl): defer the sequence/charge index so LoadIndexOnly does no f…
trishorts Jun 9, 2026
89e8c02
perf(chronologer): batched RetentionTimePredictorEquivalents for many…
trishorts Jun 9, 2026
8a249d1
test(msl): cover the deferred sequence/charge index (LoadIndexOnly la…
trishorts Jun 10, 2026
5d8dcf8
fix(msl): skip NaN-m/z prefix in MslIndex m/z queries
trishorts Jun 10, 2026
7ccacdf
chore(development): untrack committed BenchmarkDotNet.Artifacts results
Jun 10, 2026
0826b30
test(msl): assert internal-ion secondary annotation survives ReadFrag…
Jun 10, 2026
9d630e0
docs(msl): clarify proteoform scorer reports library reference peaks
Jun 10, 2026
17b45a8
style(speclib): restore space indentation, dropping tab-reindent churn
Jun 10, 2026
f215f04
chore: drop unrelated test changes from the .msl PR
Jun 10, 2026
d1eb53e
chore(msl): remove agent-residue comments and internal "Prompt N" refs
Jun 10, 2026
92754ed
refactor(msl-tests): rename PromptN fixtures and strip prompt-process…
Jun 10, 2026
0a6b6b4
chore: restore mzLib.nuspec version to 1.0.574
Jun 10, 2026
bdbc247
refactor(rt): drop batched Chronologer PredictRetentionTimeEquivalent…
Jun 10, 2026
2dcf742
fix(chronologer): dispose forward() intermediates to stop native-heap…
trishorts Jun 11, 2026
1fe6f85
test(rtpred): tolerate float32 batch-vs-single drift in Chronologer
trishorts Jun 15, 2026
6b18e92
refactor(chronologer): drop env-var CUDA switch from the batched pred…
trishorts Jun 15, 2026
ffe67c9
docs(msl): clarify MslFragmentIon vs MatchedFragmentIon + round-trip …
trishorts Jun 15, 2026
8fce55d
Merge branch 'master' into mzlib_speclib
trishorts Jun 16, 2026
1d096fc
chore(msl): remove Python and Rust external MSL reader implementations
Jun 18, 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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,11 @@ ModelManifest.xml

# Macintosh files
**/.DS_Store

/.claude
/.opencode
/.opencode/config/coverage.json
/AGENTS.md
# BenchmarkDotNet artifacts - machine-specific, regenerated on every run
**/BenchmarkDotNet.Artifacts/

1 change: 1 addition & 0 deletions mzLib/Development/Development.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
Expand Down
297 changes: 297 additions & 0 deletions mzLib/Development/MSL/MslBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using MassSpectrometry;
using Omics.Fragmentation;
using Omics.SpectralMatch.MslSpectralLibrary;
using Readers.SpectralLibrary;

namespace Development.MSL
{
/// <summary>
/// BenchmarkDotNet suite for core MSL library operations: write, full-load,
/// index-only-load, m/z window query, DDA lookup, index build, and RT calibration.
///
/// Each benchmark is parameterised over three library sizes:
/// NPrecursors = 1 000, 50 000, 500 000
///
/// All benchmarks share a common <see cref="GlobalSetup"/> that writes a synthetic
/// .msl file to a temp path and (for query/lookup/calibration benchmarks) loads it
/// into <see cref="_queryLib"/>. <see cref="GlobalCleanup"/> disposes and deletes
/// the temp file.
/// </summary>
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
[HideColumns("Job", "RntmId", "WarmupCount", "LaunchCount", "TargetCount")]
public class MslBenchmarks
{
// ── Parameters ────────────────────────────────────────────────────────────

[Params(1_000, 50_000, 500_000)]
public int NPrecursors { get; set; }

[Params(10)]
public int AvgFrag { get; set; }

// ── State ─────────────────────────────────────────────────────────────────

private string _tempMslPath = null!;
private MslLibrary _queryLib = null!;
private IReadOnlyList<MslLibraryEntry> _entries = null!;
private MslIndex _lastIndex;
private MslLibrary _lastCalibratedLib;

// ── Setup / Cleanup ───────────────────────────────────────────────────────

[GlobalSetup]
public void GlobalSetup()
{
_tempMslPath = Path.Combine(Path.GetTempPath(),
$"msl_bench_{NPrecursors}_{Guid.NewGuid():N}.msl");
_entries = GenerateEntries(NPrecursors, AvgFrag);
MslWriter.Write(_tempMslPath, _entries);
_queryLib = MslLibrary.Load(_tempMslPath);
}

[IterationCleanup]
public void IterationCleanup()
{
if (_lastIndex is IDisposable disposableIndex)
disposableIndex.Dispose();
_lastIndex = null;

_lastCalibratedLib?.Dispose();
_lastCalibratedLib = null;
}

[GlobalCleanup]
public void GlobalCleanup()
{
IterationCleanup();
_queryLib?.Dispose();
_queryLib = null!;
if (File.Exists(_tempMslPath))
File.Delete(_tempMslPath);
}

// ── Write ─────────────────────────────────────────────────────────────────

/// <summary>
/// Measures the time to serialise <see cref="NPrecursors"/> synthetic entries to
/// a new .msl file. Each iteration writes to a fresh temp path to prevent OS
/// write-caching from dominating subsequent runs.
/// </summary>
[Benchmark]
public void Write_MslLibrary()
{
string path = Path.Combine(Path.GetTempPath(),
$"msl_write_{Guid.NewGuid():N}.msl");
try
{
MslWriter.Write(path, _entries);
}
finally
{
if (File.Exists(path))
File.Delete(path);
}
}

// ── Full load ─────────────────────────────────────────────────────────────

/// <summary>
/// Measures the time to fully deserialise an .msl file — header, string table,
/// protein table, all precursor records, and all fragment blocks — into an
/// in-memory <see cref="MslLibrary"/> ready for querying.
/// </summary>
[Benchmark]
public void FullLoad_MslLibrary()
{
using MslLibrary lib = MslLibrary.Load(_tempMslPath);
}

// ── Index-only load ───────────────────────────────────────────────────────

/// <summary>
/// Measures the end-to-end cost of opening an .msl file in index-only mode:
/// read the header, deserialise the precursor section into the in-memory index,
/// and release the FileStream via Dispose.
///
/// The return type is <c>void</c> and each iteration uses a <c>using</c>
/// declaration so that the <see cref="MslLibrary"/> (and its underlying
/// FileStream) is disposed before the method returns. This prevents multiple
/// undisposed instances from accumulating across BenchmarkDotNet iterations,
/// which would cause a Windows file-locking error in GlobalCleanup.
///
/// The measured time correctly reflects the real-world cost a caller pays when
/// preparing an index-only library for DIA window queries: open → read precursor
/// index → dispose.
/// </summary>
[Benchmark]
public void IndexOnlyLoad_MslLibrary()
{
using MslLibrary lib = MslLibrary.LoadIndexOnly(_tempMslPath);
}

// ── m/z window queries ────────────────────────────────────────────────────

/// <summary>
/// Measures the latency of a single <see cref="MslLibrary.QueryMzWindow"/> call
/// over the fully-loaded library. Uses a fixed 25 Da window centred at 800 m/z.
/// </summary>
[Benchmark]
public int QueryMzWindow_SingleQuery()
{
ReadOnlySpan<MslPrecursorIndexEntry> results =
_queryLib.QueryMzWindow(787.5f, 812.5f);
return results.Length;
}

/// <summary>
/// Measures the throughput of 1 000 sequential <see cref="MslLibrary.QueryMzWindow"/>
/// calls with 25 Da windows stepping by 1 Da from 400 m/z.
/// Represents the hot path during a DIA isolation-window sweep.
/// </summary>
[Benchmark]
public long QueryMzWindow_1000Queries()
{
long total = 0;
for (int i = 0; i < 1000; i++)
{
float centre = 400f + i;
ReadOnlySpan<MslPrecursorIndexEntry> results =
_queryLib.QueryMzWindow(centre - 12.5f, centre + 12.5f);
total += results.Length;
}
return total;
}

// ── DDA lookup ────────────────────────────────────────────────────────────

/// <summary>
/// Measures O(1) dictionary lookup for a sequence/charge key that exists in the
/// library. Return value prevents dead-code elimination.
/// </summary>
[Benchmark]
public bool DdaLookup_ExistingEntry()
{
return _queryLib.TryGetEntry(
_entries[0].FullSequence,
_entries[0].ChargeState,
out _);
}

/// <summary>
/// Measures O(1) dictionary lookup for a sequence/charge key that does NOT exist
/// in the library. Confirms that the negative path is equally fast.
/// </summary>
[Benchmark]
public bool DdaLookup_MissingEntry()
{
return _queryLib.TryGetEntry("ZZZZZZNOTPRESENT", 99, out _);
}

// ── Index build ───────────────────────────────────────────────────────────

/// <summary>
/// Measures the time to construct an <see cref="MslIndex"/> from a list of
/// already-loaded <see cref="MslLibraryEntry"/> objects (in-memory sort + index
/// build, no I/O). The loader delegate provides direct array access so the
/// sequence/charge dictionary is fully populated — matching the real usage pattern
/// inside <see cref="MslLibrary.Load"/>.
/// </summary>
[Benchmark]
public MslIndex BuildIndex_FromEntries()
{
_lastIndex = MslIndex.Build(_entries, i =>
i >= 0 && i < _entries.Count ? _entries[i] : null);
return _lastIndex;
}

// ── RT calibration ────────────────────────────────────────────────────────

/// <summary>
/// Measures the time to apply a linear iRT → run-RT calibration to all precursors
/// in the loaded library, producing a new calibrated <see cref="MslLibrary"/>.
/// This involves copying and transforming every <see cref="MslPrecursorIndexEntry"/>
/// in the sorted index array and rebuilding the sequence/charge dictionary.
/// </summary>
[Benchmark]
public MslLibrary RtCalibration_LinearTransform()
{
_lastCalibratedLib = _queryLib.WithCalibratedRetentionTimes(slope: 1.2, intercept: -5.0);
return _lastCalibratedLib;
}

// ── Synthetic data generation ─────────────────────────────────────────────

/// <summary>
/// Builds a deterministic list of synthetic <see cref="MslLibraryEntry"/> objects.
/// Precursor m/z values are spread across 400–1000 Thomson so that range queries
/// return a realistic non-zero hit count at all three library sizes.
/// Internal to allow reuse from <see cref="MslVsMspBenchmarks"/>.
/// </summary>
internal static IReadOnlyList<MslLibraryEntry> GenerateEntries(int nPrecursors, int avgFrag)
{
var rng = new Random(42);
var entries = new List<MslLibraryEntry>(nPrecursors);

string[] aa = { "A", "C", "D", "E", "F", "G", "H", "I", "K", "L",
"M", "N", "P", "Q", "R", "S", "T", "V", "W", "Y" };

for (int i = 0; i < nPrecursors; i++)
{
// Unique sequence of 7–20 residues, deterministic from index.
// The index suffix guarantees uniqueness across all NPrecursors values.
int seqLen = 7 + (i % 14);
var sb = new StringBuilder(seqLen + 10);
for (int j = 0; j < seqLen; j++)
sb.Append(aa[(i * 7 + j * 13) % aa.Length]);
sb.Append(i);
string seq = sb.ToString();

int charge = 2 + (i % 3); // 2, 3, or 4
double mz = 400.0 + (i % 1000) * 0.6 + rng.NextDouble() * 0.1;
double irt = -30.0 + i * (120.0 / nPrecursors) + rng.NextDouble();

var fragments = new List<MslFragmentIon>(avgFrag);
for (int f = 0; f < avgFrag; f++)
{
fragments.Add(new MslFragmentIon
{
Mz = (float)(100.0 + f * 80.0 + rng.NextDouble() * 10),
Intensity = (float)(rng.NextDouble() + 0.01), // always > 0
ProductType = (f % 2 == 0) ? ProductType.y : ProductType.b,
FragmentNumber = f + 1,
Charge = 1,
NeutralLoss = 0.0
});
}

entries.Add(new MslLibraryEntry
{
FullSequence = seq,
BaseSequence = seq,
PrecursorMz = mz,
ChargeState = charge,
RetentionTime = irt,
IsDecoy = false,
IsProteotypic = true,
ProteinAccession = "BENCH_PROT",
ProteinName = "Benchmark protein",
GeneName = "BENCH",
Source = MslFormat.SourceType.Predicted,
MoleculeType = MslFormat.MoleculeType.Peptide,
DissociationType = DissociationType.HCD,
Nce = 28,
MatchedFragmentIons = fragments
});
}

return entries;
}
}
}
Loading
Loading