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 inputChannels, string channelName, int startingNumber, List allChannels, int clipLength, out List errorList, int defaultEncoding) { Test.Module.CalculatedChannel[] calculatedChannels = null; errorList = new List(); 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); } /// /// 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 /// /// /// /// private static IList PerformCalculationsAggregate(Calculation calculation, List 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>(); //this will hold the output data var dOutput = new List(); //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(); //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(); var rates = new List(); 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; } /// /// performs calculations for a binary calculation channel (integrate/differentiate/FFT, etc) /// /// /// /// private static IList 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(); } 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); //} /// /// performs the IR-Tracc 3D calculation /// based on issue /// 7489 Implement 2D/3D IRTRACC support /// /// /// /// /// private static void PerformCalculation(out IList dAX, out IList dAY, out IList dAZ, out IList 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(length); dAY = new List(length); dAZ = new List(length); dOut = new List(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 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(); eventAggregator.GetEvent().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 dax, out IList day, out IList daz, out IList 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 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(); eventAggregator.GetEvent().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 }; } /// /// creates a calculated channel for binary operations (sine/cosine/integral/ffs) /// /// /// /// /// /// /// /// private static Test.Module.CalculatedChannel[] CreateChannelsBinaryOperation(string testId, string folder, Calculation calculation, List 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 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(); foreach (var euSample in euData) { data.Add((short)(euSample * scaleFactor)); } return data.ToArray(); } /// /// Validate name of the new calculated channel /// /// name of the new calculated channel /// existing channels /// If null - calling form MakeCalculatedChannels() in Download.xaml.cs /// list of errors /// if anyy errors - returns false public static bool ValidateChannelName(string channelName, List inputChannels, List allChannels, out List errorList) { errorList = new List(); 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, ICloneable { object ICloneable.Clone() { var l = new List(); l.AddRange(ToArray()); return l; } } #endregion } }