/// <summary> /// Calculate features from a unidimensional time-based feature /// </summary> 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, ExciteOMeterManager.ConvertFloatToString(timestamps[timestamps.Count - 1]) + "," + ExciteOMeterManager.ConvertFloatToString(featureValue)); // 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> /// 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); }
void PostProcessing() { if (isSampleBasedFeature && matchLengthOfInputSignal) { // CASE: Match length of input signal, recording has finished but input signal is larger than features. // This saves in the log the number of lines remaining to match the length of the input. idxStartRepeating = isFirstCalculatedFeature? 0 : (overlappingSamples - offsetSamplesTimestamp); for (int i = idxStartRepeating; i < timestamps.Count; 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)); } } ////////////// POSTPROCESSING // Postprocessing control instancesFinishedPostprocessing++; if (instancesFinishedPostprocessing == numInstances) { // All feature extraction have finished postprocessing ExciteOMeterManager.instance.PostProcessingExciteOMeterLevel(); // Reset instancesFinishedPostprocessing = 0; } }
void ProcessStringLog(ExciteOMeter.DataType type, float timestamp, string message, MarkerLabel label) { bool written = LoggerController.instance.WriteLine(LogName.EventsAndMarkers, ExciteOMeterManager.ConvertFloatToString(timestamp) + "," + type.ToString() + "," + message + "," + label.ToString()); if (!written) { Debug.LogWarning("The Logger Controller has not been setup to store strings. Please setup a file with LogID EventsAndMarkers."); } }
/// <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(float[] newSample, double timeStamp) { //TODO: Use the timestamp from the sensor, which is in nanoseconds. EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), newSample[0]); LoggerController.instance.WriteLine(LogName.VariableRrInterval, ExciteOMeterManager.GetTimestampString() + "," + ExciteOMeterManager.ConvertFloatToString(newSample[0])); }
/// <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(short[] newSample, double timeStamp) { //TODO: The event only sends float[], all samples need to be parsed to float EoM_Events.Send_OnDataReceived(VariableType, ExciteOMeterManager.GetTimestamp(), (float)newSample[0]); LoggerController.instance.WriteLine(LogName.VariableHeartRate, ExciteOMeterManager.GetTimestampString() + "," + ExciteOMeterManager.ConvertFloatToString(newSample[0], 0)); }
public void FixedUpdate() { if (isConfigured && ExciteOMeterManager.currentlyRecordingSession) { // Timer control elapsedTime += Time.fixedDeltaTime; // Send data each "sendingPeriod" if (elapsedTime >= sendingPeriod) { // Reset timer for next event elapsedTime = 0.0f; // Define array transformArray[0] = objectToTrack.position.x; transformArray[1] = objectToTrack.position.y; transformArray[2] = objectToTrack.position.z; transformArray[3] = objectToTrack.localRotation.w; // Quaternion without axis. transformArray[4] = objectToTrack.localRotation.x; // Q in the axis i transformArray[5] = objectToTrack.localRotation.y; // Q in the axis j transformArray[6] = objectToTrack.localRotation.z; // Q in the axis k transformArray[7] = objectToTrack.localEulerAngles.x; // Pitch (up-down) transformArray[8] = objectToTrack.localEulerAngles.y; // Yaw (left-right) transformArray[9] = objectToTrack.localEulerAngles.z; // Roll (towards shoulders) // Create string to save in CSV transformArrayText = ""; foreach (float v in transformArray) { transformArrayText += "," + ExciteOMeterManager.ConvertFloatToString(v, 4); } logIsWriting = LoggerController.instance.WriteLine(logToWrite, ExciteOMeterManager.GetTimestampString() + transformArrayText); if (!logIsWriting) { Debug.LogWarning("Error writing movement data. Please setup LoggerController with a file with LogID is" + logToWrite.ToString()); } //Debug.Log("Sending Movement from " + gameObject.name + " > " + transformArrayText); // Send an event with the multidimensional data for the receivers taht can handle multidimensionality EoM_Events.Send_OnDataArrayReceived(DataType.Headset_array, ExciteOMeterManager.GetTimestamp(), transformArray); // Send values individually, because they need to be stored in the .json file as unidimensional data, so that they can be visualized // --------- TODO //// 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 (transformArray.Length != typesTransformArray.Length) { Debug.LogError("The movement arrays are not the same length"); } for (int i = 0; i < transformArray.Length; i++) { EoM_Events.Send_OnDataReceived(typesTransformArray[i], ExciteOMeterManager.GetTimestamp(), transformArray[i]); } /// -------------------------------------------------------- } } }
////////////// 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); }
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, ExciteOMeterManager.ConvertFloatToString(timestampEOM) + "," + ExciteOMeterManager.ConvertFloatToString(valueEOM, 5)); } return(true); }