void TimeBasedCalculation() { // New window time is the original window time minus overlapping window. Set elapsed time in proportional position. elapsedWindowTime = windowTime * overlappingFraction; // Check that data is available if (timestamps.Count == 0 && dataBuffer.Count == 0) { ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString()); return; } // Calculate feature featureValue = CalculateFeature(timestamps.ToArray(), dataBuffer.ToArray()); // Send events and log in file ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[timestamps.Count - 1] + ", " + featureValue.ToString()); EoM_Events.Send_OnDataReceived(outputDataType, timestamps[timestamps.Count - 1], featureValue); LoggerController.instance.WriteLine(logIdentifier, timestamps[timestamps.Count - 1] + "," + featureValue.ToString()); // Rearrange overlap in signal // Overlap should not be greater than 95%, because it would generate very often feature calculations that might affect performance. int elementsToDelete = (int)(Mathf.Clamp(1.0f - (overlappingFraction), 0f, 0.95f) * dataBuffer.Count); timestamps.RemoveRange(0, elementsToDelete); dataBuffer.RemoveRange(0, elementsToDelete); }
/// <summary> /// Override this method to implement whatever should happen with the samples... /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use /// coroutines for more complexe processing tasks to distribute processing time over /// several frames /// </summary> /// <param name="newSample"></param> /// <param name="timeStamp"></param> protected void Process2(int numSamples, int[,] newSamples, double[] timeStamps) //protected override void Process(int numSamples, int[,] newSamples, double[] timeStamps) { ExciteOMeterManager.DebugLog("ECG NumSamples: " + numSamples); // pull as long samples are available for (int i = 0; i < numSamples; i++) { if (newSamples[0, i] > 1.0e7) // Check values with bad parsing { ExciteOMeterManager.DebugLog("Error parsing value: " + BitConverter.ToString(BitConverter.GetBytes(newSamples[0, i])) + ", " + newSamples[0, i].ToString("F2")); continue; } else { EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), newSamples[0, i]); LoggerController.instance.WriteLine(LogName.VariableRawECG, newSamples[0, i].ToString("F0") + "," + newSamples[0, i].ToString("F0")); } } // Debug.Log($"Receiving from stream {StreamName}, first sample {newSample[0]}"); //TODO: The event only sends float[], all samples need to be parsed to float // EoM_Events.Send_OnDataReceived(VariableType, new float[1]{(float) newSample[0]}); //LoggerController.instance.WriteLine(LogName.VariableRawECG, timestamp.ToString("F0") + "," + newSample[0].ToString("F0")); }
protected override float CalculateFeature(float[] timestamps, float[] values) { /* * REFERENCE: https://www.frontiersin.org/articles/10.3389/fpubh.2017.00258/full * * RMSSD * The root mean square of successive differences between normal heartbeats (RMSSD) is obtained by first calculating each successive time difference between heartbeats in ms. Then, each of the values is squared and the result is averaged before the square root of the total is obtained. While the conventional minimum recording is 5 min, researchers have proposed ultra-short-term periods of 10 s (30), 30 s (31), and 60 s (36). */ int length = values.Length; if (length < 2) { ExciteOMeterManager.DebugLog("It is not possible to calculate RMSSD with less than 2 values"); return(0); } // Calculation N = length - 1; // Size of final vector of differences cumsum = 0; for (int i = 0; i < N; i++) { diff = values[i + 1] - values[i]; cumsum += (float)Math.Pow(diff, 2); } ; result = (float)Math.Sqrt(cumsum / N); return(result); }
/// <summary> /// Calculate feature value from a unidimensional sample-based feature /// </summary> void SampleBasedCalculation() { // Check that data is available if (timestamps.Count == 0 && dataBuffer.Count == 0) { // ExciteOMeterManager.DebugLog("No timestamps or data were found to calculate features"); ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString()); return; } // Calculate feature featureValue = CalculateFeature(timestamps.ToArray(), dataBuffer.ToArray()); // Calculate offset of timestamp that corresponds to the calculated feature (# displacements to the left in timestamps) // Examples: Assume `sampleBuffer=5` // If `offsetSamplesTimestamp=0`, t for calculated feature is [t-4,t-3,...,t] // If `offsetSamplesTimestamp=3`, t for calculated feature is [t-1,t,t+1,t+2,t+3] indexOffsetForTimestamp = sampleBuffer - offsetSamplesTimestamp - 1; // Send events and log in file ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[indexOffsetForTimestamp] + ", " + featureValue.ToString()); // Flag to know if it is the first calculation of the feature. // If so, the new feature has to match all the timestamps existing before the first timestamp of the feature. if (matchLengthOfInputSignal) { if (isFirstCalculatedFeature) { isFirstCalculatedFeature = false; idxStartRepeating = 0; // No previous data in array, repeat from beginning of input timestamps. } else { // CASE: Match length and buffer already contains data from previous window // Based on overlap and offset, the position where to start repeating timestamps is the following formula. idxStartRepeating = overlappingSamples - offsetSamplesTimestamp; } // Fill the previous timestamps of the input signal with the same value of this feature. for (int i = idxStartRepeating; i <= indexOffsetForTimestamp; i++) { // Write in files to collect data corresponding to EoM_Events.Send_OnDataReceived(outputDataType, timestamps[i], featureValue); LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[i]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue)); } } else { Debug.LogWarning("Error calculating feature. Matching sampling error: logIdentifier" + logIdentifier.ToString()); //// CASE: DO NOT MATCH LENGTH OF INPUT SIGNAL, BUT USE TIMESTAMP DIFFERENT THAN LAST SAMPLE //EoM_Events.Send_OnDataReceived(outputDataType, timestamps[indexOffsetForTimestamp], featureValue); //LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[indexOffsetForTimestamp]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue)); } // Rearrange overlap in signal elementsToDelete = sampleBuffer - overlappingSamples; timestamps.RemoveRange(0, elementsToDelete); dataBuffer.RemoveRange(0, elementsToDelete); }
/// <summary> /// Callback method for the Resolver gets called each time the resolver misses a stream within its cache /// </summary> /// <param name="stream"></param> public virtual void AStreamGotLost(LSLStreamInfoWrapper stream) { if (!isTheExpected(stream)) { return; } ExciteOMeterManager.DebugLog(string.Format("LSL Stream {0} Lost for {1}", stream.Name, name)); OnStreamLost(); }
/// <summary> /// Callback method for the Resolver gets called each time the resolver found a stream /// </summary> /// <param name="stream"></param> public virtual void AStreamIsFound(LSLStreamInfoWrapper stream) { if (!isTheExpected(stream)) { return; } ExciteOMeterManager.DebugLog(string.Format("LSL Stream {0} found for {1}", stream.Name, name)); // In case the signal emulator is running. Deactivate it. EoM_SignalEmulator.DisableEmulator(); inlet = new LSL.liblsl.StreamInlet(stream.Item); expectedChannels = stream.ChannelCount; OnStreamAvailable(); }
/// <summary> /// Override this method to implement whatever should happen with the samples... /// IMPORTANT: Avoid heavy processing logic within this method, update a state and use /// coroutines for more complexe processing tasks to distribute processing time over /// several frames /// </summary> /// <param name="newSample"></param> /// <param name="timeStamp"></param> protected override void Process(int[] newSample, double timestamp) { //Assuming that a sample contains at least 3 values for x,y,z // Debug.Log($"Receiving from stream {StreamName}, first sample {newSample[0]}"); if (newSample[0] > 1.0e7) // Check values with bad parsing { ExciteOMeterManager.DebugLog("Error parsing value ACC: " + BitConverter.ToString(BitConverter.GetBytes(newSample[0])) + ", " + newSample[0].ToString("F2")); } else { //EoM_Events.Send_OnDataReceived(VariableType, new float[3]{(float) newSample[0], (float) newSample[1], (float) newSample[2]}); // TODO: The event for ACC should be a different delegate receiving float[] instead of single float. EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[0]); EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[1]); EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[2]); LoggerController.instance.WriteLine(LogName.VariableRawACC, timestamp.ToString("F2") + "," + newSample[0].ToString("F0") + "," + newSample[1].ToString("F0") + "," + newSample[2].ToString("F0")); } }
private void SetLoggingState(bool state) { // Define first timestamp of the log if (state == true) { // Started logging firstTimestamp = ExciteOMeterManager.GetTimestamp(); ExciteOMeterManager.DebugLog("First timestamp at " + firstTimestamp.ToString()); isFirstTimestampConfigured = true; } else { // Stopped logging isFirstTimestampConfigured = false; firstTimestamp = 0.0f; ExciteOMeterManager.DebugLog("First timestamp back to " + firstTimestamp.ToString()); } foreach (DataLogger l in loggers) { l.IsLogging = currentlyLogging; } }
public static bool Calculate(SessionVariables sessionData) { // Check that both arrays are the same length int N = sessionData.RMSSD.timestamp.Count; // Assess length of each timeseries, by default are assummed equal bool equalLength = true, useRRi = false, useRMSSD = false; if (sessionData.RRi.timestamp.Count != sessionData.RMSSD.timestamp.Count) { // timeseries are different length equalLength = false; N = Math.Min(sessionData.RRi.timestamp.Count, sessionData.RMSSD.timestamp.Count); // Which is the shorter signal? if (N == sessionData.RRi.timestamp.Count) { useRRi = true; } else if (N == sessionData.RMSSD.timestamp.Count) { useRMSSD = true; } } // Mean values float meanRRi = 0f; float meanRMSSD = 0f; for (int i = 0; i < N; i++) { // Check timestamps are similar :: +/-10ms offset if (Math.Abs(sessionData.RRi.timestamp[i] - sessionData.RMSSD.timestamp[i]) > 0.1f) { ExciteOMeterManager.DebugLog("WARNING: Two timestamps differ in more than 0.1s"); } // Cumulative sum meanRRi += sessionData.RRi.value[i]; meanRMSSD += sessionData.RMSSD.value[i]; } meanRRi = meanRRi / N; meanRMSSD = meanRMSSD / N; // Standard deviation float stdRRi = 0f; float stdRMSSD = 0f; for (int i = 0; i < N; i++) { stdRRi += (float)Math.Pow(sessionData.RRi.value[i] - meanRRi, 2); stdRMSSD += (float)Math.Pow(sessionData.RMSSD.value[i] - meanRMSSD, 2); } stdRRi = (float)Math.Sqrt(stdRRi / N); stdRMSSD = (float)Math.Sqrt(stdRMSSD / N); // Placeholder for z-score double zScoreRRi = 0f, percentileRRi = 0f; double zScoreRMSSD = 0f, percentileRMSSD = 0f; // Final EoM level float timestampEOM = 0.0f; float valueEOM = 0.0f; // Calculate final EoM level for (int i = 0; i < N; i++) { zScoreRRi = (double)((sessionData.RRi.value[i] - meanRRi) / stdRRi); zScoreRMSSD = (double)((sessionData.RMSSD.value[i] - meanRMSSD) / stdRMSSD); percentileRRi = Phi(zScoreRRi); percentileRMSSD = Phi(zScoreRMSSD); // Average of percentile of RRI and percentile of RMSSD valueEOM = (1 - (float)(percentileRRi + percentileRMSSD) / 2.0f); if (equalLength) { timestampEOM = (sessionData.RRi.timestamp[i]); } else if (useRRi) { timestampEOM = (sessionData.RRi.timestamp[i]); } else if (useRMSSD) { timestampEOM = (sessionData.RMSSD.timestamp[i]); } // Add to class that is exported as JSON SessionVariablesController.instance.WritePostProcessedExciteOMeterIndex(timestampEOM, valueEOM); // Add to CSV file LoggerController.instance.WriteLine(LogName.EOM, timestampEOM.ToString("F6") + "," + valueEOM.ToString("F5")); } return(true); }
////////////// MULTIDIMENSIONAL FEATURES /// <summary> /// Calculate features from a MULTIDIMENSIONAL sample-based feature /// </summary> void SampleBasedCalculationArray() { // Check that data is available if (timestamps.Count == 0 && dataBufferArray.Count == 0) { // ExciteOMeterManager.DebugLog("No timestamps or data were found to calculate features"); ExciteOMeterManager.LogInFile("No incoming data " + incomingDataType.ToString() + " was found to calculate feature " + outputDataType.ToString()); return; } // Calculate feature featureArray = CalculateFeatureArray(timestamps.ToArray(), dataBufferArray.ToArray()); // Calculate offset of timestamp that corresponds to the calculated feature (# displacements to the left in timestamps) // Examples: Assume `sampleBuffer=5` // If `offsetSamplesTimestamp=0`, t for calculated feature is [t-4,t-3,...,t] // If `offsetSamplesTimestamp=3`, t for calculated feature is [t-1,t,t+1,t+2,t+3] indexOffsetForTimestamp = sampleBuffer - offsetSamplesTimestamp - 1; // Send events and log in file ExciteOMeterManager.DebugLog("A new feature was calculated in " + outputDataType.ToString() + ": " + timestamps[indexOffsetForTimestamp] + ", Length: " + featureArray.Length.ToString()); // Flag to know if it is the first calculation of the feature. // If so, the new feature has to match all the timestamps existing before the first timestamp of the feature. if (matchLengthOfInputSignal) { if (isFirstCalculatedFeature) { isFirstCalculatedFeature = false; idxStartRepeating = 0; // No previous data in array, repeat from beginning of input timestamps. } else { // CASE: Match length and buffer already contains data from previous window // Based on overlap and offset, the position where to start repeating timestamps is the following formula. idxStartRepeating = overlappingSamples - offsetSamplesTimestamp; } // Get the DataType for each of the features that are calculated DataType[] featureDataTypes = Constants.SubsetOfFeaturesTransformDataTypes(logIdentifier); // Fill the previous timestamps of the input signal with the same value of this feature. for (int i = idxStartRepeating; i <= indexOffsetForTimestamp; i++) { // Write in files to collect data corresponding to // Create string to save in CSV string featureArrayText = ""; foreach (float v in featureArray) { featureArrayText += "," + ExciteOMeterManager.ConvertFloatToString(v, 4); } bool logIsWriting = LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[i]) + featureArrayText); if (!logIsWriting) { Debug.LogWarning("Error writing movement data. Please setup LoggerController with a file with LogID is" + logIdentifier.ToString()); } //// --------- TODO //// Send an event with the multidimensional data for the receivers taht can handle multidimensionality //EoM_Events.Send_OnDataArrayReceived(outputDataType, timestamps[i], featureArray); //// Visualizer is designed to analyze unidimensional data, therefore multidimensional needs to be sent one by one to the system //StartCoroutine(SendDataEventsMovement(ExciteOMeterManager.GetTimestamp())); // BUG: Sending events from the coroutine does not seem to be received... // --------- // If the above works, delete the remaining code!! ---------------------- if (featureDataTypes.Length != featureArray.Length) { Debug.LogError("Mismatch between the calculated array of features and the expected, in feature with logIdentifier" + logIdentifier); return; } for (int j = 0; j < featureArray.Length; j++) { EoM_Events.Send_OnDataReceived(featureDataTypes[j], timestamps[i], featureArray[j]); } /// -------------------------------------------------------- } } else { // CASE: DO NOT MATCH LENGTH OF INPUT SIGNAL, BUT USE TIMESTAMP DIFFERENT THAN LAST SAMPLE EoM_Events.Send_OnDataReceived(outputDataType, timestamps[indexOffsetForTimestamp], featureValue); LoggerController.instance.WriteLine(logIdentifier, ExciteOMeterManager.ConvertFloatToString(timestamps[indexOffsetForTimestamp]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue)); } // Rearrange overlap in signal elementsToDelete = sampleBuffer - overlappingSamples; timestamps.RemoveRange(0, elementsToDelete); dataBufferArray.RemoveRange(0, elementsToDelete); }