diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index 3aa700eb02..7b7365cfd5 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -147,6 +147,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.OnnxTransform. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.LightGBM.StaticPipe", "src\Microsoft.ML.LightGBM.StaticPipe\Microsoft.ML.LightGBM.StaticPipe.csproj", "{22C51B08-ACAE-47B2-A312-462DC239A23B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.TimeSeries.StaticPipe", "src\Microsoft.ML.TimeSeries.StaticPipe\Microsoft.ML.TimeSeries.StaticPipe.csproj", "{06A147ED-15EA-4106-9105-9B745125B470}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -781,6 +783,18 @@ Global {22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-Intrinsics|Any CPU.Build.0 = Release-Intrinsics|Any CPU {22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-netfx|Any CPU.ActiveCfg = Release-netfx|Any CPU {22C51B08-ACAE-47B2-A312-462DC239A23B}.Release-netfx|Any CPU.Build.0 = Release-netfx|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug-Intrinsics|Any CPU.ActiveCfg = Debug-Intrinsics|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug-Intrinsics|Any CPU.Build.0 = Debug-Intrinsics|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug-netfx|Any CPU.ActiveCfg = Debug-netfx|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Debug-netfx|Any CPU.Build.0 = Debug-netfx|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release|Any CPU.Build.0 = Release|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release-Intrinsics|Any CPU.ActiveCfg = Release-Intrinsics|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release-Intrinsics|Any CPU.Build.0 = Release-Intrinsics|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release-netfx|Any CPU.ActiveCfg = Release-netfx|Any CPU + {06A147ED-15EA-4106-9105-9B745125B470}.Release-netfx|Any CPU.Build.0 = Release-netfx|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -843,6 +857,7 @@ Global {2F25EF6A-C754-45BE-AD9E-7DDF46A1B51A} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {D1324668-9568-40F4-AA55-30A9A516C230} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {22C51B08-ACAE-47B2-A312-462DC239A23B} = {09EADF06-BE25-4228-AB53-95AE3E15B530} + {06A147ED-15EA-4106-9105-9B745125B470} = {09EADF06-BE25-4228-AB53-95AE3E15B530} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs index 2cc96116b6..4c76446e5a 100644 --- a/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs +++ b/src/Microsoft.ML.Core/Properties/AssemblyInfo.cs @@ -45,6 +45,7 @@ [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.HalLearners.StaticPipe" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.OnnxTransform.StaticPipe" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.LightGBM.StaticPipe" + PublicKey.Value)] +[assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.TimeSeries.StaticPipe" + PublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "Microsoft.ML.Internal.MetaLinearLearner" + InternalPublicKey.Value)] [assembly: InternalsVisibleTo(assemblyName: "TreeVisualizer" + InternalPublicKey.Value)] diff --git a/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj b/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj new file mode 100644 index 0000000000..08eaf4f029 --- /dev/null +++ b/src/Microsoft.ML.TimeSeries.StaticPipe/Microsoft.ML.TimeSeries.StaticPipe.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + + + + + + + + + diff --git a/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs b/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs new file mode 100644 index 0000000000..5bb29f7f99 --- /dev/null +++ b/src/Microsoft.ML.TimeSeries.StaticPipe/TimeSeriesStatic.cs @@ -0,0 +1,314 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.ML.Core.Data; +using Microsoft.ML.StaticPipe.Runtime; +using Microsoft.ML.TimeSeriesProcessing; + +namespace Microsoft.ML.StaticPipe +{ + using IidBase = Microsoft.ML.TimeSeriesProcessing.SequentialAnomalyDetectionTransformBase; + using SsaBase = Microsoft.ML.TimeSeriesProcessing.SequentialAnomalyDetectionTransformBase; + + /// + /// Static API extension methods for . + /// + public static class IidChangePointStaticExtensions + { + private sealed class OutColumn : Vector + { + public PipelineColumn Input { get; } + + public OutColumn( + Scalar input, + int confidence, + int changeHistoryLength, + IidBase.MartingaleType martingale, + double eps) + : base(new Reconciler(confidence, changeHistoryLength, martingale, eps), input) + { + Input = input; + } + } + + private sealed class Reconciler : EstimatorReconciler + { + private readonly int _confidence; + private readonly int _changeHistoryLength; + private readonly IidBase.MartingaleType _martingale; + private readonly double _eps; + + public Reconciler( + int confidence, + int changeHistoryLength, + IidBase.MartingaleType martingale, + double eps) + { + _confidence = confidence; + _changeHistoryLength = changeHistoryLength; + _martingale = martingale; + _eps = eps; + } + + public override IEstimator Reconcile(IHostEnvironment env, + PipelineColumn[] toOutput, + IReadOnlyDictionary inputNames, + IReadOnlyDictionary outputNames, + IReadOnlyCollection usedNames) + { + Contracts.Assert(toOutput.Length == 1); + var outCol = (OutColumn)toOutput[0]; + return new IidChangePointEstimator(env, + inputNames[outCol.Input], + outputNames[outCol], + _confidence, + _changeHistoryLength, + _martingale, + _eps); + } + } + + /// + /// Perform IID change point detection over a column of time series data. See . + /// + public static Vector IidChangePointDetect( + this Scalar input, + int confidence, + int changeHistoryLength, + IidBase.MartingaleType martingale = IidBase.MartingaleType.Power, + double eps = 0.1) => new OutColumn(input, confidence, changeHistoryLength, martingale, eps); + } + + /// + /// Static API extension methods for . + /// + public static class IidSpikeDetectorStaticExtensions + { + private sealed class OutColumn : Vector + { + public PipelineColumn Input { get; } + + public OutColumn(Scalar input, + int confidence, + int pvalueHistoryLength, + IidBase.AnomalySide side) + : base(new Reconciler(confidence, pvalueHistoryLength, side), input) + { + Input = input; + } + } + + private sealed class Reconciler : EstimatorReconciler + { + private readonly int _confidence; + private readonly int _pvalueHistoryLength; + private readonly IidBase.AnomalySide _side; + + public Reconciler( + int confidence, + int pvalueHistoryLength, + IidBase.AnomalySide side) + { + _confidence = confidence; + _pvalueHistoryLength = pvalueHistoryLength; + _side = side; + } + + public override IEstimator Reconcile(IHostEnvironment env, + PipelineColumn[] toOutput, + IReadOnlyDictionary inputNames, + IReadOnlyDictionary outputNames, + IReadOnlyCollection usedNames) + { + Contracts.Assert(toOutput.Length == 1); + var outCol = (OutColumn)toOutput[0]; + return new IidSpikeEstimator(env, + inputNames[outCol.Input], + outputNames[outCol], + _confidence, + _pvalueHistoryLength, + _side); + } + } + + /// + /// Perform IID spike detection over a column of time series data. See . + /// + public static Vector IidSpikeDetect( + this Scalar input, + int confidence, + int pvalueHistoryLength, + IidBase.AnomalySide side = IidBase.AnomalySide.TwoSided + ) => new OutColumn(input, confidence, pvalueHistoryLength, side); + } + + /// + /// Static API extension methods for . + /// + public static class SsaChangePointStaticExtensions + { + private sealed class OutColumn : Vector + { + public PipelineColumn Input { get; } + + public OutColumn(Scalar input, + int confidence, + int changeHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + ErrorFunctionUtils.ErrorFunction errorFunction, + SsaBase.MartingaleType martingale, + double eps) + : base(new Reconciler(confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, errorFunction, martingale, eps), input) + { + Input = input; + } + } + + private sealed class Reconciler : EstimatorReconciler + { + private readonly int _confidence; + private readonly int _changeHistoryLength; + private readonly int _trainingWindowSize; + private readonly int _seasonalityWindowSize; + private readonly ErrorFunctionUtils.ErrorFunction _errorFunction; + private readonly SsaBase.MartingaleType _martingale; + private readonly double _eps; + + public Reconciler( + int confidence, + int changeHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + ErrorFunctionUtils.ErrorFunction errorFunction, + SsaBase.MartingaleType martingale, + double eps) + { + _confidence = confidence; + _changeHistoryLength = changeHistoryLength; + _trainingWindowSize = trainingWindowSize; + _seasonalityWindowSize = seasonalityWindowSize; + _errorFunction = errorFunction; + _martingale = martingale; + _eps = eps; + } + + public override IEstimator Reconcile(IHostEnvironment env, + PipelineColumn[] toOutput, + IReadOnlyDictionary inputNames, + IReadOnlyDictionary outputNames, + IReadOnlyCollection usedNames) + { + Contracts.Assert(toOutput.Length == 1); + var outCol = (OutColumn)toOutput[0]; + return new SsaChangePointEstimator(env, + inputNames[outCol.Input], + outputNames[outCol], + _confidence, + _changeHistoryLength, + _trainingWindowSize, + _seasonalityWindowSize, + _errorFunction, + _martingale, + _eps); + } + } + + /// + /// Perform SSA change point detection over a column of time series data. See . + /// + public static Vector SsaChangePointDetect( + this Scalar input, + int confidence, + int changeHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + ErrorFunctionUtils.ErrorFunction errorFunction = ErrorFunctionUtils.ErrorFunction.SignedDifference, + SsaBase.MartingaleType martingale = SsaBase.MartingaleType.Power, + double eps = 0.1) => new OutColumn(input, confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, errorFunction, martingale, eps); + } + + /// + /// Static API extension methods for . + /// + public static class SsaSpikeDetecotStaticExtensions + { + private sealed class OutColumn : Vector + { + public PipelineColumn Input { get; } + + public OutColumn(Scalar input, + int confidence, + int pvalueHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + SsaBase.AnomalySide side, + ErrorFunctionUtils.ErrorFunction errorFunction) + : base(new Reconciler(confidence, pvalueHistoryLength, trainingWindowSize, seasonalityWindowSize, side, errorFunction), input) + { + Input = input; + } + } + + private sealed class Reconciler : EstimatorReconciler + { + private readonly int _confidence; + private readonly int _pvalueHistoryLength; + private readonly int _trainingWindowSize; + private readonly int _seasonalityWindowSize; + private readonly SsaBase.AnomalySide _side; + private readonly ErrorFunctionUtils.ErrorFunction _errorFunction; + + public Reconciler( + int confidence, + int pvalueHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + SsaBase.AnomalySide side, + ErrorFunctionUtils.ErrorFunction errorFunction) + { + _confidence = confidence; + _pvalueHistoryLength = pvalueHistoryLength; + _trainingWindowSize = trainingWindowSize; + _seasonalityWindowSize = seasonalityWindowSize; + _side = side; + _errorFunction = errorFunction; + } + + public override IEstimator Reconcile(IHostEnvironment env, + PipelineColumn[] toOutput, + IReadOnlyDictionary inputNames, + IReadOnlyDictionary outputNames, + IReadOnlyCollection usedNames) + { + Contracts.Assert(toOutput.Length == 1); + var outCol = (OutColumn)toOutput[0]; + return new SsaSpikeEstimator(env, + inputNames[outCol.Input], + outputNames[outCol], + _confidence, + _pvalueHistoryLength, + _trainingWindowSize, + _seasonalityWindowSize, + _side, + _errorFunction); + } + } + + /// + /// Perform SSA spike detection over a column of time series data. See . + /// + public static Vector SsaSpikeDetect( + this Scalar input, + int confidence, + int changeHistoryLength, + int trainingWindowSize, + int seasonalityWindowSize, + SsaBase.AnomalySide side = SsaBase.AnomalySide.TwoSided, + ErrorFunctionUtils.ErrorFunction errorFunction = ErrorFunctionUtils.ErrorFunction.SignedDifference + ) => new OutColumn(input, confidence, changeHistoryLength, trainingWindowSize, seasonalityWindowSize, side, errorFunction); + + } +} diff --git a/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj b/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj index bc7f32d660..9b23e12fc9 100644 --- a/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj +++ b/test/Microsoft.ML.TimeSeries.Tests/Microsoft.ML.TimeSeries.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs new file mode 100644 index 0000000000..6e78c83bdf --- /dev/null +++ b/test/Microsoft.ML.TimeSeries.Tests/TimeSeriesStaticTests.cs @@ -0,0 +1,245 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.ML.Data; +using Microsoft.ML.RunTests; +using Microsoft.ML.StaticPipe; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.ML.Tests +{ + public sealed class TimeSeriesStaticTests : BaseTestBaseline + { + public TimeSeriesStaticTests(ITestOutputHelper output) : base(output) + { + } +#pragma warning disable CS0649 // Ignore unintialized field warning + private sealed class ChangePointPrediction + { + // Note that this field must be named "Data"; we ultimately convert + // to a dynamic IDataView in order to extract AsEnumerable + // predictions and that process uses "Data" as the default column + // name for an output column from a static pipeline. + [VectorType(4)] + public double[] Data; + } + + private sealed class SpikePrediction + { + [VectorType(3)] + public double[] Data; + } +#pragma warning restore CS0649 + + private sealed class Data + { + public float Value; + + public Data(float value) => Value = value; + } + + [Fact] + public void ChangeDetection() + { + var env = new MLContext(conc: 1); + const int Size = 10; + var data = new List(Size); + var dataView = env.CreateStreamingDataView(data); + for (int i = 0; i < Size / 2; i++) + data.Add(new Data(5)); + + for (int i = 0; i < Size / 2; i++) + data.Add(new Data((float)(5 + i * 1.1))); + + // Convert to statically-typed data view. + var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar }); + // Build the pipeline + var staticLearningPipeline = staticData.MakeNewEstimator() + .Append(r => r.Value.IidChangePointDetect(80, Size)); + // Train + var detector = staticLearningPipeline.Fit(staticData); + // Transform + var output = detector.Transform(staticData); + + // Get predictions + var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator(); + ChangePointPrediction row = null; + List expectedValues = new List() { 0, 5, 0.5, 5.1200000000000114E-08, 0, 5, 0.4999999995, 5.1200000046080209E-08, 0, 5, 0.4999999995, 5.1200000092160303E-08, + 0, 5, 0.4999999995, 5.12000001382404E-08}; + int index = 0; + while (enumerator.MoveNext() && index < expectedValues.Count) + { + row = enumerator.Current; + + Assert.Equal(expectedValues[index++], row.Data[0], precision: 7); + Assert.Equal(expectedValues[index++], row.Data[1], precision: 7); + Assert.Equal(expectedValues[index++], row.Data[2], precision: 7); + Assert.Equal(expectedValues[index++], row.Data[3], precision: 7); + } + } + + [Fact] + public void ChangePointDetectionWithSeasonality() + { + var env = new MLContext(conc: 1); + const int ChangeHistorySize = 10; + const int SeasonalitySize = 10; + const int NumberOfSeasonsInTraining = 5; + const int MaxTrainingSize = NumberOfSeasonsInTraining * SeasonalitySize; + + var data = new List(); + var dataView = env.CreateStreamingDataView(data); + + for (int j = 0; j < NumberOfSeasonsInTraining; j++) + for (int i = 0; i < SeasonalitySize; i++) + data.Add(new Data(i)); + + for (int i = 0; i < ChangeHistorySize; i++) + data.Add(new Data(i * 100)); + + // Convert to statically-typed data view. + var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar }); + // Build the pipeline + var staticLearningPipeline = staticData.MakeNewEstimator() + .Append(r => r.Value.SsaChangePointDetect(95, ChangeHistorySize, MaxTrainingSize, SeasonalitySize)); + // Train + var detector = staticLearningPipeline.Fit(staticData); + // Transform + var output = detector.Transform(staticData); + + // Get predictions + var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator(); + ChangePointPrediction row = null; + List expectedValues = new List() { 0, -3.31410598754883, 0.5, 5.12000000000001E-08, 0, 1.5700820684432983, 5.2001145245395008E-07, + 0.012414560443710681, 0, 1.2854313254356384, 0.28810801662678009, 0.02038940454467935, 0, -1.0950627326965332, 0.36663890634019225, 0.026956459625565483}; + + int index = 0; + while (enumerator.MoveNext() && index < expectedValues.Count) + { + row = enumerator.Current; + + CompareNumbersWithTolerance(expectedValues[index++], row.Data[0], digitsOfPrecision: 5); // Alert + CompareNumbersWithTolerance(expectedValues[index++], row.Data[1], digitsOfPrecision: 5); // Raw score + CompareNumbersWithTolerance(expectedValues[index++], row.Data[2], digitsOfPrecision: 5); // P-Value score + CompareNumbersWithTolerance(expectedValues[index++], row.Data[3], digitsOfPrecision: 5); // Martingale score + } + } + + [Fact] + public void SpikeDetection() + { + var env = new MLContext(conc: 1); + const int Size = 10; + const int PvalHistoryLength = Size / 4; + + // Generate sample series data with a spike + List data = new List(Size); + var dataView = env.CreateStreamingDataView(data); + for (int i = 0; i < Size / 2; i++) + data.Add(new Data(5)); + data.Add(new Data(10)); // This is the spike + for (int i = 0; i < Size / 2 - 1; i++) + data.Add(new Data(5)); + + // Convert to statically-typed data view. + var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar }); + // Build the pipeline + var staticLearningPipeline = staticData.MakeNewEstimator() + .Append(r => r.Value.IidSpikeDetect(80, PvalHistoryLength)); + // Train + var detector = staticLearningPipeline.Fit(staticData); + // Transform + var output = detector.Transform(staticData); + + // Get predictions + var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator(); + var expectedValues = new List() { + // Alert Score P-Value + new double[] {0, 5, 0.5}, + new double[] {0, 5, 0.5}, + new double[] {0, 5, 0.5}, + new double[] {0, 5, 0.5}, + new double[] {0, 5, 0.5}, + new double[] {1, 10, 0.0}, // alert is on, predicted spike + new double[] {0, 5, 0.261375}, + new double[] {0, 5, 0.261375}, + new double[] {0, 5, 0.50}, + new double[] {0, 5, 0.50} + }; + + SpikePrediction row = null; + for (var i = 0; enumerator.MoveNext() && i < expectedValues.Count; i++) + { + row = enumerator.Current; + + CompareNumbersWithTolerance(expectedValues[i][0], row.Data[0], digitsOfPrecision: 7); + CompareNumbersWithTolerance(expectedValues[i][1], row.Data[1], digitsOfPrecision: 7); + CompareNumbersWithTolerance(expectedValues[i][2], row.Data[2], digitsOfPrecision: 7); + } + } + + [Fact] + public void SsaSpikeDetection() + { + var env = new MLContext(conc: 1); + const int Size = 16; + const int ChangeHistoryLength = Size / 4; + const int TrainingWindowSize = Size / 2; + const int SeasonalityWindowSize = Size / 8; + + // Generate sample series data with a spike + List data = new List(Size); + var dataView = env.CreateStreamingDataView(data); + for (int i = 0; i < Size / 2; i++) + data.Add(new Data(5)); + data.Add(new Data(10)); // This is the spike + for (int i = 0; i < Size / 2 - 1; i++) + data.Add(new Data(5)); + + // Convert to statically-typed data view. + var staticData = dataView.AssertStatic(env, c => new { Value = c.R4.Scalar }); + // Build the pipeline + var staticLearningPipeline = staticData.MakeNewEstimator() + .Append(r => r.Value.SsaSpikeDetect(80, ChangeHistoryLength, TrainingWindowSize, SeasonalityWindowSize)); + // Train + var detector = staticLearningPipeline.Fit(staticData); + // Transform + var output = detector.Transform(staticData); + + // Get predictions + var enumerator = output.AsDynamic.AsEnumerable(env, true).GetEnumerator(); + var expectedValues = new List() { + // Alert Score P-Value + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {0, 0.0, 0.5}, + new double[] {1, 5.0, 0.0}, // alert is on, predicted spike + new double[] {1, -2.5, 0.093146}, + new double[] {0, -2.5, 0.215437}, + new double[] {0, 0.0, 0.465745}, + new double[] {0, 0.0, 0.465745}, + new double[] {0, 0.0, 0.261375}, + new double[] {0, 0.0, 0.377615}, + new double[] {0, 0.0, 0.50} + }; + + SpikePrediction row = null; + for (var i = 0; enumerator.MoveNext() && i < expectedValues.Count; i++) + { + row = enumerator.Current; + + CompareNumbersWithTolerance(expectedValues[i][0], row.Data[0], digitsOfPrecision: 6); + CompareNumbersWithTolerance(expectedValues[i][1], row.Data[1], digitsOfPrecision: 6); + CompareNumbersWithTolerance(expectedValues[i][2], row.Data[2], digitsOfPrecision: 6); + } + } + } +}