/src/GOVSS.cpp

https://github.com/olympum/GoldenCheetah · C++ · 356 lines · 247 code · 53 blank · 56 comment · 26 complexity · e7dc520a473fed9c945278e0807c5d08 MD5 · raw file

  1. /*
  2. * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
  3. * Based on Coggan.cpp modified by Alejandro Martinez (amtriathlon@gmail.com)
  4. *
  5. * This program is free software; you can redistribute it and/or modify it
  6. * under the terms of the GNU General Public License as published by the Free
  7. * Software Foundation; either version 2 of the License, or (at your option)
  8. * any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful, but WITHOUT
  11. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12. * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  13. * more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc., 51
  17. * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  18. */
  19. #include "RideMetric.h"
  20. #include "PaceZones.h"
  21. #include "Units.h"
  22. #include <math.h>
  23. #include <algorithm>
  24. #include <QApplication>
  25. // NOTE: This code follows the description of LNP, IWF and GOVSS in
  26. // "Calculation of Power Output and Quantification of Training Stress in
  27. // Distance Runners: The Development of the GOVSS Algorithm", by Phil Skiba:
  28. // http://www.physfarm.com/govss.pdf
  29. // Additional details are taken from a spreadsheet published by Dr. Skiba:
  30. // GOVSSCalculatorVer10 (creation date 4-mar-2006)
  31. // Running Power based on speed and slope
  32. static inline double running_power( double weight, double height,
  33. double speed, double slope=0.0,
  34. double distance=0.0) {
  35. // Aero contribution - probably needs refinement
  36. double cAero = 0.25*0.01*pow(speed, 2)*pow(height,-3);
  37. // Energy Cost of Running according to slope
  38. double cSlope = 155.4*pow(slope,5) - 30.4*pow(slope,4) - 43.3*pow(slope,3) + 46.3*pow(slope,2) + 19.5*slope + 3.6;
  39. // Efficiency
  40. double eff = 0.25 + 0.054 * speed;
  41. // Kinetic Energy contribution doesn't seem to be right, it is not used.
  42. double cKin = distance ? pow(speed,2)/distance : 0.0;
  43. return (cAero + cSlope*eff*(1 - 0.5*speed/8.33) + cKin)*weight*speed;
  44. }
  45. // Lactate Normalized Power, used for GOVSS and xPace calculation
  46. class LNP : public RideMetric {
  47. Q_DECLARE_TR_FUNCTIONS(LNP)
  48. double lnp;
  49. double secs;
  50. public:
  51. LNP() : lnp(0.0), secs(0.0)
  52. {
  53. setSymbol("govss_lnp");
  54. setInternalName("LNP");
  55. }
  56. void initialize() {
  57. setName("LNP");
  58. setType(RideMetric::Average);
  59. setMetricUnits("watts");
  60. setImperialUnits("watts");
  61. setPrecision(0);
  62. }
  63. void compute(const RideFile *ride, const Zones *, int,
  64. const HrZones *, int,
  65. const QHash<QString,RideMetric*> &,
  66. const Context *) {
  67. if(ride->recIntSecs() == 0) return;
  68. // LNP only makes sense for running
  69. if (!ride->isRun()) return;
  70. // unconst naughty boy, get athlete's data
  71. RideFile *uride = const_cast<RideFile*>(ride);
  72. double weight = uride->getWeight();
  73. double height = uride->getHeight();
  74. int rollingwindowsize120 = 120 / ride->recIntSecs();
  75. int rollingwindowsize30 = 30 / ride->recIntSecs();
  76. double total = 0.0;
  77. int count = 0;
  78. // no point doing a rolling average if the
  79. // sample rate is greater than the rolling average
  80. // window!!
  81. if (rollingwindowsize30 > 1) {
  82. QVector<double> rollingSpeed(rollingwindowsize120);
  83. QVector<double> rollingSlope(rollingwindowsize120);
  84. QVector<double> rollingPower(rollingwindowsize30);
  85. int index120 = 0;
  86. int index30 = 0;
  87. double sumSpeed = 0.0;
  88. double sumSlope = 0.0;
  89. double sumPower = 0.0;
  90. // loop over the data and convert to a rolling
  91. // average for the given windowsize
  92. for (int i=0; i<ride->dataPoints().size(); i++) {
  93. double speed = ride->dataPoints()[i]->kph/3.6;
  94. sumSpeed += speed;
  95. sumSpeed -= rollingSpeed[index120];
  96. rollingSpeed[index120] = speed;
  97. double speed120 = sumSpeed/std::min(count+1, rollingwindowsize120); // speed rolling average
  98. double slope = ride->dataPoints()[i]->slope/100.0;
  99. sumSlope += slope;
  100. sumSlope -= rollingSlope[index120];
  101. rollingSlope[index120] = slope;
  102. double slope120 = sumSlope/std::min(count+1, rollingwindowsize120); // slope rolling average
  103. // running power based on 120sec averages
  104. double watts = running_power(weight, height, speed120, slope120); // sumSpeed*ride->recIntSecs()); KE contribution disabled
  105. sumPower += watts;
  106. sumPower -= rollingPower[index30];
  107. rollingPower[index30] = watts;
  108. total += pow(sumPower/std::min(count+1, rollingwindowsize30), 4); // raise rolling average to 4th power
  109. count ++;
  110. // move index on/round
  111. index120 = (index120 >= rollingwindowsize120-1) ? 0 : index120+1;
  112. index30 = (index30 >= rollingwindowsize30-1) ? 0 : index30+1;
  113. }
  114. }
  115. if (count) {
  116. lnp = pow(total/count, 0.25);
  117. secs = count * ride->recIntSecs();
  118. } else {
  119. lnp = secs = 0;
  120. }
  121. setValue(lnp);
  122. setCount(secs);
  123. }
  124. RideMetric *clone() const { return new LNP(*this); }
  125. };
  126. // xPace: constant Pace which, on flat surface, gives same Lactate Normalized Power
  127. class XPace : public RideMetric {
  128. Q_DECLARE_TR_FUNCTIONS(XPace)
  129. double xPace;
  130. public:
  131. XPace() : xPace(0.0)
  132. {
  133. setSymbol("xPace");
  134. setInternalName("xPace");
  135. }
  136. void initialize() {
  137. setName(tr("xPace"));
  138. setType(RideMetric::Average);
  139. setMetricUnits(tr("min/km"));
  140. setImperialUnits(tr("min/mile"));
  141. setPrecision(1);
  142. setConversion(KM_PER_MILE);
  143. }
  144. void compute(const RideFile *ride, const Zones *, int,
  145. const HrZones *, int,
  146. const QHash<QString,RideMetric*> &deps,
  147. const Context *) {
  148. // xPace only makes sense for running
  149. if (!ride->isRun()) return;
  150. // unconst naughty boy, get athlete's data
  151. RideFile *uride = const_cast<RideFile*>(ride);
  152. double weight = uride->getWeight();
  153. double height = uride->getHeight();
  154. assert(deps.contains("govss_lnp"));
  155. LNP *lnp = dynamic_cast<LNP*>(deps.value("govss_lnp"));
  156. assert(lnp);
  157. double lnp_watts = lnp->value(true);
  158. // search for speed which gives flat power within 0.001watt of LNP
  159. // up to around 10 iterations for speed within 0.01m/s or ~1sec/km
  160. double low = 0.0, high = 10.0, speed;
  161. if (lnp_watts <= 0.0) speed = low;
  162. else if (lnp_watts >= running_power(weight, height, high)) speed = high;
  163. else do {
  164. speed = (low + high)/2.0;
  165. double watts = running_power(weight, height, speed);
  166. if (abs(watts - lnp_watts) < 0.001) break;
  167. else if (watts < lnp_watts) low = speed;
  168. else if (watts > lnp_watts) high = speed;
  169. } while (high - low > 0.01);
  170. // divide by zero or stupidly low pace
  171. if (speed > 0.01) xPace = (1000.0/60.0) / speed;
  172. else xPace = 0.0;
  173. setValue(xPace);
  174. }
  175. RideMetric *clone() const { return new XPace(*this); }
  176. };
  177. // Running Threshold Power based on CV, used for GOVSS calculation
  178. class RTP : public RideMetric {
  179. Q_DECLARE_TR_FUNCTIONS(RTP)
  180. public:
  181. RTP()
  182. {
  183. setSymbol("govss_rtp");
  184. setInternalName("RTP");
  185. }
  186. void initialize() {
  187. setName(tr("RTP"));
  188. setType(RideMetric::Average);
  189. setMetricUnits(tr("watts"));
  190. setImperialUnits(tr("watts"));
  191. setPrecision(0);
  192. }
  193. void compute(const RideFile *ride, const Zones *, int ,
  194. const HrZones *, int,
  195. const QHash<QString,RideMetric*> &,
  196. const Context *context) {
  197. // LNP only makes sense for running
  198. if (!ride->isRun()) return;
  199. // unconst naughty boy, get athlete's data
  200. RideFile *uride = const_cast<RideFile*>(ride);
  201. double weight = uride->getWeight();
  202. double height = uride->getHeight();
  203. const PaceZones *zones = context->athlete->paceZones();
  204. int zoneRange = context->athlete->paceZones()->whichRange(ride->startTime().date());
  205. // did user override for this ride?
  206. double cv = ride->getTag("CV","0").toInt();
  207. // not overriden so use the set value
  208. // if it has been set at all
  209. if (!cv && zones && zoneRange >= 0)
  210. cv = zones->getCV(zoneRange);
  211. // Running power at cv on flat surface
  212. double watts = running_power(weight, height, cv/3.6); //120*cv/3.6); KE contribution disabled
  213. setValue(watts);
  214. }
  215. RideMetric *clone() const { return new RTP(*this); }
  216. };
  217. // Intensity Weighting Factor, used for GOVSS calculation
  218. class IWF : public RideMetric {
  219. Q_DECLARE_TR_FUNCTIONS(IWF)
  220. double reli;
  221. double secs;
  222. public:
  223. IWF() : reli(0.0), secs(0.0)
  224. {
  225. setSymbol("govss_iwf");
  226. setInternalName("IWF");
  227. }
  228. void initialize() {
  229. setName(tr("IWF"));
  230. setType(RideMetric::Average);
  231. setMetricUnits(tr(""));
  232. setImperialUnits(tr(""));
  233. setPrecision(2);
  234. }
  235. void compute(const RideFile *ride, const Zones *, int,
  236. const HrZones *, int,
  237. const QHash<QString,RideMetric*> &deps,
  238. const Context *) {
  239. // IWF only makes sense for running
  240. if (!ride->isRun()) return;
  241. assert(deps.contains("govss_lnp"));
  242. LNP *lnp = dynamic_cast<LNP*>(deps.value("govss_lnp"));
  243. assert(lnp);
  244. assert(deps.contains("govss_rtp"));
  245. RTP *rtp = dynamic_cast<RTP*>(deps.value("govss_rtp"));
  246. assert(rtp);
  247. reli = rtp->value(true) ? lnp->value(true) / rtp->value(true) : 0;
  248. secs = lnp->count();
  249. setValue(reli);
  250. setCount(secs);
  251. }
  252. RideMetric *clone() const { return new IWF(*this); }
  253. };
  254. // GOVSS Metric for running
  255. class GOVSS : public RideMetric {
  256. Q_DECLARE_TR_FUNCTIONS(GOVSS)
  257. double score;
  258. public:
  259. GOVSS() : score(0.0)
  260. {
  261. setSymbol("govss");
  262. setInternalName("GOVSS");
  263. }
  264. void initialize() {
  265. setName("GOVSS");
  266. setType(RideMetric::Total);
  267. }
  268. void compute(const RideFile *ride, const Zones *, int,
  269. const HrZones *, int,
  270. const QHash<QString,RideMetric*> &deps,
  271. const Context *) {
  272. // GOVSS only makes sense for running
  273. if (!ride->isRun()) return;
  274. assert(deps.contains("govss_lnp"));
  275. assert(deps.contains("govss_rtp"));
  276. assert(deps.contains("govss_iwf"));
  277. LNP *lnp = dynamic_cast<LNP*>(deps.value("govss_lnp"));
  278. assert(lnp);
  279. RideMetric *iwf = deps.value("govss_iwf");
  280. assert(iwf);
  281. RideMetric *rtp = deps.value("govss_rtp");
  282. assert(rtp);
  283. double normWork = lnp->value(true) * lnp->count();
  284. double rawGOVSS = normWork * iwf->value(true);
  285. double workInAnHourAtRTP = rtp->value(true) * 3600;
  286. score = workInAnHourAtRTP ? rawGOVSS / workInAnHourAtRTP * 100.0 : 0;
  287. setValue(score);
  288. }
  289. RideMetric *clone() const { return new GOVSS(*this); }
  290. };
  291. static bool addAllGOVSS() {
  292. RideMetricFactory::instance().addMetric(LNP());
  293. RideMetricFactory::instance().addMetric(RTP());
  294. QVector<QString> deps;
  295. deps.append("govss_lnp");
  296. RideMetricFactory::instance().addMetric(XPace(), &deps);
  297. deps.append("govss_rtp");
  298. RideMetricFactory::instance().addMetric(IWF(), &deps);
  299. deps.append("govss_iwf");
  300. RideMetricFactory::instance().addMetric(GOVSS(), &deps);
  301. return true;
  302. }
  303. static bool GOVSSAdded = addAllGOVSS();