Add planned/expected PMC to R/Python APIs

Part 2 of #4652
This commit is contained in:
Alejandro Martinez
2025-07-11 15:33:24 -03:00
parent 9726e70e74
commit bd367e607a
8 changed files with 115 additions and 40 deletions

View File

@@ -165,7 +165,6 @@ void PMCData::refresh()
// back to null date if not set, just to get round date arithmetic
if (start_ == QDate(9999,12,31)) start_ = QDate();
QVector<double> expected_stress;
// We got a valid range ?
if (start_ != QDate() && end_ != QDate() && start_ < end_) {
@@ -183,7 +182,7 @@ void PMCData::refresh()
planned_sb_.resize(days_+1); // for SB tomorrow!
planned_rr_.resize(days_);
expected_stress.resize(days_);
expected_stress_.resize(days_);
expected_lts_.resize(days_);
expected_sts_.resize(days_);
expected_sb_.resize(days_+1); // for SB tomorrow!
@@ -221,7 +220,6 @@ void PMCData::refresh()
//
// STEP TWO What are the seedings and ride values
//
bool sbToday = appsettings->cvalue(context->athlete->cyclist, GC_SB_TODAY).toInt();
const double lte = (double)exp(-1.0/ltsDays_);
const double ste = (double)exp(-1.0/stsDays_);
@@ -238,7 +236,7 @@ void PMCData::refresh()
planned_sb_.fill(0);
planned_rr_.fill(0);
expected_stress.fill(0);
expected_stress_.fill(0);
expected_lts_.fill(0);
expected_sts_.fill(0);
expected_sb_.fill(0);
@@ -294,11 +292,11 @@ void PMCData::refresh()
}
} else if (start_.addDays(offset).daysTo(QDate::currentDate()) < 0) {
if (item->planned) {
expected_stress[offset] += value;
expected_stress_[offset] += value;
}
} else {
if (! item->planned) {
expected_stress[offset] += value;
expected_stress_[offset] += value;
}
}
}
@@ -306,14 +304,14 @@ void PMCData::refresh()
}
if (todayOffset > -1) {
// Special case today: Use actual stress if available, otherwise planned
expected_stress[todayOffset] = (todayActualStress > 0) ? todayActualStress : todayPlannedStress;
expected_stress_[todayOffset] = (todayActualStress > 0) ? todayActualStress : todayPlannedStress;
}
delete df;
calculateMetrics(days_, stress_, lts_, sts_, sb_, rr_);
calculateMetrics(days_, planned_stress_, planned_lts_, planned_sts_, planned_sb_, planned_rr_);
calculateMetrics(days_, expected_stress, expected_lts_, expected_sts_, expected_sb_, expected_rr_);
calculateMetrics(days_, expected_stress_, expected_lts_, expected_sts_, expected_sb_, expected_rr_);
//qDebug()<<"refresh PMC in="<<timer.elapsed()<<"ms";

View File

@@ -68,6 +68,18 @@ class PMCData : public QObject {
QVector<double> &sb() { return sb_; }
QVector<double> &rr() { return rr_; }
QVector<double> &plannedStress() { return planned_stress_; }
QVector<double> &plannedLts() { return planned_lts_; }
QVector<double> &plannedSts() { return planned_sts_; }
QVector<double> &plannedSb() { return planned_sb_; }
QVector<double> &plannedRr() { return planned_rr_; }
QVector<double> &expectedStress() { return expected_stress_; }
QVector<double> &expectedLts() { return expected_lts_; }
QVector<double> &expectedSts() { return expected_sts_; }
QVector<double> &expectedSb() { return expected_sb_; }
QVector<double> &expectedRr() { return expected_rr_; }
// index into the arrays
int indexOf(QDate) ;
@@ -129,7 +141,7 @@ class PMCData : public QObject {
int days_;
QVector<double> stress_, lts_, sts_, sb_, rr_;
QVector<double> planned_stress_, planned_lts_, planned_sts_, planned_sb_, planned_rr_;
QVector<double> expected_lts_, expected_sts_, expected_sb_, expected_rr_;
QVector<double> expected_stress_, expected_lts_, expected_sts_, expected_sb_, expected_rr_;
bool isstale; // needs refreshing

View File

@@ -2090,7 +2090,7 @@ Bindings::rideFileCacheMeanmax(RideFileCache* cache) const
}
PyObject*
Bindings::seasonPmc(bool all, QString metric) const
Bindings::seasonPmc(bool all, QString metric, QString type) const
{
Context *context = python->contexts.value(threadid()).context;
@@ -2150,11 +2150,25 @@ Bindings::seasonPmc(bool all, QString metric) const
// just copy
for(unsigned int k=0; k<size; k++) {
PyList_SET_ITEM(stress, k, PyFloat_FromDouble(pmcData.stress()[k]));
PyList_SET_ITEM(lts, k, PyFloat_FromDouble(pmcData.lts()[k]));
PyList_SET_ITEM(sts, k, PyFloat_FromDouble(pmcData.sts()[k]));
PyList_SET_ITEM(sb, k, PyFloat_FromDouble(pmcData.sb()[k]));
PyList_SET_ITEM(rr, k, PyFloat_FromDouble(pmcData.rr()[k]));
if (type == "Planned") {
PyList_SET_ITEM(stress, k, PyFloat_FromDouble(pmcData.plannedStress()[k]));
PyList_SET_ITEM(lts, k, PyFloat_FromDouble(pmcData.plannedLts()[k]));
PyList_SET_ITEM(sts, k, PyFloat_FromDouble(pmcData.plannedSts()[k]));
PyList_SET_ITEM(sb, k, PyFloat_FromDouble(pmcData.plannedSb()[k]));
PyList_SET_ITEM(rr, k, PyFloat_FromDouble(pmcData.plannedRr()[k]));
} else if (type == "Expected") {
PyList_SET_ITEM(stress, k, PyFloat_FromDouble(pmcData.expectedStress()[k]));
PyList_SET_ITEM(lts, k, PyFloat_FromDouble(pmcData.expectedLts()[k]));
PyList_SET_ITEM(sts, k, PyFloat_FromDouble(pmcData.expectedSts()[k]));
PyList_SET_ITEM(sb, k, PyFloat_FromDouble(pmcData.expectedSb()[k]));
PyList_SET_ITEM(rr, k, PyFloat_FromDouble(pmcData.expectedRr()[k]));
} else {
PyList_SET_ITEM(stress, k, PyFloat_FromDouble(pmcData.stress()[k]));
PyList_SET_ITEM(lts, k, PyFloat_FromDouble(pmcData.lts()[k]));
PyList_SET_ITEM(sts, k, PyFloat_FromDouble(pmcData.sts()[k]));
PyList_SET_ITEM(sb, k, PyFloat_FromDouble(pmcData.sb()[k]));
PyList_SET_ITEM(rr, k, PyFloat_FromDouble(pmcData.rr()[k]));
}
}
} else {
@@ -2165,11 +2179,25 @@ Bindings::seasonPmc(bool all, QString metric) const
// day today
if (start.addDays(k) >= range.from && start.addDays(k) <= range.to) {
PyList_SET_ITEM(stress, index, PyFloat_FromDouble(pmcData.stress()[k]));
PyList_SET_ITEM(lts, index, PyFloat_FromDouble(pmcData.lts()[k]));
PyList_SET_ITEM(sts, index, PyFloat_FromDouble(pmcData.sts()[k]));
PyList_SET_ITEM(sb, index, PyFloat_FromDouble(pmcData.sb()[k]));
PyList_SET_ITEM(rr, index, PyFloat_FromDouble(pmcData.rr()[k]));
if (type == "Planned") {
PyList_SET_ITEM(stress, index, PyFloat_FromDouble(pmcData.plannedStress()[k]));
PyList_SET_ITEM(lts, index, PyFloat_FromDouble(pmcData.plannedLts()[k]));
PyList_SET_ITEM(sts, index, PyFloat_FromDouble(pmcData.plannedSts()[k]));
PyList_SET_ITEM(sb, index, PyFloat_FromDouble(pmcData.plannedSb()[k]));
PyList_SET_ITEM(rr, index, PyFloat_FromDouble(pmcData.plannedRr()[k]));
} else if (type == "Expected") {
PyList_SET_ITEM(stress, index, PyFloat_FromDouble(pmcData.expectedStress()[k]));
PyList_SET_ITEM(lts, index, PyFloat_FromDouble(pmcData.expectedLts()[k]));
PyList_SET_ITEM(sts, index, PyFloat_FromDouble(pmcData.expectedSts()[k]));
PyList_SET_ITEM(sb, index, PyFloat_FromDouble(pmcData.expectedSb()[k]));
PyList_SET_ITEM(rr, index, PyFloat_FromDouble(pmcData.expectedRr()[k]));
} else {
PyList_SET_ITEM(stress, index, PyFloat_FromDouble(pmcData.stress()[k]));
PyList_SET_ITEM(lts, index, PyFloat_FromDouble(pmcData.lts()[k]));
PyList_SET_ITEM(sts, index, PyFloat_FromDouble(pmcData.sts()[k]));
PyList_SET_ITEM(sb, index, PyFloat_FromDouble(pmcData.sb()[k]));
PyList_SET_ITEM(rr, index, PyFloat_FromDouble(pmcData.rr()[k]));
}
index++;
}
}

View File

@@ -98,7 +98,7 @@ class Bindings {
PyObject* activityMetrics(bool compare=false) const;
PyObject* seasonMetrics(bool all=false, QString filter=QString(), bool compare=false) const;
PythonDataSeries *metrics(QString metric, bool all=false, QString filter=QString()) const;
PyObject* seasonPmc(bool all=false, QString metric=QString("BikeStress")) const;
PyObject* seasonPmc(bool all=false, QString metric=QString("BikeStress"), QString type=QString("Actual")) const;
PyObject* seasonMeasures(bool all=false, QString group=QString("Body")) const;
// working with meanmax data

View File

@@ -369,7 +369,7 @@ public:
PyObject* activityMetrics(bool compare=false) /TransferBack/;
PyObject* seasonMetrics(bool all=false, QString filter=QString(), bool compare=false) /TransferBack/;
PythonDataSeries *metrics(QString metric, bool all=false, QString filter=QString()) /TransferBack/;
PyObject* seasonPmc(bool all=false, QString metric=QString("BikeStress")) /TransferBack/;
PyObject* seasonPmc(bool all=false, QString metric=QString("BikeStress"), QString type=QString("Actual")) /TransferBack/;
PyObject* seasonMeasures(bool all=false, QString group=QString("Body")) /TransferBack/;
// working with meanmax data

View File

@@ -678,19 +678,24 @@ static PyObject *meth_Bindings_seasonPmc(PyObject *sipSelf, PyObject *sipArgs, P
::QString a1def = QString("BikeStress");
::QString* a1 = &a1def;
int a1State = 0;
::QString a2def = QString("Actual");
::QString* a2 = &a2def;
int a2State = 0;
::Bindings *sipCpp;
static const char *sipKwdList[] = {
sipName_all,
sipName_metric,
sipName_type,
};
if (sipParseKwdArgs(&sipParseErr, sipArgs, sipKwds, sipKwdList, NULL, "B|bJ1", &sipSelf, sipType_Bindings, &sipCpp, &a0, sipType_QString,&a1, &a1State))
if (sipParseKwdArgs(&sipParseErr, sipArgs, sipKwds, sipKwdList, NULL, "B|bJ1J1", &sipSelf, sipType_Bindings, &sipCpp, &a0, sipType_QString,&a1, &a1State, sipType_QString,&a2, &a2State))
{
PyObject * sipRes;
sipRes = sipCpp->seasonPmc(a0,*a1);
sipRes = sipCpp->seasonPmc(a0,*a1,*a2);
sipReleaseType(a1,sipType_QString,a1State);
sipReleaseType(a2,sipType_QString,a2State);
return sipRes;
}

View File

@@ -210,8 +210,8 @@ RTool::RTool()
{ "GC.season.intervals", (DL_FUNC) &RTool::seasonIntervals, 2 },
{ "GC.season.meanmax", (DL_FUNC) &RTool::seasonMeanmax, 3 },
{ "GC.season.peaks", (DL_FUNC) &RTool::seasonPeaks, 5 },
// return a data.frame of pmc series (all=FALSE, metric="BikeStress")
{ "GC.season.pmc", (DL_FUNC) &RTool::pmc, 2 },
// return a data.frame of pmc series (all=FALSE, metric="BikeStress", type="Actual")
{ "GC.season.pmc", (DL_FUNC) &RTool::pmc, 3 },
// return a data.frame of measure fields (all=FALSE, group="Body")
{ "GC.season.measures", (DL_FUNC) &RTool::measures, 2 },
{ "GC.chart.set", (DL_FUNC) &RTool::setChart, 6 },
@@ -305,7 +305,7 @@ RTool::RTool()
"GC.season <- function(all=FALSE, compare=FALSE) { .Call(\"GC.season\", all, compare) }\n"
"GC.season.metrics <- function(all=FALSE, filter=\"\", compare=FALSE) { .Call(\"GC.season.metrics\", all, filter, compare) }\n"
"GC.season.intervals <- function(type=NULL, compare=FALSE) { .Call(\"GC.season.intervals\", type, compare) }\n"
"GC.season.pmc <- function(all=FALSE, metric=\"BikeStress\") { .Call(\"GC.season.pmc\", all, metric) }\n"
"GC.season.pmc <- function(all=FALSE, metric=\"BikeStress\", type=\"Actual\") { .Call(\"GC.season.pmc\", all, metric, type) }\n"
"GC.season.measures <- function(all=FALSE, group=\"Body\") { .Call(\"GC.season.measures\", all, group) }\n"
"GC.season.meanmax <- function(all=FALSE, filter=\"\", compare=FALSE) { .Call(\"GC.season.meanmax\", all, filter, compare) }\n"
// find peaks does a few validation checks on the R side
@@ -317,7 +317,7 @@ RTool::RTool()
"}\n"
// these 2 added for backward compatibility, may be deprecated
"GC.metrics <- function(all=FALSE, filter=\"\", compare=FALSE) { .Call(\"GC.season.metrics\", all, filter, compare) }\n"
"GC.pmc <- function(all=FALSE, metric=\"BikeStress\") { .Call(\"GC.season.pmc\", all, metric) }\n"
"GC.pmc <- function(all=FALSE, metric=\"BikeStress\", type=\"Actual\") { .Call(\"GC.season.pmc\", all, metric, type) }\n"
// charts
"GC.setChart <- function(title=\"\", type=1, animate=FALSE, legpos=2, stack=FALSE, orientation=2) { .Call(\"GC.chart.set\", title, type, animate, legpos ,stack, orientation)}\n"
@@ -3330,7 +3330,7 @@ RTool::activityMetrics(SEXP pCompare)
}
SEXP
RTool::pmc(SEXP pAll, SEXP pMetric)
RTool::pmc(SEXP pAll, SEXP pMetric, SEXP pType)
{
// parse parameters
// p1 - all=TRUE|FALSE - return all metrics or just within
@@ -3342,6 +3342,10 @@ RTool::pmc(SEXP pAll, SEXP pMetric)
pMetric = Rf_coerceVector(pMetric, STRSXP);
QString metric (CHAR(STRING_ELT(pMetric,0)));
// p3 - type="Actual" - PMC type: Actual/Planned/Expected
pMetric = Rf_coerceVector(pType, STRSXP);
QString type (CHAR(STRING_ELT(pType,0)));
// return a dataframe with PMC data for all or the current season
// XXX uses the default half-life
if (rtool->context) {
@@ -3416,11 +3420,25 @@ RTool::pmc(SEXP pAll, SEXP pMetric)
if (all) {
// just copy
for(unsigned int k=0; k<size; k++) REAL(stress)[k] = pmcData.stress()[k];
for(unsigned int k=0; k<size; k++) REAL(lts)[k] = pmcData.lts()[k];
for(unsigned int k=0; k<size; k++) REAL(sts)[k] = pmcData.sts()[k];
for(unsigned int k=0; k<size; k++) REAL(sb)[k] = pmcData.sb()[k];
for(unsigned int k=0; k<size; k++) REAL(rr)[k] = pmcData.rr()[k];
if (type == "Planned") {
for(unsigned int k=0; k<size; k++) REAL(stress)[k] = pmcData.plannedStress()[k];
for(unsigned int k=0; k<size; k++) REAL(lts)[k] = pmcData.plannedLts()[k];
for(unsigned int k=0; k<size; k++) REAL(sts)[k] = pmcData.plannedSts()[k];
for(unsigned int k=0; k<size; k++) REAL(sb)[k] = pmcData.plannedSb()[k];
for(unsigned int k=0; k<size; k++) REAL(rr)[k] = pmcData.plannedRr()[k];
} else if (type == "Expected") {
for(unsigned int k=0; k<size; k++) REAL(stress)[k] = pmcData.expectedStress()[k];
for(unsigned int k=0; k<size; k++) REAL(lts)[k] = pmcData.expectedLts()[k];
for(unsigned int k=0; k<size; k++) REAL(sts)[k] = pmcData.expectedSts()[k];
for(unsigned int k=0; k<size; k++) REAL(sb)[k] = pmcData.expectedSb()[k];
for(unsigned int k=0; k<size; k++) REAL(rr)[k] = pmcData.expectedRr()[k];
} else {
for(unsigned int k=0; k<size; k++) REAL(stress)[k] = pmcData.stress()[k];
for(unsigned int k=0; k<size; k++) REAL(lts)[k] = pmcData.lts()[k];
for(unsigned int k=0; k<size; k++) REAL(sts)[k] = pmcData.sts()[k];
for(unsigned int k=0; k<size; k++) REAL(sb)[k] = pmcData.sb()[k];
for(unsigned int k=0; k<size; k++) REAL(rr)[k] = pmcData.rr()[k];
}
} else {
@@ -3430,11 +3448,25 @@ RTool::pmc(SEXP pAll, SEXP pMetric)
// day today
if (day >= from && day <= to) {
REAL(stress)[index] = pmcData.stress()[k];
REAL(lts)[index] = pmcData.lts()[k];
REAL(sts)[index] = pmcData.sts()[k];
REAL(sb)[index] = pmcData.sb()[k];
REAL(rr)[index] = pmcData.rr()[k];
if (type == "Planned") {
REAL(stress)[index] = pmcData.plannedStress()[k];
REAL(lts)[index] = pmcData.plannedLts()[k];
REAL(sts)[index] = pmcData.plannedSts()[k];
REAL(sb)[index] = pmcData.plannedSb()[k];
REAL(rr)[index] = pmcData.plannedRr()[k];
} else if (type == "Expected") {
REAL(stress)[index] = pmcData.expectedStress()[k];
REAL(lts)[index] = pmcData.expectedLts()[k];
REAL(sts)[index] = pmcData.expectedSts()[k];
REAL(sb)[index] = pmcData.expectedSb()[k];
REAL(rr)[index] = pmcData.expectedRr()[k];
} else {
REAL(stress)[index] = pmcData.stress()[k];
REAL(lts)[index] = pmcData.lts()[k];
REAL(sts)[index] = pmcData.sts()[k];
REAL(sb)[index] = pmcData.sb()[k];
REAL(rr)[index] = pmcData.rr()[k];
}
index++;
}
day++;

View File

@@ -70,7 +70,7 @@ class RTool {
static SEXP seasonIntervals(SEXP type, SEXP compare);
static SEXP seasonMeanmax(SEXP all, SEXP filter, SEXP compare);
static SEXP seasonPeaks(SEXP all, SEXP filter, SEXP compare, SEXP series, SEXP duration);
static SEXP pmc(SEXP all, SEXP metric);
static SEXP pmc(SEXP all, SEXP metric, SEXP type);
static SEXP measures(SEXP all, SEXP group);
// charts