From 16e2451e90cd82435a6a1f20a1fb6ffe3db2ddfb Mon Sep 17 00:00:00 2001 From: Leif Warland Date: Wed, 22 Feb 2017 21:49:39 +0100 Subject: [PATCH] Added HRV metric --- src/Charts/LTMPlot.cpp | 41 ++++- src/Core/RideCache.cpp | 6 + src/Metrics/HrvMetrics.cpp | 349 +++++++++++++++++++++++++++++++++++++ src/Metrics/RideMetric.cpp | 4 +- src/Metrics/RideMetric.h | 5 +- src/src.pro | 2 +- 6 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 src/Metrics/HrvMetrics.cpp diff --git a/src/Charts/LTMPlot.cpp b/src/Charts/LTMPlot.cpp index 38f3abd50..cce38d4be 100644 --- a/src/Charts/LTMPlot.cpp +++ b/src/Charts/LTMPlot.cpp @@ -2625,7 +2625,6 @@ LTMPlot::createMetricData(Context *context, LTMSettings *settings, MetricDetail // do we aggregate ? bool aggZero = metricDetail.metric ? metricDetail.metric->aggregateZero() : false; - n=-1; int lastDay=0; unsigned long secondsPerGroupBy=0; @@ -2636,6 +2635,9 @@ LTMPlot::createMetricData(Context *context, LTMSettings *settings, MetricDetail if (!SearchFilterBox::isNull(metricDetail.datafilter)) spec.addMatches(SearchFilterBox::matches(context, metricDetail.datafilter)); + // + double ymean_prev=0.0; + foreach (RideItem *ride, context->athlete->rideCache->rides()) { // filter out unwanted stuff @@ -2694,6 +2696,8 @@ LTMPlot::createMetricData(Context *context, LTMSettings *settings, MetricDetail // first time thru if (n<0) n=0; + ymean_prev = ride->getStdMeanForSymbol(metricDetail.symbol); + y[n] = value; x[n] = currentDay - groupForDate(settings->start.date(), settings->groupBy); @@ -2730,6 +2734,32 @@ LTMPlot::createMetricData(Context *context, LTMSettings *settings, MetricDetail case RideMetric::Peak: if (value > y[n]) y[n] = value; break; + case RideMetric::MeanSquareRoot: + if (value) y[n] = sqrt((pow(y[n],2)*secondsPerGroupBy + pow(value,2)*seconds)/(secondsPerGroupBy+seconds)); + break; + case RideMetric::StdDev: + if (value) + { + double ymean_next = ride->getStdMeanForSymbol(metricDetail.symbol); + double ymean = (secondsPerGroupBy*ymean_prev + ymean_next*seconds)/(secondsPerGroupBy + seconds); + + // Combining two standard deviations using + // the formula: + // + // sqrt(((n1-1)*S1^2+(n2-1)*S2^2+n1*(ymean_1-ymean)^2+n2*(ymean_2-ymean)^2)/(n1+n2)) + // + // where: + // + // ymean = (n1*ymean_1 + n2*ymean_2)/(n1+n2) + + y[n] = pow(y[n],2)*(secondsPerGroupBy-1) + pow(value,2)*(seconds-1); + y[n] += pow(ymean_prev - ymean,2)*secondsPerGroupBy + pow(ymean_next - ymean,2)*seconds; + y[n] /= (secondsPerGroupBy + seconds); + y[n] = sqrt(y[n]); + + ymean_prev = ymean; + } + break; } secondsPerGroupBy += seconds; // increment for same group } @@ -2842,6 +2872,9 @@ LTMPlot::createFormulaData(Context *context, LTMSettings *settings, MetricDetail case RideMetric::Peak: if (value > y[n]) y[n] = value; break; + case RideMetric::MeanSquareRoot: + if (value) y[n] = sqrt((pow(y[n],2)*secondsPerGroupBy + pow(value,2)*value)/(secondsPerGroupBy+seconds)); + break; } secondsPerGroupBy += seconds; // increment for same group } @@ -2969,6 +3002,9 @@ LTMPlot::createBestsData(Context *, LTMSettings *settings, MetricDetail metricDe case RideMetric::Peak: if (value > y[n]) y[n] = value; break; + case RideMetric::MeanSquareRoot: + if (value) y[n] = sqrt((pow(y[n],2)*secondsPerGroupBy + pow(value,2)*value)/(secondsPerGroupBy+seconds)); + break; } secondsPerGroupBy += seconds; // increment for same group } @@ -3418,6 +3454,9 @@ LTMPlot::createPMCData(Context *context, LTMSettings *settings, MetricDetail met case RideMetric::Peak: if (value > y[n]) y[n] = value; break; + case RideMetric::MeanSquareRoot: + if (value) y[n] = sqrt((pow(y[n],2)*secondsPerGroupBy + pow(value,2)*value)/(secondsPerGroupBy+seconds)); + break; } secondsPerGroupBy += seconds; // increment for same group } diff --git a/src/Core/RideCache.cpp b/src/Core/RideCache.cpp index 310f246e2..db5371d07 100644 --- a/src/Core/RideCache.cpp +++ b/src/Core/RideCache.cpp @@ -540,6 +540,12 @@ RideCache::getAggregate(QString name, Specification spec, bool useMetricUnits, b if (value > rvalue) rvalue = value; break; } + case RideMetric::MeanSquareRoot: + { + rvalue = sqrt((pow(rvalue*rcount, 2) + pow(value*count,2))/(rcount + count)); + rcount += count; + break; + } } } diff --git a/src/Metrics/HrvMetrics.cpp b/src/Metrics/HrvMetrics.cpp new file mode 100644 index 000000000..40656c6c7 --- /dev/null +++ b/src/Metrics/HrvMetrics.cpp @@ -0,0 +1,349 @@ +/* + * + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "RideMetric.h" +#include "Context.h" +#include "RideItem.h" +#include "RideFile.h" + +#define ABS(x) ((x) >= 0 ? (x) : -(x)) + +class RRNormalFraction : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(RRNormalFraction) + + // NN/RR is the fraction of total RR intervals that are classified + // as normal-to-normal (NN) intervals and included in the + // calculation of HRV statistics +public: + + RRNormalFraction() + { + setSymbol("nn_rr_fraction"); + setInternalName("NN/RR fraction"); + } + + void initialize() + { + setName(tr("Fraction of normal RR intervals")); + setMetricUnits(tr("pct")); + setImperialUnits(tr("pct")); + setType(RideMetric::Average); + setDescription(tr("Measure of RR readability")); + } + + void compute(RideItem *item, Specification, const QHash &) { + double total, count; + bool this_state; + bool last_state = false; + + XDataSeries *series = item->ride()->xdata("HRV"); + + if (series) { + + total = count = 0; + + foreach(XDataPoint *p, series->datapoints) + { + this_state = p->number[1] > 0; + if (this_state && last_state) + { + total++; + } + last_state = this_state; + count++; + } + + setValue(count > 0 ? total/count*100.0: 100.0); + setCount(count); + } + else { + setValue(RideFile::NIL); + setCount(0); + } + } + + bool isRelevantForRide(RideItem) const { return true; } + + RideMetric *clone() const { return new RRNormalFraction(*this); } +}; + +static bool nnrrFractionAdded = + RideMetricFactory::instance().addMetric(RRNormalFraction()); + + +class avnn : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(avnn) + +public: + + avnn() + { + setSymbol("AVNN"); + setInternalName("AVNN_HRV"); + } + + void initialize() + { + setName(tr("Average of all NN intervals")); + setMetricUnits(tr("msec")); + setImperialUnits(tr("msec")); + setType(RideMetric::Average); + setDescription(tr("Average of all NN intervals")); + } + + void compute(RideItem *item, Specification, const QHash &) { + + double total, count; + bool last_state = false; + bool this_state; + + XDataSeries *series = item->ride()->xdata("HRV"); + + if (series) { + + total = count = 0; + + foreach(XDataPoint *p, series->datapoints) + { + this_state = p->number[1]>0; + if (this_state && last_state) + { + total += p->number[0]; + count++; + } + last_state = this_state; + } + + setValue(count > 0 ? total/count: total); + setCount(count); + } + else { + setValue(RideFile::NIL); + setCount(0); + } + } + + bool isRelevantForRide(RideItem) const { return true; } + + RideMetric *clone() const { return new avnn(*this); } +}; + +static bool avnnAdded = + RideMetricFactory::instance().addMetric(avnn()); + + +class sdnn : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(sdnn) + +private: + + double stdmean_; + +public: + + sdnn() + { + setSymbol("SDNN"); + setInternalName("SDNN_HRV"); + stdmean_ = 0.0f; + } + + void initialize() + { + setName(tr("Standard deviation of NN")); + setMetricUnits(tr("msec")); + setImperialUnits(tr("msec")); + setType(RideMetric::StdDev); + setDescription(tr("Standard deviation of all NN intervals")); + } + + void compute(RideItem *item, Specification, const QHash &) { + double sum, sum2, count; + bool last_state = false; + bool this_state; + + XDataSeries *series = item->ride()->xdata("HRV"); + + if (series) { + + sum = sum2 = count = 0; + + foreach(XDataPoint *p, series->datapoints) + { + this_state = p->number[1] > 0; + if (this_state && last_state) + { + sum += p->number[0]; + sum2 += pow(p->number[0], 2); + count++; + } + last_state = this_state; + } + + if (count>1) + { + stdmean_ = sum/count; + setValue(count > 1 ? sqrt((sum2 - sum*stdmean_)/(count - 1)): 0.0f); + } + else + { + setValue(0.0f); + } + setCount(count); + } + else { + setValue(RideFile::NIL); + setCount(0); + } + } + + double stdmean(){ return stdmean_; } + + bool isRelevantForRide(RideItem) const { return true; } + + RideMetric *clone() const { return new sdnn(*this); } +}; + +static bool sdnnAdded = + RideMetricFactory::instance().addMetric(sdnn()); + + +class rmssd : public RideMetric { + Q_DECLARE_TR_FUNCTIONS(rmssd) + +public: + + rmssd() + { + setSymbol("rMSSD"); + setInternalName("rMSSD_HRV"); + } + + void initialize() + { + setName(tr("rMSSD")); + setMetricUnits(tr("msec")); + setImperialUnits(tr("msec")); + setType(RideMetric::MeanSquareRoot); + setDescription(tr("Square root of the mean of the squares of differences between adjacent NN intervals")); + } + + void compute(RideItem *item, Specification, const QHash &) { + double sum, count; + + XDataSeries *series = item->ride()->xdata("HRV"); + + if (series && series->datapoints.count() > 2 ) + { + sum = count = 0; + + for (int i=2; i < series->datapoints.count(); i++) + if ( + series->datapoints[i]->number[1] > 0 && + series->datapoints[i-1]->number[1] > 0 && + series->datapoints[i-2]->number[1] > 0 + ) + { + sum += pow(series->datapoints[i]->number[0] - series->datapoints[i-1]->number[0], 2); + count++; + } + setValue(count > 1 ? sqrt(sum/count): 0); + setCount(count); + } + else { + setValue(RideFile::NIL); + setCount(0); + } + } + + bool isRelevantForRide(RideItem) const { return true; } + + RideMetric *clone() const { return new rmssd(*this); } +}; + +static bool rmssdAdded = + RideMetricFactory::instance().addMetric(rmssd()); + + +class pnnx : public RideMetric { + +public: + double msec; + + pnnx(double msec_val) + { + msec = msec_val; + setSymbol(QString("pNN").append(QString::number(msec, 'f', 0))); + setInternalName(QString("pNN_HRV").insert(3, QString::number(msec, 'f', 0))); + }; + + void compute(RideItem *item, Specification, const QHash &) { + double nnx , count; + + XDataSeries *series = item->ride()->xdata("HRV"); + + if (series && series->datapoints.count() > 2 ) + { + nnx = count = 0; + + for (int i=2; i < series->datapoints.count(); i++) + { + if ( + series->datapoints[i]->number[1] > 0 && + series->datapoints[i-1]->number[1] > 0 && + series->datapoints[i-2]->number[1] > 0 + ) + { + if (ABS(series->datapoints[i]->number[0] - series->datapoints[i-1]->number[0]) > msec) + { + nnx++; + } + count++; + } + } + + setValue(count > 0 ? nnx/count*100.0 : 0.0); + setCount(count); + } + else { + setValue(RideFile::NIL); + setCount(0); + } + } + + void initialize() + { + setName(QString("pNN").append(QString::number(msec, 'f', 0))); + setMetricUnits(tr("pct")); + setImperialUnits(tr("pct")); + setType(RideMetric::Average); + setDescription(QString("Percentage of differences between adjacent NN intervals that are greater than ms").insert(78, QString::number(msec, 'f', 0))); + } + + RideMetric *clone() const { return new pnnx(*this); } + bool isRelevantForRide(RideItem) const { return true; } +}; + +static bool addPnnx() +{ + for (int i=1; i<10; i++) + { RideMetricFactory::instance().addMetric(pnnx(i*5));} + return true; +} + +static bool pnnxadded = addPnnx(); diff --git a/src/Metrics/RideMetric.cpp b/src/Metrics/RideMetric.cpp index 524f5fa72..0a0847ed5 100644 --- a/src/Metrics/RideMetric.cpp +++ b/src/Metrics/RideMetric.cpp @@ -144,8 +144,8 @@ // 134 22 Jul 2016 Damien Grauser Add Stride length // 135 10 Aug 2016 Ale Martinez Added Average Swim Pace for the 4 Strokes // 136 17 Oct 2016 Ale Martinez Changed Best Times units to minutes - -int DBSchemaVersion = 136; +// 137 16 Feb 2017 Leif Warland Added HrvMetrics +int DBSchemaVersion = 137; RideMetricFactory *RideMetricFactory::_instance; QVector RideMetricFactory::noDeps; diff --git a/src/Metrics/RideMetric.h b/src/Metrics/RideMetric.h index 34fd1042c..3085747d1 100644 --- a/src/Metrics/RideMetric.h +++ b/src/Metrics/RideMetric.h @@ -53,7 +53,7 @@ class RideMetric { public: - enum metrictype { Total, Average, Peak, Low, RunningTotal } types; + enum metrictype { Total, Average, Peak, Low, RunningTotal, MeanSquareRoot, StdDev} types; typedef enum metrictype MetricType; int index_; @@ -175,6 +175,9 @@ public: setValue(((value(true) * count()) + (other.value(true) * other.count())) / (count()+other.count())); break; + case MeanSquareRoot: + setValue(sqrt((pow(value(true), 2)* count() + pow(other.value(true), 2)* other.count()) + /(count()+other.count()))); } } diff --git a/src/src.pro b/src/src.pro index 50e6b0321..362c33aa7 100644 --- a/src/src.pro +++ b/src/src.pro @@ -819,7 +819,7 @@ SOURCES += Metrics/aBikeScore.cpp Metrics/aCoggan.cpp Metrics/AerobicDecoupling. Metrics/PaceZones.cpp Metrics/PDModel.cpp Metrics/PeakPace.cpp Metrics/PeakPower.cpp Metrics/PMCData.cpp Metrics/RideMetadata.cpp \ Metrics/RideMetric.cpp Metrics/RunMetrics.cpp Metrics/SwimMetrics.cpp Metrics/SpecialFields.cpp Metrics/Statistic.cpp Metrics/SustainMetric.cpp Metrics/SwimScore.cpp \ Metrics/TimeInZone.cpp Metrics/TRIMPPoints.cpp Metrics/UserMetric.cpp Metrics/UserMetricParser.cpp Metrics/VDOTCalculator.cpp \ - Metrics/VDOT.cpp Metrics/WattsPerKilogram.cpp Metrics/WPrime.cpp Metrics/Zones.cpp + Metrics/VDOT.cpp Metrics/WattsPerKilogram.cpp Metrics/WPrime.cpp Metrics/Zones.cpp Metrics/HrvMetrics.cpp ## Planning and Compliance SOURCES += Planning/PlanningWindow.cpp