Пример #1
0
        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"));
        }
Пример #3
0
        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);
        }
Пример #5
0
        /// <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();
        }
Пример #6
0
        /// <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();
        }
Пример #7
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(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"));
            }
        }
Пример #8
0
        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;
            }
        }
Пример #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, 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);
        }