/* SaeJ211FilterUtility.cs $Log: FilterUtility.cs,v $ Revision 1.3 2008/11/11 21:56:41 DTS\paul.hrissikopoulos Added Brian's suggested filtering changes. Revision 1.2 2007/01/26 21:53:50 Paul Hrissikopoulos Added bulk of October 2003 SAE J211-compliant filter utility implementation. Revision 1.1 2007/01/13 00:42:00 Paul Hrissikopoulos Added initial support for October 2003-compliant filtering. Revision 1.3 2006/10/26 22:06:47 Paul Hrissikopoulos Added channel data filtering. Revision 1.2 2006/10/23 16:07:16 Paul Hrissikopoulos Added data filtering phase and related support. Revision 1.1 2006/10/18 23:44:11 Paul Hrissikopoulos Added SaeJ211FilterUtility class. Copyright © 2006 Diversified Technical Systems, Inc. All Rights Reserved */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using DTS.Common.Utilities; using DTS.Common.Utilities.DotNetProgrammingConstructs; using System.Linq; namespace DTS.Common.Utilities.SaeJ211 { /// /// Utility for filtering data according to the NHTSA implementation of the /// SAE J211 standard. /// public class FilterUtility : Exceptional { /// /// Get/set the calibration scaling factor to be applied to data, post-filtering. /// Useful for "last minute" scaling or inversion. Defaults to "1.0". /// public double CalibrationFactor { get => _CalibrationFactor.Value; set => _CalibrationFactor.Value = value; } private readonly RangeRestrictedDoubleProperty _CalibrationFactor = new RangeRestrictedDoubleProperty(-1.0, 1.0, 1.0, true); /// /// Get/set the sampling rate (samples/sec) of the data to be filtered. This /// property must be set by user before the data can be filtered. /// public double SampleRate { get => _SampleRate.Value; set => _SampleRate.Value = value; } private readonly RangeRestrictedDoubleProperty _SampleRate = new RangeRestrictedDoubleProperty(0.0, 1000000000.0, 0, false); /// /// Get/set SAE Channel Filter Class to be applied to data. This property must /// be set by user before the data can be filtered. /// public ChannelFilter Cfc { get => _ChannelFilter.Value; set { _ChannelFilter.Value = value; _cfc = new CfcValueAttributeCoder().DecodeAttributeValue(_ChannelFilter.Value); } } private readonly Property _ChannelFilter = new Property( typeof(FilterUtility).Namespace + ".FilterUtility.ChannelFilter", ChannelFilter.Unfiltered, false ); double _cfc; /// /// Get/set the ad hoc frequency of this filter. /// public double AdHocFrequency { get => _AdHocFrequency.Value; set => _AdHocFrequency.Value = value; } private readonly Property _AdHocFrequency = new Property( typeof(FilterUtility).Namespace + ".FilterUtility", -1, false ); /// /// Get/set the zero baseline flag. Defaults to "false". /// public bool ZeroBaseline { get => _ZeroBaseline.Value; set => _ZeroBaseline.Value = value; } private readonly Property _ZeroBaseline = new Property( typeof(FilterUtility).Namespace + ".FilterUtility.ZeroBaseline", false, true ); /// /// Get/set the size (in sec) of the padding added to the beginning and ending of /// the data before filtering to "squelch out" startup spikes. A negative value /// will cause pad to default to 5% of data size. /// public double PadSize { get => _PadSize.Value; set => _PadSize.Value = value; } private readonly Property _PadSize = new Property( typeof(FilterUtility).Namespace + ".FilterUtility.PadSize", -1.0, true ); /// /// Types of pre/post-padding data population available. /// public enum DataPaddingType { /// /// Create data padding by repeatedly copying value of first and last samples. /// FirstAndLast, /// /// Create data padding by mirroring data at beginning and end of data vector. /// Mirror, /// /// Pad the data with zeros /// Zero } /// /// Get/set the type of data padding appended/prepended to the data prior to /// being subjected to the filtering algorithm. /// public DataPaddingType DataPadding { get => _DataPadding.Value; set => _DataPadding.Value = value; } private readonly Property _DataPadding = new Property( typeof(FilterUtility).Namespace + ".FilterUtility.DataPaddingType", DataPaddingType.Mirror, true ); /// /// Get the string that describes the type of filtering performed by this object. /// public string Type => "Butterworth, 4-pole"; public uint NumberOfPreDataPointsToInclude { get => _NumberOfPreDataPointsToInclude.Value; set => _NumberOfPreDataPointsToInclude.Value = value; } private readonly Property _NumberOfPreDataPointsToInclude = new Property(typeof(FilterUtility).Namespace + ".FilterUtility.NumberOfPreDataPointsToInclude", 0, true); /// /// Initialize an instance of the SaeJ211FilterUtility class. /// public FilterUtility() { } /// /// Convert the specified frequency to an equivalent CFC value. Note that the /// "Big 4" CFC values get slightly special treatment. /// /// /// /// The frequency to be converted. /// /// /// /// The CFC equivalent of the specified frequency. /// /// private double FrequencyToCfc(double frequency) { try { double cfc = -1; foreach (ChannelFilter filter in Enum.GetValues(typeof(ChannelFilter))) { if (frequency == (int)filter) { cfc = new CfcValueAttributeCoder().DecodeAttributeValue(filter); break; } } return cfc >= 0 ? cfc : frequency * 0.6; } catch (System.Exception ex) { throw new FilterUtility.Exception("encountered problem converting frequency to CFC value", ex); } } public delegate void InvalidDataDelegate(); public delegate void SetProgressDelegate(double value = -1D); /// /// Apply the SAE J211 filter algorithm (4-pole, phaseless, lo-pass) to the specified /// data using to the current FilterTool properties. /// /// /// /// An array of s containing the raw data to be filtered. /// /// /// /// /// /// An array of s containing the filtered data. /// /// public virtual double[] ApplyFilter(double[] data, InvalidDataDelegate invalidDataFunc = null, bool bUseLegacyTDCFilterAdjustment = false, SetProgressDelegate setProgress = null) { try { if (null != invalidDataFunc) { var invalids = from d in data where double.IsNaN(d) select d; if (invalids.Any()) { invalidDataFunc(); } } //////////////////////////////////////////////////////////////////////////////////// // IMPLEMENTATION NOTE: // // Pads the data with 5% of the original buffer size fore and aft to "squelch" out // filter start-up spikes. Optionally zeros baseline by subtracting the average // of the first 5% of the original data points (skips data which is padded by this // method). Optionally scales the data after filtering. //////////////////////////////////////////////////////////////////////////////////// if (null == data) throw new ArgumentNullException("attempted to apply filter to null data buffer reference"); else { if (Cfc == ChannelFilter.Unfiltered) return data; if (Cfc == ChannelFilter.UnfilteredZero) { return data; } double t, pi, wd, wa, d0, d1, d2, e1, e2; double f1, f2, f3, f4, f5; //////////////////////////////////////////////////////////////////////////////////// // Formula setup //////////////////////////////////////////////////////////////////////////////////// t = 1.0 / SampleRate; pi = 4.0 * System.Math.Atan(1.0); wd = 2.0 * pi * (Cfc == ChannelFilter.AdHoc ? FrequencyToCfc(AdHocFrequency) : _cfc) * 2.0775; wa = (System.Math.Sin(wd * (t / 2.0))) / (System.Math.Cos(wd * (t / 2.0))); // The original formula d0 = (wa * wa) / (1 + System.Math.Sqrt(2.0) * wa + (wa * wa)); d1 = 2.0 * d0; d2 = d0; e1 = -2.0 * ((wa * wa) - 1) / (1 + System.Math.Sqrt(2.0) * wa + (wa * wa)); e2 = (-1 + System.Math.Sqrt(2.0) * wa - (wa * wa)) / (1 + System.Math.Sqrt(2.0) * wa + (wa * wa)); var DataEnd = data.Length - 1; int Pad; if (PadSize >= 0.0) { Pad = (int)(PadSize * SampleRate); } else { // Calculate padding as either 5% of all data, or the time required // for theoretical filter stabilization. Pad = (int)System.Math.Max(0.05 * DataEnd, 4 / AdHocFrequency * SampleRate); } var Z = new double[2 * Pad + data.Length]; var A = new double[2 * Pad + data.Length]; var paddedData = new double[2 * Pad + data.Length]; var FirstIndexOfBeginPad = 0; var LastIndexOfBeginPad = Pad - 1; if ((DataPadding == DataPaddingType.Mirror) && (Pad - FirstIndexOfBeginPad >= data.Length)) { // Avoid fault in Mirror mode DataPadding = DataPaddingType.FirstAndLast; } switch (DataPadding) { // // Populate the start-side pad according to this instance's current // DataPadding setting. // case DataPaddingType.FirstAndLast: for (var i = FirstIndexOfBeginPad; i <= LastIndexOfBeginPad; i++) paddedData[i] = data[0]; // 1st point of data break; case DataPaddingType.Mirror: for (var i = FirstIndexOfBeginPad; i <= LastIndexOfBeginPad; i++) paddedData[i] = data[Pad - i]; // Mirror of data break; case DataPaddingType.Zero: for (var i = FirstIndexOfBeginPad; i <= LastIndexOfBeginPad; i++) paddedData[i] = 0; break; default: throw new FilterUtility.Exception("implementor attempted to use an undefined data padding type"); } // Copy raw data into newData after initial pad. var FirstIndexOfData = LastIndexOfBeginPad + 1; var LastIndexOfData = FirstIndexOfData + DataEnd; for (var i = FirstIndexOfData; i <= LastIndexOfData; i++) paddedData[i] = data[i - Pad]; var FirstIndexOfEndPad = LastIndexOfData + 1; var LastIndexOfEndPad = LastIndexOfData + Pad; switch (DataPadding) { // // Populate the end-side pad according to this instance's current // DataPadding setting. // case DataPaddingType.FirstAndLast: for (var i = FirstIndexOfEndPad; i <= LastIndexOfEndPad; i++) paddedData[i] = data[DataEnd]; // Last point of data break; case DataPaddingType.Mirror: for (var i = FirstIndexOfEndPad; i <= LastIndexOfEndPad; i++) paddedData[i] = paddedData[2 * LastIndexOfData - i]; // Mirror of data break; case DataPaddingType.Zero: for (var i = FirstIndexOfEndPad; i <= LastIndexOfEndPad; i++) paddedData[i] = 0; break; default: throw new FilterUtility.Exception("implementor attempted to use an undefined data padding type"); } var PaddedDataEnd = LastIndexOfEndPad; // DataEnd + 2 * Pad; // You need to set initial values before the calculation for (var i = 0; i <= 9; i++) Z[PaddedDataEnd] += 0.1 * paddedData[i]; Z[PaddedDataEnd - 1] = Z[PaddedDataEnd]; //////////////////////////////////////////////////////////////////////////////////// // Data passed through filter backward //////////////////////////////////////////////////////////////////////////////////// long travel = (PaddedDataEnd - 2); var progress = 0D; for (var j = travel; j >= 0; j--) { f1 = (d0 * paddedData[j]); // Breaking f2 = (d1 * paddedData[j + 1]); // down f3 = (d2 * paddedData[j + 2]); // a big f4 = (e1 * Z[j + 1]); // formula f5 = (e2 * Z[j + 2]); // into smaller "groups". Z[j] = f1 + f2 + f3 + f4 + f5; // Putting the "groups" back together var percent = System.Math.Floor(33D * ((travel - j) / (double)travel)); if (percent > progress) { setProgress?.Invoke(percent); progress = percent; } } setProgress?.Invoke(33); //////////////////////////////////////////////////////////////////////////////////// // Data passed through filter forward //////////////////////////////////////////////////////////////////////////////////// // You need to set initial values before the calculation for (var i = 0; i <= 9; i++) A[0] += 0.1 * Z[i]; A[1] = A[0]; for (var k = 3; k <= PaddedDataEnd; k++) { f1 = (d0 * Z[k]); // Breaking f2 = (d1 * Z[k - 1]); // down f3 = (d2 * Z[k - 2]); // a big f4 = (e1 * A[k - 1]); // formula f5 = (e2 * A[k - 2]); // into smaller "groups". A[k] = f1 + f2 + f3 + f4 + f5; // Putting the "groups" back together var percent = System.Math.Floor(33 + 33D * ((PaddedDataEnd - k) / (double)PaddedDataEnd)); if (percent > progress) { setProgress?.Invoke(percent); progress = percent; } } setProgress?.Invoke(66); //////////////////////////////////////////////////////////////////////////////////// // Prepare baseline calculation, if required //////////////////////////////////////////////////////////////////////////////////// var baselineSum = 0.0; // Compute baseline if it's going to be applied. if (ZeroBaseline) for (var n = Pad; n <= (2 * Pad); n++) baselineSum += A[n]; // Baseline adjustment. var adjust = baselineSum / Pad; //////////////////////////////////////////////////////////////////////////////////// // Apply baseline and/or calibration factor, then finalize filtered data //////////////////////////////////////////////////////////////////////////////////// var filteredIndex = 0; var filteredData = new double[data.Length]; //8747 - TDC data shifts one point when filtered //this allows us to preserve that shift on demand var numOfPreDataPointsToInclude = NumberOfPreDataPointsToInclude; if (bUseLegacyTDCFilterAdjustment) { numOfPreDataPointsToInclude++; } travel = (PaddedDataEnd - (Pad + numOfPreDataPointsToInclude)) - (Pad - numOfPreDataPointsToInclude); for (var k = (int)(Pad - numOfPreDataPointsToInclude); k <= (PaddedDataEnd - (Pad + numOfPreDataPointsToInclude)); k++) { // Zero baseline, if requested. if (ZeroBaseline) A[k] = A[k] - adjust; // Multiplying by the calibration factor unless factor is 1.0 // AFTER the filtering has been finished. if (1.0 != CalibrationFactor) A[k] = A[k] * CalibrationFactor; // Copy finished product into return buffer. filteredData[filteredIndex++] = A[k]; var percent = System.Math.Floor(66D + 33D * (filteredIndex / (double)travel)); if (percent > progress) { setProgress?.Invoke(percent); progress = percent; } } setProgress?.Invoke(100); // The deed is done. Share the pain. return filteredData; } } catch (System.Exception ex) { throw new FilterUtility.Exception("encountered problem SAE J211-filtering data", ex); } } } }