Files
GoldenCheetah/src/MUPlot.cpp
2015-06-07 16:31:56 +01:00

642 lines
18 KiB
C++

/*
* Copyright (c) 2014 Mark Liversedge (liversedge@gmail.com)
* Copyright (c) 2014 Andy Froncioni
*
* 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 "MUPlot.h"
#include "MUWidget.h"
#include "Context.h"
#include "Athlete.h"
#include "Zones.h"
#include "Colors.h"
#include <unistd.h>
#include <QDebug>
#include <qwt_series_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_marker.h>
#include <qwt_symbol.h>
#include <qwt_scale_engine.h>
#include <qwt_scale_widget.h>
#include <qwt_color_map.h>
#include <algorithm> // for std::lower_bound
#include "CriticalPowerWindow.h"
#include "CPPlot.h"
#include "GcOverlayWidget.h"
#include "Settings.h"
#include "TimeUtils.h"
double PI = 3.14159265359f;
MUPlot::MUPlot(MUWidget *muw, CriticalPowerWindow *parent, Context *context)
: QwtPlot(parent), QwtSyntheticPointData(MUN),
context(context), muw(muw), parent(parent), modelCurve(NULL), slowCurve(NULL), fastCurve(NULL), mmpCurve(NULL)
{
// initalise all the model stufff
// probably abstract this out later
muSet = NULL;
slowHandle = NULL;
slowLine = NULL;
slowDrag = false;
fastHandle = NULL;
fastLine = NULL;
fastDrag = false;
setAutoDelete(false);
setAutoFillBackground(true);
static_cast<QwtPlotCanvas*>(canvas())->setFrameStyle(QFrame::NoFrame);
// left yAxis scale prettify
QwtScaleDraw *sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
sd->enableComponent(QwtScaleDraw::Ticks, false);
sd->enableComponent(QwtScaleDraw::Backbone, false);
setAxisScaleDraw(yLeft, sd);
setAxisTitle(yLeft, tr("w(x)"));
setAxisMaxMinor(yLeft, 0);
plotLayout()->setAlignCanvasToScales(true);
// 0.5 increments on bottom axis
QwtValueList ytick[QwtScaleDiv::NTickTypes];
for (double i=0.0f; i<=10.0f; i+= 1.0)
ytick[QwtScaleDiv::MajorTick]<<i;
setAxisScaleDiv(yLeft,QwtScaleDiv(0.0f,10.0f,ytick));
// bottom xAxis scale prettify
sd = new QwtScaleDraw;
sd->setTickLength(QwtScaleDiv::MajorTick, 3);
sd->enableComponent(QwtScaleDraw::Ticks, false);
sd->enableComponent(QwtScaleDraw::Backbone, false);
setAxisScaleDraw(xBottom, sd);
setAxisTitle(xBottom, tr("Motor Unit, x"));
setAxisMaxMinor(xBottom, 0);
// 0.2 increments on bottom axis
QwtValueList xtick[QwtScaleDiv::NTickTypes];
for (double i=0.0f; i<=1.0f; i+= 0.2)
xtick[QwtScaleDiv::MajorTick]<<i;
setAxisScaleDiv(xBottom,QwtScaleDiv(0.0f,1.0f,xtick));
// now color everything we created
configChanged(CONFIG_APPEARANCE);
// set to a 2 Normal Model
setModel(2);
// set mouse tracker
parent->setMouseTracking(true);
installEventFilter(parent);
new muMouseTracker(this);
}
// set colours mostly
void
MUPlot::configChanged(qint32)
{
QPalette palette;
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
setPalette(palette);
axisWidget(QwtPlot::xBottom)->setPalette(palette);
axisWidget(QwtPlot::yLeft)->setPalette(palette);
setCanvasBackground(GColor(CPLOTBACKGROUND));
}
// get the fonts and colors right for the axis scales
void
MUPlot::setAxisTitle(int axis, QString label)
{
// setup the default fonts
QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());
QwtText title(label);
title.setColor(GColor(CPLOTMARKER));
title.setFont(stGiles);
QwtPlot::setAxisFont(axis, stGiles);
QwtPlot::setAxisTitle(axis, title);
}
double
MUPlot::y(double x) const
{
switch(model) {
case 0 : return 0;
case 1 : return slowNormal->y(x);
default:
case 2 : return slowNormal->y(x) + fastNormal->y(x); // additive
}
}
double
MUPlot::totalY()
{
double tot = 0;
// sum all points -- only really needed for 2 normal model
for (int x=0; x<MUN; x++) {
tot += y(double(x)/MUN);
}
return tot;
}
// We are going to support multiple models; normal, normal-skew, linear
// So for now lets make it a parameter for the model curve creation
// The model may end up being comprised of 'sub models' for each fibre type
void
MUPlot::setModel(int model)
{
this->model = model;
// first lets clear any curves we shouldn't be displaying
// no model curve if not power !
if (modelCurve) {
modelCurve->detach();
delete modelCurve;
modelCurve = NULL;
}
if (slowCurve) {
slowCurve->detach();
delete slowCurve;
slowCurve = NULL;
}
if (slowHandle) {
slowHandle->detach();
delete slowHandle;
slowHandle = NULL;
}
if (slowLine) {
slowLine->detach();
delete slowLine;
slowLine = NULL;
}
if (fastCurve) {
fastCurve->detach();
delete fastCurve;
fastCurve = NULL;
}
if (fastHandle) {
fastHandle->detach();
delete fastHandle;
fastHandle = NULL;
}
if (fastLine) {
fastLine->detach();
delete fastLine;
fastLine = NULL;
}
if (mmpCurve) {
mmpCurve->detach();
delete mmpCurve;
mmpCurve = NULL;
}
switch (model) {
case 0 : // no model - do nothing
{
}
break;
case 2 : // 2 Normal distribution -- drops through to case 1 below
{
fastCurve = new QwtPlotCurve( "MU Distribution" );
fastCurve->setData(fastNormal = new MUNormal(MU_FASTMEAN, 0.05f));
fastCurve->attach(this);
QColor handleColor = QColor(Qt::magenta).darker(30); // customise (?)
handleColor.setAlpha(64);
// now a mean line
fastLine = new QwtPlotMarker();
fastLine->setLineStyle(QwtPlotMarker::VLine);
fastLine->setLinePen(QPen(QColor(handleColor), 0, Qt::SolidLine));
fastLine->setValue(MU_FASTMEAN, 0); // mean
fastLine->setZ(50);
fastLine->attach(this);
// now add a handle
QColor color = GColor(CPLOTBACKGROUND);
double x = MU_FASTMEAN; // mean is ok
double y = 0.05f * 40.0f; // variance scaled to w(x)
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Rect);
sym->setSize(8);
sym->setPen(QPen(handleColor)); // customise ?
sym->setBrush(QBrush(color));
fastHandle = new QwtPlotMarker();
fastHandle->setValue(x, y);
fastHandle->setYAxis(yLeft);
fastHandle->setZ(100);
fastHandle->setSymbol(sym);
fastHandle->attach(this);
}
case 1 : // Normal distribution
{
slowCurve = new QwtPlotCurve( "MU Distribution" );
slowCurve->setData(slowNormal = new MUNormal(MU_SLOWMEAN, 0.05f));
slowCurve->attach(this);
QColor handleColor = GColor(CCP).darker(30);
handleColor.setAlpha(64);
// now a mean line
slowLine = new QwtPlotMarker();
slowLine->setLineStyle(QwtPlotMarker::VLine);
slowLine->setLinePen(QPen(QColor(handleColor), 0, Qt::SolidLine));
slowLine->setValue(MU_SLOWMEAN, 0); // mean
slowLine->setZ(50);
slowLine->attach(this);
// now add a handle
QColor color = GColor(CPLOTBACKGROUND);
double x = MU_SLOWMEAN; // mean is ok
double y = 0.05f * 40.0f; // variance scaled to w(x)
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Rect);
sym->setSize(8);
sym->setPen(QPen(handleColor)); // customise ?
sym->setBrush(QBrush(color));
slowHandle = new QwtPlotMarker();
slowHandle->setValue(x, y);
slowHandle->setYAxis(yLeft);
slowHandle->setZ(100);
slowHandle->setSymbol(sym);
slowHandle->attach(this);
// mmp curve
mmpCurve = new QwtPlotCurve( "MU Distribution" );
mmpCurve->attach(parent->cpPlot);
}
break;
}
// set the colors etc for our curve(s)
if (fastCurve) {
// antialias
bool antialias = appsettings->value(this, GC_ANTIALIAS, false).toBool();
if (antialias) fastCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
// color and brush
QColor color = QColor(Qt::magenta); //XXX customise
QPen pen(color);
pen.setWidth(1.0);
color.setAlpha(80);
QBrush brush(color);
fastCurve->setPen(pen);
fastCurve->setBrush(brush);
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setPen(QPen(Qt::yellow)); // customise ?
sym->setBrush(QBrush(Qt::yellow));
}
if (slowCurve) {
// antialias
bool antialias = appsettings->value(this, GC_ANTIALIAS, false).toBool();
if (antialias) slowCurve->setRenderHint(QwtPlotItem::RenderAntialiased);
// color and brush
QColor color = GColor(CCP); // customise ?
QPen pen(color);
pen.setWidth(1.0);
color.setAlpha(80);
QBrush brush(color);
slowCurve->setPen(pen);
slowCurve->setBrush(brush);
QwtSymbol *sym = new QwtSymbol;
sym->setStyle(QwtSymbol::Ellipse);
sym->setSize(4);
sym->setPen(QPen(Qt::yellow)); // customise ?
sym->setBrush(QBrush(Qt::yellow));
mmpCurve->setPen(QPen(Qt::yellow));
mmpCurve->setSymbol(sym);
mmpCurve->setBrush(Qt::NoBrush);
// and refresh the MMP
setMUSet();
}
if (model) {
modelCurve = new QwtPlotCurve( "Total Distribution" );
modelCurve->setData(this); // we provide the data too!
QColor color = QColor(Qt::yellow); //XXX customise
QPen pen(color);
pen.setWidth(1.0);
color.setAlpha(80);
modelCurve->setPen(pen);
modelCurve->setBrush(Qt::NoBrush);
modelCurve->attach(this);
}
replot();
}
bool
MUPlot::eventFilter(QObject *, QEvent *e)
{
switch (e->type()) {
case QEvent::MouseButtonPress:
{
if (!slowDrag && !fastDrag) {
// where is the mouse ?
QPoint pos = QCursor::pos();
// slow handle ?
QPoint mpos = canvas()->mapToGlobal(QPoint(transform(xBottom, slowHandle->value().x()),
transform(yLeft, slowHandle->value().y())));
int dx = mpos.x() - pos.x();
int dy = mpos.y() - pos.y();
if ((dx > -6 && dx < 6) && (dy > -6 && dy < 6)) {
// close enough
slowDrag = true;
return false;
}
// fast handle ?
mpos = canvas()->mapToGlobal(QPoint(transform(xBottom, fastHandle->value().x()),
transform(yLeft, fastHandle->value().y())));
dx = mpos.x() - pos.x();
dy = mpos.y() - pos.y();
if ((dx > -6 && dx < 6) && (dy > -6 && dy < 6)) {
// close enough
fastDrag = true;
return false;
}
}
}
break;
case QEvent::MouseButtonRelease:
{
if (fastDrag) {
fastDrag = false;
}
if (slowDrag) {
slowDrag = false;
}
}
break;
default:
break;
}
return false;
}
void
MUPlot::mouseMoved()
{
if ((!slowDrag && !fastDrag)) return;
// where is the mouse ?
QPoint pos = QCursor::pos();
// cursor position
QPoint cpos = canvas()->mapFromGlobal(pos);
double mean = invTransform(xBottom,cpos.x());
double variance = invTransform(yLeft, cpos.y()) / 40.0f;
// dragging slow
if (fastDrag) {
fastNormal->setMean(mean);
fastNormal->setVariance(variance);
// move the handle
fastHandle->setValue(mean, variance * 40.0f);
// move the line
fastLine->setValue(mean, 0);
}
// dragging slow
if (slowDrag) {
slowNormal->setMean(mean);
slowNormal->setVariance(variance);
// move the handle
slowHandle->setValue(mean, variance * 40.0f);
// move the line
slowLine->setValue(mean, 0);
}
// redo MMP
setMUSet();
replot();
}
void
MUPlot::setMUSet()
{
// set the MU distribution, starting conditions
// prior to determining the MMP etc from those
// starting conditions.
// this is a TEMPLATE and is used in assignment and never
// changed -- this is to speed up creation of worker threads
// all using the same starting conditions
if (muSet == NULL) {
// allocate the array
muSet = new QVector<MUPool>(MUN); // MUN is usually set to 1000
}
// we do this every time as the parameters are editable
// by the user, so they may change from the UI
double totaly = totalY(); // add up distribution
for (int x=0; x<MUN; x++) {
(*muSet)[x].twitch = double(x) / double(MUN);
(*muSet)[x].pmax = muw->pmax0->value() + ((muw->pmax1->value() - muw->pmax0->value()) * (double(x) / double(MUN)));
(*muSet)[x].pmin = muw->pmin0->value() + ((muw->pmin1->value() - muw->pmin0->value()) * (double(x) / double(MUN)));
(*muSet)[x].tau = muw->tau0->value() + ((muw->tau1->value() - muw->tau0->value()) * (double(x) / double(MUN)));
(*muSet)[x].alpha = muw->alpha->value();
// get mass from the model
(*muSet)[x].mass = y(double(x)/double(MUN)) / totaly * muw->mass->value();
}
// now refresh MMP Curve using the new MU set
QVector<double> xseries;
QVector<double> yseries;
// for now we use the same worker in serial
// we might reiplement this as an OpenCL kernel or similar
// to perform in parallel for performance
MUSet worker(muSet);
xseries << 1.0/60.00f;
yseries << worker.pMax;
int inc = (worker.pMax-worker.pMin) / MUSAMPLES;
for (int i=worker.pMax - inc; i > worker.pMin; i -= inc) {
double duration = worker.applyLoad(i);
if (duration) {
xseries << double(duration) / 60.0f;
yseries << double(i);
}
}
xseries << double(3 * 3600)/60.00f;
yseries << worker.pMin;
mmpCurve->setSamples(xseries, yseries);
parent->cpPlot->replot();
}
//
// Working with a set of motor units
//
MUSet::MUSet(QVector<MUPool>* set) : muSet(*set)
{
setMinMax();
}
// reset the pool
void
MUSet::reset(QVector<MUPool>* set)
{
muSet = *set;
setMinMax();
}
void
MUSet::setMinMax()
{
pMax = 0; // whats the max power this distribution can muster?
pMin = 0; // whats the max power this distribution can muster?
for (int x=0; x<muSet.size(); x++) {
pMax += muSet[x].mass * muSet[x].pmax;
pMin += muSet[x].mass * muSet[x].pmin;
}
}
// apply load and return maximum duration
// this load can be applied
int
MUSet::applyLoad(int watts)
{
// lets not waste our time with out of bound questions
if (watts > pMax || watts < pMin) return 0;
// initialise starting conditions
QVector<double> pMu(MUN);
QVector<double> act(MUN);
for(int x=0; x<MUN; x++) {
pMu[x] = muSet[x].pmax;
act[x] = -1;
}
int duration = 0;
while (duration < (3 * 3600)) { // anything more than 3 hours is too long dude
// lets get a second of that power
double ptot = 0; // how much power did we generate ?
int x = 0; // how far across the pool did we get ?
// lets recruite some fibers!
while (ptot < watts && x < MUN) {
// is this bin fired yet ?
if (act[x] < 0) { // first time!
act[x] = duration; // we just fired it!
}
// how long since we fired this sucker ?
int wasfired = duration - act[x];
// if we've passed tau seconds we need to apply some fatigue
if (wasfired > muSet[x].tau) {
// apply fatigue to pmax
pMu[x] = (pMu[x] - muSet[x].pmin) * exp(-1.0 * muSet[x].alpha * (double(x)/MUN) * (wasfired - muSet[x].tau)) + muSet[x].pmin;
}
// take the power
ptot += muSet[x].mass * pMu[x];
// next bin
x ++;
}
// we exhausted fibers
if (x == MUN) return duration; // we're cooked
// well we managed to hang in there for another second !
duration++;
}
// too long .. return 3 hours
return duration;
}