Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CompMs.Common.Enum;
using CompMs.Common.Extension;
using CompMs.Common.Interfaces;
using CompMs.Common.Utility;
using System;
using System.Collections.Generic;
using System.IO;
Expand Down Expand Up @@ -83,7 +84,7 @@ private static Node GetSimpleNode<T>(PeakScanPair<T> peakScanPair, double minVal
}

private static string GetOntologyColor<T>(T spot) where T : IMoleculeProperty, IChromatogramPeak {
var isCharacterized = !spot.Name.IsEmptyOrNull() && !spot.Name.Contains("Unknown") && !spot.Name.Contains("w/o MS2") && !spot.Name.Contains("RIKEN");
var isCharacterized = AnnotationName.IsReferenceMatched(spot.Name);
if (isCharacterized && MetaboliteColorCode.metabolite_colorcode.TryGetValue(spot.Ontology, out var backgroundcolor)) {
return backgroundcolor;
}
Expand Down
114 changes: 114 additions & 0 deletions src/Common/CommonStandard/Utility/AnnotationName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;

namespace CompMs.Common.Utility
{
/// <summary>
/// Owns the prefixes MS-DIAL writes into a feature name when an annotation was
/// not accepted as a reference match, and the predicate that recognizes them.
/// </summary>
/// <remarks>
/// The suggestion prefixes are written by
/// <c>DataAccess.SetMoleculeMsPropertyAsSuggested</c> and
/// <c>DataAccess.SetPeptideMsPropertyAsSuggested</c> in MsdialCore. Both are
/// reached only from the annotation branches where
/// <c>MsScanMatchResult.IsReferenceMatched</c> is false, so every prefixed name is
/// a suggestion rather than an accepted identification.
///
/// Writers and the predicate live together here so a renamed prefix cannot leave a
/// reader stale. MS-DIAL 4 wrote a single "w/o MS2:" prefix for its precursor-only
/// suggestions; MS-DIAL 5 splits that bucket into <see cref="NoMs2Prefix"/> and
/// <see cref="LowScorePrefix"/>, and a reader that knows only the MS-DIAL 4
/// spelling silently treats both MS-DIAL 5 shapes as characterized.
///
/// This type lives in CommonStandard because the vocabulary is shared by
/// CommonStandard (molecular networking) and MsdialCore (export), and the project
/// dependency runs MsdialCore -> CommonStandard only.
/// </remarks>
public static class AnnotationName
{
/// <summary>Separator between a prefix and the reference name.</summary>
public const string PrefixSeparator = ": ";

/// <summary>
/// MS-DIAL 5 prefix for a precursor-only suggestion: the feature has no
/// product-ion spectrum at all (MS2RawSpectrumID &lt; 0).
/// </summary>
public const string NoMs2Prefix = "no MS2";

/// <summary>
/// MS-DIAL 5 prefix for a suggestion that does have a product-ion spectrum but
/// did not meet the reference-search acceptance criteria.
/// </summary>
public const string LowScorePrefix = "low score";

/// <summary>
/// Prefix written by the MS-DIAL 5 peptide suggestion path, and by every
/// MS-DIAL 4 precursor-only suggestion.
/// </summary>
public const string WithoutMs2Prefix = "w/o MS2";

/// <summary>Prefix of an unannotated feature.</summary>
public const string UnknownPrefix = "Unknown";

/// <summary>Placeholder written when a reference field is absent.</summary>
public const string NullPrefix = "null";

/// <summary>Placeholder written when a reference field is present but empty.</summary>
public const string EmptyPrefix = "empty";

/// <summary>Prefix of an in-house RIKEN placeholder record.</summary>
public const string RikenPrefix = "RIKEN";

/// <summary>
/// Matched instead of <see cref="WithoutMs2Prefix"/> so that the MS-DIAL 4
/// "w/o MS2:" spelling, which has no space before the colon, is also covered.
/// </summary>
private const string WITHOUT_MS2_MATCH_PREFIX = "w/o";

private static readonly string[] NOT_REFERENCE_MATCHED_PREFIXES = {
UnknownPrefix,
NullPrefix,
EmptyPrefix,
NoMs2Prefix,
LowScorePrefix,
WITHOUT_MS2_MATCH_PREFIX,
RikenPrefix,
};

/// <summary>Builds the name of a precursor-only suggestion.</summary>
public static string AsNoMs2(string referenceName) {
return NoMs2Prefix + PrefixSeparator + referenceName;
}

/// <summary>Builds the name of a suggestion that failed the search criteria.</summary>
public static string AsLowScore(string referenceName) {
return LowScorePrefix + PrefixSeparator + referenceName;
}

/// <summary>Builds the name of a peptide suggestion without product-ion evidence.</summary>
public static string AsWithoutMs2(string referenceName) {
return WithoutMs2Prefix + PrefixSeparator + referenceName;
}

/// <summary>
/// Returns true when <paramref name="name"/> reads as an accepted reference
/// match rather than an unannotated feature or a suggestion.
/// </summary>
/// <remarks>
/// This is a predicate on the exported name only. Where the authoritative
/// <c>MsScanMatchResult</c> is available, evaluate that instead.
/// </remarks>
public static bool IsReferenceMatched(string name) {
if (string.IsNullOrWhiteSpace(name)) {
return false;
}
var value = name.TrimStart();
foreach (var prefix in NOT_REFERENCE_MATCHED_PREFIXES) {
if (value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {
return false;
}
}
return true;
}
}
}
16 changes: 4 additions & 12 deletions src/MSDIAL5/MsdialCore/Utility/DataAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ public static void SetPeptideMsPropertyAsSuggested(ChromatogramPeakFeature featu
var type = AdductIon.GetAdductIon(adductString);

feature.SetAdductType(type);
feature.Name = "w/o MS2: " + result.Name;
feature.Name = AnnotationName.AsWithoutMs2(result.Name);
}

public static void SetMoleculeMsProperty(ChromatogramPeakFeature feature, MoleculeMsReference reference, MsScanMatchResult result, bool isTextDB = false) {
Expand All @@ -1218,10 +1218,10 @@ public static void SetMoleculeMsPropertyAsSuggested(ChromatogramPeakFeature feat
SetMoleculePropertyCore(feature, reference);
feature.SetAdductType(reference.AdductType);
if (feature.MS2RawSpectrumID < 0) {
feature.Name = "no MS2: " + result.Name;
feature.Name = AnnotationName.AsNoMs2(result.Name);
}
else {
feature.Name = "low score: " + result.Name;
feature.Name = AnnotationName.AsLowScore(result.Name);
}
}

Expand Down Expand Up @@ -1428,15 +1428,7 @@ public static double GetSpotValue(AlignmentChromPeakFeature spotProperty, string
}

public static bool IsReferenceMatchedName(string name) {
if (string.IsNullOrWhiteSpace(name)) {
return false;
}
var value = name.TrimStart();
return !value.StartsWith("Unknown", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("null", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("empty", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("w/o", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("RIKEN", StringComparison.OrdinalIgnoreCase);
return AnnotationName.IsReferenceMatched(name);
}

public static List<ChromatogramPeakFeature> GetChromPeakFeatureObjectsIntegratingRtAndDriftData(List<ChromatogramPeakFeature> features) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using CompMs.Common.Components;
using CompMs.Common.DataObj.NodeEdge;
using CompMs.Common.DataObj.Property;
using CompMs.Common.Enum;
using CompMs.Common.Interfaces;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;

namespace CompMs.Common.Algorithm.Function.Tests
{
[TestClass]
public class MoleculerNetworkingTests
{
private const string ONTOLOGY = "SM";
private static readonly string ONTOLOGY_COLOR = MetaboliteColorCode.metabolite_colorcode[ONTOLOGY];
private const string UNCHARACTERIZED_COLOR = "rgb(0,0,0)";

private sealed class TestSpot : IMoleculeProperty, IChromatogramPeak
{
public int ID { get; set; }
public ChromXs ChromXs { get; set; } = new ChromXs(1d);
public double Mass { get; set; } = 700d;
public double Intensity { get; set; }
public string Name { get; set; }
public Formula Formula { get; set; }
public string Ontology { get; set; }
public string SMILES { get; set; }
public string InChIKey { get; set; }
}

/// <summary>
/// Every node carries an ontology MetaboliteColorCode knows, so the background
/// colour is decided by the name shape alone.
/// </summary>
private static Dictionary<string, string> GetNodeColorsByName(params string[] names) {
var spots = names
.Select((name, i) => new TestSpot { ID = i, Name = name, Ontology = ONTOLOGY, Intensity = 100d * (i + 1), })
.ToList();
var scans = spots
.Select(spot => (IMSScanProperty)new MSScanProperty(spot.ID, spot.Mass, new RetentionTime(1d), IonMode.Positive))
.ToList();

var instance = new MoleculerNetworkingBase()
.GetMolecularNetworkInstance(spots, scans, new MolecularNetworkingQuery(), report: null);

return instance.Root.nodes.ToDictionary(node => node.data.Name, node => node.data.backgroundcolor);
}

/// <summary>
/// Regression test for the ontology colour of the four MS-DIAL 5 name shapes.
/// The predicate used to test only the MS-DIAL 4 "w/o MS2" spelling, so
/// "no MS2: " and "low score: " suggestions were coloured as if they were
/// accepted annotations.
/// </summary>
[TestMethod]
public void OntologyColorIsGivenOnlyToAcceptedAnnotations() {
var colors = GetNodeColorsByName(
"SM 18:1;O2/16:0",
"no MS2: SM 18:1;O2/16:0",
"low score: SM 18:1;O2/16:0",
"Unknown");

Assert.AreEqual(ONTOLOGY_COLOR, colors["SM 18:1;O2/16:0"], "an accepted reference match keeps its ontology colour");
Assert.AreEqual(UNCHARACTERIZED_COLOR, colors["no MS2: SM 18:1;O2/16:0"], "a precursor-only suggestion has no product-ion evidence");
Assert.AreEqual(UNCHARACTERIZED_COLOR, colors["low score: SM 18:1;O2/16:0"], "a low-score suggestion failed the search criteria");
Assert.AreEqual(UNCHARACTERIZED_COLOR, colors["Unknown"], "an unannotated feature has no ontology");
}

[TestMethod]
public void OntologyColorIsWithheldFromPeptideSuggestions() {
var colors = GetNodeColorsByName("SM 18:1;O2/16:0", "w/o MS2: SM 18:1;O2/16:0");

Assert.AreEqual(ONTOLOGY_COLOR, colors["SM 18:1;O2/16:0"]);
Assert.AreEqual(UNCHARACTERIZED_COLOR, colors["w/o MS2: SM 18:1;O2/16:0"]);
}
}
}
75 changes: 75 additions & 0 deletions tests/Common/CommonStandardTests/Utility/AnnotationNameTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;

Comment on lines +1 to +2
namespace CompMs.Common.Utility.Tests
{
[TestClass]
public class AnnotationNameTests
{
[TestMethod]
public void IsReferenceMatchedAcceptsPlainName() {
Assert.IsTrue(AnnotationName.IsReferenceMatched("PC 34:1"));
Assert.IsTrue(AnnotationName.IsReferenceMatched("FA 16:0"));
Assert.IsTrue(AnnotationName.IsReferenceMatched("Reference compound"));
}

[TestMethod]
public void IsReferenceMatchedRejectsNoMs2Suggestion() {
// MS-DIAL 5 precursor-only suggestion: no product-ion spectrum at all.
Assert.IsFalse(AnnotationName.IsReferenceMatched("no MS2: PC 34:1"));
}

[TestMethod]
public void IsReferenceMatchedRejectsLowScoreSuggestion() {
// MS-DIAL 5 suggestion that has a product-ion spectrum but failed the
// reference-search acceptance criteria.
Assert.IsFalse(AnnotationName.IsReferenceMatched("low score: PC 34:1"));
}

[TestMethod]
public void IsReferenceMatchedRejectsUnknown() {
Assert.IsFalse(AnnotationName.IsReferenceMatched("Unknown"));
Assert.IsFalse(AnnotationName.IsReferenceMatched("unknown feature"));
}

[TestMethod]
public void IsReferenceMatchedRejectsWithoutMs2Suggestion() {
// Still written by the MS-DIAL 5 peptide suggestion path, and by MS-DIAL 4.
Assert.IsFalse(AnnotationName.IsReferenceMatched("w/o MS2: PC 34:1"));
Assert.IsFalse(AnnotationName.IsReferenceMatched("w/o MS2:PC 34:1"));
}

[TestMethod]
public void IsReferenceMatchedRejectsPlaceholders() {
Assert.IsFalse(AnnotationName.IsReferenceMatched(null));
Assert.IsFalse(AnnotationName.IsReferenceMatched(""));
Assert.IsFalse(AnnotationName.IsReferenceMatched(" "));
Assert.IsFalse(AnnotationName.IsReferenceMatched("null"));
Assert.IsFalse(AnnotationName.IsReferenceMatched("empty"));
Assert.IsFalse(AnnotationName.IsReferenceMatched("RIKEN MS/MS"));
}

[TestMethod]
public void IsReferenceMatchedIgnoresLeadingWhitespace() {
Assert.IsFalse(AnnotationName.IsReferenceMatched(" no MS2: PC 34:1"));
Assert.IsFalse(AnnotationName.IsReferenceMatched(" low score: PC 34:1"));
}

[TestMethod]
public void WritersProduceTheHistoricalSpellings() {
Assert.AreEqual("no MS2: PC 34:1", AnnotationName.AsNoMs2("PC 34:1"));
Assert.AreEqual("low score: PC 34:1", AnnotationName.AsLowScore("PC 34:1"));
Assert.AreEqual("w/o MS2: PC 34:1", AnnotationName.AsWithoutMs2("PC 34:1"));
}

/// <summary>
/// The invariant that the stale "w/o MS2"-only predicate violated: every name a
/// suggestion writer produces must be rejected by the reader.
/// </summary>
[TestMethod]
public void EverySuggestionWriterProducesANonMatchedName() {
Assert.IsFalse(AnnotationName.IsReferenceMatched(AnnotationName.AsNoMs2("PC 34:1")));
Assert.IsFalse(AnnotationName.IsReferenceMatched(AnnotationName.AsLowScore("PC 34:1")));
Assert.IsFalse(AnnotationName.IsReferenceMatched(AnnotationName.AsWithoutMs2("PC 34:1")));
}
}
}
4 changes: 3 additions & 1 deletion tests/MSDIAL5/MsdialCoreTests/Utility/DataAccessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ public void GetAverageSpectrumTest() {

[TestMethod()]
public void ReferenceMatchedExportUsesAnnotationName() {
foreach (var name in new[] { "", " ", "Unknown", "unknown feature", "null", "empty", "w/o MS2: compound", "RIKEN MS/MS" }) {
// "no MS2: " and "low score: " are the two shapes SetMoleculeMsPropertyAsSuggested
// writes in MS-DIAL 5; "w/o MS2: " is still written by the peptide path.
foreach (var name in new[] { "", " ", "Unknown", "unknown feature", "null", "empty", "no MS2: compound", "low score: compound", "w/o MS2: compound", "RIKEN MS/MS" }) {
var peak = new AlignmentChromPeakFeature { Name = name };
Assert.AreEqual("FALSE", DataAccess.GetSpotValueAsString(peak, "Reference matched"), name);
Assert.AreEqual(0d, DataAccess.GetSpotValue(peak, "Reference matched"), name);
Expand Down
Loading