Files
DP44/DTS Viewer/DTS.Viewer.Modules/DTS.Viewer.AddCalculatedChannel/Model/CalculatedChannelCreator.cs
2026-04-17 14:55:32 -04:00

970 lines
50 KiB
C#

using DTS.Common.Enums.Sensors;
using DTS.Common.Utilities.Logging;
using DTS.Common.Utils;
using DTS.Serialization;
using DTS.Slice.Control;
using DTS.Common;
using DTS.Common.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static DTS.Viewer.AddCalculatedChannel.AddCalculatedChannelViewModel;
using Prism.Ioc;
using DTS.Common.Calculations;
using DTS.Common.Utilities;
using Prism.Events;
namespace DTS.Viewer.AddCalculatedChannel.Model
{
public class CalculatedChannelCreator
{
private static readonly int ChannelNumberCalculationChannelIndicator = 100000;
public static Test.Module.CalculatedChannel[] CreateChannels(string testId,
string folder,
Calculation calculation,
List<Test.Module.Channel> inputChannels,
string channelName,
int startingNumber,
List<Test.Module.Channel> allChannels,
int clipLength,
out List<string> errorList,
int defaultEncoding)
{
Test.Module.CalculatedChannel[] calculatedChannels = null;
errorList = new List<string>();
switch (calculation)
{
case Calculation.ThreeDIRTracc: calculatedChannels = Create3DIRTraccChannels(testId, folder, inputChannels, channelName, ThreeDIRTraccType.Thorax, startingNumber, defaultEncoding); break;
case Calculation.ThreeDIRTraccLowerThorax: calculatedChannels = Create3DIRTraccChannels(testId, folder, inputChannels, channelName, ThreeDIRTraccType.LowerThorax, startingNumber, defaultEncoding); break;
case Calculation.ThreeDIRTraccAbdomen: calculatedChannels = Create3DIRTraccChannels(testId, folder, inputChannels, channelName, ThreeDIRTraccType.Abdomen, startingNumber, defaultEncoding); break;
case Calculation.SUM:
case Calculation.AVE:
case Calculation.Resultant:
case Calculation.HIC:
calculatedChannels = CreateChannelsAggregateOperation(testId, folder, calculation, inputChannels, channelName, startingNumber, clipLength);
break;
default: calculatedChannels = CreateChannelsBinaryOperation(testId, folder, calculation, inputChannels, channelName, startingNumber); break;
}
if (null == calculatedChannels) return null;
for (var i = 0; i < calculatedChannels.Length; i++)
{
calculatedChannels[i].AbsoluteDisplayOrder = startingNumber + i;
}
if (ValidateChannelName(channelName, inputChannels, allChannels, out errorList)) return calculatedChannels;
//ReportErrors(errorList);
return null;
}
private static Serialization.SliceRaw.File.PersistentChannel CreatePersistentInformationObject(Test.Module.CalculatedChannel channel, string filepath)
{
var channelHeader = new Serialization.SliceRaw.File.BinaryChannelHeader
{
NumberOfTriggers = (ushort)(channel.ParentModule.TriggerSampleNumbers.Count)
};
channelHeader.TriggerSampleNumbers = new ulong[channelHeader.NumberOfTriggers];
// Do trigger sample information.
for (var i = 0; i < channelHeader.NumberOfTriggers; i++)
{
channelHeader.TriggerSampleNumbers[i] = channel.ParentModule.TriggerSampleNumbers[i];
}
// Do EU information.
if (channel is Common.DAS.Concepts.DAS.Channel.IEngineeringUnitAware)
{ //
// Persistent object property accessors already pad out EU so data will be word-aligned,
// so we'll want to make sure we do the same thing here.
//
var eu = (channel as Common.DAS.Concepts.DAS.Channel.IEngineeringUnitAware).EngineeringUnits;
var paddedEu = (1 == eu.Length % 2) ? (eu.Clone() as string).PadRight(eu.Length + 1, ' ') : eu;
channelHeader.EuFieldLengthWithTerminator = (ushort)(paddedEu.Length + 1);
channelHeader.EngineeringUnit = new char[channelHeader.EuFieldLengthWithTerminator - 1];
for (var j = 0; j < channelHeader.EuFieldLengthWithTerminator - 1; j++)
{
channelHeader.EngineeringUnit[j] = paddedEu[j];
}
}
else
{
channelHeader.EuFieldLengthWithTerminator = 1;
channelHeader.EngineeringUnit = new char[0];
}
// Do ISO code information.
channelHeader.IsoCode = new char[16];
// Make sure memory-mapped file does NOT exist (persistent channel object will overwrite header but append data
// after old file data... don't want that!)
if (System.IO.File.Exists(filepath))
{
GC.Collect();
FileUtils.DeleteFileOrMove(filepath, APILogger.Log);
////Rename existing channel file and kick off thread that will deleted it some time later. This needs to
////happen in this case as the test tree still contains references to the original file and it can't be deleted
////here. After a new channel is added, the old one is removed. The worker thread should wait long enough for that
////to happen (or better, check to see when it does) and then delete the file. This should eliminate the File.IO
////Exception that was being thrown.
//string tmpfilepath = filepath + ".bak";
//APILogger.Log("Renaming: " + filepath + " to " + tmpfilepath + " and deferring deletion.");
//System.IO.Path.ChangeExtension(filepath, Guid.NewGuid().ToString());
//System.Threading.ThreadPool.QueueUserWorkItem(DeleteChannelFile, tmpfilepath);
}
// Create the persistent channel object and return it.
return new Serialization.SliceRaw.File.PersistentChannel(filepath, channelHeader, true);
}
/// <summary>
/// this function takes a calculation and a list of input channels and returns
/// a list of doubles that is the output of doing that calculation
/// </summary>
/// <param name="calculation"></param>
/// <param name="channels"></param>
/// <returns></returns>
private static IList<double> PerformCalculationsAggregate(Calculation calculation,
List<Test.Module.Channel> channels)
{
var maxSampleRate = channels.Select(ch => ch.ParentModule.SampleRateHz).Max();
//this will hold the unfiltered input data for all the input channels
var unfilteredDataEU = new List<List<double>>();
//this will hold the output data
var dOutput = new List<double>();
//this will hold the emc channel data for the input channels (the input channels already have this data
//but keeping a local reference here removes some repeated casting that we'd have to do
var emcChannels = new List<Event.Module.Channel>();
//we may have different start times and the sample indices may not align, if we pick the first channel as a reference
//channel, then we can keep track of the delta for each channels start and the reference channel start in terms of samples
foreach (var ch in channels)
{
var emc = ch.emc as Event.Module.Channel;
emcChannels.Add(emc);
var data = emc.GetUnfilteredDataEu();
unfilteredDataEU.Add(data);
var start = GetTimeOfFirstSample(emc);
}
//any aggregate channels (SUM/AVE) just keep a single double and update it as needed while processing samples
var dAggregateValue = 0D;
var bAdd = true;
var currentChannelSampleIndex = 0;
//to properly aggregate, we need to put both datasets to the right sample rate
//using interpolation
//we need to calculate the min/max time across all channels and then
//the specific min of each channel, this tells us how to align data
var minStart = channels.Select(ch => ((double)ch.ParentModule.TriggerSampleNumbers[0] - ch.ParentModule.StartRecordSampleNumber) / ch.ParentModule.SampleRateHz).Min();
var minEnd = double.MaxValue;
var channelOffsetStarts = new List<int>();
var rates = new List<double>();
for (int i = 0; i < channels.Count; i++)
{
var ch = channels[i];
var chStart = ((double)ch.ParentModule.TriggerSampleNumbers[0] - ch.ParentModule.StartRecordSampleNumber) / ch.ParentModule.SampleRateHz;
var channelOffsetStart = (int)((chStart - minStart) * ch.ParentModule.SampleRateHz);
channelOffsetStarts.Add(channelOffsetStart);
var chEnd = ch.ParentModule.NumberOfSamples - chStart * ch.ParentModule.SampleRateHz;
minEnd = Math.Min(minEnd, chEnd / ch.ParentModule.SampleRateHz);
var rate = maxSampleRate / ch.ParentModule.SampleRateHz;
rates.Add(rate);
}
minStart = -1D * Math.Truncate(1000D * minStart) / 1000D;
minEnd = Math.Truncate(1000D * minEnd) / 1000D;
var totalSamples = Convert.ToInt32(Math.Floor((minEnd - minStart) * maxSampleRate));
for (var iSampleIDX = 0; iSampleIDX < totalSamples; iSampleIDX++)
{
var timeAtIndex =
dAggregateValue = 0;
bAdd = true;
var dSumSquares = 0D;
for (var iChannel = 0; iChannel < emcChannels.Count; iChannel++)
{
var rate = rates[iChannel];
var indexAtCurrentTime = (iSampleIDX - channelOffsetStarts[iChannel]) / rate;
var thisChannelsIndexAtCurrentTime = Convert.ToInt32(Math.Floor(indexAtCurrentTime));
var step = Convert.ToInt32(Math.Ceiling(indexAtCurrentTime) - thisChannelsIndexAtCurrentTime);
if (currentChannelSampleIndex < 0 || currentChannelSampleIndex >= unfilteredDataEU[iChannel].Count)
{
bAdd = false;
break;
}
var dataAtPoint = unfilteredDataEU[iChannel][thisChannelsIndexAtCurrentTime];
var increment = 0D;
if ((1 + thisChannelsIndexAtCurrentTime) < unfilteredDataEU[iChannel].Count)
{
increment = (unfilteredDataEU[iChannel][1 + thisChannelsIndexAtCurrentTime] - dataAtPoint) / rate;
}
else
{
increment = (dataAtPoint - unfilteredDataEU[iChannel][thisChannelsIndexAtCurrentTime - 1]) / rate;
}
dataAtPoint += (increment * step);
dAggregateValue += dataAtPoint;
//HIC must be in g's
if (calculation == Calculation.HIC)
{
if (((Event.Module.AnalogInputChannel)emcChannels[iChannel]).EngineeringUnits.ToLower().Trim() != "g")
{
//convert from m/sec^2 to g
dataAtPoint *= 9.80665D;
}
}
dSumSquares += Math.Pow(dataAtPoint, 2);
}
if (!bAdd) continue;
switch (calculation)
{
case Calculation.AVE: dOutput.Add(dAggregateValue / channels.Count); break;
case Calculation.SUM: dOutput.Add(dAggregateValue); break;
case Calculation.Resultant:
case Calculation.HIC:
dOutput.Add(Math.Sqrt(dSumSquares));
break;
}
}
return dOutput;
}
/// <summary>
/// performs calculations for a binary calculation channel (integrate/differentiate/FFT, etc)
/// </summary>
/// <param name="calculation"></param>
/// <param name="channel"></param>
/// <returns></returns>
private static IList<double> PerformCalculationBinary(Calculation calculation, Event.Module.Channel channel)
{
var data = channel.GetUnfilteredDataEu();
var sampleRate = Convert.ToInt32(channel.ParentModule.SampleRateHz);
switch (calculation)
{
case Calculation.Integral:
{
var db = new ClonableDoubles();
db.AddRange(data.ToArray());
var integral = new Common.Utilities.Math.Nhtsa.Integration(db, sampleRate);
return integral.Range;
}
case Calculation.DoubleIntegral:
{
var db = new ClonableDoubles();
db.AddRange(data.ToArray());
var integral = new Common.Utilities.Math.Nhtsa.Integration(db, sampleRate);
db = new ClonableDoubles();
db.AddRange(integral.Range);
var doubleIntegral = new Common.Utilities.Math.Nhtsa.Integration(db, sampleRate);
return doubleIntegral.Range;
}
case Calculation.Derivative:
{
var db = new ClonableDoubles();
db.AddRange(data.ToArray());
var derivative = new Common.Utilities.Math.Nhtsa.Differentiation(db, sampleRate);
return derivative.Range;
}
case Calculation.Sin:
{
return channel.DataEu.Select(euSample => Math.Sin(euSample)).ToList();
}
case Calculation.Cos:
{
return channel.DataEu.Select(euSample => Math.Cos(euSample)).ToList();
}
}
return new List<double>();
}
private static Test.Module.CalculatedChannel CreateCalculatedChannelsIRTRACC(Calculation calculation, Test.Module.Channel[] inputChannels)
{
switch (calculation)
{
case Calculation.ThreeDIRTracc:
case Calculation.ThreeDIRTraccAbdomen:
case Calculation.ThreeDIRTraccLowerThorax:
return Test.Module.CalculatedChannel.CreateInstance(new[]
{
inputChannels[0],
inputChannels[1],
inputChannels[2]
});
default:
return Test.Module.CalculatedChannel.CreateInstance(inputChannels[0]);
}
}
//private Event.Module.Channel GetSourceEMC()
//{
// switch (_calculation)
// {
// case Calculation.ThreeDIRTracc:
// case Calculation.ThreeDIRTraccAbdomen:
// case Calculation.ThreeDIRTraccLowerThorax:
// return
// (cbIRTracc.SelectedItem as ChannelHelper).MyChannel.emc as Event.Module.Channel;
// default:
// return _sourceChannel.emc as Event.Module.Channel;
// }
//}
//private static void DeleteChannelFile(object filepath)
//{
// FileUtils.DeleteFileOrMove((string)filepath, APILogger.Log);
//}
/// <summary>
/// performs the IR-Tracc 3D calculation
/// based on issue
/// 7489 Implement 2D/3D IRTRACC support
/// </summary>
/// <param name="dAX"></param>
/// <param name="dAY"></param>
/// <param name="dAZ"></param>
/// <param name="dOut"></param>
private static void PerformCalculation(out IList<double> dAX, out IList<double> dAY, out IList<double> dAZ, out IList<double> dOut,
Test.Module.Channel irTraccChannel, Test.Module.Channel rPot1Channel, Test.Module.Channel rPot2Channel, ThreeDIRTraccType irtraccType)
{
var maxRate = Math.Max(rPot1Channel.ParentModule.SampleRateHz, rPot2Channel.ParentModule.SampleRateHz);
maxRate = Math.Max(maxRate, irTraccChannel.ParentModule.SampleRateHz);
var irtraccEMC = irTraccChannel.emc as Event.Module.Channel;
var rPot1EMC = rPot1Channel.emc as Event.Module.Channel;
var rPot2EMC = rPot2Channel.emc as Event.Module.Channel;
if (0 != maxRate % rPot1Channel.ParentModule.SampleRateHz) { throw new InvalidOperationException($"Sample rate: {maxRate} is not a multiple of sample rate: {rPot1Channel.ParentModule.SampleRateHz}"); }
if (0 != maxRate % rPot2Channel.ParentModule.SampleRateHz) { throw new InvalidOperationException($"Sample rate: {maxRate} is not a multiple of sample rate: {rPot2Channel.ParentModule.SampleRateHz}"); }
if (0 != maxRate % irTraccChannel.ParentModule.SampleRateHz) { throw new InvalidOperationException($"Sample rate: {maxRate} is not a multiple of sample rate: {irTraccChannel.ParentModule.SampleRateHz}"); }
//this is the EU data for each channel NOTE - ALL CHANNELS MUST NOT BE USING ZEROING!!! (we handle zeroing already below)
var irTraccEUData = irtraccEMC.GetUnfilteredDataEu();
DiskUtility.ReplaceDataIfNeeded(ref irTraccEUData, "DISPLEU.txt");
//var irTraccmVData = irtraccEMC.GetUnfilteredDataMV();
var rPot1EUData = rPot1EMC.GetUnfilteredDataEu();
DiskUtility.ReplaceDataIfNeeded(ref rPot1EUData, "YPOTEU.txt");
var rPot2EUData = rPot2EMC.GetUnfilteredDataEu();
DiskUtility.ReplaceDataIfNeeded(ref rPot2EUData, "ZPOTEU.txt");
//this calculates start time for each channel
var startIRTracc = (double)(irTraccChannel.ParentModule.TriggerSampleNumbers[0] - irTraccChannel.ParentModule.StartRecordSampleNumber) / irTraccChannel.ParentModule.SampleRateHz;
startIRTracc = -1D * Math.Truncate(startIRTracc * 1000D) / 1000D;
var startRPot1 = (double)(rPot1Channel.ParentModule.TriggerSampleNumbers[0] - rPot1Channel.ParentModule.StartRecordSampleNumber) / rPot1Channel.ParentModule.SampleRateHz;
startRPot1 = -1D * Math.Truncate(startRPot1 * 1000D) / 1000D;
var startRPot2 = (double)(rPot2Channel.ParentModule.TriggerSampleNumbers[0] - rPot2Channel.ParentModule.StartRecordSampleNumber) / rPot2Channel.ParentModule.SampleRateHz;
startRPot2 = -1D * Math.Truncate(startRPot2 * 1000D) / 1000D;
//this calculates the end for each channel
var endIRTracc = (irTraccChannel.ParentModule.NumberOfSamples + startIRTracc * irTraccChannel.ParentModule.SampleRateHz) / irTraccChannel.ParentModule.SampleRateHz;
endIRTracc = Math.Truncate(endIRTracc * 1000D) / 1000D;
var endRPot1 = (rPot1Channel.ParentModule.NumberOfSamples + startRPot1 * rPot1Channel.ParentModule.SampleRateHz) / rPot1Channel.ParentModule.SampleRateHz;
endRPot1 = Math.Truncate(endRPot1 * 1000D) / 1000D;
var endRPot2 = (rPot2Channel.ParentModule.NumberOfSamples + startRPot2 * rPot2Channel.ParentModule.SampleRateHz) / rPot2Channel.ParentModule.SampleRateHz;
endRPot2 = Math.Truncate(endRPot2 * 1000D) / 1000D;
//here we select the latest start between channels
var start = Math.Max(startIRTracc, startRPot1);
start = Math.Max(start, startRPot2);
//here we find the earliest end between the channels
var end = Math.Min(endIRTracc, endRPot1);
end = Math.Min(end, endRPot2);
// we will super sample to the highest sample rate, and use a common start/stop between all channels.
var length = Convert.ToInt32(Math.Floor((end - start) * maxRate));
//these are the containers for our output data
dAX = new List<double>(length);
dAY = new List<double>(length);
dAZ = new List<double>(length);
dOut = new List<double>(length);
var aicIRTRACC = irTraccChannel as Test.Module.AnalogInputChannel;
var aicRPot1 = rPot1Channel as Test.Module.AnalogInputChannel;
var aicRPOT2 = rPot2Channel as Test.Module.AnalogInputChannel;
//calculates the rate of each channel relative to the highest rate of any of the channels
var rateIRTracc = Convert.ToInt32(Math.Ceiling(maxRate / irTraccChannel.ParentModule.SampleRateHz));
var rateRPot1 = Convert.ToInt32(Math.Ceiling(maxRate / rPot1Channel.ParentModule.SampleRateHz));
var rateRPot2 = Convert.ToInt32(Math.Ceiling(maxRate / rPot2Channel.ParentModule.SampleRateHz));
var R0 = aicIRTRACC.LinearizationFormula.CalibrationFactor * Math.Pow(aicIRTRACC.ZeroPoint, aicIRTRACC.LinearizationFormula.LinearizationExponent);
//we are converting to volt as sensitivity is in mV/V and our original calculation is in deg/V/V
var θy0 = aicRPot1.InitialEu; //((1000D/aicRPot1.Sensitivity)/aicRPot1.FactoryExcitationVoltage)*aicRPot1.ZeroPoint;
var θz0 = aicRPOT2.InitialEu; //((1000D/aicRPOT2.Sensitivity)/aicRPOT2.FactoryExcitationVoltage)*aicRPOT2.ZeroPoint;
//all the formulas use the first data point to zero the output channel data, these variables will hold that zero data for out output channels
//this is columns V through Y
var xa1_0 = double.NaN;
var ya1_0 = double.NaN;
var za1_0 = double.NaN;
var dOut_0 = double.NaN;
//your delta and D0 are dependent on your 3D-IRTRACC type, we get the constant for each type from the config file
var δ = 0D;
var D0 = 0D;
switch (irtraccType)
{
case ThreeDIRTraccType.Abdomen:
δ = SensorConstants.δAbdomen;
D0 = SensorConstants.D0Abdomen;
break;
case ThreeDIRTraccType.Thorax:
δ = SensorConstants.δThorax;
D0 = SensorConstants.D0Thorax;
break;
case ThreeDIRTraccType.LowerThorax:
δ = SensorConstants.δThoraxLower;
D0 = SensorConstants.D0ThoraxLower;
break;
default:
throw new NotSupportedException("unsupported irtracc type: " + irtraccType.ToString());
}
//calculates the offset in samples for each channels start relative to the
//latest common start time between channels
var irTraccOffsetStart = (int)(startIRTracc - start) * irTraccChannel.ParentModule.SampleRateHz;
var rPot1OffsetStart = (int)(startRPot1 - start) * rPot1Channel.ParentModule.SampleRateHz;
var rPot2OffsetStart = (int)(startRPot2 - start) * rPot2Channel.ParentModule.SampleRateHz;
//go through all samples in the output, calculate the index for each channel and do some calcs
for (var i = 0; i < length; i++)
{
//this is the index for the given time, this index could be theoretical
//and doesn't exist (ie 1.5)
var indexAtCurrentTimeIRTracc = (i - irTraccOffsetStart) / rateIRTracc;
//this is the actual index that physically exists (1 for 1.5 for instance)
var actualIndexAtCurrentTimeIRTracc = Convert.ToInt32(Math.Floor(indexAtCurrentTimeIRTracc));
//this is the linear interpolation "step" between this sample and the next
//sample
var stepIRTracc = Convert.ToInt32(Math.Ceiling(indexAtCurrentTimeIRTracc) - actualIndexAtCurrentTimeIRTracc);
var indexAtCurrentTimeRPot1 = (i - rPot1OffsetStart) / rateRPot1;
var actualIndexAtCurrentTimeRPot1 = Convert.ToInt32(Math.Floor(indexAtCurrentTimeRPot1));
var stepRPot1 = Convert.ToInt32(Math.Ceiling(indexAtCurrentTimeIRTracc) - actualIndexAtCurrentTimeIRTracc);
var indexAtCurrentTimeRPot2 = (i - rPot2OffsetStart) / rateRPot2;
var actualIndexAtCurrentTimeRPot2 = Convert.ToInt32(Math.Floor(indexAtCurrentTimeRPot2));
var stepRPot2 = Convert.ToInt32(Math.Ceiling(indexAtCurrentTimeRPot2) - actualIndexAtCurrentTimeRPot2);
var incrementIRTracc = 0D;
var valueIRTraccAtPoint = irTraccEUData[actualIndexAtCurrentTimeIRTracc];
var incrementRPot1 = 0D;
var valueRPot1AtPoint = rPot1EUData[actualIndexAtCurrentTimeRPot1];
var incrementRPot2 = 0D;
var valueRPot2AtPoint = rPot2EUData[actualIndexAtCurrentTimeRPot2];
//calculate the interpolation value per step for channel
//for instance for 10k and 20k sps for sample 2 of the 10k
//increment would be (data[2]-data[1])/2
if ((1 + actualIndexAtCurrentTimeIRTracc) < irTraccEUData.Count)
{
incrementIRTracc = (irTraccEUData[1 + actualIndexAtCurrentTimeIRTracc] - valueIRTraccAtPoint) / rateIRTracc;
}
else
{
incrementIRTracc = (valueIRTraccAtPoint - irTraccEUData[actualIndexAtCurrentTimeIRTracc - 1]) / rateIRTracc;
}
if ((1 + actualIndexAtCurrentTimeRPot1) < rPot1EUData.Count)
{
incrementRPot1 = (rPot1EUData[1 + actualIndexAtCurrentTimeRPot1] - valueRPot1AtPoint) / rateRPot1;
}
else
{
incrementRPot1 = (valueRPot1AtPoint - rPot1EUData[actualIndexAtCurrentTimeRPot1 - 1]) / rateRPot1;
}
if ((1 + actualIndexAtCurrentTimeRPot2) < rPot2EUData.Count)
{
incrementRPot2 = (rPot2EUData[1 + actualIndexAtCurrentTimeRPot2] - valueRPot2AtPoint) / rateRPot2;
}
else
{
incrementRPot2 = (valueRPot2AtPoint = rPot2EUData[actualIndexAtCurrentTimeRPot2 - 1]) / rateRPot2;
}
//math magic from excel
//double b = D0 + System.Math.Abs(irTraccEUData[i] - calFactorInterceptRemoval) - R0;
//double R = aicIRTRACC.LinearizationFormula.CalibrationFactor*
// System.Math.Pow(irTraccmVData[i]/1000D, aicIRTRACC.LinearizationFormula.LinearizationExponent);
//D0+(R-R0) this is column N
//double r = D0 + (R - R0);
var r = valueIRTraccAtPoint + incrementIRTracc * stepIRTracc;
//θy'=θy-θy0 this is column O
var θyprime = valueRPot1AtPoint + incrementRPot1 * stepRPot1 - θy0;
//θz'=θz-θz0 this is column p
var θzprime = valueRPot2AtPoint + incrementRPot2 * stepRPot2 - θz0;
//this is column r
var xa1 = -1D * δ * Math.Sin(θyprime * Math.PI / 180) + r * Math.Cos(θzprime * Math.PI / 180) * Math.Cos(θyprime * Math.PI / 180);
//this is column s
var ya1 = r * Math.Sin(θzprime * Math.PI / 180);
//this is column t
var za1 = -1D * δ * Math.Cos(θyprime * Math.PI / 180D) - r * Math.Cos(θzprime * Math.PI / 180D) * Math.Sin(θyprime * Math.PI / 180D);
//assign the output channel zero points if needed
if (double.IsNaN(xa1_0)) { xa1_0 = xa1; }
if (double.IsNaN(ya1_0)) { ya1_0 = ya1; }
if (double.IsNaN(za1_0)) { za1_0 = za1; }
//add in our data to the output channels
dAX.Add(xa1 - xa1_0);
dAY.Add(ya1 - ya1_0);
dAZ.Add(za1 - za1_0);
//this is column Z
var temp = Math.Sqrt(Math.Pow(r, 2) + Math.Pow(δ, 2));
if (double.IsNaN(dOut_0)) { dOut_0 = temp; }
dOut.Add(temp - dOut_0);
}
}
private static double GetTimeOfFirstSample(Event.Module.Channel channel)
{
return ((double)channel.ParentModule.TriggerSampleNumbers[0] -
channel.ParentModule.StartRecordSampleNumber) / channel.ParentModule.SampleRateHz;
}
private static Test.Module.CalculatedChannel[] Create3DIRTraccChannels(string testId, string folder,
List<Test.Module.Channel> inputChannels, string channelName, ThreeDIRTraccType irTraccType, int offset, int defaultEncoding)
{
System.Diagnostics.Trace.Assert(3 == inputChannels.Count, "3D IR-TRACC requires 3 channels");
var maxRate = inputChannels.Select(ch => ch.ParentModule.SampleRateHz).Max();
var distinctRates = inputChannels.Select(ch => ch.ParentModule.SampleRateHz).Distinct().ToArray();
foreach (var rate in distinctRates)
{
if (0 != maxRate % rate)
{
throw new NotSupportedException($"Sample rate: {maxRate} is not a multiple of sample rate: {rate}");
}
}
if (distinctRates.Length > 1)
{
var eventAggregator = ContainerLocator.Container.Resolve<IEventAggregator>();
eventAggregator.GetEvent<PageErrorEvent>().Publish(new PageErrorArg(new[] { Resources.StringResources.SuperSamplingWarning }, null));
}
var irtraccEMC = inputChannels[0].emc as Event.Module.Channel;
var rPot1EMC = inputChannels[1].emc as Event.Module.Channel;
var rPot2EMC = inputChannels[2].emc as Event.Module.Channel;
var calc = Calculation.ThreeDIRTracc;
switch (irTraccType)
{
case ThreeDIRTraccType.Abdomen: calc = Calculation.ThreeDIRTraccAbdomen; break;
case ThreeDIRTraccType.Thorax: calc = Calculation.ThreeDIRTracc; break;
case ThreeDIRTraccType.LowerThorax: calc = Calculation.ThreeDIRTraccLowerThorax; break;
}
var ccDeltaAX = CreateCalculatedChannelsIRTRACC(calc, new[] { inputChannels[0], inputChannels[1], inputChannels[2] });
ccDeltaAX.ChannelDescriptionString = channelName + " (dAX)";
var ccDeltaAY = CreateCalculatedChannelsIRTRACC(calc, new[] { inputChannels[0], inputChannels[1], inputChannels[2] });
ccDeltaAY.ChannelDescriptionString = channelName + " (dAY)";
var ccDeltaAZ = CreateCalculatedChannelsIRTRACC(calc, new[] { inputChannels[0], inputChannels[1], inputChannels[2] });
ccDeltaAZ.ChannelDescriptionString = channelName + " (dAZ)";
var ccDistanceDelta = CreateCalculatedChannelsIRTRACC(calc, new[] { inputChannels[0], inputChannels[1], inputChannels[2] });
ccDistanceDelta.ChannelDescriptionString = channelName;
var outputChannels = new Test.Module.CalculatedChannel[] { ccDeltaAX, ccDeltaAY, ccDeltaAZ, ccDistanceDelta };
foreach (var cc in outputChannels)
{
cc.Calculation = calc.ToString();
cc.AbsoluteDisplayOrder = ChannelNumberCalculationChannelIndicator + irtraccEMC.AbsoluteDisplayOrder + offset;
cc.Number = ChannelNumberCalculationChannelIndicator + offset;
cc.ProportionalToExcitation = false;
cc.ZeroMvInADC = 0;
cc.OriginalOffsetADC = 0;
cc.Data.MvPerEu = 1;
cc.Data.Multiplier = 1;
cc.Data.UnitConversion = 1;
cc.Data.UserOffsetEU = 0;
cc.ExcitationVoltage = 0;
cc.Excitation = 0;
cc.ZeroMethod = ZeroMethodType.None;
cc.RemoveOffset = false;
cc.ChannelId = irtraccEMC.ChannelId + "_" + (cc.Number + offset);
cc.ChannelGroupName = irtraccEMC.ChannelGroupName;
cc.IsInverted = false;
cc.PreTestZeroLevelAdc = 0;
cc.SerialNumber = cc.ChannelDescriptionString;
var emc = new Event.Module.AnalogInputChannel(cc, irtraccEMC.ParentModule, cc.Number);
cc.emc = emc;
offset++;
}
ccDeltaAX.EngineeringUnits = "mm";
ccDeltaAY.EngineeringUnits = "mm";
ccDeltaAZ.EngineeringUnits = "mm";
ccDistanceDelta.EngineeringUnits = "mm";
PerformCalculation(out IList<double> dax, out IList<double> day, out IList<double> daz, out IList<double> dDistance, inputChannels[0], inputChannels[1], inputChannels[2], irTraccType);
irtraccEMC = null;
rPot1EMC = null;
rPot2EMC = null;
var datas = new[] { dax, day, daz, dDistance };
for (var i = 0; i < datas.Length && i < outputChannels.Length; i++)
{
var data = datas[i];
var scaleFactor = 0D;
var adcData = ScaleEuData(ref data, out scaleFactor);
outputChannels[i].Sensitivity = scaleFactor;
outputChannels[i].DesiredRange = Constants.ADC_MIDPOINT / scaleFactor;
//adjust the output channel with the max rate we super sampled to
//and update the number of samples...
outputChannels[i].SampleRateHz = maxRate;
outputChannels[i].ParentModule.NumberOfSamples = Convert.ToUInt64(data.Count);
((Event.Module.AnalogInputChannel)outputChannels[i].emc).ParentModule.SampleRateHz = maxRate;
((Event.Module.AnalogInputChannel)outputChannels[i].emc).ParentModule.NumberOfSamples = Convert.ToUInt64(data.Count);
var f = new Serialization.SliceRaw.File { DefaultEncoding = defaultEncoding };
var fileName = new Serialization.SliceRaw.File().GetCalculatedChannelFileNameFromTestNameAndChannelNumber(testId, outputChannels[i].Number);
var filepath = System.IO.Path.Combine(folder, fileName);
outputChannels[i].Data.ScaleFactorMv = 1 / scaleFactor;
outputChannels[i].Data.ScaleFactorEU = 1 / scaleFactor;
outputChannels[i].LinearizationFormula.MarkValid(false);
outputChannels[i].PersistentChannelInfo = CreatePersistentInformationObject(outputChannels[i], filepath);
outputChannels[i].PersistentChannelInfo.BeginAppendSession();
outputChannels[i].PersistentChannelInfo.AppendSessionData(adcData.ToArray());
outputChannels[i].PersistentChannelInfo.EndAppendSession();
var writer = (f.Exporter as Serialization.SliceRaw.File.Writer);
writer?.CreatePersistentChannel(outputChannels[i], outputChannels[i].ParentModule.NumberOfSamples, outputChannels[i].ParentModule.SampleRateHz, null, null, 0, 0);
var bypass = false;
var reader = f.Importer as Serialization.SliceRaw.File.Reader;
reader?.ReadChannel(outputChannels[i], outputChannels[i].ParentModule, filepath, ref bypass);
(outputChannels[i].emc as Event.Module.Channel).UnfilteredData = outputChannels[i].PersistentChannelInfo;
(outputChannels[i].emc as Event.Module.Channel).Scaler.SetScaleFactorMv(1D / scaleFactor);
}
return outputChannels.ToArray();
}
private static Test.Module.CalculatedChannel[] CreateChannelsAggregateOperation(string testId, string folder,
Calculation calculation, List<Test.Module.Channel> inputChannels, string channelName, int channelOffset, int clipLength)
{
System.Diagnostics.Trace.Assert(1 < inputChannels.Count,
calculation.ToString() + " requires at least 1 channel");
var sourceEmc = inputChannels[0].emc as Event.Module.Channel;
var sourceChannel = inputChannels[0];
var maxSampleRate = inputChannels.Select(ch => ch.ParentModule.SampleRateHz).Max();
var distinctRates = inputChannels.Select(ch => ch.ParentModule.SampleRateHz).Distinct().ToArray();
foreach (var rate in distinctRates)
{
if (maxSampleRate % rate != 0)
{
throw new NotSupportedException($"Sample rate: {maxSampleRate} is not a multiple of {rate}");
}
}
if (distinctRates.Length > 1)
{
var eventAggregator = ContainerLocator.Container.Resolve<IEventAggregator>();
eventAggregator.GetEvent<PageErrorEvent>().Publish(new PageErrorArg(new[] { Resources.StringResources.SuperSamplingWarning }, null));
}
//Create new test channel
var cc = CreateCalculatedChannelsIRTRACC(calculation, new[] { sourceChannel });
cc.SampleRateHz = maxSampleRate;
cc.Calculation = calculation.ToString();
cc.ChannelDescriptionString = channelName;
cc.AbsoluteDisplayOrder = ChannelNumberCalculationChannelIndicator + channelOffset;
cc.Number = ChannelNumberCalculationChannelIndicator + channelOffset;
cc.ProportionalToExcitation = false;
cc.ZeroMvInADC = 0;
cc.OriginalOffsetADC = 0;
cc.Data.MvPerEu = 1;
cc.Data.Multiplier = sourceEmc.Multiplier;
cc.Data.UnitConversion = sourceEmc.UnitConversion;
cc.Data.UserOffsetEU = sourceEmc.UserOffsetEU;
cc.ExcitationVoltage = 0;
cc.Excitation = 0;
cc.ZeroMethod = ZeroMethodType.None;
cc.RemoveOffset = false;
cc.ChannelId = sourceChannel.ChannelId + "_" + cc.Number;
cc.ChannelGroupName = sourceChannel.ChannelGroupName;
cc.IsInverted = false;
var emc = new Event.Module.AnalogInputChannel(cc, sourceEmc.ParentModule, sourceEmc.AbsoluteNumber);
cc.emc = emc;
//Do the maths. Use source channel as source for data as it's properly set up.
var euData = PerformCalculationsAggregate(calculation, inputChannels);
cc.ParentModule.NumberOfSamples = Convert.ToUInt64(euData.Count);
emc.ParentModule.NumberOfSamples = Convert.ToUInt64(euData.Count);
//Don't use the source channel emc anymore.
sourceEmc = null;
//Scale the data
var adcData = ScaleEuData(ref euData, out double scaleFactor);
cc.Sensitivity = scaleFactor;
//Test by writing source channel data. Readback should be identical
var f = new Serialization.SliceRaw.File();
f.DefaultEncoding = Encoding.Unicode.CodePage;
//Create file with from source channel. At this point it's not complete -- it still needs number of
//samples, etc.
var filename = new Serialization.SliceRaw.File().GetCalculatedChannelFileNameFromTestNameAndChannelNumber(testId, cc.Number);
var filepath = System.IO.Path.Combine(folder, filename);
cc.Data.ScaleFactorMv = 1 / scaleFactor;
cc.Data.ScaleFactorEU = 1 / scaleFactor;
switch (calculation)
{
default:
cc.DesiredRange = Constants.ADC_MIDPOINT / scaleFactor;
if (sourceChannel is Test.Module.AnalogInputChannel analogInputChannel) { cc.EngineeringUnits = analogInputChannel.EngineeringUnits; }
else { cc.EngineeringUnits = "NOT DEFINED"; }
break;
case Calculation.Sin:
case Calculation.Cos:
cc.DesiredRange = 1.1;
cc.EngineeringUnits = "rads";
break;
}
//All done with channel. Create persistent object with what's in the channel object
cc.PersistentChannelInfo = CreatePersistentInformationObject(cc, filepath);
//Write out the data to the memory-mapped file
cc.PersistentChannelInfo.BeginAppendSession();
cc.PersistentChannelInfo.AppendSessionData(adcData.ToArray());
cc.PersistentChannelInfo.EndAppendSession();
//Use the export writer to update the binary channel fields with everything that's missing. This will also
//Update the CRC.
var writer = (f.Exporter as Serialization.SliceRaw.File.Writer);
writer.CreatePersistentChannel(cc, cc.ParentModule.NumberOfSamples, cc.ParentModule.SampleRateHz, null, null, 0, 0);
//At this point the PersistentChannel is blown away. Read it back in and hook it up.
var bypass = false;
(f.Importer as Serialization.SliceRaw.File.Reader).ReadChannel(cc, cc.ParentModule, filepath, ref bypass);
emc.UnfilteredData = cc.PersistentChannelInfo;
if (calculation == Calculation.HIC)
{
var channelData = new ChannelData("g");
channelData.FilteredEU = euData.ToArray();
var hic = HeadInjuryCriterion.GetHeadInjuryCriterion(channelData, cc.SampleRateHz, clipLength);
cc.T1 = Convert.ToUInt64(hic.StartSample);
cc.T2 = Convert.ToUInt64(hic.EndSample);
cc.HIC = hic.HIC;
}
return new[] { cc };
}
/// <summary>
/// creates a calculated channel for binary operations (sine/cosine/integral/ffs)
/// </summary>
/// <param name="testId"></param>
/// <param name="folder"></param>
/// <param name="calculation"></param>
/// <param name="inputChannels"></param>
/// <param name="channelName"></param>
/// <param name="channelOffset"></param>
/// <returns></returns>
private static Test.Module.CalculatedChannel[] CreateChannelsBinaryOperation(string testId, string folder,
Calculation calculation, List<Test.Module.Channel> inputChannels, string channelName, int channelOffset)
{
var sourceEmc = inputChannels[0].emc as Event.Module.Channel;
var sourceChannel = inputChannels[0];
//Create new test channel
var cc = CreateCalculatedChannelsIRTRACC(calculation, new[] { sourceChannel });
cc.Calculation = calculation.ToString();
cc.ChannelDescriptionString = channelName;
cc.AbsoluteDisplayOrder = ChannelNumberCalculationChannelIndicator + channelOffset;
cc.Number = ChannelNumberCalculationChannelIndicator + channelOffset;
cc.ProportionalToExcitation = false;
cc.ZeroMvInADC = 0;
cc.OriginalOffsetADC = 0;
cc.Data.MvPerEu = 1;
cc.Data.Multiplier = sourceEmc.Multiplier;
cc.Data.UnitConversion = sourceEmc.UnitConversion;
cc.Data.UserOffsetEU = sourceEmc.UserOffsetEU;
cc.Excitation = 0;
cc.ZeroMethod = ZeroMethodType.None;
cc.RemoveOffset = false;
cc.ChannelId = sourceChannel.ChannelId + "_" + cc.Number;
cc.ChannelGroupName = sourceChannel.ChannelGroupName;
cc.IsInverted = false;
var emc = new Event.Module.AnalogInputChannel(cc, sourceEmc.ParentModule, sourceEmc.AbsoluteNumber);
cc.emc = emc;
//Do the maths. Use source channel as source for data as it's properly set up.
var euData = PerformCalculationBinary(calculation, sourceEmc);
//Don't use the source channel emc anymore.
sourceEmc = null;
//Scale the data
var adcData = ScaleEuData(ref euData, out var scaleFactor);
cc.Sensitivity = scaleFactor;
//Test by writing source channel data. Readback should be identical
var f = new Serialization.SliceRaw.File { DefaultEncoding = Encoding.Unicode.CodePage };
//Create file with from source channel. At this point it's not complete -- it still needs number of
//samples, etc.
var filename = new Serialization.SliceRaw.File().GetCalculatedChannelFileNameFromTestNameAndChannelNumber(testId, cc.Number);
var filepath = System.IO.Path.Combine(folder, filename);
cc.Data.ScaleFactorMv = 1 / scaleFactor;
cc.Data.ScaleFactorEU = 1 / scaleFactor;
switch (calculation)
{
default:
cc.DesiredRange = Constants.ADC_MIDPOINT / scaleFactor;
if (sourceChannel is Test.Module.AnalogInputChannel analogInputChannel) { cc.EngineeringUnits = analogInputChannel.EngineeringUnits; }
else { cc.EngineeringUnits = "NOT DEFINED"; }
break;
case Calculation.Sin:
case Calculation.Cos:
cc.DesiredRange = 1.1;
cc.EngineeringUnits = "rads";
break;
}
//All done with channel. Create persistent object with what's in the channel object
cc.PersistentChannelInfo = CreatePersistentInformationObject(cc, filepath);
//Write out the data to the memory-mapped file
cc.PersistentChannelInfo.BeginAppendSession();
cc.PersistentChannelInfo.AppendSessionData(adcData.ToArray());
cc.PersistentChannelInfo.EndAppendSession();
//Use the export writer to update the binary channel fields with everything that's missing. This will also
//Update the CRC.
var writer = (f.Exporter as Serialization.SliceRaw.File.Writer);
writer.CreatePersistentChannel(cc, cc.ParentModule.NumberOfSamples, cc.ParentModule.SampleRateHz, null, null, 0, 0);
//At this point the PersistentChannel is blown away. Read it back in and hook it up.
var bypass = false;
(f.Importer as Serialization.SliceRaw.File.Reader).ReadChannel(cc, cc.ParentModule, filepath,
ref bypass);
emc.UnfilteredData = cc.PersistentChannelInfo;
return new[] { cc };
}
public enum ThreeDIRTraccType
{
Thorax,
Abdomen,
LowerThorax
}
private static short[] ScaleEuData(ref IList<double> euData, out double scaleFactor)
{
scaleFactor = 1.0;
var max = (from d in euData select Math.Abs(d)).Max();
if (0 == max)
{
max = 1;
}
//we used ABS above, so we need to consider that the signal could be bipolar and be peak to peak of 2*max
max *= 2;
if (short.MaxValue > max)
{
scaleFactor = short.MaxValue / max;
}
else
{
scaleFactor = max / short.MaxValue;
}
//Scale the data
var data = new List<short>();
foreach (var euSample in euData)
{
data.Add((short)(euSample * scaleFactor));
}
return data.ToArray();
}
/// <summary>
/// Validate name of the new calculated channel
/// </summary>
/// <param name="channelName">name of the new calculated channel</param>
/// <param name="inputChannels">existing channels</param>
/// <param name="allChannels">If null - calling form MakeCalculatedChannels() in Download.xaml.cs</param>
/// <param name="errorList">list of errors</param>
/// <returns>if anyy errors - returns false</returns>
public static bool ValidateChannelName(string channelName, List<Test.Module.Channel> inputChannels, List<Test.Module.Channel> allChannels, out List<string> errorList)
{
errorList = new List<string>();
if (allChannels == null) return errorList.Count == 0;
if (string.IsNullOrEmpty(channelName)) { errorList.Add("Channel name cannot be empty."); }
if (inputChannels.Exists(ch => ch.ChannelDescriptionString == channelName))
{ errorList.Add("Channel name is not unique. [input channels]"); }
if (allChannels.Exists(ch => ch.ChannelDescriptionString == channelName))
{ errorList.Add("Channel name is not unique. [all channels]"); }
return errorList.Count == 0;
}
#region helper classes
private sealed class ClonableDoubles : List<double>, ICloneable
{
object ICloneable.Clone()
{
var l = new List<double>();
l.AddRange(ToArray());
return l;
}
}
#endregion
}
}