385 lines
20 KiB
Plaintext
385 lines
20 KiB
Plaintext
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.ComponentModel;
|
||
|
|
using System.Globalization;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using System.Windows.Media;
|
||
|
|
using DTS.Common.Base;
|
||
|
|
using DTS.Common.Converters;
|
||
|
|
using DTS.Common.Enums.DASFactory;
|
||
|
|
using DTS.Common.Interface;
|
||
|
|
using DTS.Common.Utils;
|
||
|
|
using DTS.Common.Utilities;
|
||
|
|
using DTS.Common.Utilities.Logging;
|
||
|
|
using DTS.Viewer.Graph.Model;
|
||
|
|
using DTS.Common.Events;
|
||
|
|
using Prism.Events;
|
||
|
|
// ReSharper disable CheckNamespace
|
||
|
|
// ReSharper disable InconsistentNaming
|
||
|
|
// ReSharper disable UnusedMember.Local
|
||
|
|
// ReSharper disable UnassignedGetOnlyAutoProperty
|
||
|
|
|
||
|
|
namespace DTS.Viewer.Graph
|
||
|
|
{
|
||
|
|
public class TestDataSeriesModel : IBaseModel
|
||
|
|
{
|
||
|
|
|
||
|
|
public IGraphViewModel Parent { get; set; }
|
||
|
|
public IEventAggregator _eventAggregator { get; set; }
|
||
|
|
private IChartOptionsModel ChartOptions { get; set; }
|
||
|
|
|
||
|
|
private string _errorMessage = string.Empty;
|
||
|
|
public string ErrorMessage { get => _errorMessage; set { _errorMessage = value; OnPropertyChanged("ErrorMessage"); } }
|
||
|
|
|
||
|
|
private SetReadCalcProgressValueDelegate ReadCalcProgressDelegate { get; set; }
|
||
|
|
private void SetReadCalcProgressValue(string message, double progress)
|
||
|
|
{
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs() { ProgressMessage = message, ProgressPercent = progress, GraphVM = (GraphViewModel)Parent });
|
||
|
|
}
|
||
|
|
|
||
|
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||
|
|
public async Task<ITestDataSeries> GetTestDataAsync(ITestChannel channel, IChartOptionsModel chartOptions, bool bVolts, IPSDReportSettingsModel psdSettings = null)
|
||
|
|
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||
|
|
{
|
||
|
|
return GetTestData(channel, chartOptions, bVolts, psdSettings);
|
||
|
|
}
|
||
|
|
#pragma warning disable 1998
|
||
|
|
public async Task<List<ITestDataSeries>> GetTestDataAsync(List<ITestChannel> channels, IChartOptionsModel chartOptions, bool bVolts, IPSDReportSettingsModel psdSettings = null)
|
||
|
|
#pragma warning restore 1998
|
||
|
|
{
|
||
|
|
var testData = channels.Select(channel =>
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
chartOptions.IsDigitalChannel = channel.Bridge.StartsWith(Common.Enums.Sensors.SensorConstants.BridgeType.DigitalInput.ToString());
|
||
|
|
return GetTestData(channel, chartOptions, bVolts, psdSettings);
|
||
|
|
}
|
||
|
|
catch (DTS.Common.Exceptions.OutOfDataException ex)
|
||
|
|
{
|
||
|
|
throw new Exception($"Failed to read {channel.BinaryFileName} sample {ex.Index}");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
var msg = $"Failed to read {channel.BinaryFileName}";
|
||
|
|
APILogger.Log(msg, ex);
|
||
|
|
throw new Exception(msg);
|
||
|
|
}
|
||
|
|
}).ToList();
|
||
|
|
if (null != psdSettings && psdSettings.ShowEnvelope)
|
||
|
|
{
|
||
|
|
testData.Add(GetEnvelopeChannel(testData));
|
||
|
|
}
|
||
|
|
return testData;
|
||
|
|
}
|
||
|
|
|
||
|
|
public List<ITestDataSeries> GetTestData(List<ITestChannel> channels, IChartOptionsModel chartOptions, bool bVolts, IPSDReportSettingsModel psdSettings = null)
|
||
|
|
{
|
||
|
|
var testData = channels.Select(channel =>
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
chartOptions.IsDigitalChannel = channel.Bridge.StartsWith(Common.Enums.Sensors.SensorConstants.BridgeType.DigitalInput.ToString());
|
||
|
|
return GetTestData(channel, chartOptions, bVolts, psdSettings);
|
||
|
|
}
|
||
|
|
catch (DTS.Common.Exceptions.OutOfDataException ex)
|
||
|
|
{
|
||
|
|
throw new Exception($"Failed to read {channel.BinaryFileName} sample {ex.Index}");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
var msg = $"Failed to read {channel.BinaryFileName}";
|
||
|
|
APILogger.Log(msg, ex);
|
||
|
|
throw new Exception(msg);
|
||
|
|
}
|
||
|
|
}).ToList();
|
||
|
|
if (null != psdSettings && psdSettings.ShowEnvelope)
|
||
|
|
{
|
||
|
|
testData.Add(GetEnvelopeChannel(testData));
|
||
|
|
}
|
||
|
|
return testData;
|
||
|
|
}
|
||
|
|
public ITestDataSeries GetTestData(ITestChannel channel, IChartOptionsModel chartOptions, bool bVolts, IPSDReportSettingsModel psdSettings = null)
|
||
|
|
{
|
||
|
|
return AddTestChannelToChart(channel, chartOptions, bVolts, psdSettings);
|
||
|
|
}
|
||
|
|
|
||
|
|
private const string IEPE_BRIDGE = "IEPE";
|
||
|
|
|
||
|
|
public TestDataSeries AddTestChannelToChart(ITestChannel channel, IChartOptionsModel chartOptions, bool bVolts, IPSDReportSettingsModel psdSettings = null)
|
||
|
|
{
|
||
|
|
if (!string.IsNullOrEmpty(channel.ErrorMessage)) return null;
|
||
|
|
|
||
|
|
if (null == ReadCalcProgressDelegate) ReadCalcProgressDelegate = SetReadCalcProgressValue;
|
||
|
|
|
||
|
|
//keep FFT always unfiltered
|
||
|
|
if (chartOptions.UnitType == Common.Enums.Viewer.ChartUnitTypeEnum.FFT || chartOptions.UnitType == Common.Enums.Viewer.ChartUnitTypeEnum.PSD)
|
||
|
|
{
|
||
|
|
channel.SoftwareFilter = "none";
|
||
|
|
}
|
||
|
|
|
||
|
|
var channelData = Serialization.SliceRaw.File.Reader.ReadChannelsBinaryData(channel,
|
||
|
|
out var frequencies, out var peakMagnitude, out var peakFrequency,
|
||
|
|
ChannelFilter.AdHoc, chartOptions, bVolts, ReadCalcProgressDelegate);
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(channel.ErrorMessage)) return null;
|
||
|
|
|
||
|
|
//try to get a translated version of the recording mode
|
||
|
|
var recordingModeString = channel.ParentModule.RecordingMode;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (Enum.TryParse(channel.ParentModule.RecordingMode, out DFConstantsAndEnums.RecordingMode mode))
|
||
|
|
{
|
||
|
|
var recordingMode = RecordingModeExtensions.ToRecordingModes(mode);
|
||
|
|
recordingModeString = EnumDescriptionTypeConverter.GetEnumDescription(recordingMode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception) { }
|
||
|
|
|
||
|
|
var chData = new TestDataSeries()
|
||
|
|
{
|
||
|
|
TestGroup = channel.Group,
|
||
|
|
TestId = channel.TestId,
|
||
|
|
TestSetupName = channel.TestSetupName,
|
||
|
|
ChannelId = channel.ChannelId,
|
||
|
|
GroupName = channel.ChannelGroupName,
|
||
|
|
HardwareChannel = channel.HardwareChannelName,
|
||
|
|
Bridge = channel.Bridge,
|
||
|
|
SWAAF = channel.SoftwareFilter,
|
||
|
|
HWAAF = channel.ParentModule.AaFilterRateHz.ToString(CultureInfo.CurrentCulture),
|
||
|
|
SampleRate = channel.ParentModule.SampleRateHz.ToString(CultureInfo.CurrentCulture),
|
||
|
|
RecordingMode = recordingModeString,
|
||
|
|
ISOCode = channel.IsoCode,
|
||
|
|
ISOChannelName = channel.IsoChannelName,
|
||
|
|
UserCode = channel.UserCode,
|
||
|
|
UserChannelName = channel.UserChannelName,
|
||
|
|
ChannelName = channel.ChannelName2,
|
||
|
|
Description = channel.Description,
|
||
|
|
SensorSN = channel.SerialNumber,
|
||
|
|
Excitation = channel.Bridge == IEPE_BRIDGE ? "---" : channel.MeasuredExcitationVoltage.ToString("N3"),
|
||
|
|
Polarity = channel.SensorPolarity,
|
||
|
|
EngineeringUnits = channel.Eu
|
||
|
|
};
|
||
|
|
|
||
|
|
var y = new List<double>((int)channelData[1].Length);
|
||
|
|
var x = new List<double>((int)channelData[0].Length);
|
||
|
|
var curSample = 0;
|
||
|
|
var timeMultiplier = Convert.ToDecimal(chartOptions.TimeUnitType == Common.Enums.Viewer.TimeUnitTypeEnum.MS ? 1000D : 1D);
|
||
|
|
|
||
|
|
if (chartOptions.UnitType == Common.Enums.Viewer.ChartUnitTypeEnum.FFT && null == psdSettings) //FFT (and also not PSD)
|
||
|
|
{
|
||
|
|
chData.FFT = true;
|
||
|
|
chData.PeakFrequency = peakFrequency;
|
||
|
|
chData.PeakMagnitude = peakMagnitude;
|
||
|
|
chData.Xvalue = channelData[0];
|
||
|
|
chData.Yvalue = channelData[1];
|
||
|
|
chData.SetStatsFromYValues();
|
||
|
|
}
|
||
|
|
else if (null == psdSettings) //regular data?
|
||
|
|
{
|
||
|
|
chData.FFT = false;
|
||
|
|
var timeZeroIndex = channel.ParentModule.TriggerSampleNumbers.Count != 0
|
||
|
|
? channel.ParentModule.TriggerSampleNumbers[0]
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
var timeUnitRatio = channel.ParentModule.SampleRateHz / timeMultiplier;
|
||
|
|
var startingSample = (int)timeZeroIndex - channel.ParentModule.StartRecordSampleNumber;
|
||
|
|
|
||
|
|
if (channel.HIC != 0 && channel.T2Sample > 0)
|
||
|
|
{
|
||
|
|
chData.HIC = true;
|
||
|
|
chData.HICValue = channel.HIC.ToString("N2");
|
||
|
|
var time1 = (channel.T1Sample - (double)startingSample) / channel.SampleRateHz;
|
||
|
|
var time2 = (channel.T2Sample - (double)startingSample) / channel.SampleRateHz;
|
||
|
|
chData.T1Time = (time1 * 1000).ToString("N4");
|
||
|
|
chData.T2Time = (time2 * 1000).ToString("N4");
|
||
|
|
}
|
||
|
|
chData.Xvalue = channelData[0].Select(val => Convert.ToDouble((decimal)val * timeMultiplier)).ToArray();
|
||
|
|
chData.Yvalue = channelData[1];
|
||
|
|
chData.SetStatsFromChannel(channel);
|
||
|
|
}
|
||
|
|
else //PSD
|
||
|
|
{
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = DTS.Common.Strings.Strings.GeneratingPSD_ResizingData, ProgressPercent = 0 });
|
||
|
|
var timeZeroIndex = channel.ParentModule.TriggerSampleNumbers.Count != 0
|
||
|
|
? channel.ParentModule.TriggerSampleNumbers[0]
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
var timeUnitRatio = channel.ParentModule.SampleRateHz / timeMultiplier;
|
||
|
|
var startingSample = (int)timeZeroIndex - channel.ParentModule.StartRecordSampleNumber;
|
||
|
|
|
||
|
|
//Apply PSD Settings
|
||
|
|
//Step 1: trim data to selected range
|
||
|
|
var selectStart = (int)(startingSample + psdSettings.DataStart * (double)timeUnitRatio);
|
||
|
|
var selectEnd = (int)(startingSample + psdSettings.DataEnd * (double)timeUnitRatio);
|
||
|
|
|
||
|
|
if (selectEnd > selectStart)
|
||
|
|
{
|
||
|
|
channelData[0] = channelData[0].Skip(selectStart).Take(selectEnd - selectStart).ToArray();
|
||
|
|
channelData[1] = channelData[1].Skip(selectStart).Take(selectEnd - selectStart).ToArray();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
selectEnd = curSample;
|
||
|
|
}
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = string.Empty, ProgressPercent = 50 });
|
||
|
|
|
||
|
|
//Step 2: get window type
|
||
|
|
FftSharp.WindowType type;
|
||
|
|
switch (psdSettings.WindowType)
|
||
|
|
{
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.Rectangle:
|
||
|
|
type = FftSharp.WindowType.Rectangular;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.Hamming:
|
||
|
|
type = FftSharp.WindowType.Hamming;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.Blackman:
|
||
|
|
type = FftSharp.WindowType.Blackman;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.BlackmanHarris:
|
||
|
|
type = FftSharp.WindowType.BlackmanHarris;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.FlatTop:
|
||
|
|
type = FftSharp.WindowType.FlatTop;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowType.Hanning:
|
||
|
|
default:
|
||
|
|
type = FftSharp.WindowType.Hanning;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
FftSharp.WindowAveragingType averagingType;
|
||
|
|
switch (psdSettings.WindowAveragingType)
|
||
|
|
{
|
||
|
|
case Common.Enums.Viewer.Reports.WindowAveragingType.PeakHoldMax:
|
||
|
|
averagingType = FftSharp.WindowAveragingType.PeakHoldMax;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowAveragingType.PeakHoldMin:
|
||
|
|
averagingType = FftSharp.WindowAveragingType.PeakHoldMin;
|
||
|
|
break;
|
||
|
|
case Common.Enums.Viewer.Reports.WindowAveragingType.Averaging:
|
||
|
|
default:
|
||
|
|
averagingType = FftSharp.WindowAveragingType.Averaging;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
//Step 3: ffts require input length be an even power of 2
|
||
|
|
var next = Utils.GetEnclosingPower2(channelData[1].Length);
|
||
|
|
var values = channelData[1];
|
||
|
|
if (values.Length < next)
|
||
|
|
{
|
||
|
|
Array.Resize(ref values, next);
|
||
|
|
}
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = string.Empty, ProgressPercent = 100 });
|
||
|
|
|
||
|
|
////Step 4: apply a band pass on the input if requested
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = DTS.Common.Strings.Strings.GeneratingPSD_ApplyingFilters, ProgressPercent = 0 });
|
||
|
|
if (psdSettings.LowPassFilterEnabled)
|
||
|
|
{
|
||
|
|
values = Exocortex.DSP.PassFilter.LowPass(values, channel.ParentModule.SampleRateHz, (double)psdSettings.LowPassFilterFrequency, (Exocortex.DSP.PassFilterType)psdSettings.LowPassFilterType, (uint)psdSettings.LowPassFilterOrder);
|
||
|
|
}
|
||
|
|
if (psdSettings.HighPassFilterEnabled)
|
||
|
|
{
|
||
|
|
values = Exocortex.DSP.PassFilter.HighPass(values, channel.ParentModule.SampleRateHz, (double)psdSettings.HighPassFilterFrequency, (Exocortex.DSP.PassFilterType)psdSettings.HighPassFilterType, (uint)psdSettings.HighPassFilterOrder);
|
||
|
|
}
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = string.Empty, ProgressPercent = 100 });
|
||
|
|
|
||
|
|
//Step 5: get the PSD
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = DTS.Common.Strings.Strings.GeneratingPSD, ProgressPercent = 0 });
|
||
|
|
var psd = FftSharp.Transform.PSD_Welch(values, channel.ParentModule.SampleRateHz, type, (int)psdSettings.WindowWidth, (int)psdSettings.WindowOverlappingPercent, averagingType, ReadCalcProgressDelegate);
|
||
|
|
var freq = FftSharp.Transform.FFTfreq(channel.ParentModule.SampleRateHz, psd.Length);
|
||
|
|
freq[0] = 1;
|
||
|
|
_eventAggregator.GetEvent<GraphChannelReadCalcProgressChangedEvent>().Publish(new GraphChannelReadCalcProgressChangedEventArgs()
|
||
|
|
{ GraphVM = Parent, ProgressMessage = string.Empty, ProgressPercent = 100 });
|
||
|
|
|
||
|
|
//Step 6: Calculate GRMS
|
||
|
|
//math from https://blog.endaq.com/why-the-power-spectral-density-psd-is-the-gold-standard-of-vibration-analysis#Benefits
|
||
|
|
chData.GRMS = CalculateGRMS(freq, psd);
|
||
|
|
|
||
|
|
chData.Yvalue = psd;
|
||
|
|
chData.Xvalue = freq;
|
||
|
|
|
||
|
|
chData.FFT = true;
|
||
|
|
chData.SetStatsFromYValues();
|
||
|
|
}
|
||
|
|
|
||
|
|
channel.Xmax = chData.Xvalue.Max();
|
||
|
|
channel.Xmin = chData.Xvalue.Min();
|
||
|
|
|
||
|
|
chData.GraphColor = new SolidColorBrush(channel.ChannelColor == Colors.Transparent ? Colors.Blue : channel.ChannelColor);
|
||
|
|
return chData;
|
||
|
|
}
|
||
|
|
|
||
|
|
private ITestDataSeries GetEnvelopeChannel(List<ITestDataSeries> data)
|
||
|
|
{
|
||
|
|
if (null == data || data.Count == 0) return new TestDataSeries();
|
||
|
|
//Create blank-ish data series but set to envelope
|
||
|
|
var chData = new TestDataSeries
|
||
|
|
{
|
||
|
|
TestGroup = DTS.Common.Strings.Strings.EnvelopeUnique,
|
||
|
|
TestId = data[0].TestId,
|
||
|
|
TestSetupName = data[0].TestSetupName,
|
||
|
|
ChannelId = DTS.Common.Strings.Strings.EnvelopeUnique,
|
||
|
|
GroupName = DTS.Common.Strings.Strings.EnvelopeUnique,
|
||
|
|
HardwareChannel = DTS.Common.Strings.Strings.EnvelopeUnique,
|
||
|
|
Bridge = DTS.Common.Strings.Strings.Table_NA,
|
||
|
|
SWAAF = DTS.Common.Strings.Strings.Table_NA,
|
||
|
|
HWAAF = DTS.Common.Strings.Strings.Table_NA,
|
||
|
|
SampleRate = data[0].SampleRate,
|
||
|
|
RecordingMode = string.Empty,
|
||
|
|
ISOCode = string.Empty,
|
||
|
|
ISOChannelName = string.Empty,
|
||
|
|
UserCode = string.Empty,
|
||
|
|
UserChannelName = string.Empty,
|
||
|
|
ChannelName = DTS.Common.Strings.Strings.Envelope,
|
||
|
|
Description = DTS.Common.Strings.Strings.Envelope,
|
||
|
|
SensorSN = DTS.Common.Strings.Strings.Envelope,
|
||
|
|
Excitation = DTS.Common.Strings.Strings.Table_NA,
|
||
|
|
Polarity = DTS.Common.Strings.Strings.Table_NA
|
||
|
|
};
|
||
|
|
//freq series is the same
|
||
|
|
var freq = data[0].Xvalue.ToList();
|
||
|
|
//now get the max value at each frequency (the "envelope")
|
||
|
|
var psd = new List<double>();
|
||
|
|
for (var i = 0; i < data[0].Yvalue.Length; i++)
|
||
|
|
{
|
||
|
|
psd.Add(data.Max(tds => tds.Yvalue[i]));
|
||
|
|
}
|
||
|
|
//calculate GRMS of this new "channel"
|
||
|
|
chData.GRMS = CalculateGRMS(freq.ToArray(), psd.ToArray());
|
||
|
|
|
||
|
|
chData.Yvalue = psd.ToArray();
|
||
|
|
chData.Xvalue = freq.ToArray();
|
||
|
|
|
||
|
|
chData.FFT = true;
|
||
|
|
chData.GraphColor = new SolidColorBrush(Colors.Black);
|
||
|
|
|
||
|
|
return chData;
|
||
|
|
}
|
||
|
|
|
||
|
|
private double CalculateGRMS(double[] freq, double[] psd)
|
||
|
|
{
|
||
|
|
var aRMS = new List<double>();
|
||
|
|
for (var i = 0; i < psd.Length - 2 && i < freq.Length - 2; i++)
|
||
|
|
{
|
||
|
|
var N = Math.Log10(psd[i + 1] / psd[i]) / Math.Log10(freq[i + 1] / freq[i]);
|
||
|
|
var ai = N.EqualsDigitPrecision(-1, 1) ?
|
||
|
|
(psd[i] * freq[i]) * Math.Log(freq[i + 1] / freq[i]) :
|
||
|
|
(psd[i] / Math.Pow(freq[i], N)) * (1 / (N + 1)) * (Math.Pow(freq[i + 1], N + 1) - Math.Pow(freq[i], N + 1));
|
||
|
|
if (!double.IsNaN(ai) && !double.IsInfinity(ai)) aRMS.Add(ai);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Math.Sqrt(aRMS.Sum());
|
||
|
|
}
|
||
|
|
|
||
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
||
|
|
public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
|
||
|
|
public bool IsSaved { get; }
|
||
|
|
}
|
||
|
|
}
|