mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
.. add option to apply a decay factor to CP and W' when plotting the model curve in CP Plot. .. since we always fit to observations <20mins the mostly submax points at longer durations in the general population do not impact the fit at all. .. the decay factors for w' and cp have been fit to the results of: Effects of Two Hours of Heavy-Intensity Exercise on the Power- Duration Relationship Clark IE, Vanhatalo A, Bailey SJ, Wylie LJ, Kirby BS, Wilkins BW, Jones AM. https://europepmc.org/abstract/med/29521722
1425 lines
39 KiB
C++
1425 lines
39 KiB
C++
/*
|
|
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
|
|
*
|
|
* 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 "PDModel.h"
|
|
#include "LTMTrend.h"
|
|
#include "lmcurve.h"
|
|
|
|
//extern ztable PD_ZTABLE;
|
|
// base class for all models
|
|
PDModel::PDModel(Context *context) :
|
|
QwtSyntheticPointData(PDMODEL_MAXT),
|
|
fit(Envelope),
|
|
inverseTime(false),
|
|
context(context),
|
|
sanI1(0), sanI2(0), anI1(0), anI2(0), aeI1(0), aeI2(0), laeI1(0), laeI2(0),
|
|
cp(0), tau(0), t0(0), minutes(false)
|
|
{
|
|
setInterval(QwtInterval(1, PDMODEL_MAXT));
|
|
setSize(PDMODEL_MAXT);
|
|
}
|
|
|
|
// set data using doubles always
|
|
void
|
|
PDModel::setData(QVector<double> meanMaxPower)
|
|
{
|
|
cp = tau = t0 = 0; // reset on new data
|
|
int size = meanMaxPower.size();
|
|
|
|
// truncate the data for 2p and 3p models
|
|
if (model < 3) size = size < 1200 ? size : 1200;
|
|
|
|
data = meanMaxPower;
|
|
data.resize(size);
|
|
|
|
// generate t data, in seconds
|
|
tdata.resize(size);
|
|
for(int i=0; i<size; i++) tdata[i] = i+1;
|
|
emit dataChanged();
|
|
}
|
|
|
|
void
|
|
PDModel::setPtData(QVector<double>p, QVector<double>t)
|
|
{
|
|
cp = tau = t0 = 0;
|
|
data = p;
|
|
tdata = t;
|
|
for(int i=0; i<t.count(); i++) {
|
|
tdata[i] = tdata[i] * 60;
|
|
}
|
|
emit dataChanged();
|
|
}
|
|
void
|
|
PDModel::setMinutes(bool x)
|
|
{
|
|
minutes = x;
|
|
if (minutes) {
|
|
setInterval(QwtInterval(1.00f / 60.00f, double(PDMODEL_MAXT)/ 60.00f));
|
|
setSize(PDMODEL_MAXT);
|
|
}
|
|
}
|
|
|
|
double PDModel::x(unsigned int index) const
|
|
{
|
|
double returning = 1;
|
|
|
|
// don't start at zero !
|
|
index += 1;
|
|
|
|
if (minutes) returning = (double(index)/60.00f);
|
|
else returning = index;
|
|
|
|
// reverse !
|
|
if (inverseTime) return 1.00f / returning;
|
|
else return returning;
|
|
}
|
|
|
|
|
|
// set data using floats, we convert
|
|
void
|
|
PDModel::setData(QVector<float> meanMaxPower)
|
|
{
|
|
cp = tau = t0 = 0; // reset on new data
|
|
int size = meanMaxPower.size();
|
|
|
|
// truncate the data for 2p and 3p models
|
|
if (model < 3) size = size < 1200 ? size : 1200;
|
|
|
|
data.resize(size);
|
|
tdata.resize(size);
|
|
for (int i=0; i< size; i++) {
|
|
data[i] = meanMaxPower[i];
|
|
tdata[i] = i+1;
|
|
}
|
|
emit dataChanged();
|
|
}
|
|
|
|
// set the intervals to search for bests
|
|
void
|
|
PDModel::setIntervals(double sanI1, double sanI2, double anI1, double anI2,
|
|
double aeI1, double aeI2, double laeI1, double laeI2)
|
|
{
|
|
this->sanI1 = sanI1;
|
|
this->sanI2 = sanI2;
|
|
this->anI1 = anI1;
|
|
this->anI2 = anI2;
|
|
this->aeI1 = aeI1;
|
|
this->aeI2 = aeI2;
|
|
this->laeI1 = laeI1;
|
|
this->laeI2 = laeI2;
|
|
|
|
emit intervalsChanged();
|
|
}
|
|
|
|
// used to wrap a function call when deriving parameters
|
|
QMutex calllmfit;
|
|
static PDModel *calllmfitmodel = NULL;
|
|
static double calllmfitf(double t, const double *p) {
|
|
return static_cast<PDModel*>(calllmfitmodel)->f(t, p);
|
|
}
|
|
|
|
// using the data and intervals from above, derive the
|
|
// cp, tau and t0 values needed for the model
|
|
// this is the function originally found in CPPlot
|
|
void
|
|
PDModel::deriveCPParameters(int model)
|
|
{
|
|
// bit of a hack, but the model deriving code is shared
|
|
// basically because it does pretty much the same thing
|
|
// for all the models and I didn't want to abstract it
|
|
// any further, so we pass the subclass as a model number
|
|
// to control which intervals and formula to use
|
|
// where model is
|
|
// 0 = CP 2 parameter
|
|
// 1 = CP 3 parameter
|
|
// 2 = Extended Model (Damien)
|
|
// 3 = Veloclinic (Mike P)
|
|
// 4 = Ward Smith
|
|
fitsummary="";
|
|
|
|
if (fit == LinearRegression) {
|
|
|
|
// first lets just do a linear regression on the data as supplied
|
|
QVector<double> joules;
|
|
QVector<double> t;
|
|
QVector<double> p;
|
|
for(int i=0; i<data.count(); i++) {
|
|
if (tdata[i] > 120 && tdata[i] <= 1200) {
|
|
joules << (data[i]*tdata[i]);
|
|
t << tdata[i];
|
|
p << data[i];
|
|
}
|
|
}
|
|
LTMTrend lr(t.constData(),joules.constData(), t.count());
|
|
//fprintf(stderr, "Linear regression: CP=%f, W'=%f\n", lr.slope(), lr.intercept()); fflush(stderr);
|
|
|
|
double par[3];
|
|
par[0]=lr.slope();
|
|
par[1]=lr.intercept();
|
|
par[2]=0;
|
|
setParms(par);
|
|
|
|
// get vector of residuals
|
|
QVector<double> residuals(t.size());
|
|
double errtot=0;
|
|
double sse=0;
|
|
double meany=0;
|
|
for(int i=0; i<t.size(); i++){
|
|
double py = y(t[i]/60.0f); // predicted y
|
|
meany += p[i]; // calculatng mean divided at end
|
|
double err = p[i] - py; // error for this point
|
|
errtot += err; // for mean errors divided at end
|
|
sse += err*err; // sum of squared errors for r2 calc
|
|
residuals[i]=err; // vector of residuals
|
|
}
|
|
|
|
// lets use to calc R2
|
|
meany /= double(t.size());
|
|
double sv=0;
|
|
for(int i=0; i<t.size(); i++) {
|
|
sv += (p[i]-meany) * (p[i]-meany);
|
|
}
|
|
double R2=1-(sse/sv);
|
|
|
|
// carry on with RMSE
|
|
double mean = errtot/double(t.size());
|
|
errtot = 0;
|
|
|
|
// mean of residuals^2
|
|
for(int i=0; i<t.size(); i++) errtot += pow(residuals[i]-mean, 2);
|
|
mean = errtot / double(t.size());
|
|
|
|
// RMSE
|
|
double RMSE=sqrt(mean);
|
|
fitsummary = QString("RMSE %1 watts R<sup>2</sup>=%3 [LR] %2 points").arg(RMSE, 0, 'f', 2).arg(t.size()).arg(R2, 0, 'f', 3);
|
|
|
|
} else if (fit == LeastSquares && this->nparms() > 0) {
|
|
// only try lmfit if the model supports that
|
|
|
|
// used for fit
|
|
QVector<double> p, t;
|
|
|
|
// starting values for parameters
|
|
double par[3];
|
|
|
|
if (model == 0) {
|
|
|
|
// CLASSIC CP
|
|
// we need to prepare the data for the fit
|
|
// mostly this means classic CP only fits to
|
|
// points between 2mins and 20mins to avoid
|
|
// data that is short and rate limited or long
|
|
// and likely submaximal or fatigued (or both!)
|
|
//fprintf(stderr, "points=%d\n", data.count()); fflush(stderr);
|
|
for(int i=0; i<data.count(); i++) {
|
|
if (tdata[i] >= 120 && tdata[i] <= 1200) {
|
|
p << data[i];
|
|
t << tdata[i];
|
|
}
|
|
}
|
|
//fprintf(stderr, " used points=%d\n", p.count()); fflush(stderr);
|
|
|
|
// if not enough data use everything in desperation...
|
|
if (p.count() < 3) {
|
|
p.resize(0);
|
|
t.resize(0);
|
|
for(int i=0; i<data.count(); i++) {
|
|
p << data[i];
|
|
t << tdata[i];
|
|
}
|
|
}
|
|
|
|
// set starting values cp and w'
|
|
par[0] = 200;
|
|
par[1] = 15000;
|
|
|
|
} else if (model==1) {
|
|
|
|
// MORTON 3 PARAMETER
|
|
// Just skip post 20 mins to avoid data that
|
|
// is most likely submaximal or fatigued
|
|
for(int i=0; i<data.count(); i++) {
|
|
if (tdata[i] <= 1200) {
|
|
p << data[i];
|
|
t << tdata[i];
|
|
}
|
|
}
|
|
|
|
// if not enough data use everything in desperation...
|
|
if (p.count() < 3) {
|
|
p.resize(0);
|
|
t.resize(0);
|
|
for(int i=0; i<data.count(); i++) {
|
|
p << data[i];
|
|
t << tdata[i];
|
|
}
|
|
}
|
|
|
|
// set starting values cp, w' and k
|
|
par[0] = 250;
|
|
par[1] = 18000;
|
|
par[2] = 32;
|
|
|
|
|
|
} else {
|
|
p = data;
|
|
t = tdata;
|
|
}
|
|
|
|
lm_control_struct control = lm_control_double;
|
|
lm_status_struct status;
|
|
|
|
// use forwarder via global variable, so mutex around this !
|
|
calllmfit.lock();
|
|
calllmfitmodel = this;
|
|
|
|
//fprintf(stderr, "Fitting ...\n" ); fflush(stderr);
|
|
lmcurve(this->nparms(), par, p.count(), t.constData(), p.constData(), calllmfitf, &control, &status);
|
|
|
|
// release for others
|
|
calllmfit.unlock();
|
|
|
|
//fprintf(stderr, "Results:\n" );
|
|
//fprintf(stderr, "status after %d function evaluations:\n %s\n",
|
|
// status.nfev, lm_infmsg[status.outcome] ); fflush(stderr);
|
|
//fprintf(stderr,"obtained parameters:\n"); fflush(stderr);
|
|
//for (int i = 0; i < this->nparms(); ++i)
|
|
// fprintf(stderr," par[%i] = %12g\n", i, par[i]);
|
|
//fprintf(stderr,"obtained norm:\n %12g\n", status.fnorm ); fflush(stderr);
|
|
|
|
// save away the values we fit to.
|
|
this->setParms(par);
|
|
|
|
// calculate RMSE
|
|
|
|
// get vector of residuals
|
|
QVector<double> residuals(p.size());
|
|
double errtot=0;
|
|
for(int i=0; i<p.size(); i++){
|
|
double err = p[i] - y(t[i]/60.0f);
|
|
errtot += err; // for mean
|
|
residuals[i]=err;
|
|
}
|
|
double mean = errtot/double(p.size());
|
|
errtot = 0;
|
|
|
|
// mean of residuals^2
|
|
for(int i=0; i<p.size(); i++) errtot += pow(residuals[i]-mean, 2);
|
|
mean = errtot / double(p.size());
|
|
|
|
// RMSE
|
|
double RMSE=sqrt(mean);
|
|
fitsummary = QString("RMSE %1 watts [LM] %2 points").arg(RMSE, 0, 'f', 2).arg(p.size());
|
|
|
|
} else {
|
|
|
|
// bounds on anaerobic interval in minutes
|
|
const double t1 = anI1;
|
|
const double t2 = anI2;
|
|
|
|
// bounds on aerobic interval in minutes
|
|
const double t3 = aeI1;
|
|
const double t4 = aeI2;
|
|
|
|
// bounds of these time values in the data
|
|
int i1, i2, i3, i4;
|
|
|
|
// find the indexes associated with the bounds
|
|
// the first point must be at least the minimum for the anaerobic interval, or quit
|
|
for (i1 = 0; i1 < t1; i1++)
|
|
if (i1 + 1 > data.size())
|
|
return;
|
|
// the second point is the maximum point suitable for anaerobicly dominated efforts.
|
|
for (i2 = i1; i2 + 1 <= t2; i2++)
|
|
if (i2 + 1 > data.size())
|
|
return;
|
|
// the third point is the beginning of the minimum duration for aerobic efforts
|
|
for (i3 = i2; i3 < t3; i3++)
|
|
if (i3 + 1 > data.size())
|
|
return;
|
|
for (i4 = i3; i4 + 1 <= t4; i4++)
|
|
if (i4 + 1 > data.size())
|
|
break;
|
|
|
|
// initial estimate of tau
|
|
if (tau == 0) tau = 1;
|
|
|
|
// initial estimate of cp (if not already available)
|
|
if (cp == 0) cp = 300;
|
|
|
|
// initial estimate of t0: start small to maximize sensitivity to data
|
|
t0 = 0;
|
|
|
|
// lower bound on tau
|
|
const double tau_min = 0.5;
|
|
|
|
// convergence delta for tau
|
|
const double tau_delta_max = 1e-4;
|
|
const double t0_delta_max = 1e-4;
|
|
|
|
// previous loop value of tau and t0
|
|
double tau_prev;
|
|
double t0_prev;
|
|
|
|
// maximum number of loops
|
|
const int max_loops = 100;
|
|
|
|
// loop to convergence
|
|
int iteration = 0;
|
|
do {
|
|
|
|
// clear cherries
|
|
map.clear();
|
|
|
|
// bounds check, don't go on for ever
|
|
if (iteration++ > max_loops) break;
|
|
|
|
// record the previous version of tau, for convergence
|
|
tau_prev = tau;
|
|
t0_prev = t0;
|
|
|
|
// estimate cp, given tau
|
|
int i;
|
|
cp = 0;
|
|
bool changed=false;
|
|
int index=0;
|
|
double value=0;
|
|
for (i = i3; i < i4; i++) {
|
|
double cpn = data[i] / (1 + tau / (t0 + i / 60.0));
|
|
if (cp < cpn) {
|
|
cp = cpn;
|
|
index=i; value=data[i];
|
|
changed=true;
|
|
}
|
|
}
|
|
if(changed) map.insert(index,value);
|
|
|
|
|
|
// if cp = 0; no valid data; give up
|
|
if (cp == 0.0)
|
|
return;
|
|
|
|
// estimate tau, given cp
|
|
tau = tau_min;
|
|
changed=false;
|
|
for (i = i1; i <= i2; i++) {
|
|
double taun = (data[i] / cp - 1) * (i / 60.0 + t0) - t0;
|
|
if (tau < taun) {
|
|
changed=true;
|
|
index=i;
|
|
value=data[i];
|
|
tau = taun;
|
|
}
|
|
}
|
|
if (changed) map.insert(index,value);
|
|
|
|
// estimate t0 - but only for veloclinic/3parm cp
|
|
// where model is non-zero (CP2 is nonzero)
|
|
if (model) t0 = tau / (data[1] / cp - 1) - 1 / 60.0;
|
|
|
|
//qDebug()<<"CONVERGING:"<<iteration<<"t0="<<t0<<"tau="<<tau<<"cp="<<cp;
|
|
|
|
} while ((fabs(tau - tau_prev) > tau_delta_max) || (fabs(t0 - t0_prev) > t0_delta_max));
|
|
|
|
//fprintf(stderr, "tau=%f, t0= %f\n", tau, t0); fflush(stderr);
|
|
|
|
// RMSE etc calc for summary
|
|
calcSummary();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
PDModel::calcSummary()
|
|
{
|
|
// get vector of residuals
|
|
QVector<double> residuals(data.size());
|
|
double errtot=0;
|
|
for(int i=0; i<data.size(); i++){
|
|
double err = data[i] - y((i+1)/60.0f);
|
|
errtot += err; // for mean
|
|
residuals[i]=err;
|
|
}
|
|
double mean = errtot/double(data.size());
|
|
errtot = 0;
|
|
|
|
// mean of residuals^2
|
|
for(int i=0; i<data.size(); i++) errtot += pow(residuals[i]-mean, 2);
|
|
mean = errtot / double(data.size());
|
|
|
|
// RMSE
|
|
double RMSE=sqrt(mean);
|
|
fitsummary = QString("RMSE %1 watts [envelope] %2 points").arg(RMSE, 0, 'f', 2).arg(data.size());
|
|
}
|
|
|
|
//
|
|
// Classic 2 Parameter Model
|
|
//
|
|
|
|
CP2Model::CP2Model(Context *context) :
|
|
PDModel(context)
|
|
{
|
|
// set default intervals to search CP 2-3 mins and 10-20 mins
|
|
anI1=120;
|
|
anI2=200;
|
|
aeI1=720;
|
|
aeI2=1200;
|
|
model = 0;
|
|
|
|
connect (this, SIGNAL(dataChanged()), this, SLOT(onDataChanged()));
|
|
connect (this, SIGNAL(intervalsChanged()), this, SLOT(onIntervalsChanged()));
|
|
}
|
|
|
|
// P(t) - return y for t in 2 parameter model
|
|
double
|
|
CP2Model::y(double t) const
|
|
{
|
|
// don't start at zero !
|
|
t += (!minutes?1.00f:1/60.00f);
|
|
|
|
// adjust to seconds
|
|
if (minutes) t *= 60.00f;
|
|
|
|
// classic model - W' / t + CP
|
|
return (cp * tau * 60) / t + cp;
|
|
}
|
|
|
|
// 2 parameter model can calculate these
|
|
double
|
|
CP2Model::WPrime()
|
|
{
|
|
// kjoules
|
|
return (cp * tau * 60);
|
|
}
|
|
|
|
double
|
|
CP2Model::CP()
|
|
{
|
|
return cp;
|
|
}
|
|
|
|
// could have just connected signal to slot
|
|
// but might want to be more sophisticated in future
|
|
void CP2Model::onDataChanged()
|
|
{
|
|
// calc tau etc and make sure the interval is
|
|
// set correctly - i.e. 'domain of validity'
|
|
deriveCPParameters(0);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
|
|
}
|
|
|
|
void CP2Model::onIntervalsChanged()
|
|
{
|
|
deriveCPParameters(0);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
}
|
|
|
|
//
|
|
// Morton 3 Parameter Model
|
|
//
|
|
CP3Model::CP3Model(Context *context) :
|
|
PDModel(context)
|
|
{
|
|
// set default intervals to search CP 2-3 mins and 12-20mins
|
|
anI1=120;
|
|
anI2=200;
|
|
aeI1=720;
|
|
aeI2=1200;
|
|
|
|
model = 1;
|
|
modelDecay = false;
|
|
|
|
connect (this, SIGNAL(dataChanged()), this, SLOT(onDataChanged()));
|
|
connect (this, SIGNAL(intervalsChanged()), this, SLOT(onIntervalsChanged()));
|
|
}
|
|
|
|
// P(t) - return y for t in 2 parameter model
|
|
double
|
|
CP3Model::y(double t) const
|
|
{
|
|
// don't start at zero !
|
|
t += (!minutes?1.00f:1/60.00f);
|
|
|
|
// adjust to seconds
|
|
if (minutes) t *= 60.00f;
|
|
|
|
// decay value (75% at 10 hrs)
|
|
double cpdecay=1.0;
|
|
double wdecay=1.0;
|
|
|
|
// just use a constant for now - it modifies CP/W' so should adjust to
|
|
// athlete without needing to be fitted (?)
|
|
if (modelDecay) {
|
|
cpdecay = 2.0-(1.0/exp(-0.000009*t));
|
|
wdecay = 2.0-(1.0/exp(-0.000025*t));
|
|
}
|
|
// typical values: CP=285.355547 tau=0.500000 t0=0.381158
|
|
|
|
// classic model - W' / t + CP
|
|
return (cp*cpdecay) * (double(1.00f)+(tau*wdecay) /(((double(t)/double(60))+t0)));
|
|
}
|
|
|
|
// 2 parameter model can calculate these
|
|
double
|
|
CP3Model::WPrime()
|
|
{
|
|
// kjoules
|
|
return (cp * tau * 60);
|
|
}
|
|
|
|
double
|
|
CP3Model::CP()
|
|
{
|
|
return cp;
|
|
}
|
|
|
|
double
|
|
CP3Model::PMax()
|
|
{
|
|
// casting to double across to ensure we don't lose precision
|
|
// but basically its the max value of the curve at time t of 1s
|
|
// which is cp * 1 + tau / ((t/60) + t0)
|
|
return cp * (double(1.00f)+tau /(((double(1)/double(60))+t0)));
|
|
}
|
|
|
|
|
|
// could have just connected signal to slot
|
|
// but might want to be more sophisticated in future
|
|
void CP3Model::onDataChanged()
|
|
{
|
|
// calc tau etc and make sure the interval is
|
|
// set correctly - i.e. 'domain of validity'
|
|
deriveCPParameters(1);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
|
|
}
|
|
|
|
void CP3Model::onIntervalsChanged()
|
|
{
|
|
deriveCPParameters(1);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
}
|
|
|
|
//
|
|
// Ward Smith Model
|
|
//
|
|
WSModel::WSModel(Context *context) : PDModel(context)
|
|
{
|
|
// set default intervals to search CP 30-60
|
|
anI1=1800;
|
|
anI2=2400;
|
|
aeI1=2400;
|
|
aeI2=3600;
|
|
|
|
model = 3;
|
|
|
|
connect (this, SIGNAL(dataChanged()), this, SLOT(onDataChanged()));
|
|
connect (this, SIGNAL(intervalsChanged()), this, SLOT(onIntervalsChanged()));
|
|
}
|
|
|
|
// P(t) - return y for t in 2 parameter model
|
|
double
|
|
WSModel::y(double t) const
|
|
{
|
|
// don't start at zero !
|
|
t += (!minutes?1.00f:1/60.00f);
|
|
|
|
// adjust to seconds
|
|
if (minutes) t *= 60.00f;
|
|
|
|
//WPrime and PMax
|
|
double WPrime = cp * tau * 60;
|
|
double PMax = cp * (double(1.00f)+tau /(((double(1)/double(60))+t0)));
|
|
double wPMax = PMax-cp;
|
|
|
|
// WS Model P(t) = W'/t * (1- exp(-t/( W'/(wPmax)))) + CP
|
|
return ((WPrime / (double(t))) * (1- exp(-t/(WPrime/wPMax)))) + cp;
|
|
}
|
|
|
|
// 2 parameter model can calculate these
|
|
double
|
|
WSModel::WPrime()
|
|
{
|
|
// kjoules
|
|
return (cp * tau * 60);
|
|
}
|
|
|
|
double
|
|
WSModel::CP()
|
|
{
|
|
return cp;
|
|
}
|
|
|
|
double
|
|
WSModel::FTP()
|
|
{
|
|
return y(45 * 60);
|
|
}
|
|
|
|
double
|
|
WSModel::PMax()
|
|
{
|
|
// casting to double across to ensure we don't lose precision
|
|
// but basically its the max value of the curve at time t of 1s
|
|
// which is cp * 1 + tau / ((t/60) + t0)
|
|
double WPrime = cp * tau * 60;
|
|
double PMax = cp * (double(1.00f)+tau /(((double(1)/double(60))+t0)));
|
|
double wPMax = PMax-cp;
|
|
|
|
// WS Model P(t) = W'/t * (1- exp(-t/( W'/(wPmax)))) + CP
|
|
return ((WPrime / (double(1))) * (1- exp(-1/(WPrime/wPMax)))) + cp;
|
|
}
|
|
|
|
|
|
// could have just connected signal to slot
|
|
// but might want to be more sophisticated in future
|
|
void WSModel::onDataChanged()
|
|
{
|
|
// calc tau etc and make sure the interval is
|
|
// set correctly - i.e. 'domain of validity'
|
|
deriveCPParameters(4);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
|
|
}
|
|
|
|
void WSModel::onIntervalsChanged()
|
|
{
|
|
deriveCPParameters(4);
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
}
|
|
|
|
//
|
|
// Veloclinic Multicomponent Model
|
|
//
|
|
// p(t) = pc1 + pc2
|
|
// Power at time t is the sum of;
|
|
// pc1 - the power from component 1 (fast twitch pools)
|
|
// pc2 - the power from component 2 (slow twitch motor units)
|
|
//
|
|
// The inputs are derived from the CP2-20 model and 3 constants:
|
|
//
|
|
// Pmax - as derived from the CP2-20 model (via t0)
|
|
// w1 - W' as derived from the CP2-20 model
|
|
// p1 - pmax - cp as derived from the CP2-20 model
|
|
// p2 - cp as derived from the CP2-20 model
|
|
// tau1 - W'1 / p1
|
|
// tau2 - 15,000
|
|
// w2 - A slow twitch W' derived from p2 * tau2
|
|
// alpha- 0.1 thru -0.1, we default to zero
|
|
// beta - 1.0
|
|
//
|
|
// Fast twitch component is:
|
|
// pc1(t) = W'1 / t * (1-exp(-t/tau1)) * ((1-exp(-t/10)) ^ (1/alpha))
|
|
//
|
|
// Slow twitch component has three formulations:
|
|
// sprint capped linear) pc2(t) = p2 * tau2 * (1-exp(-t/tau2))
|
|
// sprint capped regeneration) pc2(t) = p2 / (1 + t/tau2)
|
|
// sprint capped exponential) pc2(t) = p2 / (1 + t/5400) ^ (1/beta)
|
|
//
|
|
// Currently deciding which of the three formulations to use
|
|
// as the base for GoldenCheetah (we have enough models already !)
|
|
MultiModel::MultiModel(Context *context) :
|
|
PDModel(context),
|
|
variant(0), w1(0), p1(0), p2(0), tau1(0), tau2(0), alpha(0), beta(0)
|
|
{
|
|
// set default intervals to search CP 30-60
|
|
// uses the same as the 3 parameter model
|
|
anI1=1800;
|
|
anI2=2400;
|
|
aeI1=2400;
|
|
aeI2=3600;
|
|
|
|
model = 4;
|
|
variant = 0; // use exp top/bottom by default.
|
|
|
|
connect (this, SIGNAL(dataChanged()), this, SLOT(onDataChanged()));
|
|
connect (this, SIGNAL(intervalsChanged()), this, SLOT(onIntervalsChanged()));
|
|
}
|
|
|
|
void
|
|
MultiModel::setVariant(int variant)
|
|
{
|
|
this->variant = variant;
|
|
emit intervalsChanged(); // refresh then
|
|
}
|
|
|
|
|
|
// P(t) - return y for t in 2 parameter model
|
|
double
|
|
MultiModel::y(double t) const
|
|
{
|
|
// don't start at zero !
|
|
t += (!minutes?1.00f:1/60.00f);
|
|
|
|
// adjust to seconds
|
|
if (minutes) t *= 60.00f;
|
|
|
|
// two component model
|
|
double pc1 = w1 / t * (1.00f - exp(-t/tau1)) * pow(1-exp(-t/10.00f), alpha);
|
|
|
|
// which variant for pc2 ?
|
|
double pc2 = 0.0f;
|
|
switch (variant) {
|
|
|
|
default:
|
|
case 0 : // exponential top and bottom
|
|
pc2 = p2 * tau2 / t * (1-exp(-t/tau2));
|
|
break;
|
|
|
|
case 1 : // linear feedback
|
|
pc2 = p2 / (1+t/tau2);
|
|
break;
|
|
|
|
case 2 : // regeneration
|
|
pc2 = pow(p2 / (1+t/5400),1/beta);
|
|
//pc2 = p2 / pow((1+t/5400),(1/beta));
|
|
break;
|
|
|
|
}
|
|
|
|
return (pc1 + pc2);
|
|
}
|
|
|
|
double
|
|
MultiModel::FTP()
|
|
{
|
|
if (data.size()) return y(minutes ? 60 : 3600);
|
|
return 0;
|
|
}
|
|
|
|
// 2 parameter model can calculate these
|
|
double
|
|
MultiModel::WPrime()
|
|
{
|
|
// kjoules -- add in difference between CP60 from
|
|
// velo model and cp as derived
|
|
return w1;
|
|
}
|
|
|
|
double
|
|
MultiModel::CP()
|
|
{
|
|
if (data.size()) return y(minutes ? 60 : 3600);
|
|
return 0;
|
|
}
|
|
|
|
double
|
|
MultiModel::PMax()
|
|
{
|
|
// casting to double across to ensure we don't lose precision
|
|
// but basically its the max value of the curve at time t of 1s
|
|
// which is cp * 1 + tau / ((t/60) + t0)
|
|
return cp * (double(1.00f)+tau /(((double(1)/double(60))+t0)));
|
|
}
|
|
|
|
|
|
// could have just connected signal to slot
|
|
// but might want to be more sophisticated in future
|
|
void MultiModel::onDataChanged()
|
|
{
|
|
// calc tau etc and make sure the interval is
|
|
// set correctly - i.e. 'domain of validity'
|
|
deriveCPParameters(3);
|
|
|
|
// and veloclinic parameters too;
|
|
w1 = cp*tau*60; // initial estimate from classic cp model
|
|
p1 = PMax() - cp;
|
|
p2 = cp;
|
|
tau1 = w1 / p1;
|
|
tau2 = 15000;
|
|
alpha = 0.0f;
|
|
beta = 1.0;
|
|
|
|
// now account for model -- this is rather problematic
|
|
// since the formula uses cp/w' as derived via CP220 but
|
|
// the resulting W' is higher.
|
|
//w1 = (cp + (cp-CP())) * tau * 60;
|
|
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
}
|
|
|
|
void MultiModel::onIntervalsChanged()
|
|
{
|
|
deriveCPParameters(3);
|
|
|
|
// and veloclinic parameters too;
|
|
w1 = cp*tau*60; // initial estimate from classic model
|
|
p1 = PMax() - cp;
|
|
p2 = cp;
|
|
tau1 = w1 / p1;
|
|
tau2 = 15000;
|
|
alpha = 0.0f;
|
|
beta = 1.0;
|
|
|
|
// now account for model -- this is rather problematic
|
|
// since the formula uses cp/w' as derived via CP220 but
|
|
// the resulting W' is higher.
|
|
w1 = (cp + (cp-CP())) * tau * 60;
|
|
|
|
|
|
setInterval(QwtInterval(tau, PDMODEL_MAXT));
|
|
}
|
|
|
|
//
|
|
// Extended CP Model
|
|
//
|
|
ExtendedModel::ExtendedModel(Context *context) :
|
|
PDModel(context),
|
|
paa(0), paa_dec(0), ecp(0), etau(0), ecp_del(0), tau_del(0), ecp_dec(0),
|
|
ecp_dec_del(0)
|
|
{
|
|
// set default intervals to search Extended CP
|
|
sanI1=20;
|
|
sanI2=90;
|
|
anI1=120;
|
|
anI2=300;
|
|
aeI1=420;
|
|
aeI2=1800;
|
|
laeI1=3600;
|
|
laeI2=30000;
|
|
|
|
model = 3;
|
|
|
|
connect (this, SIGNAL(dataChanged()), this, SLOT(onDataChanged()));
|
|
connect (this, SIGNAL(intervalsChanged()), this, SLOT(onIntervalsChanged()));
|
|
}
|
|
|
|
// P(t) - return y for t in Extended model
|
|
double
|
|
ExtendedModel::y(double t) const
|
|
{
|
|
// don't start at zero !
|
|
if (t == 0) qDebug() << "ExtendedModel t=0 !!";
|
|
|
|
if (!minutes) t /= 60.00f;
|
|
|
|
// ATP/PCr in muscles
|
|
double immediate = paa*(1.20-0.20*exp(-1*t))*exp(paa_dec*(t));
|
|
|
|
// anaerobic glycolysis
|
|
double nonoxidative = ecp * (1-exp(tau_del*t)) * (1-exp(ecp_del*t)) * (1+ecp_dec*exp(ecp_dec_del/t)) * (etau/(t));
|
|
|
|
// aerobic glycolysis and lipolysis
|
|
double oxidative = ecp * (1-exp(tau_del*t)) * (1-exp(ecp_del*t)) * (1+ecp_dec*exp(ecp_dec_del/t)) * (1);
|
|
|
|
// sum of all for time t
|
|
return immediate + nonoxidative + oxidative;
|
|
}
|
|
|
|
// 2 parameter model can calculate these
|
|
double
|
|
ExtendedModel::WPrime()
|
|
{
|
|
// kjoules
|
|
return (ecp * etau * 60);
|
|
}
|
|
|
|
double
|
|
ExtendedModel::CP()
|
|
{
|
|
return ecp;
|
|
}
|
|
|
|
double
|
|
ExtendedModel::PMax()
|
|
{
|
|
// casting to double across to ensure we don't lose precision
|
|
// but basically its the max value of the curve at time t of 1s
|
|
// which is cp * 1 + tau / ((t/60) + t0)
|
|
return paa*(1.20-0.20*exp(-1*(1/60.0)))*exp(paa_dec*(1/60.0)) + ecp * (1-exp(tau_del*(1/60.0))) * (1-exp(ecp_del*(1/60.0))) * (1+ecp_dec*exp(ecp_dec_del/(1/60.0))) * ( 1 + etau/(1/60.0));
|
|
}
|
|
|
|
double
|
|
ExtendedModel::FTP()
|
|
{
|
|
// casting to double across to ensure we don't lose precision
|
|
// but basically its the max value of the curve at time t of 1s
|
|
// which is cp * 1 + tau / ((t/60) + t0)
|
|
return paa*(1.20-0.20*exp(-1*60.0))*exp(paa_dec*(60.0)) + ecp * (1-exp(tau_del*(60.0))) * (1-exp(ecp_del*60.0)) * (1+ecp_dec*exp(ecp_dec_del/60.0)) * ( 1 + etau/(60.0));
|
|
}
|
|
|
|
|
|
// could have just connected signal to slot
|
|
// but might want to be more sophisticated in future
|
|
void
|
|
ExtendedModel::onDataChanged()
|
|
{
|
|
// calc tau etc and make sure the interval is
|
|
// set correctly - i.e. 'domain of validity'
|
|
deriveExtCPParameters();
|
|
setInterval(QwtInterval(etau, PDMODEL_MAXT));
|
|
|
|
}
|
|
|
|
void
|
|
ExtendedModel::onIntervalsChanged()
|
|
{
|
|
deriveExtCPParameters();
|
|
setInterval(QwtInterval(etau, PDMODEL_MAXT));
|
|
}
|
|
|
|
void
|
|
ExtendedModel::deriveExtCPParameters()
|
|
{
|
|
// initial estimates
|
|
paa = 1000;
|
|
etau = 1.2;
|
|
ecp = 300;
|
|
paa_dec = -2;
|
|
ecp_del = -0.9;
|
|
tau_del = -4.8;
|
|
ecp_dec = -0.6;
|
|
ecp_dec_del = -180;
|
|
|
|
#if 0
|
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
// LEAST SQUARES FIT WITH ECP MODEL REQUIRES
|
|
// CONSTRAINTS TO BE VALIDATED AND REFINED
|
|
// THE MODEL IS GROSSLY OVERFITTING WITHOUT
|
|
// PLAUSIBLE CONSTRAINTS FOR DECAY/DELAY
|
|
// PARAMETERS
|
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
// now, if we have a ls fit, at least we have some
|
|
// half decent starting parameters to work from
|
|
if (fit == LeastSquares) {
|
|
|
|
// used for fit
|
|
QVector<double> p, t;
|
|
|
|
p = data;
|
|
t = tdata;
|
|
|
|
fprintf(stderr, "derive %d params using lmfit!\n", this->nparms()); fflush(stderr);
|
|
|
|
// set starting values
|
|
double par[8];
|
|
par[0]= paa;
|
|
par[1]= paa_dec;
|
|
par[2]= ecp;
|
|
par[3]= etau;
|
|
par[4]= tau_del;
|
|
par[5]= ecp_del;
|
|
par[6]= ecp_dec;
|
|
par[7]= ecp_dec_del;
|
|
|
|
lm_control_struct control = lm_control_double;
|
|
lm_status_struct status;
|
|
|
|
// use forwarder via global variable, so mutex around this !
|
|
calllmfit.lock();
|
|
calllmfitmodel = this;
|
|
|
|
fprintf(stderr, "Fitting ...\n" ); fflush(stderr);
|
|
lmcurve(this->nparms(), par, p.count(), t.constData(), p.constData(), calllmfitf, &control, &status);
|
|
|
|
// release for others
|
|
calllmfit.unlock();
|
|
|
|
fprintf(stderr, "Results:\n" );
|
|
fprintf(stderr, "status after %d function evaluations:\n %s\n",
|
|
status.nfev, lm_infmsg[status.outcome] ); fflush(stderr);
|
|
|
|
fprintf(stderr,"obtained parameters:\n"); fflush(stderr);
|
|
for (int i = 0; i < this->nparms(); ++i)
|
|
fprintf(stderr," par[%i] = %12g\n", i, par[i]);
|
|
fprintf(stderr,"obtained norm:\n %12g\n", status.fnorm ); fflush(stderr);
|
|
|
|
// save away the values we fit to.
|
|
this->setParms(par);
|
|
|
|
} else {
|
|
#endif
|
|
|
|
// bounds on anaerobic interval in minutes
|
|
const double t1 = sanI1;
|
|
const double t2 = sanI2;
|
|
|
|
// bounds on anaerobic interval in minutes
|
|
const double t3 = anI1;
|
|
const double t4 = anI2;
|
|
|
|
// bounds on aerobic interval in minutes
|
|
const double t5 = aeI1;
|
|
const double t6 = aeI2;
|
|
|
|
// bounds on long aerobic interval in minutes
|
|
const double t7 = laeI1;
|
|
const double t8 = laeI2;
|
|
|
|
// bounds of these time values in the data
|
|
int i1, i2, i3, i4, i5, i6, i7, i8;
|
|
|
|
// find the indexes associated with the bounds
|
|
// the first point must be at least the minimum for the anaerobic interval, or quit
|
|
for (i1 = 0; i1 < t1; i1++)
|
|
if (i1 + 1 > data.size())
|
|
return;
|
|
// the second point is the maximum point suitable for anaerobicly dominated efforts.
|
|
for (i2 = i1; i2 + 1 <= t2; i2++)
|
|
if (i2 + 1 >= data.size())
|
|
return;
|
|
|
|
// the third point is the beginning of the minimum duration for aerobic efforts
|
|
for (i3 = i2; i3 < t3; i3++)
|
|
if (i3 + 1 >= data.size())
|
|
return;
|
|
for (i4 = i3; i4 + 1 <= t4; i4++)
|
|
if (i4 + 1 >= data.size())
|
|
break;
|
|
|
|
// the fifth point is the beginning of the minimum duration for aerobic efforts
|
|
for (i5 = i4; i5 < t5; i5++)
|
|
if (i5 + 1 >= data.size())
|
|
return;
|
|
for (i6 = i5; i6 + 1 <= t6; i6++)
|
|
if (i6 + 1 >= data.size())
|
|
break;
|
|
|
|
// the first point must be at least the minimum for the anaerobic interval, or quit
|
|
for (i7 = i6; i7 < t7; i7++)
|
|
if (i7 + 1 >= data.size())
|
|
return;
|
|
// the second point is the maximum point suitable for anaerobicly dominated efforts.
|
|
for (i8 = i7; i8 + 1 <= t8; i8++)
|
|
if (i8 + 1 >= data.size())
|
|
break;
|
|
|
|
|
|
|
|
// previous loop values
|
|
double etau_prev;
|
|
double paa_prev;
|
|
double paa_dec_prev;
|
|
double ecp_del_prev;
|
|
double ecp_dec_prev;
|
|
|
|
// maximum number of loops
|
|
const int max_loops = 100;
|
|
|
|
// loop to convergence
|
|
int iteration = 0;
|
|
do {
|
|
|
|
// bounds check, don't go on for ever
|
|
if (iteration++ > max_loops) break;
|
|
|
|
// clear last point used
|
|
map.clear();
|
|
|
|
// record the previous version of tau, for convergence
|
|
etau_prev = etau;
|
|
paa_prev = paa;
|
|
paa_dec_prev = paa_dec;
|
|
ecp_del_prev = ecp_del;
|
|
ecp_dec_prev = ecp_dec;
|
|
|
|
//
|
|
// Solve for CP [asymptote]
|
|
//
|
|
int i;
|
|
ecp = 0;
|
|
bool changed=false;
|
|
double val = 0;
|
|
int index=0;
|
|
for (i = i5; i <= i6; i++) {
|
|
double ecpn = (data[i] - paa * (1.20-0.20*exp(-1*(i/60.0))) * exp(paa_dec*(i/60.0))) / (1-exp(tau_del*i/60.0)) / (1-exp(ecp_del*i/60.0)) / (1+ecp_dec*exp(ecp_dec_del/(i/60.0))) / ( 1 + etau/(i/60.0));
|
|
|
|
if (ecp < ecpn) {
|
|
ecp = ecpn;
|
|
val = data[i];
|
|
index=i;
|
|
changed=true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
map.insert(index,val);
|
|
//qDebug()<<iteration<<"eCP Resolving: cp="<<ecp<<"tau="<<etau<<"p[i]"<<val;
|
|
}
|
|
|
|
|
|
// if cp = 0; no valid data; give up
|
|
if (ecp == 0.0) {
|
|
return;
|
|
}
|
|
|
|
//
|
|
// SOLVE FOR TAU [curvature constant]
|
|
//
|
|
etau = etau_min;
|
|
changed=false;
|
|
val = 0;
|
|
for (i = i3; i <= i4; i++) {
|
|
double etaun = ((data[i] - paa * (1.20-0.20*exp(-1*(i/60.0))) * exp(paa_dec*(i/60.0))) / ecp / (1-exp(tau_del*i/60.0)) / (1-exp(ecp_del*i/60.0)) / (1+ecp_dec*exp(ecp_dec_del/(i/60.0))) - 1) * (i/60.0);
|
|
|
|
if (etau < etaun) {
|
|
etau = etaun;
|
|
val=data[i];
|
|
index=i;
|
|
changed=true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
map.insert(index,val);
|
|
//qDebug()<<iteration<<"eCP Resolving: tau="<<etau<<"CP="<<ecp<<"p[i]"<<val;
|
|
}
|
|
|
|
|
|
//
|
|
// SOLVE FOR PAA_DEC [decay rate for ATP-PCr energy system]
|
|
//
|
|
paa_dec = paa_dec_min;
|
|
changed=false;
|
|
val=0;
|
|
for (i = i1; i <= i2; i++) {
|
|
double paa_decn = log((data[i] - ecp * (1-exp(tau_del*i/60.0)) * (1-exp(ecp_del*i/60.0)) * (1+ecp_dec*exp(ecp_dec_del/(i/60.0))) * ( 1 + etau/(i/60.0)) ) / paa / (1.20-0.20*exp(-1*(i/60.0))) ) / (i/60.0);
|
|
|
|
if (paa_dec < paa_decn && paa_decn < paa_dec_max) {
|
|
paa_dec = paa_decn;
|
|
changed=true;
|
|
val=data[i];
|
|
index=i;
|
|
}
|
|
}
|
|
if(changed) {
|
|
//qDebug()<<iteration<<"eCP Resolving: paa_dec="<<paa_dec<<"CP="<<ecp<<"p[i]"<<val;
|
|
map.insert(index,val);
|
|
}
|
|
|
|
//
|
|
// SOLVE FOR PAA [max power]
|
|
//
|
|
paa = paa_min;
|
|
double _avg_paa = 0.0;
|
|
int count=1;
|
|
changed=false;
|
|
val=0;
|
|
for (i = 1; i <= 8; i++) {
|
|
double paan = (data[i] - ecp * (1-exp(tau_del*i/60.0)) * (1-exp(ecp_del*i/60.0)) * (1+ecp_dec*exp(ecp_dec_del/(i/60.0))) * ( 1 + etau/(i/60.0))) / exp(paa_dec*(i/60.0)) / (1.20-0.20*exp(-1*(i/60.0)));
|
|
_avg_paa = (double)((count-1)*_avg_paa+paan)/count;
|
|
|
|
if (paa < paan) {
|
|
paa = paan;
|
|
changed=true;
|
|
val=data[i];
|
|
index=i;
|
|
}
|
|
count++;
|
|
}
|
|
if (changed) {
|
|
map.insert(index,val);
|
|
//qDebug()<<iteration<<"eCP Resolving: paa="<<paa<<"CP="<<ecp<<"p[i]"<<val;
|
|
}
|
|
|
|
if (_avg_paa<0.95*paa) {
|
|
paa = _avg_paa;
|
|
}
|
|
|
|
|
|
//
|
|
// SOLVE FOR ECP_DEC [Fatigue rate; CHO, Motivation, Hydration etc]
|
|
//
|
|
ecp_dec = ecp_dec_min;
|
|
changed=false;
|
|
val=0;
|
|
for (i = i7; i <= i8; i=i+120) {
|
|
double ecp_decn = ((data[i] - paa * (1.20-0.20*exp(-1*(i/60.0))) * exp(paa_dec*(i/60.0))) / ecp / (1-exp(tau_del*i/60.0)) / (1-exp(ecp_del*i/60.0)) / ( 1 + etau/(i/60.0)) -1 ) / exp(ecp_dec_del/(i / 60.0));
|
|
|
|
if (ecp_decn > 0) ecp_decn = 0;
|
|
|
|
if (ecp_dec < ecp_decn) {
|
|
ecp_dec = ecp_decn;
|
|
changed=true;
|
|
val=data[i];
|
|
index=i;
|
|
}
|
|
}
|
|
if (changed) {
|
|
//qDebug()<<iteration<<"eCP Resolving: ecp_dec="<<ecp_dec<<"CP="<<ecp<<"p[i]"<<val;
|
|
map.insert(index,val);
|
|
}
|
|
|
|
|
|
} while ((fabs(etau - etau_prev) > etau_delta_max) ||
|
|
(fabs(paa - paa_prev) > paa_delta_max) ||
|
|
(fabs(paa_dec - paa_dec_prev) > paa_dec_delta_max) ||
|
|
(fabs(ecp_del - ecp_del_prev) > ecp_del_delta_max) ||
|
|
(fabs(ecp_dec - ecp_dec_prev) > ecp_dec_delta_max)
|
|
);
|
|
|
|
// What did we get ...
|
|
// To help debug this below we output the derived values
|
|
// commented out for release, its quite a mouthful !
|
|
|
|
int pMax = paa*(1.20-0.20*exp(-1*(1/60.0)))*exp(paa_dec*(1/60.0)) + ecp *
|
|
(1-exp(tau_del*(1/60.0))) * (1-exp(ecp_del*(1/60.0))) *
|
|
(1+ecp_dec*exp(ecp_dec_del/(1/60.0))) *
|
|
(1+etau/(1/60.0));
|
|
|
|
int mmp60 = paa*(1.20-0.20*exp(-1*60.0))*exp(paa_dec*(60.0)) + ecp *
|
|
(1-exp(tau_del*(60.0))) * (1-exp(ecp_del*60.0)) *
|
|
(1+ecp_dec*exp(ecp_dec_del/60.0)) *
|
|
(1+etau/(60.0));
|
|
|
|
#if 0
|
|
qDebug() <<"eCP(5.3) " << "paa" << paa << "ecp" << ecp << "etau" << etau
|
|
<< "paa_dec" << paa_dec << "ecp_del" << ecp_del << "ecp_dec"
|
|
<< ecp_dec << "ecp_dec_del" << ecp_dec_del;
|
|
|
|
qDebug() <<"eCP(5.3) " << "pmax" << pMax << "mmp60" << mmp60;
|
|
}
|
|
#endif
|
|
// get vector of residuals
|
|
QVector<double> residuals(data.size());
|
|
double errtot=0;
|
|
for(int i=0; i<data.size(); i++){
|
|
double err = data[i] - y((i+1)/60.0f);
|
|
errtot += err; // for mean
|
|
residuals[i]=err;
|
|
}
|
|
double mean = errtot/double(data.size());
|
|
errtot = 0;
|
|
|
|
// mean of residuals^2
|
|
for(int i=0; i<data.size(); i++) errtot += pow(residuals[i]-mean, 2);
|
|
mean = errtot / double(data.size());
|
|
|
|
// RMSE
|
|
double RMSE=sqrt(mean);
|
|
fitsummary = QString("RMSE %1 watts [envelope] %2 points").arg(RMSE, 0, 'f', 2).arg(data.size());
|
|
}
|
|
|
|
QList<QPointF>
|
|
PDModel::cherries()
|
|
{
|
|
QList<QPointF> returning;
|
|
|
|
QMapIterator<int,double> it(map);
|
|
it.toFront();
|
|
while(it.hasNext()) {
|
|
it.next();
|
|
returning << QPointF(it.key(), it.value());
|
|
}
|
|
return returning;
|
|
}
|
|
|
|
void MultiModel::loadParameters(QList<double>&here)
|
|
{
|
|
cp = here[0];
|
|
tau = here[1];
|
|
t0 = here[2];
|
|
w1 = here[3];
|
|
p1 = here[4];
|
|
p2 = here[5];
|
|
tau1 = here[6];
|
|
tau2 = here[7];
|
|
alpha = here[8];
|
|
beta = here[9];
|
|
}
|
|
|
|
void MultiModel::saveParameters(QList<double>&here)
|
|
{
|
|
here.clear();
|
|
here << cp;
|
|
here << tau;
|
|
here << t0;
|
|
here << w1;
|
|
here << p1;
|
|
here << p2;
|
|
here << tau1;
|
|
here << tau2;
|
|
here << alpha;
|
|
here << beta;
|
|
}
|
|
|
|
void ExtendedModel::loadParameters(QList<double>&here)
|
|
{
|
|
cp = here[0];
|
|
tau = here[1];
|
|
t0 = here[2];
|
|
paa = here[3];
|
|
etau = here[4];
|
|
ecp = here[5];
|
|
paa_dec = here[6];
|
|
ecp_del = here[7];
|
|
tau_del = here[8];
|
|
ecp_dec = here[9];
|
|
ecp_dec_del = here[10];
|
|
}
|
|
|
|
void ExtendedModel::saveParameters(QList<double>&here)
|
|
{
|
|
here.clear();
|
|
here << cp;
|
|
here << tau;
|
|
here << t0;
|
|
here << paa;
|
|
here << etau;
|
|
here << ecp;
|
|
here << paa_dec;
|
|
here << ecp_del;
|
|
here << tau_del;
|
|
here << ecp_dec;
|
|
here << ecp_dec_del;
|
|
}
|
|
|
|
// 2 and 3 parameter models only load and save cp, tau and t0
|
|
void CP2Model::loadParameters(QList<double>&here)
|
|
{
|
|
cp = here[0];
|
|
tau = here[1];
|
|
t0 = here[2];
|
|
}
|
|
|
|
void CP2Model::saveParameters(QList<double>&here)
|
|
{
|
|
here.clear();
|
|
here << cp;
|
|
here << tau;
|
|
here << t0;
|
|
}
|
|
|
|
void CP3Model::loadParameters(QList<double>&here)
|
|
{
|
|
cp = here[0];
|
|
tau = here[1];
|
|
t0 = here[2];
|
|
}
|
|
void CP3Model::saveParameters(QList<double>&here)
|
|
{
|
|
here.clear();
|
|
here << cp;
|
|
here << tau;
|
|
here << t0;
|
|
}
|
|
void WSModel::loadParameters(QList<double>&here)
|
|
{
|
|
cp = here[0];
|
|
tau = here[1];
|
|
t0 = here[2];
|
|
}
|
|
void WSModel::saveParameters(QList<double>&here)
|
|
{
|
|
here.clear();
|
|
here << cp;
|
|
here << tau;
|
|
here << t0;
|
|
}
|