/// <summary> /// Возвращает фрагмент сигнала, соответствующий периоду первого стимула; /// по возможности сглаженный со стабилизированной амплитудой, /// </summary> /// <param name="ismid"></param> /// <param name="all_data">data package to which returned channel data belongs</param> /// <param name="begin_mark">beginning of the relevant signal</param> /// <param name="end_mark">end of the relevant signal in the returned signal's buffer</param> /// <returns></returns> private ChannelData GetSignal( HrvRawData ismid, ref PatientPhysioData all_data, ref long begin_mark, ref long end_mark) { var data = ismid.PhysioData; // and we must have at least some physio data System.Diagnostics.Debug.Assert(data != null); all_data = data; ChannelData result = null; // first, we take all the data var ecg_data = data.GetChannelDataBySignalType(SignalType.ECG); var ppg_data = data.GetChannelDataBySignalType(SignalType.PPG); // select appropriate signal using 'a priori' information about priority of signal types result = GetPreferredSignal(ecg_data, ppg_data); if (result == null) { return(null); } begin_mark = ismid.PhSyncBegin; end_mark = ismid.PhSyncEnd; // Возвращаем результат return(result); }
/// <summary> /// throws exception if something is wrong with the input data /// </summary> private void TestIfDataAreAcceptable( HrvRawData hrvData, SvmrRawData svmrData, out TestInfo hrvTestInfo, out TestInfo svmrTestInfo) { if (null == hrvData) { throw new ArgumentOutOfRangeException( "This summary generator requires HRV & SVMR data. HRV test data not found." ); } if (null == svmrData) { throw new ArgumentOutOfRangeException( "This summary generator requires HRV & SVMR data. SVMR test data not found." ); } if (ReferenceEquals(hrvData, svmrData)) { throw new Exception("Logic error. Please report the error to solution vendor!"); } svmrTestInfo = svmrData.TestInfo; hrvTestInfo = hrvData.TestInfo; if (ReferenceEquals(svmrTestInfo, hrvTestInfo)) { throw new Exception( "2 methods must have different result instances. Please report the error to developers!" ); } if (svmrTestInfo.Patient == null) { throw new Exception("SVMR data doesn't have employee/patient information!"); } if (hrvTestInfo.Patient == null) { throw new Exception("HRV data doesn't have employee/patient information!"); } if (svmrTestInfo.Patient.Id != hrvTestInfo.Patient.Id) { throw new Exception( "Summary report will only be generated based on results belonging to a single employee!" ); } }
public SummaryDocument GenerateSummary(string databaseId, Guid inspectionId, Employee employee, SvmrRawData svmrData, HrvRawData hrvData) { TestIfDataAreAcceptable(hrvData, svmrData, out TestInfo hrvTestInfo, out TestInfo _); _log.Info($"Input data for employee '{employee}' seem to be accessible"); DateTimeOffset completionDate = GetCompletionDate(hrvData, svmrData); var hostName = hrvTestInfo.MachineName; new RusHydroHrvAnalyzer().GetHrvConclusion( hrvData, hrvData.TestInfo.TestId, out PreShiftHrvConclusion hrvConclusion); new RusHydroSvmrAnalyzer().GetSvmrConclusion( svmrData, svmrData.TestInfo.TestId, out PreShiftSvmrConclusion svmrConclusion); var finalConclusion = FinalStatusProvider.GetFinalConclusion(hrvConclusion, svmrConclusion); finalConclusion.InspectionId = inspectionId; _log.Info($"Summary prepared for employee {employee}."); return(new SummaryDocument { Employee = employee, CompletionTime = completionDate, HrvConclusion = hrvConclusion, SvmrConclusion = svmrConclusion, FinalConclusion = finalConclusion, HostName = hostName, InspectionId = inspectionId }); }
public HrvResults ProcessData(HrvRawData rawHrvData) { base.ProcessData(rawHrvData); double[] valid_intervals_durations = null; List <CardioInterval> valid_intervals = null; // recorded data contains cardio intervals // this is the case when the inspection was performed // using a cardio intervals sensor // (e.g. Bluetooth HR sensor with RR intervals) if (InputDataContainsCardioIntervals(rawHrvData)) { var heartRythmData = ReadCardioIntervalsFromResultSet(rawHrvData); valid_intervals = heartRythmData.Item1; HrvResults.RATED_HR_MARKS = heartRythmData.Item2.ToArray(); } else { ChannelData data = null, adc_data = null; double[] hrv_marks = null; SelectAndProcessSignal(m_settings.RejectLowQualitySignalAreas, rawHrvData, out hrv_marks, out data, out adc_data); if ((null == data) || (null == adc_data)) { throw new DataProcessingException(strings.NoSignalInSourceData); } HrvResults.CRV_CARDIO_SIGNAL = new PhysioSignalView( data.GetDataAsFloat(), (int)data.BitsPerSample, (float)data.SamplingRate, data.PhysioSignalType ); HrvResults.CRV_CARDIO_SIGNAL_ADC = new PhysioSignalView( adc_data.GetDataAsFloat(), (int)adc_data.BitsPerSample, (float)adc_data.SamplingRate, adc_data.PhysioSignalType ); // отметки пульса с оценкой достоверности var hc_marks_to_rate = new List <RatedContractionMark>(hrv_marks.Length); for (int i = 0; i < hrv_marks.Length; ++i) { var mark = new RatedContractionMark { Position = hrv_marks[i], Valid = false }; hc_marks_to_rate.Add(mark); } // получаем достоверные интервалы в миллисекундах, // "хорошие" сокращения будут помечены как хорошие (по обоим параметрам), var converted_peaks = PeaksFilter.ConvertPeaksToIntervalsWithRejectionAndRatePeaks( hc_marks_to_rate, data.SamplingRate, m_settings.GetApplicableMinIntervalLength(), m_settings.GetApplicableMaxIntervalLength(), m_settings.GetApplicableMaxIntervalDeltaRelative(), adc_data); valid_intervals = converted_peaks.extracted_intervals; if (converted_peaks.extracted_intervals.Count < 5) { throw new DataProcessingException(string.Format(strings.too_few_intervals_detected_in_signal, valid_intervals.Count)); } // это все отметки пульса HrvResults.CRV_HR_MARKS = hrv_marks; // это отметки пульса с оценками качества HrvResults.RATED_HR_MARKS = converted_peaks.rated_heart_contraction_marks.ToArray(); // Спектр сигнала ФПГ HrvResults.SignalSpectrum = PpgSpectrumAnalyzer.CalculateSpectrum(data.Data, data.SamplingRate); } // только длительности интервалов valid_intervals_durations = (from interval in valid_intervals select interval.duration).ToArray(); var peakAmoDistrib = default(MinMaxModeDescriptor); // кардио-интервалы подготовлены try { var mathStatData = new StatData(); // считаем статистику по кардио-интервалам Calculator.CalcStatistics(valid_intervals_durations, mathStatData); Calculator.MakeProbabilityDensity(valid_intervals_durations, mathStatData, 440, 2000, 4); // The standard approach Calculator.MakeDistribution(valid_intervals_durations, mathStatData, 0, 2500, 50); // Find the maximized & minimized mode amplitude values peakAmoDistrib = MinMaxModeAmpFinder.GetPeakAmoDistrib(valid_intervals_durations, 50, 0, 2500, 1); HrvResults.CRV_STAT = _mapper.Map <Methods.ObjectModel.Statistics.StatData>(mathStatData); } catch (ArgumentException) { throw new DataProcessingException( string.Format(strings.too_few_valid_intervals_detected_in_signal, valid_intervals_durations.Length)); } HrvResults.Indicators.HRV_Triangular_Index = CalcHrvTriangularIndex(valid_intervals_durations); double AMo = Statistics.distribution.mode_amplitude; // Безразмерная -- в долях от 1 double Mo = Statistics.distribution.mode / 1000; // В секундах!!! //double AMo = peakAmoDistrib.ModeAmp; // Безразмерная -- в долях от 1 //double Mo = peakAmoDistrib.Mode / 1000; // В секундах!!! double M = Statistics.m / 1000; // В секундах!!! double dNN = Statistics.varRange / 1000; // В секундах!!! double Sigma = Statistics.sigma / 1000; // В секундах!!! var _AMo = Statistics.distribution.mode_amplitude; // Безразмерная -- в долях от 1 var _Mo = Statistics.distribution.mode / 1000; // В секундах!!! var AmoMax = peakAmoDistrib.MaxModeDescriptor.ModeAmp; // maximized Mode Amplitude var MoMax = peakAmoDistrib.MaxModeDescriptor.Mode / 1000; // Mode when the Mode amplitude is maximized var AmoMin = peakAmoDistrib.MinModeDescriptor.ModeAmp; // minimized Mode Amplitude var MoMin = peakAmoDistrib.MinModeDescriptor.Mode / 1000; // Mode when the Mode amplitude is minimized var AmoMid = peakAmoDistrib.MidModeDescriptor.ModeAmp; // mean Mode Amplitude var MoMid = peakAmoDistrib.MidModeDescriptor.Mode / 1000; // Mode when the Mode amplitude is close to mean // Вычислим индексы Баевского // умножаем на 100, т.к. "по Баевскому" АМо указывается в %%. HrvResults.Indicators.IN = AMo * 100 / (2.0 * Mo * dNN); HrvResults.Indicators.IN_Max = AmoMax * 100 / (2.0 * MoMax * dNN); HrvResults.Indicators.IN_MaxMode = MoMax; HrvResults.Indicators.IN_Min = AmoMin * 100 / (2.0 * MoMin * dNN); HrvResults.Indicators.IN_MinMode = MoMin; HrvResults.Indicators.IN_Mid = AmoMid * 100 / (2.0 * MoMid * dNN); HrvResults.Indicators.IN_MidMode = MoMid; // TODO: выяснить, что правильно, что нет: m_BaevskyStatistics._VPR = 1.f / (fM * fX); HrvResults.Indicators.VPR = 1.0 / (Mo * dNN); HrvResults.Indicators.PAPR = AMo * 100 / Mo; HrvResults.Indicators.IVR = AMo * 100 / dNN; // Психофизиологическая цена [адаптации] // АМ [%%] / ( Мат. ожидание [с] * сигма [с] ) // умножаем на 100, т.к. АМо д.б. в %%. HrvResults.Indicators.PPPA = AMo * 100 / (M * Sigma); // Передать в результаты теста также и сами кардиоинтервалы // с моментами их появления в записи HrvResults.CRV_INTERVALS = valid_intervals.ToArray(); // Спектр кардиоинтервалов HrvResults.IntervalsSpectrum = HrvSpectrumAnalyzer.MakeCardioIntervalsSpectrum(valid_intervals); double sum = 0.0; double n50 = (valid_intervals[0].duration > 50) ? 1 : 0; for (int i = 1; i < valid_intervals.Count; i++) { double diff = valid_intervals[i].duration - valid_intervals[i - 1].duration; sum += Math.Pow(diff, 2); if (diff > 50) { n50++; } } HrvResults.Indicators.pNN50 = 100.0 * n50 / (valid_intervals.Count - 1); HrvResults.Indicators.RMSSD = Math.Sqrt(sum); HrvResults.CRV_SCATTEROGRAMM_PARAMETERS = GetScatterParams(valid_intervals_durations); HrvResults.ResultsReliability = GetResultsReliability(HrvResults); return((HrvResults)ProcessorOutputData); }
private void SelectAndProcessSignal( bool rejectLowQualitySignalAreas, HrvRawData ismid, out double[] out_hrv_marks, out ChannelData out_channel_data, out ChannelData out_adc_data) { long begin = 0; long end = 0; out_hrv_marks = null; out_channel_data = null; PatientPhysioData all_data = null; ChannelData data = GetSignal(ismid, ref all_data, ref begin, ref end); if (rejectLowQualitySignalAreas) { // BUG: this is for Rushydro only, until there is a solution // to mask lower-quality portions of the signal at the beginning all_data.MoveMarker(begin, 12000000L); } out_adc_data = new ChannelData(data); if (data == null) { return; } if (data.PhysioSignalType == SignalType.PPG) { //int[] unfiltered_data_array = data.GetData(); int[] data_buffer = data.Data; // обрабатываем данные с помощью стандартного процессора // (фильтруем, стабилизируем амплитуду и находим фронты) // обрабатываем все данные, и даже нерелевантные, чтобы не повредить релевантные данные // (в процессе инициализации фильтра сигнал выглядит довольно страшно) double[] hrv_marks = Pulse.PpgPulseDetectorHelper.ProcessPpgData( data.SamplingRate, data_buffer, (int)data.BitsPerSample, data.DeviceTypeName); // избавляемся от лишних данных и лишних отметок пульса, // корректируя положение релевантных отметок ChannelData relevant_data = all_data.GetChannelDataFromLeftToRight(data, begin, end); long left_marker_data_count = all_data.GetChannelDataCountForMarker(data, begin); if (left_marker_data_count < 0) { left_marker_data_count = 0; } long right_marker_data_count = all_data.GetChannelDataCountForMarker(data, end); if (right_marker_data_count < 0) { right_marker_data_count = data.Data.Length; } var relevant_hrv_marks = new List <double>(hrv_marks.Length); for (int i = 0; i < hrv_marks.Length; ++i) { // добавляем в список только те отметки, которые попали между левым и правым маркером if ((hrv_marks[i] >= left_marker_data_count) && (hrv_marks[i] <= right_marker_data_count)) { relevant_hrv_marks.Add(hrv_marks[i] - ((double)left_marker_data_count)); } } out_hrv_marks = relevant_hrv_marks.ToArray(); out_channel_data = relevant_data; } else if (data.PhysioSignalType == SignalType.ECG) { // not implemented so far return; } else { // other signal types not supported return; } }
private Tuple <List <CardioInterval>, List <RatedContractionMark> > ReadCardioIntervalsFromResultSet(HrvRawData rawData) { var data_for_all = rawData.PhysioData; System.Diagnostics.Debug.Assert(data_for_all != null); // in this method we always have only one patient var all_data = rawData.PhysioData; // and we must have at least some physio data System.Diagnostics.Debug.Assert(all_data != null); // first, we take all the data var intervals = all_data.GetChannelDataBySignalType(SignalType.CardioIntervals); if (intervals == null) { // there are no intervals stored in the result set in a known format... // use InputDataContainsCardioIntervals to check before calling Read... throw new InvalidOperationException( "The test data don't contain cardio intervals. Did you check the cardio interval data are present?"); } long begin_mark = rawData.PhSyncBegin; long end_mark = rawData.PhSyncEnd; // Читаем данные из канала и заполняем массив интервалов и меток сердечных сокращений var signal = intervals.First(); logger.Info($"Using pre-recorded cardio intervals from {signal.ChannelId}"); var valid_intervals = new List <CardioInterval>(signal.Data.Length); var hc_marks = new List <RatedContractionMark>(signal.Data.Length); // запоминаем количество сэмплов для первой метки времени long startCount = (begin_mark == -1) ? 0 : all_data.GetChannelDataCountForMarker(signal, begin_mark); var intervalsMSec = signal.Data; var lastTimestampId = signal.Timestamps.Keys.Max(); var lastTimestamp_uSec = signal.Timestamps[lastTimestampId]; const double oneMillion = 1000000.0; hc_marks.Add(new RatedContractionMark() { // здесь: секунды (канал Кардио Интервалы работает с декларированной частотой 1) Position = (lastTimestamp_uSec) / oneMillion, IntervalsCount = 1, // последняя метка всегда участвует только в 1 интервале Valid = true }); // пока просто возьмем все сэмплы, и расположим их начиная от последней метки времени // считая, что все интервалы поступали непрерывно // это приемлемо для статистической обработки, // но не всегда приемлемо при использовании time-domain и frequency-domain методов for (var i = intervalsMSec.Length - 1; i >= (int)startCount; --i) { var intervalDuration_uSec = intervalsMSec[i] * 1000; hc_marks.Insert(0, new RatedContractionMark() { // здесь: секунды (канал Кардио Интервалы работает с декларированной частотой 1) Position = (lastTimestamp_uSec - intervalDuration_uSec) / oneMillion, IntervalsCount = 2, Valid = true }); valid_intervals.Insert(0, new CardioInterval( (lastTimestamp_uSec - intervalDuration_uSec) / 1000, lastTimestamp_uSec / 1000.0, i, i + 1) ); lastTimestamp_uSec = lastTimestamp_uSec - intervalDuration_uSec; } if (hc_marks.Count != 0) { hc_marks.First().IntervalsCount = 1; hc_marks.Last().IntervalsCount = 1; } // потом сравним первую метку времени и вычисленную метку времени // при больших расхождениях, результат частотного анализа ритма сердца // может быть недостоверным return(Tuple.Create(valid_intervals, hc_marks)); }