Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions pwiz-sharp/Pwiz.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Installer.Tests", "pwiz\tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsBenchmark", "Tools\Commandline\MsBenchmark\src\MsBenchmark.csproj", "{20ABA6CB-AA2D-40B2-99B0-DD7A85BC94DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsDiff", "Tools\Commandline\MsDiff\src\MsDiff.csproj", "{5D1FF001-3D1F-4A55-9E01-000000A5D1FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MsDiff.Tests", "Tools\Commandline\MsDiff\test\MsDiff.Tests.csproj", "{5D1FF002-3D1F-4A55-9E01-000000B5D1FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraData", "pwiz\src\TraData\TraData.csproj", "{8F972942-1D04-4F46-B541-6CBC521FF292}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraData.Tests", "pwiz\test\TraData.Tests\TraData.Tests.csproj", "{DC3E3099-0E07-44E1-8F04-055DD3FD3572}"
Expand Down Expand Up @@ -308,6 +312,14 @@ Global
{20ABA6CB-AA2D-40B2-99B0-DD7A85BC94DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20ABA6CB-AA2D-40B2-99B0-DD7A85BC94DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20ABA6CB-AA2D-40B2-99B0-DD7A85BC94DF}.Release|Any CPU.Build.0 = Release|Any CPU
{5D1FF001-3D1F-4A55-9E01-000000A5D1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D1FF001-3D1F-4A55-9E01-000000A5D1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D1FF001-3D1F-4A55-9E01-000000A5D1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D1FF001-3D1F-4A55-9E01-000000A5D1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{5D1FF002-3D1F-4A55-9E01-000000B5D1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D1FF002-3D1F-4A55-9E01-000000B5D1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D1FF002-3D1F-4A55-9E01-000000B5D1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D1FF002-3D1F-4A55-9E01-000000B5D1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{8F972942-1D04-4F46-B541-6CBC521FF292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F972942-1D04-4F46-B541-6CBC521FF292}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F972942-1D04-4F46-B541-6CBC521FF292}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -409,6 +421,8 @@ Global
{FF96077C-6894-4611-8B73-211F356B88A5} = {050D548F-5BA2-4CC0-839A-A485500BF642}
{2A9B3D7E-4C81-4F12-9E3A-58D1F2B7E406} = {9B31505B-3D7C-4F5F-88CC-2C018DCC041A}
{20ABA6CB-AA2D-40B2-99B0-DD7A85BC94DF} = {1044EA12-4417-4A1F-802D-3B7E0AE3324E}
{5D1FF001-3D1F-4A55-9E01-000000A5D1FF} = {1044EA12-4417-4A1F-802D-3B7E0AE3324E}
{5D1FF002-3D1F-4A55-9E01-000000B5D1FF} = {9B31505B-3D7C-4F5F-88CC-2C018DCC041A}
{8F972942-1D04-4F46-B541-6CBC521FF292} = {1044EA12-4417-4A1F-802D-3B7E0AE3324E}
{DC3E3099-0E07-44E1-8F04-055DD3FD3572} = {9B31505B-3D7C-4F5F-88CC-2C018DCC041A}
{F6D07FD2-A9F1-4A86-8F3C-9370E1EBF5C7} = {050D548F-5BA2-4CC0-839A-A485500BF642}
Expand Down
96 changes: 96 additions & 0 deletions pwiz-sharp/Tools/Commandline/MsDiff/src/ArgParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Pwiz.Data.Common.Diff;

namespace Pwiz.Tools.MsDiff;

/// <summary>Thrown when the user asked for help rather than a comparison.</summary>
internal sealed class ArgParseHelpRequested : Exception;

/// <summary>Parsed msdiff-sharp command line.</summary>
internal sealed class Config
{
public string FileA { get; set; } = string.Empty;
public string FileB { get; set; } = string.Empty;
public DiffConfig DiffConfig { get; } = new();
}

/// <summary>
/// Command-line parsing for msdiff-sharp, matching cpp <c>msdiff</c>'s boost::program_options
/// surface: two positional filenames plus <c>-p</c> and <c>-i</c>.
/// </summary>
internal static class ArgParser
{
public static string Usage() =>
"""
Usage: msdiff [options] filename1 filename2
Compare two mass spec data files.

Options:
-p [ --precision ] arg (=1e-06) : set floating point precision for comparing binary data
-i [ --ignore ] : ignore metadata (compare scan binary data and
important scan metadata only)

Questions, comments, and bug reports:
https://github.com/ProteoWizard
support@proteowizard.org
""";

public static Config Parse(string[] args)
{
ArgumentNullException.ThrowIfNull(args);
var config = new Config();
var positional = new List<string>();

for (int i = 0; i < args.Length; i++)
{
string a = args[i];
switch (a)
{
case "-h" or "--help" or "-?":
throw new ArgParseHelpRequested();

case "-i" or "--ignore":
// cpp declares this zero_tokens, so it never consumes a value.
config.DiffConfig.IgnoreMetadata = true;
break;

case "-p" or "--precision":
config.DiffConfig.Precision = ParsePrecision(Next(args, ref i, a));
break;

default:
if (a.StartsWith("--precision=", StringComparison.Ordinal))
config.DiffConfig.Precision = ParsePrecision(a["--precision=".Length..]);
else if (a.StartsWith('-') && a.Length > 1)
throw new ArgumentException($"unrecognized option '{a}'");
else
positional.Add(a);
break;
}
}

// cpp throws the usage string when it does not get exactly two filenames.
if (positional.Count != 2)
throw new ArgumentException(
$"expected exactly 2 filenames, got {positional.Count}");

config.FileA = positional[0];
config.FileB = positional[1];
return config;
}

private static string Next(string[] args, ref int i, string option)
{
if (i + 1 >= args.Length) throw new ArgumentException($"option '{option}' requires a value");
return args[++i];
}

private static double ParsePrecision(string s)
{
// Invariant culture on purpose: "1e-5" must parse the same wherever this runs, and the
// container sets no particular culture.
if (!double.TryParse(s, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out double v))
throw new ArgumentException($"could not parse precision '{s}'");
return v;
}
}
89 changes: 89 additions & 0 deletions pwiz-sharp/Tools/Commandline/MsDiff/src/Differ.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Pwiz.Data.MsData;
using Pwiz.Data.MsData.Diff;
using Pwiz.Data.MsData.Readers;
using Pwiz.Vendor.Bruker;
using Pwiz.Vendor.Thermo;
using Pwiz.Vendor.Waters;

namespace Pwiz.Tools.MsDiff;

/// <summary>Reads two data files and reports how they differ.</summary>
internal sealed class Differ
{
private readonly ReaderList _readers;

public Differ()
{
// The same list msconvert builds, so msdiff can read anything msconvert can write from.
_readers = ThermoReaderRegistration.CreateDefaultWithThermo();
_readers.Add(new Reader_Bruker());
_readers.Add(new Reader_Waters());
_readers.Add(new Pwiz.Vendor.Agilent.Reader_Agilent());
_readers.Add(new Pwiz.Vendor.Sciex.Reader_Sciex());
_readers.Add(new Pwiz.Vendor.Shimadzu.Reader_Shimadzu());
_readers.Add(new Pwiz.Vendor.UNIFI.Reader_UNIFI());
_readers.Add(new Pwiz.Vendor.UIMF.Reader_UIMF());
_readers.Add(new Pwiz.Vendor.Mobilion.Reader_Mobilion());
}

/// <summary>
/// Compares the two files named in <paramref name="config"/>, writing the report to
/// <paramref name="output"/>. Returns 1 when they differ and 0 when they do not, matching
/// cpp msdiff's <c>return diff;</c>.
/// </summary>
public int Run(Config config, TextWriter output)
{
// Disposed: a vendor- or mzML-backed MSData holds its source file open, and a caller
// that diffs then replaces one of the inputs would otherwise hit a sharing violation.
using var a = Read(config.FileA);
using var b = Read(config.FileB);

var result = MSDataDiff.Compare(a, b, config.DiffConfig);

// The two summary lines come first and are always written, even when nothing differs.
//
// cpp reaches the same wording by printing the diff object through TextWriter, which
// renders each list as "<name> (N <units>)" holding only the entries that differ. It
// prints nothing at all when the files match, because the whole report is guarded by
// `if (diff)`. Callers that grep for "0 spectra" therefore see a FAILURE for two
// identical files - the one case where matching cpp byte for byte would be worse than
// being correct, so these lines are unconditional here.
output.WriteLine(Summary("spectrumList", result.SpectraDiffering, "spectra"));
output.WriteLine(Summary("chromatogramList", result.ChromatogramsDiffering, "chromatograms"));

if (!result.Differs) return 0;

output.Write(result.Report);
return 1;
}

/// <summary>
/// Renders one list's summary line: <c>spectrumList (0 spectra)</c> when nothing differs,
/// <c>spectrumList (12 differing spectra)</c> when something does.
/// </summary>
/// <remarks>
/// The extra word in the non-zero form is deliberate, and it is the one place this port
/// does not simply reproduce cpp's wording. Consumers test for equality by grepping for
/// the unanchored string "0 spectra" - the container's vendor sweep does exactly that -
/// and cpp's uniform "(N spectra)" makes that grep match the tail of any count ending in
/// zero. A Mobilion fixture differing in all 19570 spectra prints "(19570 spectra)",
/// which contains "0 spectra", so the sweep would report a PASS for a file that matched
/// nothing. Putting a word between the digits and the unit means only a real zero can
/// match. The right long-term fix is to anchor the grep on "(0 spectra)"; until every
/// consumer does, emitting a string that cannot be misread is the safer half of the deal.
/// </remarks>
private static string Summary(string listName, int differing, string unit) =>
differing == 0
? $"{listName} (0 {unit})"
: $"{listName} ({differing} differing {unit})";

private MSData Read(string filename)
{
if (!File.Exists(filename) && !Directory.Exists(filename))
throw new FileNotFoundException($"no such file or directory: {filename}", filename);

var msd = new MSData();
_readers.Read(filename, msd, new ReaderConfig());
return msd;
}
}
75 changes: 75 additions & 0 deletions pwiz-sharp/Tools/Commandline/MsDiff/src/MsDiff.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>Pwiz.Tools.MsDiff</RootNamespace>
<AssemblyName>msdiff</AssemblyName>
<Description>Command-line comparison of two mass spec data files. Port of pwiz_tools/commandline/msdiff.</Description>

<!-- Inherits from Pwiz.Vendor.Thermo - the Thermo SDK needs en-US culture. -->
<InvariantGlobalization>false</InvariantGlobalization>

<!-- See the same switch in MsConvert.csproj: the wiff2 SDK deserializes its bundled
assembly resources through BinaryFormatter. -->
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<UseWindowsForms>False</UseWindowsForms>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Util\Util.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Common\Common.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\MsData\MsData.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Analysis\Analysis.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Common\Vendor.Common.csproj" />
</ItemGroup>

<!-- The same vendor set msconvert carries. cpp msdiff builds an ExtendedReaderList, so it can
diff a vendor file directly against an mzML; keeping that means a reader missing here
would silently degrade to "unknown format" rather than compare. -->
<ItemGroup>
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Thermo\Thermo.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Bruker\Bruker.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Waters\Waters.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Agilent\Agilent.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Sciex\Sciex.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Shimadzu\Shimadzu.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\UNIFI\UNIFI.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\UIMF\UIMF.csproj" />
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Mobilion\Mobilion.csproj" />
<!-- Runtime-only plugin; see the identical block in MsConvert.csproj. -->
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Sciex\Wiff2\Sciex.Wiff2.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ProjectReference>
<ProjectReference Include="$(PwizSharpRoot)pwiz\src\Vendor\Sciex\OfxLoggingStub\OfxLoggingStub.csproj"
Condition="'$(NativeVendorsAvailable)' == 'true'">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Content</OutputItemType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ProjectReference>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="MsDiff.Tests" />
</ItemGroup>

<!-- Side-by-side wiff2 dlls. Same rationale and the same trailing-slash trap documented in
MsConvert.csproj's copy of this target; without it every .wiff2 fails to open. -->
<Target Name="StageWiff2NativeAssemblies" AfterTargets="Build" Condition="'$(NativeVendorsAvailable)' == 'true'">
<PropertyGroup>
<_SciexWiff2DepsDir>$(PwizVendorArchivesPath)\Sciex</_SciexWiff2DepsDir>
<_SciexAssembliesDir>$(PwizVendorAssembliesPath)\Sciex</_SciexAssembliesDir>
<_Wiff2OutDir>$([MSBuild]::EnsureTrailingSlash('$(OutputPath)'))wiff2</_Wiff2OutDir>
</PropertyGroup>
<MakeDir Directories="$(_Wiff2OutDir)" />
<Copy SourceFiles="$(_SciexWiff2DepsDir)\System.Data.SQLite.dll" DestinationFiles="$(_Wiff2OutDir)\System.Data.SQLite.dll" OverwriteReadOnlyFiles="true" />
<Copy SourceFiles="$(_SciexAssembliesDir)\SQLite.Interop.dll" DestinationFiles="$(_Wiff2OutDir)\SQLite.Interop.dll" OverwriteReadOnlyFiles="true" />
<Copy SourceFiles="$(_SciexWiff2DepsDir)\Unity.Abstractions.dll" DestinationFiles="$(_Wiff2OutDir)\Unity.Abstractions.dll" OverwriteReadOnlyFiles="true" />
<Copy SourceFiles="@(VendorNativeCrtFile)" DestinationFolder="$(_Wiff2OutDir)" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true" />
</Target>

<!-- VC++ redistributable beside the vendor DLLs; see the targets file. -->
<Import Project="$(PwizRepoRoot)\pwiz_tools\Shared\Lib\VendorNativeCrt.targets" />

</Project>
39 changes: 39 additions & 0 deletions pwiz-sharp/Tools/Commandline/MsDiff/src/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Pwiz.Tools.MsDiff;

/// <summary>Entry point for the msdiff-sharp CLI.</summary>
public static class Program
{
/// <summary>
/// Runs msdiff-sharp. Returns 0 when the two files match, 1 when they differ or the run
/// failed, and 2 for a bad command line - the first two matching cpp msdiff, whose
/// <c>main</c> returns the diff itself and 1 from its catch blocks.
/// </summary>
public static int Main(string[] args)
{
// Hook the vendor SDK on-demand resolver before any Reader_* is touched, exactly as
// msconvert does; without it a vendor input fails to load its SDK.
Pwiz.Vendor.Common.VendorSdkLoader.RegisterAssemblyResolver();
try
{
var config = ArgParser.Parse(args);
return new Differ().Run(config, Console.Out);
}
catch (ArgParseHelpRequested)
{
Console.Out.WriteLine(ArgParser.Usage());
return 0;
}
catch (ArgumentException ex)
{
Console.Error.WriteLine("Error: " + ex.Message);
Console.Error.WriteLine();
Console.Error.WriteLine(ArgParser.Usage());
return 2;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
return 1;
}
}
}
Loading
Loading