/* * Copyright (c) 2016 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 "CPSolver.h" #include CPSolver::CPSolver(Context *context) : context(context) { integral = (appsettings->value(NULL, GC_WBALFORM, "int").toString() == "int"); } // set the data to solve void CPSolver::setData(CPSolverConstraints constraints, QList rides) { // remember the rides this->constraints=constraints; this->rides=rides; // set the vectors! foreach(RideItem *item, rides) { // we don't do null well if (!item || !item->ride()) continue; // each reference gets a separate data series foreach(RideFilePoint *rp, item->ride()->referencePoints()) { // skip other reference types if (rp->secs <= 0) continue; // ok, now we have a point we need to get the power data // from the start to the point of exhaustion into a // 1 second sample array data << power1s(item->ride(), rp->secs); } } } // get a 1s array to the point secs QVector CPSolver::power1s(RideFile *f, double secs) { // its already in 1s samples so just pull it in QVector returning; foreach(RideFilePoint *p, f->resample(1, 0)->dataPoints()) { if (p->secs < secs) returning << p->watts; else break; } return returning; } // compute the cost, using the settings passed double CPSolver::cost(WBParms parms) { // returning sum(W'bal ^ 2) // loop through each ride for now, but avoid foreach() // since it will make a copy of the contents which has // a significant performance impact double sumwb2=0; for(int i=0; i &ride, WBParms parms) { // compute w'bal for the ride using the paramters // XXX for now we use the integral model double I=0.00f; int t=0; double wpbal=parms.W; foreach(int watts, ride) { if (integral) { // INTEGRAL I += exp(((double)(t) / parms.TAU)) * (watts > parms.CP ? watts-parms.CP : 0); wpbal = parms.W - (exp(-((double)(t) / parms.TAU)) * I); } else { // DIFFERENTIAL wpbal += watts < parms.CP ? ((double(parms.TAU)/100.0f) * (parms.W - wpbal)/parms.W * (parms.CP - watts) ) : (parms.CP-watts); } t++; } // we solve for W'bal=500 as it is not possible to completely // exhaust W', 500 is the point at which most athletes will // fail to continue, on average. // See: http://www.ncbi.nlm.nih.gov/pubmed/24509723 return wpbal - 500; } // get us a neighbour WBParms CPSolver::neighbour(WBParms p, int k, int kmax) { WBParms returning; // a crucial aspect of the simulated annealling algorithm is that // we search a wide space for solutions as we start searching, but // as time passes we look in a much smaller range; i.e. we distribute // across the search space up front, but hone in as we get nearer the end // wild ass guesses at the beginning down to very closest neighbours // start at 150% and drop down to 1% double factor = (double(kmax - k) / (double(kmax))); // range from where we are now from hi to lo // value range (e.g. 400 is range of CP between 100-500) int CPrange = 3 + ((constraints.cpto - constraints.cpf) * factor); int Wrange = 101 + ((constraints.wto - constraints.wf) * factor); int TAUrange = 3 + ((constraints.tto - constraints.tf) * factor); int it=0; // scale rand() to our range (32767 is typical for RAND_MAX) double f = double(Wrange) / double(RAND_MAX); do { returning.CP = p.CP + (rand()%CPrange - (CPrange/2)); returning.W = p.W + (int(double(rand())*f)%Wrange - (Wrange/2)); returning.TAU = p.TAU + (rand()%TAUrange - (TAUrange/2)); } while (it++ < 3 && (returning.CP < constraints.cpf || returning.CP > constraints.cpto || returning.W > constraints.cpto || returning.W < constraints.cpf || returning.TAU < constraints.tf || returning.TAU > constraints.tto)); // if we failed to randomise just check bounds if (returning.CP > constraints.cpto) returning.CP = constraints.cpto; if (returning.CP < constraints.cpf) returning.CP = constraints.cpf; if (returning.W > constraints.wto) returning.W = constraints.wto; if (returning.W < constraints.wf) returning.W = constraints.wf; if (returning.TAU > constraints.tto) returning.TAU = constraints.tto; if (returning.TAU < constraints.tf) returning.TAU = constraints.tf; return returning; } void CPSolver::reset() { rides.clear(); data.clear(); } void CPSolver::start() { // set starting conditions from first ride if (data.count() == 0 || rides.count() == 0) return; // to flag when to stop halt = false; // set starting conditions at maximals s0.CP = constraints.cpto; s0.W = constraints.wto; s0.TAU = constraints.tto; QTime p; p.start(); // initial conditions srand((unsigned int) time (NULL)); // seed ONCE! double E = cost(s0); double Ebest = E; WBParms s = s0; WBParms sbest = s; // 10,000 iterations at most int k=0; int kmax = 100000; // give up when we're on it or run out of loops while (halt == false && k < kmax) { WBParms snew = neighbour(s, k, kmax); double Enew = cost(snew); // progress update k=0 means stop so we offset by one emit current(k+1, snew,Enew); // probability - always 1 if better, but randomly accept higher double random = double(rand()%101)/100.00f; double temp = temperature(double(k)/double(kmax)); double prob = probability(E,Enew,temp); if (prob > random) { s = snew; E = Enew; } // is it better than our very best? if (E < Ebest) { Ebest = E; sbest = s; // k of zero means stop so we offset by one emit newBest(k+1, sbest, Ebest); //qDebug()< &) { // does it even have an exhaustion point? double returning = RideFile::NA; // don't do for intervals or null rides or rides with not exhaustion points! if (spec.interval() == NULL && item && item->ride() && item->ride()->referencePoints().count()) { // find first and compute for it foreach(RideFilePoint *p, item->ride()->referencePoints()) { if (p->secs) { double CP = 250; double W = 20000; // set from the ride if (item->context->athlete->zones(item->isRun)) { int zoneRange = item->context->athlete->zones(item->isRun)->whichRange(item->dateTime.date()); CP = zoneRange >= 0 ? item->context->athlete->zones(item->isRun)->getCP(zoneRange) : 0; W = zoneRange >= 0 ? item->context->athlete->zones(item->isRun)->getWprime(zoneRange) : 0; // did we override CP in metadata / metrics ? int oCP = item->getText("CP","0").toInt(); if (oCP) CP=oCP; int oW = item->getText("W'","0").toInt(); if (oW) W=oW; } double bestR=0; double bestW=0; bool first=true; // set to 1s recording RideFile *f = item->ride()->resample(1,0); for(double r=0.2; r<0.9; r += 0.001) { double wpbal=W; // loop through and count RideFileIterator it(item->ride(), spec); while (it.hasNext()) { struct RideFilePoint *point = it.next(); // iterate for this point wpbal += point->watts < CP ? (r * (W - wpbal)/W * (CP - point->watts)) : (CP-point->watts); if (point->secs > p->secs) break; } if (first || fabs(wpbal-double(500.0f))present.contains("P"); } RideMetric *clone() const { return new BestR(*this); } }; static bool addMetrics() { RideMetricFactory::instance().addMetric(BestR()); return true; } static bool added = addMetrics();