From 46e3534b8e96273ebca1487f6917ea6b345988e2 Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Wed, 19 Aug 2026 11:34:13 +0900 Subject: [PATCH 1/2] Clarify Console info and RT correction commands --- .../MsdialCoreTestApp/Process/EicProcess.cs | 275 ------------------ .../MsdialCoreTestApp/Process/MainProcess.cs | 110 +++++-- .../Process/RtCorrectionProcess.cs | 259 +++++++++++++++++ tests/MSDIAL5/MsdialCoreTestApp/Program.cs | 3 +- .../Process/MainProcessCommandTests.cs | 40 +++ 5 files changed, 391 insertions(+), 296 deletions(-) create mode 100644 tests/MSDIAL5/MsdialCoreTestApp/Process/RtCorrectionProcess.cs create mode 100644 tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Process/EicProcess.cs b/tests/MSDIAL5/MsdialCoreTestApp/Process/EicProcess.cs index 648107fae..20099e9cb 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Process/EicProcess.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Process/EicProcess.cs @@ -1,13 +1,10 @@ -using CompMs.App.MsdialConsole.Parser; using CompMs.Common.Components; using CompMs.Common.DataObj; using CompMs.Common.Enum; using CompMs.Common.Extension; -using CompMs.Common.Parser; using CompMs.MsdialCore.DataObj; using CompMs.MsdialCore.Parameter; using CompMs.MsdialCore.Parser; -using CompMs.MsdialLcmsApi.Parameter; using CompMs.RawDataHandler.Core; using System; using System.Collections.Generic; @@ -32,10 +29,6 @@ public int Run(string[] args) { if (subcommand == "project") { return RunProject(args); } - if (subcommand == "rtcorrection") { - return RunRtCorrection(args); - } - Console.Error.WriteLine($"Invalid eic subcommand: {subcommand}"); return -1; } @@ -90,85 +83,6 @@ public int RunProject(FileInfo inputFile, FileInfo outputFile, string outputForm return -1; } - public int RunRtCorrection( - IReadOnlyList inputPaths, - FileInfo libraryFile, - FileInfo outputFile, - FileInfo? methodFile, - FileInfo? selectionFile, - IonMode ionMode, - AcquisitionType acquisitionType) { - if (inputPaths.Count == 0) { - return ArgsError(); - } - if (!libraryFile.Exists) { - Console.Error.WriteLine($"RT correction standard library was not found: {libraryFile.FullName}"); - return -1; - } - - var standards = TextLibraryParser.StandardTextLibraryReader(libraryFile.FullName, out var error) - ?.Where(standard => standard.IsTargetMolecule) - .OrderBy(standard => standard.ChromXs.RT.Value) - .ToList() ?? []; - if (!error.IsEmptyOrNull()) { - Console.Error.WriteLine(error); - } - if (standards.Count == 0) { - Console.Error.WriteLine("No enabled RT correction standards were found."); - return -1; - } - - var rawFiles = ResolveRawFiles(inputPaths.Select(path => path.FullName)); - if (rawFiles.Count == 0) { - Console.Error.WriteLine("No supported raw data files were found."); - return -1; - } - - Directory.CreateDirectory(Path.GetDirectoryName(outputFile.FullName) ?? "."); - var parameter = methodFile is null - ? new MsdialLcmsParameter() - : ConfigParser.ReadForLcmsParameter(methodFile.FullName); - parameter.IonMode = ionMode; - parameter.CompoundListForRtCorrectionPath = libraryFile.FullName; - parameter.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.ExcuteRtCorrection = true; - parameter.RtCorrectionPeakSelectionFilePath = selectionFile?.FullName ?? string.Empty; - var analysisFiles = CreateRtCorrectionAnalysisFiles(rawFiles, acquisitionType, outputFile.FullName); - RetentionTimeCorrectionProcess.Prepare( - analysisFiles, - parameter, - Path.GetDirectoryName(outputFile.FullName) ?? "."); - var analysisFilesByPath = analysisFiles.ToDictionary( - file => Path.GetFullPath(file.AnalysisFilePath), - StringComparer.OrdinalIgnoreCase); - using var writer = new StreamWriter(outputFile.FullName, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - writer.WriteLine("FileName,FilePath,StandardId,StandardName,ReferenceRT,RTTolerance,TargetMz,MzTolerance,MinimumHeight,ScanId,RT,CorrectedRT,Intensity,SmoothedIntensity"); - var successfulFiles = 0; - foreach (var rawFile in rawFiles) { - Console.WriteLine($"Extracting RT correction EICs: {rawFile}"); - var spectra = LoadMeasurement(rawFile); - if (spectra.Count == 0) { - Console.Error.WriteLine($"No raw spectra were found: {rawFile}"); - continue; - } - WriteRtCorrectionEics( - writer, - rawFile, - standards, - spectra, - ionMode, - acquisitionType, - analysisFilesByPath[Path.GetFullPath(rawFile)]); - successfulFiles++; - } - - if (successfulFiles == 0) { - Console.Error.WriteLine("RT correction EIC extraction failed for every input file."); - return -1; - } - Console.WriteLine($"RT correction EIC audit CSV: {outputFile.FullName}"); - return 0; - } - private int RunRaw(string[] args) { var inputFile = string.Empty; @@ -213,194 +127,6 @@ private int RunRaw(string[] args) return RunRaw(new FileInfo(inputFile), new FileInfo(outputFile), targets, acquisitionType); } - private int RunRtCorrection(string[] args) { - var inputPaths = new List(); - FileInfo? libraryFile = null; - FileInfo? methodFile = null; - FileInfo? selectionFile = null; - FileInfo? outputFile = null; - var acquisitionType = AcquisitionType.DDA; - var ionMode = IonMode.Positive; - - for (var i = 2; i < args.Length; i++) { - if (args[i] == "-i" && i + 1 < args.Length) { - inputPaths.Add(ToFileSystemInfo(args[++i])); - } - else if (args[i] == "-library" && i + 1 < args.Length) { - libraryFile = new FileInfo(args[++i]); - } - else if (args[i] == "-o" && i + 1 < args.Length) { - outputFile = new FileInfo(args[++i]); - } - else if (args[i] == "-m" && i + 1 < args.Length) { - methodFile = new FileInfo(args[++i]); - } - else if (args[i] == "-selection" && i + 1 < args.Length) { - selectionFile = new FileInfo(args[++i]); - } - else if (args[i] == "-acquisitiontype" && i + 1 < args.Length) { - if (!Enum.TryParse(args[++i], true, out acquisitionType)) { - return ArgsError(); - } - } - else if (args[i] == "-ionmode" && i + 1 < args.Length) { - if (!Enum.TryParse(args[++i], true, out ionMode)) { - return ArgsError(); - } - } - } - - if (inputPaths.Count == 0 || libraryFile is null || outputFile is null) { - return ArgsError(); - } - return RunRtCorrection(inputPaths, libraryFile, outputFile, methodFile, selectionFile, ionMode, acquisitionType); - } - - private static List CreateRtCorrectionAnalysisFiles( - IReadOnlyList rawFiles, - AcquisitionType acquisitionType, - string outputFile) { - var outputDirectory = Path.GetDirectoryName(Path.GetFullPath(outputFile)) ?? "."; - var runId = DateTime.Now.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); - return rawFiles.Select((rawFile, index) => { - var name = Path.GetFileNameWithoutExtension(rawFile); - return new AnalysisFileBean { - AnalysisFileId = index, - AnalysisFileIncluded = true, - AnalysisFileName = name, - AnalysisFilePath = Path.GetFullPath(rawFile), - AnalysisFileAnalyticalOrder = index + 1, - AnalysisFileClass = "Sample", - AnalysisFileType = AnalysisFileType.Sample, - AcquisitionType = acquisitionType, - RetentionTimeCorrectionBean = new RetentionTimeCorrectionBean( - Path.Combine(outputDirectory, $"{name}_{runId}.rtc")), - }; - }).ToList(); - } - - private static void WriteRtCorrectionEics( - StreamWriter writer, - string rawFile, - IReadOnlyList standards, - IReadOnlyList spectra, - IonMode ionMode, - AcquisitionType acquisitionType, - AnalysisFileBean analysisFile) { - var rawSpectra = new RawSpectra(spectra, ionMode, acquisitionType); - var originalRts = analysisFile.RetentionTimeCorrectionBean.OriginalRt; - var correctedRts = analysisFile.RetentionTimeCorrectionBean.PredictedRt; - foreach (var standard in standards) { - var referenceRt = standard.ChromXs.RT.Value; - var begin = Math.Max(0d, referenceRt - standard.RetentionTimeTolerance); - var end = referenceRt + standard.RetentionTimeTolerance; - var range = new ChromatogramRange(begin, end, ChromXType.RT, ChromXUnit.Min); - using var chromatogram = rawSpectra.GetMS1ExtractedChromatogram( - new MzRange(standard.PrecursorMz, standard.MassTolerance), range); - using var smoothed = chromatogram.ChromatogramSmoothing(SmoothingMethod.LinearWeightedMovingAverage, 3); - for (var i = 0; i < chromatogram.Length; i++) { - writer.WriteLine(string.Join(",", - CsvEscape(Path.GetFileNameWithoutExtension(rawFile)), - CsvEscape(Path.GetFullPath(rawFile)), - standard.ScanID, - CsvEscape(standard.Name), - referenceRt.ToString(CultureInfo.InvariantCulture), - standard.RetentionTimeTolerance.ToString(CultureInfo.InvariantCulture), - standard.PrecursorMz.ToString(CultureInfo.InvariantCulture), - standard.MassTolerance.ToString(CultureInfo.InvariantCulture), - standard.MinimumPeakHeight.ToString(CultureInfo.InvariantCulture), - chromatogram.Id(i), - chromatogram.Time(i).ToString(CultureInfo.InvariantCulture), - MapCorrectedRetentionTime(chromatogram.Time(i), originalRts, correctedRts) - .ToString(CultureInfo.InvariantCulture), - chromatogram.Intensity(i).ToString(CultureInfo.InvariantCulture), - smoothed.Intensity(i).ToString(CultureInfo.InvariantCulture))); - } - } - } - - private static double MapCorrectedRetentionTime( - double retentionTime, - IReadOnlyList? originalRts, - IReadOnlyList? correctedRts) { - if (originalRts is null || correctedRts is null || originalRts.Count == 0 - || originalRts.Count != correctedRts.Count) { - return retentionTime; - } - if (retentionTime <= originalRts[0]) { - return correctedRts[0]; - } - var last = originalRts.Count - 1; - if (retentionTime >= originalRts[last]) { - return correctedRts[last]; - } - var low = 0; - var high = last; - while (low + 1 < high) { - var middle = low + (high - low) / 2; - if (originalRts[middle] < retentionTime) { - low = middle; - } - else { - high = middle; - } - } - var lower = low; - var upper = high; - var width = originalRts[upper] - originalRts[lower]; - if (width <= 0d) { - return correctedRts[lower]; - } - var ratio = (retentionTime - originalRts[lower]) / width; - return correctedRts[lower] + ratio * (correctedRts[upper] - correctedRts[lower]); - } - - private static List ResolveRawFiles(IEnumerable inputPaths) { - var files = new List(); - foreach (var inputPath in inputPaths) { - if ((File.Exists(inputPath) && IsSupportedRawPath(inputPath)) || IsVendorDataDirectory(inputPath)) { - files.Add(Path.GetFullPath(inputPath)); - continue; - } - if (File.Exists(inputPath)) { - Console.Error.WriteLine($"Unsupported raw data path: {inputPath}"); - continue; - } - if (!Directory.Exists(inputPath)) { - Console.Error.WriteLine($"Input path was not found: {inputPath}"); - continue; - } - - files.AddRange(Directory.EnumerateFileSystemEntries(inputPath) - .Where(path => File.Exists(path) || IsVendorDataDirectory(path)) - .Where(IsSupportedRawPath) - .Select(Path.GetFullPath)); - } - return files.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(path => path).ToList(); - } - - private static bool IsVendorDataDirectory(string path) { - return Directory.Exists(path) && IsSupportedRawPath(path); - } - - private static bool IsSupportedRawPath(string path) { - var extension = Path.GetExtension(path); - return extension.Equals(".abf", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".cdf", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".d", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".ibf", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".mzml", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".raw", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".wiff", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".wiff2", StringComparison.OrdinalIgnoreCase); - } - - private static FileSystemInfo ToFileSystemInfo(string path) { - return Directory.Exists(path) - ? new DirectoryInfo(path) - : new FileInfo(path); - } - private static void WriteRawCsv(string outputPath, IReadOnlyList targets, IReadOnlyList spectra, AcquisitionType acquisitionType) { var ionMode = spectra.FirstOrDefault(s => s.MsLevel == 1)?.ScanPolarity == ScanPolarity.Negative ? IonMode.Negative : IonMode.Positive; @@ -632,7 +358,6 @@ private static IReadOnlyList LoadMeasurement(string inputFile) { private static int ArgsError() { Console.Error.WriteLine("MsdialConsoleApp.exe eic raw -i -o -target [-target ...] [-acquisitiontype DDA|DIA]"); Console.Error.WriteLine("MsdialConsoleApp.exe eic project -i -o [-format csv|json]"); - Console.Error.WriteLine("MsdialConsoleApp.exe eic rtcorrection -i [-i ...] -library -o [-m ] [-selection ] [-ionmode Positive|Negative] [-acquisitiontype DDA|SWATH|AIF]"); return -1; } diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs b/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs index e57b6d19b..4cf707a11 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs @@ -2,27 +2,65 @@ using CompMs.App.MsdialConsole.Properties; using CompMs.Common.Enum; using CompMs.Common.Extension; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.CommandLine; using System.IO; +using System.Linq; namespace CompMs.App.MsdialConsole.Process; public static class MainProcess { - public const string LcmsAlignmentQaMatrixCapability = "lcms_alignment_qa_matrix"; + public const string LcmsAlignmentQaMatrixFeature = "lcms_alignment_qa_matrix"; + public const string RtCorrectionReviewFeature = "rt_correction_review"; - public static void SetCapabilitiesCommand(Command root) { - var cmd = new Command("capabilities", "List machine-readable MS-DIAL Console capabilities"); - cmd.SetAction(_ => { - Console.WriteLine("msdial.console.capabilities.v1"); - Console.WriteLine(LcmsAlignmentQaMatrixCapability); - return 0; - }); + public static void SetInfoCommand(Command root) { + var cmd = new Command("info", "Show the Console version and workflow features available to external tools"); + var format = new Option("--format") { + Description = "Output format: text for users or json for external workflow tools", + DefaultValueFactory = _ => "text", + }; + cmd.Options.Add(format); + cmd.SetAction(parseResult => WriteConsoleInfo(parseResult.GetValue(format))); root.Add(cmd); } + private static int WriteConsoleInfo(string? format) { + if (String.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { + var payload = new { + schema = "msdial.console.info.v1", + version = Resources.VERSION, + features = new object[] { + new { + id = LcmsAlignmentQaMatrixFeature, + description = "Export an LC-MS alignment quality-assurance matrix (*.qa.tsv)", + }, + new { + id = RtCorrectionReviewFeature, + description = "Prepare retention-time correction anchor EICs and correction profiles", + command = "rtcorrection", + }, + }, + }; + Console.WriteLine(JsonConvert.SerializeObject(payload, Formatting.Indented)); + return 0; + } + if (!String.Equals(format, "text", StringComparison.OrdinalIgnoreCase)) { + Console.Error.WriteLine($"Unknown info format: {format}. Use text or json."); + return 1; + } + + Console.WriteLine($"MS-DIAL Console {Resources.VERSION}"); + Console.WriteLine("Workflow features detectable by external applications:"); + Console.WriteLine($" {LcmsAlignmentQaMatrixFeature}"); + Console.WriteLine(" Export an LC-MS alignment quality-assurance matrix (*.qa.tsv)."); + Console.WriteLine($" {RtCorrectionReviewFeature}"); + Console.WriteLine(" Prepare retention-time correction anchor EICs and correction profiles with the rtcorrection command."); + return 0; + } + public static int CreateMsp4Model(string inputMspFile, string inputEdgeFile, string outputMspFile) { new MoleculerNetworkProcess().GetMsp4Model(inputMspFile, inputEdgeFile, outputMspFile); return 1; @@ -486,15 +524,50 @@ public static void SetEicCommand(Command root) { parseResult.GetRequiredValue(projectOutput), parseResult.GetValue(format))); - var rtCorrection = new Command("rtcorrection", "Export RT correction EIC audit data"); + eic.Subcommands.Add(raw); + eic.Subcommands.Add(project); + root.Subcommands.Add(eic); + } + + public static void SetRtCorrectionCommand(Command root) { + root.Subcommands.Add(CreateRtCorrectionCommand(hidden: false)); + + // Keep the pre-release command path working without advertising it in help. + var eic = root.Subcommands.FirstOrDefault(command => command.Name == "eic"); + eic?.Subcommands.Add(CreateRtCorrectionCommand(hidden: true)); + } + + private static Command CreateRtCorrectionCommand(bool hidden) { + var rtCorrection = new Command( + "rtcorrection", + "Prepare retention-time correction anchor EICs and correction profiles") { + Hidden = hidden, + }; var rtInputs = new Option("--input", "-i") { Required = true }; rtInputs.Arity = ArgumentArity.OneOrMore; - var rtLibrary = new Option("--library") { Required = true }; - var rtOutput = new Option("--output", "-o") { Required = true }; - var rtMethod = new Option("--method", "-m"); - var rtSelection = new Option("--selection"); - var rtIonMode = new Option("--ionmode") { DefaultValueFactory = _ => IonMode.Positive }; - var rtAcquisitionType = new Option("--acquisitiontype") { DefaultValueFactory = _ => AcquisitionType.DDA }; + rtInputs.Description = "Raw data files, vendor data directories, or a directory containing raw data"; + var rtLibrary = new Option("--library") { + Description = "MS-DIAL text library containing the retention-time correction anchors", + Required = true, + }; + var rtOutput = new Option("--output", "-o") { + Description = "Output CSV containing anchor EICs and corrected retention times", + Required = true, + }; + var rtMethod = new Option("--method", "-m") { + Description = "Optional MS-DIAL LC-MS parameter file", + }; + var rtSelection = new Option("--selection") { + Description = "Optional reviewed anchor-peak selection TSV", + }; + var rtIonMode = new Option("--ionmode") { + Description = "Ion mode used to extract the anchor EICs", + DefaultValueFactory = _ => IonMode.Positive, + }; + var rtAcquisitionType = new Option("--acquisitiontype") { + Description = "Acquisition type of the input data", + DefaultValueFactory = _ => AcquisitionType.DDA, + }; rtCorrection.Options.Add(rtInputs); rtCorrection.Options.Add(rtLibrary); rtCorrection.Options.Add(rtOutput); @@ -502,7 +575,7 @@ public static void SetEicCommand(Command root) { rtCorrection.Options.Add(rtSelection); rtCorrection.Options.Add(rtIonMode); rtCorrection.Options.Add(rtAcquisitionType); - rtCorrection.SetAction(parseResult => new EicProcess().RunRtCorrection( + rtCorrection.SetAction(parseResult => new RtCorrectionProcess().Run( parseResult.GetRequiredValue(rtInputs), parseResult.GetRequiredValue(rtLibrary), parseResult.GetRequiredValue(rtOutput), @@ -511,10 +584,7 @@ public static void SetEicCommand(Command root) { parseResult.GetValue(rtIonMode), parseResult.GetValue(rtAcquisitionType))); - eic.Subcommands.Add(raw); - eic.Subcommands.Add(project); - eic.Subcommands.Add(rtCorrection); - root.Subcommands.Add(eic); + return rtCorrection; } public static void SetImageGenerationCommand(Command root) { diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Process/RtCorrectionProcess.cs b/tests/MSDIAL5/MsdialCoreTestApp/Process/RtCorrectionProcess.cs new file mode 100644 index 000000000..588b4a122 --- /dev/null +++ b/tests/MSDIAL5/MsdialCoreTestApp/Process/RtCorrectionProcess.cs @@ -0,0 +1,259 @@ +using CompMs.App.MsdialConsole.Parser; +using CompMs.Common.Components; +using CompMs.Common.DataObj; +using CompMs.Common.Enum; +using CompMs.Common.Extension; +using CompMs.Common.Parser; +using CompMs.MsdialCore.DataObj; +using CompMs.MsdialLcmsApi.Parameter; +using CompMs.RawDataHandler.Core; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace CompMs.App.MsdialConsole.Process; + +public sealed class RtCorrectionProcess +{ + public int Run( + IReadOnlyList inputPaths, + FileInfo libraryFile, + FileInfo outputFile, + FileInfo? methodFile, + FileInfo? selectionFile, + IonMode ionMode, + AcquisitionType acquisitionType) { + if (inputPaths.Count == 0) { + Console.Error.WriteLine("At least one raw data input is required."); + return -1; + } + if (!libraryFile.Exists) { + Console.Error.WriteLine($"RT correction standard library was not found: {libraryFile.FullName}"); + return -1; + } + + var standards = TextLibraryParser.StandardTextLibraryReader(libraryFile.FullName, out var error) + ?.Where(standard => standard.IsTargetMolecule) + .OrderBy(standard => standard.ChromXs.RT.Value) + .ToList() ?? []; + if (!error.IsEmptyOrNull()) { + Console.Error.WriteLine(error); + } + if (standards.Count == 0) { + Console.Error.WriteLine("No enabled RT correction standards were found."); + return -1; + } + + var rawFiles = ResolveRawFiles(inputPaths.Select(path => path.FullName)); + if (rawFiles.Count == 0) { + Console.Error.WriteLine("No supported raw data files were found."); + return -1; + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputFile.FullName) ?? "."); + var parameter = methodFile is null + ? new MsdialLcmsParameter() + : ConfigParser.ReadForLcmsParameter(methodFile.FullName); + parameter.IonMode = ionMode; + parameter.CompoundListForRtCorrectionPath = libraryFile.FullName; + parameter.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.ExcuteRtCorrection = true; + parameter.RtCorrectionPeakSelectionFilePath = selectionFile?.FullName ?? string.Empty; + var analysisFiles = CreateAnalysisFiles(rawFiles, acquisitionType, outputFile.FullName); + RetentionTimeCorrectionProcess.Prepare( + analysisFiles, + parameter, + Path.GetDirectoryName(outputFile.FullName) ?? "."); + var analysisFilesByPath = analysisFiles.ToDictionary( + file => Path.GetFullPath(file.AnalysisFilePath), + StringComparer.OrdinalIgnoreCase); + using var writer = new StreamWriter( + outputFile.FullName, + false, + new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + writer.WriteLine("FileName,FilePath,StandardId,StandardName,ReferenceRT,RTTolerance,TargetMz,MzTolerance,MinimumHeight,ScanId,RT,CorrectedRT,Intensity,SmoothedIntensity"); + var successfulFiles = 0; + foreach (var rawFile in rawFiles) { + Console.WriteLine($"Extracting RT correction EICs: {rawFile}"); + var spectra = LoadMeasurement(rawFile); + if (spectra.Count == 0) { + Console.Error.WriteLine($"No raw spectra were found: {rawFile}"); + continue; + } + WriteAnchorEics( + writer, + rawFile, + standards, + spectra, + ionMode, + acquisitionType, + analysisFilesByPath[Path.GetFullPath(rawFile)]); + successfulFiles++; + } + + if (successfulFiles == 0) { + Console.Error.WriteLine("RT correction EIC extraction failed for every input file."); + return -1; + } + Console.WriteLine($"RT correction EIC audit CSV: {outputFile.FullName}"); + return 0; + } + + private static List CreateAnalysisFiles( + IReadOnlyList rawFiles, + AcquisitionType acquisitionType, + string outputFile) { + var outputDirectory = Path.GetDirectoryName(Path.GetFullPath(outputFile)) ?? "."; + var runId = DateTime.Now.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); + return rawFiles.Select((rawFile, index) => { + var name = Path.GetFileNameWithoutExtension(rawFile); + return new AnalysisFileBean { + AnalysisFileId = index, + AnalysisFileIncluded = true, + AnalysisFileName = name, + AnalysisFilePath = Path.GetFullPath(rawFile), + AnalysisFileAnalyticalOrder = index + 1, + AnalysisFileClass = "Sample", + AnalysisFileType = AnalysisFileType.Sample, + AcquisitionType = acquisitionType, + RetentionTimeCorrectionBean = new RetentionTimeCorrectionBean( + Path.Combine(outputDirectory, $"{name}_{runId}.rtc")), + }; + }).ToList(); + } + + private static void WriteAnchorEics( + StreamWriter writer, + string rawFile, + IReadOnlyList standards, + IReadOnlyList spectra, + IonMode ionMode, + AcquisitionType acquisitionType, + AnalysisFileBean analysisFile) { + var rawSpectra = new RawSpectra(spectra, ionMode, acquisitionType); + var originalRts = analysisFile.RetentionTimeCorrectionBean.OriginalRt; + var correctedRts = analysisFile.RetentionTimeCorrectionBean.PredictedRt; + foreach (var standard in standards) { + var referenceRt = standard.ChromXs.RT.Value; + var begin = Math.Max(0d, referenceRt - standard.RetentionTimeTolerance); + var end = referenceRt + standard.RetentionTimeTolerance; + var range = new ChromatogramRange(begin, end, ChromXType.RT, ChromXUnit.Min); + using var chromatogram = rawSpectra.GetMS1ExtractedChromatogram( + new MzRange(standard.PrecursorMz, standard.MassTolerance), range); + using var smoothed = chromatogram.ChromatogramSmoothing( + SmoothingMethod.LinearWeightedMovingAverage, 3); + for (var i = 0; i < chromatogram.Length; i++) { + writer.WriteLine(string.Join(",", + CsvEscape(Path.GetFileNameWithoutExtension(rawFile)), + CsvEscape(Path.GetFullPath(rawFile)), + standard.ScanID, + CsvEscape(standard.Name), + referenceRt.ToString(CultureInfo.InvariantCulture), + standard.RetentionTimeTolerance.ToString(CultureInfo.InvariantCulture), + standard.PrecursorMz.ToString(CultureInfo.InvariantCulture), + standard.MassTolerance.ToString(CultureInfo.InvariantCulture), + standard.MinimumPeakHeight.ToString(CultureInfo.InvariantCulture), + chromatogram.Id(i), + chromatogram.Time(i).ToString(CultureInfo.InvariantCulture), + MapCorrectedRetentionTime(chromatogram.Time(i), originalRts, correctedRts) + .ToString(CultureInfo.InvariantCulture), + chromatogram.Intensity(i).ToString(CultureInfo.InvariantCulture), + smoothed.Intensity(i).ToString(CultureInfo.InvariantCulture))); + } + } + } + + private static double MapCorrectedRetentionTime( + double retentionTime, + IReadOnlyList? originalRts, + IReadOnlyList? correctedRts) { + if (originalRts is null || correctedRts is null || originalRts.Count == 0 + || originalRts.Count != correctedRts.Count) { + return retentionTime; + } + if (retentionTime <= originalRts[0]) { + return correctedRts[0]; + } + var last = originalRts.Count - 1; + if (retentionTime >= originalRts[last]) { + return correctedRts[last]; + } + var low = 0; + var high = last; + while (low + 1 < high) { + var middle = low + (high - low) / 2; + if (originalRts[middle] < retentionTime) { + low = middle; + } + else { + high = middle; + } + } + var width = originalRts[high] - originalRts[low]; + if (width <= 0d) { + return correctedRts[low]; + } + var ratio = (retentionTime - originalRts[low]) / width; + return correctedRts[low] + ratio * (correctedRts[high] - correctedRts[low]); + } + + private static List ResolveRawFiles(IEnumerable inputPaths) { + var files = new List(); + foreach (var inputPath in inputPaths) { + if ((File.Exists(inputPath) && IsSupportedRawPath(inputPath)) || IsVendorDataDirectory(inputPath)) { + files.Add(Path.GetFullPath(inputPath)); + continue; + } + if (File.Exists(inputPath)) { + Console.Error.WriteLine($"Unsupported raw data path: {inputPath}"); + continue; + } + if (!Directory.Exists(inputPath)) { + Console.Error.WriteLine($"Input path was not found: {inputPath}"); + continue; + } + + files.AddRange(Directory.EnumerateFileSystemEntries(inputPath) + .Where(path => File.Exists(path) || IsVendorDataDirectory(path)) + .Where(IsSupportedRawPath) + .Select(Path.GetFullPath)); + } + return files + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(path => path) + .ToList(); + } + + private static bool IsVendorDataDirectory(string path) { + return Directory.Exists(path) && IsSupportedRawPath(path); + } + + private static bool IsSupportedRawPath(string path) { + var extension = Path.GetExtension(path); + return extension.Equals(".abf", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".cdf", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".d", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ibf", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".mzml", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".raw", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".wiff", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".wiff2", StringComparison.OrdinalIgnoreCase); + } + + private static IReadOnlyList LoadMeasurement(string inputFile) { + using var access = new RawDataAccess(inputFile, 0, false, false, false); + var measurement = access.GetMeasurement(); + return measurement?.SpectrumList ?? []; + } + + private static string CsvEscape(string value) { + if (value == null) { + return string.Empty; + } + return value.Contains(",") || value.Contains("\"") || value.Contains("\r") || value.Contains("\n") + ? $"\"{value.Replace("\"", "\"\"")}\"" + : value; + } +} diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Program.cs b/tests/MSDIAL5/MsdialCoreTestApp/Program.cs index 12090a2fe..713d317c6 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Program.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Program.cs @@ -183,8 +183,9 @@ public static Task Main(string[] args) { MainProcess.SetImmsCommand(root); MainProcess.SetMsnCommand(root); MainProcess.SetEicCommand(root); + MainProcess.SetRtCorrectionCommand(root); MainProcess.SetImageGenerationCommand(root); - MainProcess.SetCapabilitiesCommand(root); + MainProcess.SetInfoCommand(root); var parseResult = root.Parse(args); return parseResult.InvokeAsync(); } diff --git a/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs b/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs new file mode 100644 index 000000000..b6fcd2daf --- /dev/null +++ b/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs @@ -0,0 +1,40 @@ +using CompMs.App.MsdialConsole.Process; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.CommandLine; +using System.Linq; + +namespace MsdialCoreTestAppTests.Process; + +[TestClass] +public sealed class MainProcessCommandTests +{ + [TestMethod] + public void RtCorrection_IsTopLevelAndLegacyEicPathIsHidden() { + var root = BuildRoot(); + + Assert.IsNotNull(root.Subcommands.SingleOrDefault(command => command.Name == "rtcorrection")); + var eic = root.Subcommands.Single(command => command.Name == "eic"); + var legacy = eic.Subcommands.Single(command => command.Name == "rtcorrection"); + Assert.IsTrue(legacy.Hidden); + Assert.AreEqual(0, root.Parse(["rtcorrection", "--help"]).Errors.Count); + Assert.AreEqual(0, root.Parse(["eic", "rtcorrection", "--help"]).Errors.Count); + } + + [TestMethod] + public void Info_ProvidesMachineReadableFormatForExternalTools() { + var root = BuildRoot(); + var info = root.Subcommands.Single(command => command.Name == "info"); + + StringAssert.Contains(info.Description, "external tools"); + Assert.AreEqual(0, root.Parse(["info", "--format", "json"]).Errors.Count); + Assert.IsTrue(root.Parse(["capabilities"]).Errors.Count > 0); + } + + private static RootCommand BuildRoot() { + var root = new RootCommand("test"); + MainProcess.SetEicCommand(root); + MainProcess.SetRtCorrectionCommand(root); + MainProcess.SetInfoCommand(root); + return root; + } +} From 8091c4d114854cf4224d0209bde6695cc71d7093 Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Wed, 19 Aug 2026 12:29:22 +0900 Subject: [PATCH 2/2] Remove partial Console feature inventory --- .../MsdialCoreTestApp/Process/MainProcess.cs | 49 ------------------- tests/MSDIAL5/MsdialCoreTestApp/Program.cs | 1 - .../Process/MainProcessCommandTests.cs | 8 ++- 3 files changed, 3 insertions(+), 55 deletions(-) diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs b/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs index 4cf707a11..8b9cd44c1 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Process/MainProcess.cs @@ -2,7 +2,6 @@ using CompMs.App.MsdialConsole.Properties; using CompMs.Common.Enum; using CompMs.Common.Extension; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.CommandLine; @@ -13,54 +12,6 @@ namespace CompMs.App.MsdialConsole.Process; public static class MainProcess { - public const string LcmsAlignmentQaMatrixFeature = "lcms_alignment_qa_matrix"; - public const string RtCorrectionReviewFeature = "rt_correction_review"; - - public static void SetInfoCommand(Command root) { - var cmd = new Command("info", "Show the Console version and workflow features available to external tools"); - var format = new Option("--format") { - Description = "Output format: text for users or json for external workflow tools", - DefaultValueFactory = _ => "text", - }; - cmd.Options.Add(format); - cmd.SetAction(parseResult => WriteConsoleInfo(parseResult.GetValue(format))); - root.Add(cmd); - } - - private static int WriteConsoleInfo(string? format) { - if (String.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - var payload = new { - schema = "msdial.console.info.v1", - version = Resources.VERSION, - features = new object[] { - new { - id = LcmsAlignmentQaMatrixFeature, - description = "Export an LC-MS alignment quality-assurance matrix (*.qa.tsv)", - }, - new { - id = RtCorrectionReviewFeature, - description = "Prepare retention-time correction anchor EICs and correction profiles", - command = "rtcorrection", - }, - }, - }; - Console.WriteLine(JsonConvert.SerializeObject(payload, Formatting.Indented)); - return 0; - } - if (!String.Equals(format, "text", StringComparison.OrdinalIgnoreCase)) { - Console.Error.WriteLine($"Unknown info format: {format}. Use text or json."); - return 1; - } - - Console.WriteLine($"MS-DIAL Console {Resources.VERSION}"); - Console.WriteLine("Workflow features detectable by external applications:"); - Console.WriteLine($" {LcmsAlignmentQaMatrixFeature}"); - Console.WriteLine(" Export an LC-MS alignment quality-assurance matrix (*.qa.tsv)."); - Console.WriteLine($" {RtCorrectionReviewFeature}"); - Console.WriteLine(" Prepare retention-time correction anchor EICs and correction profiles with the rtcorrection command."); - return 0; - } - public static int CreateMsp4Model(string inputMspFile, string inputEdgeFile, string outputMspFile) { new MoleculerNetworkProcess().GetMsp4Model(inputMspFile, inputEdgeFile, outputMspFile); return 1; diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Program.cs b/tests/MSDIAL5/MsdialCoreTestApp/Program.cs index 713d317c6..f93191095 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Program.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Program.cs @@ -185,7 +185,6 @@ public static Task Main(string[] args) { MainProcess.SetEicCommand(root); MainProcess.SetRtCorrectionCommand(root); MainProcess.SetImageGenerationCommand(root); - MainProcess.SetInfoCommand(root); var parseResult = root.Parse(args); return parseResult.InvokeAsync(); } diff --git a/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs b/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs index b6fcd2daf..8f1fce649 100644 --- a/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs +++ b/tests/MSDIAL5/MsdialCoreTestAppTests/Process/MainProcessCommandTests.cs @@ -21,20 +21,18 @@ public void RtCorrection_IsTopLevelAndLegacyEicPathIsHidden() { } [TestMethod] - public void Info_ProvidesMachineReadableFormatForExternalTools() { + public void Root_DoesNotExposePartialFeatureInventoryCommands() { var root = BuildRoot(); - var info = root.Subcommands.Single(command => command.Name == "info"); - StringAssert.Contains(info.Description, "external tools"); - Assert.AreEqual(0, root.Parse(["info", "--format", "json"]).Errors.Count); + Assert.IsNull(root.Subcommands.SingleOrDefault(command => command.Name == "info")); Assert.IsTrue(root.Parse(["capabilities"]).Errors.Count > 0); + Assert.IsTrue(root.Parse(["info"]).Errors.Count > 0); } private static RootCommand BuildRoot() { var root = new RootCommand("test"); MainProcess.SetEicCommand(root); MainProcess.SetRtCorrectionCommand(root); - MainProcess.SetInfoCommand(root); return root; } }