From bd367e607a4dcc935edafcaea00d142916242c97 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Date: Fri, 11 Jul 2025 15:33:24 -0300 Subject: [PATCH] Add planned/expected PMC to R/Python APIs Part 2 of #4652 --- src/Metrics/PMCData.cpp | 14 ++--- src/Metrics/PMCData.h | 14 ++++- src/Python/SIP/Bindings.cpp | 50 +++++++++++++---- src/Python/SIP/Bindings.h | 2 +- src/Python/SIP/goldencheetah.sip | 2 +- src/Python/SIP/sipgoldencheetahBindings.cpp | 9 ++- src/R/RTool.cpp | 62 ++++++++++++++++----- src/R/RTool.h | 2 +- 8 files changed, 115 insertions(+), 40 deletions(-) diff --git a/src/Metrics/PMCData.cpp b/src/Metrics/PMCData.cpp index d6aa1ba24..e700ff5e0 100644 --- a/src/Metrics/PMCData.cpp +++ b/src/Metrics/PMCData.cpp @@ -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 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="< &sb() { return sb_; } QVector &rr() { return rr_; } + QVector &plannedStress() { return planned_stress_; } + QVector &plannedLts() { return planned_lts_; } + QVector &plannedSts() { return planned_sts_; } + QVector &plannedSb() { return planned_sb_; } + QVector &plannedRr() { return planned_rr_; } + + QVector &expectedStress() { return expected_stress_; } + QVector &expectedLts() { return expected_lts_; } + QVector &expectedSts() { return expected_sts_; } + QVector &expectedSb() { return expected_sb_; } + QVector &expectedRr() { return expected_rr_; } + // index into the arrays int indexOf(QDate) ; @@ -129,7 +141,7 @@ class PMCData : public QObject { int days_; QVector stress_, lts_, sts_, sb_, rr_; QVector planned_stress_, planned_lts_, planned_sts_, planned_sb_, planned_rr_; - QVector expected_lts_, expected_sts_, expected_sb_, expected_rr_; + QVector expected_stress_, expected_lts_, expected_sts_, expected_sb_, expected_rr_; bool isstale; // needs refreshing diff --git a/src/Python/SIP/Bindings.cpp b/src/Python/SIP/Bindings.cpp index 63c7f7d17..8b38d1bff 100644 --- a/src/Python/SIP/Bindings.cpp +++ b/src/Python/SIP/Bindings.cpp @@ -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= 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++; } } diff --git a/src/Python/SIP/Bindings.h b/src/Python/SIP/Bindings.h index bcf6e3d80..57bd7e080 100644 --- a/src/Python/SIP/Bindings.h +++ b/src/Python/SIP/Bindings.h @@ -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 diff --git a/src/Python/SIP/goldencheetah.sip b/src/Python/SIP/goldencheetah.sip index 21476ab69..d6a14b45e 100644 --- a/src/Python/SIP/goldencheetah.sip +++ b/src/Python/SIP/goldencheetah.sip @@ -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 diff --git a/src/Python/SIP/sipgoldencheetahBindings.cpp b/src/Python/SIP/sipgoldencheetahBindings.cpp index ad619052c..b6edc766e 100644 --- a/src/Python/SIP/sipgoldencheetahBindings.cpp +++ b/src/Python/SIP/sipgoldencheetahBindings.cpp @@ -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; } diff --git a/src/R/RTool.cpp b/src/R/RTool.cpp index f36c9d491..e0b8e8f1f 100644 --- a/src/R/RTool.cpp +++ b/src/R/RTool.cpp @@ -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= 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++; diff --git a/src/R/RTool.h b/src/R/RTool.h index 8a3c9f4f1..2be96792e 100644 --- a/src/R/RTool.h +++ b/src/R/RTool.h @@ -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