From 5cc70c21642cc7bb3f581a4b52286eb5d71ebd63 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Tue, 5 Nov 2013 17:14:05 +0000 Subject: [PATCH] Add NP and xPower Derived Data Series Calculate NP and xPower as a data series so we can plot on the ride plot. Have also added 'aPower' which will be coming in the next few days - altitude adjusted power, which will also have some associated metrics (a-xPower, a-NP, a-TSS etc). --- src/RideFile.cpp | 129 ++++++++++++++++++++++++++++++++++++++++++++++- src/RideFile.h | 40 +++++++++++++-- 2 files changed, 163 insertions(+), 6 deletions(-) diff --git a/src/RideFile.cpp b/src/RideFile.cpp index 3c4d9de48..1825e0204 100644 --- a/src/RideFile.cpp +++ b/src/RideFile.cpp @@ -41,7 +41,7 @@ RideFile::RideFile(const QDateTime &startTime, double recIntSecs) : startTime_(startTime), recIntSecs_(recIntSecs), deviceType_("unknown"), data(NULL), weight_(0), - totalCount(0) + totalCount(0), dstale(true) { command = new RideFileCommand(this); @@ -51,7 +51,7 @@ RideFile::RideFile(const QDateTime &startTime, double recIntSecs) : totalPoint = new RideFilePoint(); } -RideFile::RideFile() : recIntSecs_(0.0), deviceType_("unknown"), data(NULL), weight_(0), totalCount(0) +RideFile::RideFile() : recIntSecs_(0.0), deviceType_("unknown"), data(NULL), weight_(0), totalCount(0), dstale(true) { command = new RideFileCommand(this); @@ -82,6 +82,7 @@ RideFile::seriesName(SeriesType series) case RideFile::nm: return QString(tr("Torque")); case RideFile::watts: return QString(tr("Power")); case RideFile::xPower: return QString(tr("xPower")); + case RideFile::aPower: return QString(tr("aPower")); case RideFile::NP: return QString(tr("Normalized Power")); case RideFile::alt: return QString(tr("Altitude")); case RideFile::lon: return QString(tr("Longitude")); @@ -111,6 +112,7 @@ RideFile::unitName(SeriesType series, Context *context) case RideFile::nm: return QString(tr("N")); case RideFile::watts: return QString(tr("watts")); case RideFile::xPower: return QString(tr("watts")); + case RideFile::aPower: return QString(tr("watts")); case RideFile::NP: return QString(tr("watts")); case RideFile::alt: return QString(useMetricUnits ? tr("metres") : tr("feet")); case RideFile::lon: return QString(tr("lon")); @@ -338,6 +340,9 @@ RideFile *RideFileFactory::openRideFile(Context *context, QFile &file, result->setTag("Month", result->startTime().toString("MMMM")); result->setTag("Weekday", result->startTime().toString("ddd")); + // calculate derived data series + result->recalculateDerivedSeries(); + DataProcessorFactory::instance().autoProcess(result); // what data is present - after processor in case 'derived' or adjusted @@ -637,6 +642,9 @@ RideFilePoint::value(RideFile::SeriesType series) const case RideFile::temp : return temp; break; case RideFile::lrbalance : return lrbalance; break; case RideFile::interval : return interval; break; + case RideFile::NP : return np; break; + case RideFile::xPower : return xp; break; + case RideFile::aPower : return apower; break; default: case RideFile::none : break; @@ -696,6 +704,7 @@ RideFile::decimalsFor(SeriesType series) case nm : return 2; break; case watts : return 0; break; case xPower : return 0; break; + case aPower : return 0; break; case NP : return 0; break; case alt : return 3; break; case lon : return 6; break; @@ -725,6 +734,7 @@ RideFile::maximumFor(SeriesType series) case watts : return 2500; break; case NP : return 2500; break; case xPower : return 2500; break; + case aPower : return 2500; break; case alt : return 8850; break; // mt everest is highest point above sea level case lon : return 180; break; case lat : return 90; break; @@ -752,6 +762,7 @@ RideFile::minimumFor(SeriesType series) case nm : return 0; break; case watts : return 0; break; case xPower : return 0; break; + case aPower : return 0; break; case NP : return 0; break; case alt : return -413; break; // the Red Sea is lowest land point on earth case lon : return -180; break; @@ -798,6 +809,7 @@ void RideFile::emitSaved() { weight_ = 0; + dstale = true; emit saved(); } @@ -805,6 +817,7 @@ void RideFile::emitReverted() { weight_ = 0; + dstale = true; emit reverted(); } @@ -812,6 +825,7 @@ void RideFile::emitModified() { weight_ = 0; + dstale = true; emit modified(); } @@ -876,3 +890,114 @@ RideFile::parseRideFileName(const QString &name, QDateTime *dt) return true; } +void +RideFile::recalculateDerivedSeries() +{ + // derived data is calculated from the data that is present + // we should set to 0 where we cannot derive since we may + // be called after data is deleted or added + if (dstale == false) return; // we're already up to date + + // + // NP Initialisation -- working variables + // + QVector NProlling; + int NProllingwindowsize = 30 / (recIntSecs_ ? recIntSecs_ : 1); + if (NProllingwindowsize > 1) NProlling.resize(NProllingwindowsize); + double NPtotal = 0; + int NPcount = 0; + int NPindex = 0; + double NPsum = 0; + + // + // XPower Initialisation -- working variables + // + static const double EPSILON = 0.1; + static const double NEGLIGIBLE = 0.1; + double XPsecsDelta = recIntSecs_ ? recIntSecs_ : 1; + double XPsampsPerWindow = 25.0 / XPsecsDelta; + double XPattenuation = XPsampsPerWindow / (XPsampsPerWindow + XPsecsDelta); + double XPsampleWeight = XPsecsDelta / (XPsampsPerWindow + XPsecsDelta); + double XPlastSecs = 0.0; + double XPweighted = 0.0; + double XPtotal = 0.0; + int XPcount = 0; + + foreach(RideFilePoint *p, dataPoints_) { + + // + // NP + // + if (dataPresent.watts && NProllingwindowsize > 1) { + + dataPresent.np = true; + + // sum last 30secs + NPsum += p->watts; + NPsum -= NProlling[NPindex]; + NProlling[NPindex] = p->watts; + + // running total and count + NPtotal += pow(NPsum/NProllingwindowsize,4); // raise rolling average to 4th power + NPcount ++; + + // root for ride so far + if (NPcount && NPcount*recIntSecs_ > 30) { + p->np = pow(NPtotal / (NPcount), 0.25); + } else { + p->np = 0.00f; + } + + // move index on/round + NPindex = (NPindex >= NProllingwindowsize-1) ? 0 : NPindex+1; + + } else { + + p->np = 0.00f; + } + + // now the min and max values for NP + if (p->np > maxPoint->np) maxPoint->np = p->np; + if (p->np < minPoint->np) minPoint->np = p->np; + + // + // xPower + // + if (dataPresent.watts) { + + dataPresent.np = true; + + while ((XPweighted > NEGLIGIBLE) && (p->secs > XPlastSecs + XPsecsDelta + EPSILON)) { + XPweighted *= XPattenuation; + XPlastSecs += XPsecsDelta; + XPtotal += pow(XPweighted, 4.0); + XPcount++; + } + + XPweighted *= XPattenuation; + XPweighted += XPsampleWeight * p->watts; + XPlastSecs = p->secs; + XPtotal += pow(XPweighted, 4.0); + XPcount++; + + p->xp = pow(XPtotal / XPcount, 0.25); + } + + // now the min and max values for NP + if (p->xp > maxPoint->xp) maxPoint->xp = p->xp; + if (p->xp < minPoint->xp) minPoint->xp = p->xp; + + // aPower + // XXX coming soon. + } + + // Averages and Totals + avgPoint->np = NPcount ? (NPtotal / NPcount) : 0; + totalPoint->np = NPtotal; + + avgPoint->xp = XPcount ? (XPtotal / XPcount) : 0; + totalPoint->xp = XPtotal; + + // and we're done + dstale=false; +} diff --git a/src/RideFile.h b/src/RideFile.h index 831a6aee9..8224d4372 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -54,12 +54,18 @@ class Context; // for context; cyclist, homedir struct RideFileDataPresent { + // basic bool secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, lrbalance, interval; + + // derived + bool np,xp,apower; + // whether non-zero data of each field is present RideFileDataPresent(): secs(false), cad(false), hr(false), km(false), kph(false), nm(false), watts(false), alt(false), lon(false), lat(false), - headwind(false), slope(false), temp(false), lrbalance(false), interval(false) {} + headwind(false), slope(false), temp(false), lrbalance(false), interval(false), + np(false), xp(false), apower(false) {} }; struct RideFileInterval @@ -105,7 +111,7 @@ class RideFile : public QObject // QObject to emit signals virtual ~RideFile(); // Working with DATASERIES - enum seriestype { secs=0, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, interval, NP, xPower, vam, wattsKg, lrbalance, none }; + enum seriestype { secs=0, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, interval, NP, xPower, vam, wattsKg, lrbalance, aPower, none }; enum specialValues { noTemp = -255 }; typedef enum seriestype SeriesType; @@ -133,6 +139,17 @@ class RideFile : public QObject // QObject to emit signals void appendPoint(const RideFilePoint &); const QVector &dataPoints() const { return dataPoints_; } + // recalculate all the derived data series + // might want to move to a factory for these + // at some point, but for now hard coded + // + // YOU MUST ALWAYS CALL THIS BEFORE ACESSING + // THE DERIVED DATA. IT IS REFRESHED ON DEMAND. + // STATE IS MAINTAINED IN 'bool dstale' BELOW + // TO ENSURE IT IS ONLY REFRESHED IF NEEDED + // + void recalculateDerivedSeries(); + // Working with DATAPRESENT flags inline const RideFileDataPresent *areDataPresent() const { return &dataPresent; } bool isDataPresent(SeriesType series); @@ -211,10 +228,12 @@ class RideFile : public QObject // QObject to emit signals void deleted(); protected: + void emitSaved(); void emitReverted(); void emitModified(); + private: QString id_; // global uuid@goldencheetah.org @@ -240,19 +259,32 @@ class RideFile : public QObject // QObject to emit signals void updateMin(RideFilePoint* point); void updateMax(RideFilePoint* point); void updateAvg(RideFilePoint* point); + + bool dstale; // is derived data up to date? }; struct RideFilePoint { + // recorded data double secs, cad, hr, km, kph, nm, watts, alt, lon, lat, headwind, slope, temp, lrbalance; int interval; + + // derived data (we calculate it) + // xPower, normalised power, aPower + double xp, np, apower; + + // create blank point RideFilePoint() : secs(0.0), cad(0.0), hr(0.0), km(0.0), kph(0.0), - nm(0.0), watts(0.0), alt(0.0), lon(0.0), lat(0.0), headwind(0.0), slope(0.0), temp(-255.0), lrbalance(0), interval(0) {} + nm(0.0), watts(0.0), alt(0.0), lon(0.0), lat(0.0), headwind(0.0), slope(0.0), temp(-255.0), lrbalance(0), interval(0), xp(0), np(0), apower(0) {} + + // create point supplying all values RideFilePoint(double secs, double cad, double hr, double km, double kph, double nm, double watts, double alt, double lon, double lat, double headwind, double slope, double temp, double lrbalance, int interval) : secs(secs), cad(cad), hr(hr), km(km), kph(kph), nm(nm), - watts(watts), alt(alt), lon(lon), lat(lat), headwind(headwind), slope(slope), temp(temp), lrbalance(lrbalance), interval(interval) {} + watts(watts), alt(alt), lon(lon), lat(lat), headwind(headwind), slope(slope), temp(temp), lrbalance(lrbalance), interval(interval), xp(0), np(0), apower(0) {} + + // get the value via the series type rather than access direct to the values double value(RideFile::SeriesType series) const; };