/// <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.");
            }
        }
Example #5
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(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]));
        }
Example #6
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));
        }
Example #7
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);
        }
Example #9
0
        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);
        }