Permalink
Browse files

Banister Modelling

.. Banister model fitting using LM

.. can plot Banister curves on trends plots;
   - Performance curve (NTE+PTE)
   - Predicted CP curve (Performance curve scaled)
   - Negative Training Effect
   - Positive Training Effect

.. the code is sub-optimal and needs to be refactored
   to cache and refresh less frequently (using the same
   pattern as PMC most likely).

.. the model fitting can fail and needs to be made a
   lot more robust, along with ensuring the samples
   we fit to are appropriate.
  • Loading branch information...
liversedge committed Jan 10, 2019
1 parent c958107 commit 2aa4779df978717be69750208e79bf88ab45aeca
Showing with 504 additions and 247 deletions.
  1. +12 −162 src/Charts/LTMPlot.cpp
  2. +1 −0 src/Charts/LTMSettings.h
  3. +66 −60 src/Charts/LTMTool.cpp
  4. +2 −0 src/Core/RideCache.h
  5. +367 −16 src/Metrics/Banister.cpp
  6. +54 −9 src/Metrics/Banister.h
  7. +2 −0 src/Metrics/Estimator.h
@@ -449,9 +449,10 @@ LTMPlot::setData(LTMSettings *set)
: new QwtPlotCurve(metricDetail.uname);
current->setVisible(!metricDetail.hidden);
settings->metrics[m].curve = current;
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS)
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_BANISTER) {
//fprintf(stderr, "insert curve %s, %s\n", metricDetail.uname.toStdString().c_str(), metricDetail.bestSymbol.toStdString().c_str()); fflush(stderr);
curves.insert(metricDetail.bestSymbol, current);
else
} else
curves.insert(metricDetail.symbol, current);
stacks.insert(current, stackcounter+1);
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
@@ -603,9 +604,10 @@ LTMPlot::setData(LTMSettings *set)
: new QwtPlotCurve(metricDetail.uname);
current->setVisible(!metricDetail.hidden);
settings->metrics[m].curve = current;
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS)
if (metricDetail.type == METRIC_BEST || metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_BANISTER) {
//fprintf(stderr, "insert curve %s, %s\n", metricDetail.uname.toStdString().c_str(), metricDetail.bestSymbol.toStdString().c_str()); fflush(stderr);
curves.insert(metricDetail.bestSymbol, current);
else
} else
curves.insert(metricDetail.symbol, current);
if (appsettings->value(this, GC_ANTIALIAS, true).toBool() == true)
current->setRenderHint(QwtPlotItem::RenderAntialiased);
@@ -3546,110 +3548,11 @@ void
LTMPlot::createBanisterData(Context *context, LTMSettings *settings, MetricDetail metricDetail,
QVector<double>&x,QVector<double>&y,int&n, bool)
{
n=0;
#if 0
QString scoreType;
int stressType = STRESS_LTS;

// create a custom set of summary metric data!
if (metricDetail.type == METRIC_PM) {
int valuesType = VALUES_CALCULATED;

QString symbol = metricDetail.symbol;
if (symbol.startsWith("planned_")) {
valuesType = VALUES_PLANNED;
symbol = symbol.right(symbol.length()-8);
} else if (symbol.startsWith("expected_")) {
valuesType = VALUES_EXPECTED;
symbol = symbol.right(symbol.length()-9);
}

if (symbol.startsWith("skiba")) {
scoreType = "skiba_bike_score";
} else if (symbol.startsWith("antiss")) {
scoreType = "antiss_score";
} else if (symbol.startsWith("atiss")) {
scoreType = "atiss_score";
} else if (symbol.startsWith("coggan")) {
scoreType = "coggan_tss";
} else if (symbol.startsWith("daniels")) {
scoreType = "daniels_points";
} else if (symbol.startsWith("trimp")) {
scoreType = "trimp_points";
} else if (symbol.startsWith("work")) {
scoreType = "total_work";
} else if (symbol.startsWith("cp_")) {
scoreType = "skiba_cp_exp";
} else if (symbol.startsWith("wprime")) {
scoreType = "skiba_wprime_exp";
} else if (symbol.startsWith("distance")) {
scoreType = "total_distance";
} else if (symbol.startsWith("triscore")) {
scoreType = "triscore";
}

stressType = STRESS_LTS; // if in doubt
if (valuesType == VALUES_CALCULATED) {
if (metricDetail.symbol.endsWith("lts") || metricDetail.symbol.endsWith("ctl"))
stressType = STRESS_LTS;
else if (metricDetail.symbol.endsWith("sts") || metricDetail.symbol.endsWith("atl"))
stressType = STRESS_STS;
else if (metricDetail.symbol.endsWith("sb"))
stressType = STRESS_SB;
else if (metricDetail.symbol.endsWith("lr"))
stressType = STRESS_RR;
}
else if (valuesType == VALUES_PLANNED) {
if (metricDetail.symbol.endsWith("lts") || metricDetail.symbol.endsWith("ctl"))
stressType = STRESS_PLANNED_LTS;
else if (metricDetail.symbol.endsWith("sts") || metricDetail.symbol.endsWith("atl"))
stressType = STRESS_PLANNED_STS;
else if (metricDetail.symbol.endsWith("sb"))
stressType = STRESS_PLANNED_SB;
else if (metricDetail.symbol.endsWith("lr"))
stressType = STRESS_PLANNED_RR;
}
else if (valuesType == VALUES_EXPECTED) {
if (metricDetail.symbol.endsWith("lts") || metricDetail.symbol.endsWith("ctl"))
stressType = STRESS_EXPECTED_LTS;
else if (metricDetail.symbol.endsWith("sts") || metricDetail.symbol.endsWith("atl"))
stressType = STRESS_EXPECTED_STS;
else if (metricDetail.symbol.endsWith("sb"))
stressType = STRESS_EXPECTED_SB;
else if (metricDetail.symbol.endsWith("lr"))
stressType = STRESS_EXPECTED_RR;
}
} else {

scoreType = metricDetail.symbol; // just use the selected metric
stressType = metricDetail.stressType;
}

// banister model
Banister f(context, metricDetail.symbol, 0,0,0,0);

// initial state
PMCData *athletePMC = NULL;
PMCData *localPMC = NULL;
n = 0;

// create local PMC if filtered
if (!SearchFilterBox::isNull(metricDetail.datafilter) || settings->specification.isFiltered()) {

// don't filter for date range!!
Specification allDates = settings->specification;

// curve specific filter
if (!SearchFilterBox::isNull(metricDetail.datafilter))
allDates.addMatches(SearchFilterBox::matches(context, metricDetail.datafilter));

allDates.setDateRange(DateRange(QDate(),QDate()));
localPMC = new PMCData(context, allDates, scoreType);
}

// use global one if not filtered
if (!localPMC) athletePMC = context->athlete->getPMCFor(scoreType);

// point to the right one
PMCData *pmcData = localPMC ? localPMC : athletePMC;
// perform fit (date range todo)
f.fit();

int maxdays = groupForDate(settings->end.date(), settings->groupBy)
- groupForDate(settings->start.date(), settings->groupBy);
@@ -3676,57 +3579,7 @@ LTMPlot::createBanisterData(Context *context, LTMSettings *settings, MetricDetai
int currentDay = groupForDate(date, settings->groupBy);

// value for day
double value = 0.0f;

switch (stressType) {
case STRESS_LTS:
value = pmcData->lts(date);
break;
case STRESS_STS:
value = pmcData->sts(date);
break;
case STRESS_SB:
value = pmcData->sb(date);
break;
case STRESS_RR:
value = pmcData->rr(date);
break;
case STRESS_PLANNED_LTS:
value = pmcData->plannedLts(date);
break;
case STRESS_PLANNED_STS:
value = pmcData->plannedSts(date);
break;
case STRESS_PLANNED_SB:
value = pmcData->plannedSb(date);
break;
case STRESS_PLANNED_RR:
value = pmcData->plannedRr(date);
break;
case STRESS_EXPECTED_LTS:
value = pmcData->expectedLts(date);
if (past)
plotData = false;
break;
case STRESS_EXPECTED_STS:
value = pmcData->expectedSts(date);
if (past)
plotData = false;
break;
case STRESS_EXPECTED_SB:
value = pmcData->expectedSb(date);
if (past)
plotData = false;
break;
case STRESS_EXPECTED_RR:
value = pmcData->expectedRr(date);
if (past)
plotData = false;
break;
default:
value = 0;
break;
}
double value = f.value(date, metricDetail.stressType);

if (plotData && (value || wantZero)) {
unsigned long seconds = 1;
@@ -3785,11 +3638,8 @@ LTMPlot::createBanisterData(Context *context, LTMSettings *settings, MetricDetai
lastDay = currentDay;
}
}

// wipe away local
if (localPMC) delete localPMC;
#endif
}

void
LTMPlot::createMeasureData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n, bool)
{
@@ -104,6 +104,7 @@ class RideBest;
#define BANISTER_NTE 0
#define BANISTER_PTE 1
#define BANISTER_PERFORMANCE 2
#define BANISTER_CP 3

// type of values
#define VALUES_CALCULATED 0
@@ -1946,7 +1946,8 @@ EditMetricDetailDialog::EditMetricDetailDialog(Context *context, LTMTool *ltmToo
banisterTypeSelect->addItem(tr("Negative Training Effect (NTE)"), BANISTER_NTE);
banisterTypeSelect->addItem(tr("Positive Training Effect (PTE)"), BANISTER_PTE);
banisterTypeSelect->addItem(tr("Performance (Power Index)"), BANISTER_PERFORMANCE);
banisterTypeSelect->setCurrentIndex(metricDetail->stressType < 3 ? metricDetail->stressType : 2);
banisterTypeSelect->addItem(tr("Predicted CP (Watts)"), BANISTER_CP);
banisterTypeSelect->setCurrentIndex(metricDetail->stressType < 4 ? metricDetail->stressType : 2);

banisterWidget = new QWidget(this);
banisterWidget->setContentsMargins(0,0,0,0);
@@ -2209,6 +2210,7 @@ EditMetricDetailDialog::EditMetricDetailDialog(Context *context, LTMTool *ltmToo
connect(chooseMeasure, SIGNAL(toggled(bool)), this, SLOT(measureName()));
connect(choosePerformance, SIGNAL(toggled(bool)), this, SLOT(performanceName()));
connect(stressTypeSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(stressName()));
connect(banisterTypeSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(banisterName()));
connect(chooseMetric, SIGNAL(toggled(bool)), this, SLOT(metricSelected()));
connect(duration, SIGNAL(valueChanged(double)), this, SLOT(bestName()));
connect(durationUnits, SIGNAL(currentIndexChanged(int)), this, SLOT(bestName()));
@@ -2366,10 +2368,11 @@ EditMetricDetailDialog::banisterName()
metricDetail->bestSymbol = metricDetail->symbol;

// append type
switch(stressTypeSelect->currentIndex()) {
switch(banisterTypeSelect->currentIndex()) {
case 0: metricDetail->bestSymbol += "_nte"; break;
case 1: metricDetail->bestSymbol += "_pte"; break;
case 2: metricDetail->bestSymbol += "_perf"; break;
case 3: metricDetail->bestSymbol += "_cp"; break;
}

}
@@ -2431,7 +2434,7 @@ void
EditMetricDetailDialog::metricSelected()
{
// only in metric mode
if (!chooseMetric->isChecked() && !chooseStress->isChecked()) return;
if (!chooseMetric->isChecked() && !chooseStress->isChecked() && !chooseBanister->isChecked()) return;

// user selected a different metric
// so update accordingly
@@ -2440,73 +2443,76 @@ EditMetricDetailDialog::metricSelected()
// out of bounds !
if (index < 0 || index >= ltmTool->metrics.count()) return;

userName->setText(ltmTool->metrics[index].uname);
userUnits->setText(ltmTool->metrics[index].uunits);
curveSmooth->setChecked(ltmTool->metrics[index].smooth);
fillCurve->setChecked(ltmTool->metrics[index].fillCurve);
labels->setChecked(ltmTool->metrics[index].labels);
stack->setChecked(ltmTool->metrics[index].stack);
showBest->setValue(ltmTool->metrics[index].topN);
showOut->setValue(ltmTool->metrics[index].topOut);
baseLine->setValue(ltmTool->metrics[index].baseline);
penColor = ltmTool->metrics[index].penColor;
trendType->setCurrentIndex(ltmTool->metrics[index].trendtype);
setButtonIcon(penColor);

// curve style
switch (ltmTool->metrics[index].curveStyle) {
if (!chooseBanister->isChecked()) {

userName->setText(ltmTool->metrics[index].uname);
userUnits->setText(ltmTool->metrics[index].uunits);
curveSmooth->setChecked(ltmTool->metrics[index].smooth);
fillCurve->setChecked(ltmTool->metrics[index].fillCurve);
labels->setChecked(ltmTool->metrics[index].labels);
stack->setChecked(ltmTool->metrics[index].stack);
showBest->setValue(ltmTool->metrics[index].topN);
showOut->setValue(ltmTool->metrics[index].topOut);
baseLine->setValue(ltmTool->metrics[index].baseline);
penColor = ltmTool->metrics[index].penColor;
trendType->setCurrentIndex(ltmTool->metrics[index].trendtype);
setButtonIcon(penColor);

// curve style
switch (ltmTool->metrics[index].curveStyle) {

case QwtPlotCurve::Steps:
curveStyle->setCurrentIndex(0);
break;
case QwtPlotCurve::Lines:
curveStyle->setCurrentIndex(1);
break;
case QwtPlotCurve::Sticks:
curveStyle->setCurrentIndex(2);
break;
case QwtPlotCurve::Dots:
default:
curveStyle->setCurrentIndex(3);
break;
case QwtPlotCurve::Steps:
curveStyle->setCurrentIndex(0);
break;
case QwtPlotCurve::Lines:
curveStyle->setCurrentIndex(1);
break;
case QwtPlotCurve::Sticks:
curveStyle->setCurrentIndex(2);
break;
case QwtPlotCurve::Dots:
default:
curveStyle->setCurrentIndex(3);
break;

}
}

// curveSymbol
switch (ltmTool->metrics[index].symbolStyle) {
// curveSymbol
switch (ltmTool->metrics[index].symbolStyle) {

case QwtSymbol::NoSymbol:
curveSymbol->setCurrentIndex(0);
break;
case QwtSymbol::Ellipse:
curveSymbol->setCurrentIndex(1);
break;
case QwtSymbol::Rect:
curveSymbol->setCurrentIndex(2);
break;
case QwtSymbol::Diamond:
curveSymbol->setCurrentIndex(3);
break;
case QwtSymbol::Triangle:
curveSymbol->setCurrentIndex(4);
break;
case QwtSymbol::XCross:
curveSymbol->setCurrentIndex(5);
break;
case QwtSymbol::Hexagon:
curveSymbol->setCurrentIndex(6);
break;
case QwtSymbol::Star1:
default:
curveSymbol->setCurrentIndex(7);
break;
case QwtSymbol::NoSymbol:
curveSymbol->setCurrentIndex(0);
break;
case QwtSymbol::Ellipse:
curveSymbol->setCurrentIndex(1);
break;
case QwtSymbol::Rect:
curveSymbol->setCurrentIndex(2);
break;
case QwtSymbol::Diamond:
curveSymbol->setCurrentIndex(3);
break;
case QwtSymbol::Triangle:
curveSymbol->setCurrentIndex(4);
break;
case QwtSymbol::XCross:
curveSymbol->setCurrentIndex(5);
break;
case QwtSymbol::Hexagon:
curveSymbol->setCurrentIndex(6);
break;
case QwtSymbol::Star1:
default:
curveSymbol->setCurrentIndex(7);
break;

}
}

(*metricDetail) = ltmTool->metrics[index]; // overwrite!

// make the banister name
if (chooseStress->isChecked()) banisterName();
if (chooseBanister->isChecked()) banisterName();
// make the stress name
if (chooseStress->isChecked()) stressName();
}
Oops, something went wrong.

0 comments on commit 2aa4779

Please sign in to comment.