/*
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);
}
}
}
}