Add Banister and Performance for Running Power

- Performances are computed independently for Rides and Runs in Estimator
- Running Performances are shown in LTM Charts When all selected activities
  are runs for a Performance curve
- Banister models uses Performances from activities were score > 0
  and for the same date with matching sport, so a Banister model based
  on BikeScore/BikeStress will include only rides while one based on
  GOVSS will include only runs
- Banister (Run) chart based on GOVSS metric added for testing
This commit is contained in:
Ale Martinez
2019-04-23 11:44:38 -03:00
parent 44d17a606b
commit 8facce909a
8 changed files with 102 additions and 66 deletions

View File

@@ -3049,6 +3049,14 @@ void
LTMPlot::createEstimateData(Context *context, LTMSettings *settings, MetricDetail metricDetail,
QVector<double>&x,QVector<double>&y,int&n, bool)
{
// curve specific filter, used to figure out if all activities are runs
Specification spec = settings->specification;
if (!SearchFilterBox::isNull(metricDetail.datafilter))
spec.addMatches(SearchFilterBox::matches(context, metricDetail.datafilter));
int nActivities, nRides, nRuns, nSwims;
context->athlete->rideCache->getRideTypeCounts(spec, nActivities, nRides, nRuns, nSwims);
metricDetail.run = (nRuns > 0 && nActivities == nRuns);
// resize the curve array to maximum possible size (even if we don't need it)
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
- groupForDate(settings->start.date(), settings->groupBy);
@@ -3739,6 +3747,14 @@ LTMPlot::createMeasureData(Context *context, LTMSettings *settings, MetricDetail
void
LTMPlot::createPerformanceData(Context *context, LTMSettings *settings, MetricDetail metricDetail, QVector<double>&x,QVector<double>&y,int&n, bool)
{
// curve specific filter, used to figure out if all activities are runs
Specification spec = settings->specification;
if (!SearchFilterBox::isNull(metricDetail.datafilter))
spec.addMatches(SearchFilterBox::matches(context, metricDetail.datafilter));
int nActivities, nRides, nRuns, nSwims;
context->athlete->rideCache->getRideTypeCounts(spec, nActivities, nRides, nRuns, nSwims);
metricDetail.run = (nRuns > 0 && nActivities == nRuns);
int maxdays = groupForDate(settings->end.date(), settings->groupBy)
- groupForDate(settings->start.date(), settings->groupBy);
@@ -3757,6 +3773,9 @@ LTMPlot::createPerformanceData(Context *context, LTMSettings *settings, MetricDe
// scan for performance tests and create a map so we can lookup quickly
QHash<QDate, Performance> tests;
foreach (RideItem *item, context->athlete->rideCache->rides()) {
if (!spec.pass(item)) continue; // skip filtered out activities
if (item->dateTime.date() >= settings->start.date() && item->dateTime.date() <= settings->end.date()) {
foreach(IntervalItem *i, item->intervals()) {
if (i->istest()) {
@@ -3796,12 +3815,12 @@ LTMPlot::createPerformanceData(Context *context, LTMSettings *settings, MetricDe
}
if (metricDetail.perfs && value <= 0) {
// is there a weekly performance today?
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(date);
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(date, metricDetail.run);
if (!p.submaximal) value = p.powerIndex;
}
if (metricDetail.submax && value <= 0) {
// is there a submax weekly performance today?
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(date);
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(date, metricDetail.run);
if (p.submaximal) value = p.powerIndex;
}

View File

@@ -1794,29 +1794,11 @@ EditMetricDetailDialog::EditMetricDetailDialog(Context *context, LTMTool *ltmToo
estwpk->addWidget(wpk);
estwpk->addStretch();
// estimate for rides or runs ?
bik = new QRadioButton(tr("Bike"), this);
run = new QRadioButton(tr("Run"), this);
run->setChecked(metricDetail->run);
bik->setChecked(!metricDetail->run);
// put them into a button group because we
// also have radio buttons for watts per kilo / absolute
QButtonGroup* runGroup = new QButtonGroup(this);
runGroup->addButton(bik);
runGroup->addButton(run);
QHBoxLayout *estrun = new QHBoxLayout;
estrun->addStretch();
estrun->addWidget(bik);
estrun->addWidget(run);
estrun->addStretch();
estimateLayout->addStretch();
estimateLayout->addWidget(modelSelect);
estimateLayout->addWidget(estimateSelect);
estimateLayout->addLayout(estbestLayout);
estimateLayout->addLayout(estwpk);
estimateLayout->addLayout(estrun);
estimateLayout->addStretch();
// estimate selection
@@ -2589,7 +2571,6 @@ EditMetricDetailDialog::applyClicked()
metricDetail->datafilter = dataFilter->filter();
metricDetail->wpk = wpk->isChecked();
metricDetail->run = run->isChecked();
metricDetail->series = seriesList.at(dataSeries->currentIndex());
metricDetail->model = models[modelSelect->currentIndex()]->code();
metricDetail->estimate = estimateSelect->currentIndex(); // 0 - 5

View File

@@ -209,7 +209,6 @@ class EditMetricDetailDialog : public QDialog
QDoubleSpinBox *estimateDuration;
QComboBox *estimateDurationUnits;
QRadioButton *abs, *wpk;
QRadioButton *bik, *run;
// stress
QComboBox *stressTypeSelect; // STS, LTS, SB, RR et al

View File

@@ -188,7 +188,6 @@ Banister::getPeakCP(QDate from, QDate to, int &CP)
}
CP = max;
// RMSE
return start.addDays(index);
}
@@ -279,39 +278,20 @@ Banister::refresh()
if (score>0) {
rides++;
meanscore += score; // averaged at end
}
// get best scoring performance *test* in the ride
double todaybest=0;
foreach(IntervalItem *i, item->intervals()) {
if (i->istest()) {
double pix=i->getForSymbol("power_index");
if (pix > todaybest) todaybest = pix;
// get best scoring performance *test* in the ride
double todaybest=0;
foreach(IntervalItem *i, item->intervals()) {
if (i->istest()) {
double pix=i->getForSymbol("power_index");
if (pix > todaybest) todaybest = pix;
}
}
}
// is there a performance test already there for today?
if (todaybest > 0) {
if (performances > 0 && performanceDay[performances-1] == day) {
if (performanceScore[performances-1] < todaybest)
performanceScore[performances-1] = todaybest;
} else {
performanceDay[performances] = day;
performanceScore[performances] = todaybest;
performances++;
}
}
// if we didn't find a performance test in the ride lets see if there
// is a weekly performance already identified
if (!(todaybest > 0)) {
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(item->dateTime.date());
if (!p.submaximal && p.powerIndex > 0) {
// its not submax
todaybest = p.powerIndex;
// is there a performance test already there for today?
if (todaybest > 0) {
if (performances > 0 && performanceDay[performances-1] == day) {
if (performanceScore[performances-1] < todaybest) // validating with '<=' not '<' is vital for 2-a-days
if (performanceScore[performances-1] < todaybest)
performanceScore[performances-1] = todaybest;
} else {
performanceDay[performances] = day;
@@ -319,12 +299,31 @@ Banister::refresh()
performances++;
}
}
}
// add more space
if (performances == performanceDay.size()) {
performanceDay.resize(performanceDay.size() + (days/2));
performanceScore.resize(performanceDay.size() + (days/2));
// if we didn't find a performance test in the ride lets see if there
// is a weekly performance already identified
if (!(todaybest > 0)) {
Performance p = context->athlete->rideCache->estimator->getPerformanceForDate(item->dateTime.date(), item->isRun);
if (!p.submaximal && p.powerIndex > 0) {
// its not submax
todaybest = p.powerIndex;
if (performances > 0 && performanceDay[performances-1] == day) {
if (performanceScore[performances-1] < todaybest) // validating with '<=' not '<' is vital for 2-a-days
performanceScore[performances-1] = todaybest;
} else {
performanceDay[performances] = day;
performanceScore[performances] = todaybest;
performances++;
}
}
}
// add more space
if (performances == performanceDay.size()) {
performanceDay.resize(performanceDay.size() + (days/2));
performanceScore.resize(performanceDay.size() + (days/2));
}
}
}
@@ -566,8 +565,10 @@ void Banister::fit()
//
// power index metric
double powerIndex(double averagepower, double duration)
double powerIndex(double averagepower, double duration, bool isRun)
{
Q_UNUSED(isRun); // TODO: different parameters for Running
// so now lets work out what the 3p model says the
// typical athlete would do for the same duration
//
@@ -635,7 +636,7 @@ class PowerIndex : public RideMetric {
}
// calculate power index, 0=out of bounds
double pix = powerIndex(averagepower, duration);
double pix = powerIndex(averagepower, duration, item->isRun);
// we could convert to linear work time model before
// indexing, but they cancel out so no value in doing so

View File

@@ -32,7 +32,7 @@
extern const double typical_CP, typical_WPrime, typical_Pmax;
// calculate power index, used to model in cp plot too
extern double powerIndex(double averagepower, double duration);
extern double powerIndex(double averagepower, double duration, bool isRun=false);
// gap with no training that constitutes break in seasons
extern const int typical_SeasonBreak;

View File

@@ -262,12 +262,13 @@ Estimator::run()
double p = double(week[t]);
if (week[t]<=0) continue;
double pix = powerIndex(p, t);
double pix = powerIndex(p, t, isRun);
if (pix > bestperformance.powerIndex) {
bestperformance.duration = t;
bestperformance.power = p;
bestperformance.powerIndex = pix;
bestperformance.when = weekdates[t];
bestperformance.run = isRun;
// for filter, saves having to convert as we go
bestperformance.x = bestperformance.when.toJulianDay();
@@ -350,23 +351,23 @@ Estimator::run()
performances = perfs;
} else {
estimates.append(est);
// performances.append(perfs); // TODO running performances
performances.append(perfs);
}
lock.unlock();
// debug dump peak performances
foreach(Performance p, performances) {
printd("%f Peak: %f for %f secs on %s\n", p.powerIndex, p.power, p.duration, p.when.toString().toStdString().c_str());
printd("%s %f Peak: %f for %f secs on %s\n", p.run ? "Run" : "Bike", p.powerIndex, p.power, p.duration, p.when.toString().toStdString().c_str());
}
printd("%s Estimates end.\n", isRun ? "Run" : "Bike");
}
}
Performance Estimator::getPerformanceForDate(QDate date)
Performance Estimator::getPerformanceForDate(QDate date, bool wantrun)
{
// serial search is ok as low numberish - always takes first as should be no dupes
foreach(Performance p, performances) {
if (p.when == date) return p;
if (p.when == date && p.run == wantrun) return p;
}
return Performance(QDate(),0,0,0);
}

View File

@@ -40,6 +40,7 @@ class Performance {
QDate when, weekcommencing;
double power, duration, powerIndex;
bool submaximal; // set by the filter, user can choose to include.
bool run; // indicates a Running with power performance
double x; // different units, but basically when as a julian day
};
@@ -72,7 +73,7 @@ class Estimator : public QThread {
void calculate();
// get a performance for a given day
Performance getPerformanceForDate(QDate date);
Performance getPerformanceForDate(QDate date, bool wantrun);
protected:

View File

@@ -0,0 +1,34 @@
{
"CHART":{
"VERSION":"1",
"VIEW":"home",
"TYPE":"7",
"PROPERTIES":{
"title":"Banister (Run) ",
"subtitle":"Banister (Run) ",
"widthFactor":"2",
"heightFactor":"2",
"style":"0",
"resizable":"0",
"preset":"0",
"bin":"0",
"shade":"0",
"data":"0",
"stack":"0",
"stackWidth":"3",
"legend":"1",
"events":"0",
"banister":"1",
"filter":"search: ",
"fromDate":"Sat Jan 1 2000",
"toDate":"Sat Jan 1 2000",
"startDate":"Thu Oct 11 2018",
"lastN":"7",
"lastNX":"0",
"prevN":"0",
"settings":"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",
"useSelected":"0",
"__LAST__":"1",
}
}
}