From a1723242953077a2a374b46acfdcb3ef7cbbf64f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 24 Nov 2020 12:50:51 +0100 Subject: [PATCH 1/4] adding trimp or rscore, time constants, test duration to form --- rowers/forms.py | 44 +++++++++++++++++++++++++ rowers/interactiveplots.py | 56 ++++++++++++++------------------ rowers/templates/fitnessfit.html | 15 +++++++-- rowers/views/analysisviews.py | 14 ++++++-- rowers/views/statements.py | 2 +- 5 files changed, 93 insertions(+), 38 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index eff14e8a..3fbc75fb 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -703,6 +703,50 @@ class FitnessMetricForm(forms.Form): class Meta: fields = ['startdate','enddate','mode'] +class FitnessFitForm(forms.Form): + startdate = forms.DateField( + initial=timezone.now()-datetime.timedelta(days=365), + # widget=SelectDateWidget(years=range(1990,2050)), + widget=AdminDateWidget(), + label='Start Date') + enddate = forms.DateField( + initial=timezone.now(), + widget=AdminDateWidget(), + label='End Date') + + modechoices = ( + ('rower','indoor rower'), + ('water','on the water') + ) + + metricchoices = ( + ('trimp','TRIMP'), + ('rscore','rScore') + ) + + fitnesstest = forms.IntegerField(required=True,initial=20, + label='Test Duration (minutes)') + + kfitness = forms.IntegerField(initial=42,required=True, + label='Fitness Time Constant (days)') + + kfatigue = forms.IntegerField(initial=7,required=True, + label='Fatigue Time Constant (days)') + + metricchoice = forms.ChoiceField(required=True, + choices=metricchoices, + initial='rscore', + label='Workload Metric') + + mode = forms.ChoiceField(required=True, + choices=modechoices, + initial='rower', + label='Workout Mode' + ) + + class Meta: + fields = ['startdate','enddate','mode','fitnesstest','kfitness','kfatigue','metricchoice'] + class SessionDateShiftForm(forms.Form): shiftstartdate = forms.DateField( initial=timezone.now(), diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 5923345d..52e56fd6 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1528,22 +1528,24 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): return [script,div,js_resources,css_resources] def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, - enddate=None,nrdays=42): + enddate=None,kfitness=42,kfatigue=7,fitnesstest=20): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' dates = [] - fourminpower = [] - hourpower = [] + testpower = [] workouts = workouts.order_by('date') data = [] + fitnesstestsecs = fitnesstest*60 + for w in workouts: cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=w.id) try: df = pd.read_parquet(cpfile) df['workout'] = w.id + df['workoutdate'] = w.date.strftime('%d-%m-%Y') data.append(df) except: pass @@ -1557,7 +1559,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, errfunc = lambda pars,x,y: fitfunc(pars,x)-y for w in workouts: - ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=nrdays), + ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=kfitness), date__lte=w.date)] powerdf = df[df['workout'].isin(ids)] @@ -1577,46 +1579,39 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, thesecs = powerdf['delta'].values theavpower = powerdf['cp'].values - if thesecs.min() < 240 and thesecs.max() > 240: - ww = griddata(thesecs,theavpower,np.arange(239,241,1),method='linear',rescale=True) - powerfourmin = ww[1] + if thesecs.min() < fitnesstestsecs and thesecs.max() > fitnesstestsecs: + ww = griddata(thesecs,theavpower,np.array([fitnesstestsecs]),method='linear',rescale=True) + powertest = ww[0] else: - powerfourmin = np.nan + powertest = np.nan + - if thesecs.min() < 3600 and thesecs.max() > 3600: - ww = griddata(thesecs,theavpower,np.arange(3599,3601,1),method='linear',rescale=True) - powerhour = ww[1] - else: - powerhour = np.nan dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) - fourminpower.append(powerfourmin) - hourpower.append(powerhour) + testpower.append(powertest) + df = pd.DataFrame({ 'date':dates, - 'fourminpower':fourminpower, - 'hourpower':hourpower, + 'testpower':testpower, }) df.sort_values(['date'],inplace=True) - df['hourdup'] = df['hourpower'].shift(1) - df['fourmindup'] = df['fourminpower'].shift(1) - df['hourpower'] = df.apply(lambda x: np.nan if abs(x['hourpower'] - x['hourdup']) < 4 \ - else x['hourpower'],axis=1) - df['fourminpower'] = df.apply(lambda x: np.nan if abs(x['fourminpower'] - x['fourmindup']) < 4 \ - else x['fourminpower'],axis=1) + df['testdup'] = df['testpower'].shift(1) + df['testpower'] = df.apply(lambda x: np.nan if abs(x['testpower'] - x['testdup']) < 4 \ + else x['testpower'],axis=1) + + source = ColumnDataSource( data = dict( - fourminpower = df['fourminpower'], - hourpower = df['hourpower'], + testpower = df['testpower'], date = df['date'], fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')) ) @@ -1656,10 +1651,8 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, y_range_name = "watermark", ) - plot.circle('date','fourminpower',source=source,fill_color='green',size=10,legend_label='4 min power') - plot.circle('date','hourpower',source=source,fill_color='blue',size=10,legend_label='60 min power') - - + plot.circle('date','testpower',source=source,fill_color='green',size=10, + legend_label='{fitnesstest} min power'.format(fitnesstest=fitnesstest)) plot.xaxis.axis_label = 'Date' plot.yaxis.axis_label = 'Power (W)' @@ -1673,7 +1666,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, plot.xaxis.major_label_orientation = pi/4 plot.sizing_mode = 'stretch_both' - plot.y_range = Range1d(0,1.5*max(fourminpower)) + plot.y_range = Range1d(0,1.5*max(testpower)) startdate = datetime.datetime.combine(startdate,datetime.datetime.min.time()) enddate = datetime.datetime.combine(enddate,datetime.datetime.min.time()) @@ -1685,8 +1678,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, hover = plot.select(dict(type=HoverTool)) hover.tooltips = OrderedDict([ - ('Power 4 minutes','@fourminpower'), - ('Power 1 hour','@hourpower'), + ('Power {fitnesstest} minutes'.format(fitnesstest=fitnesstest),'@testpower'), ('Date','@fdate') ]) diff --git a/rowers/templates/fitnessfit.html b/rowers/templates/fitnessfit.html index 84c1cb5f..8c15d925 100644 --- a/rowers/templates/fitnessfit.html +++ b/rowers/templates/fitnessfit.html @@ -72,11 +72,22 @@ {% if rower.user %} -

Power Progress for {{ rower.user.first_name }}

+

Fitness Progress for {{ rower.user.first_name }}

{% else %} -

Power Progress for {{ user.first_name }}

+

Fitness Progress for {{ user.first_name }}

{% endif %} +

+ This is an experimental page. Using the form below you can set + a date range to compare a prediction of your form (from TRIMP or rScore values + representing your workload). The Fitness Time Constant is the + time constant describing your fitness decline when you stop training. The + Fatigue Time Constant (shorter than fitness time constant) is the time + constant describing fatigue decline after a session. You have to select + a test duration that represents your fitness goal and is a duration for which + you regularly do maximal or submaximal efforts during your regular rowing workouts. +

+
  • diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index c976449b..0788b16a 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -1551,17 +1551,22 @@ def fitness_from_cp_view(request,userid=0,mode='rower', therower = getrequestrower(request,userid=userid) theuser = therower.user - + kfitness = 42 + kfatigue = 7 + fitnesstest = 20 if request.method == 'POST': - form = FitnessMetricForm(request.POST) + form = FitnessFitForm(request.POST) if form.is_valid(): startdate = form.cleaned_data['startdate'] enddate = form.cleaned_data['enddate'] mode = form.cleaned_data['mode'] + kfitness = form.cleaned_data['kfitness'] + kfatigue = form.cleaned_data['kfatigue'] + fitnesstest = form.cleaned_data['fitnesstest'] else: - form = FitnessMetricForm() + form = FitnessFitForm() workouts = Workout.objects.filter(user=therower,date__gte=startdate, date__lte=enddate, @@ -1576,6 +1581,9 @@ def fitness_from_cp_view(request,userid=0,mode='rower', workouts,theuser, workoutmode=mode,startdate=startdate, enddate=enddate, + kfitness=kfitness, + kfatigue=kfatigue, + fitnesstest=fitnesstest, ) breadcrumbs = [ diff --git a/rowers/views/statements.py b/rowers/views/statements.py index fc2cdb13..19f6fa1b 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -76,7 +76,7 @@ from rowers.forms import ( disqualifiers,SearchForm,BillingForm,PlanSelectForm, VideoAnalysisCreateForm,WorkoutSingleSelectForm, VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm, - StravaChartForm, + StravaChartForm,FitnessFitForm ) from django.urls import reverse, reverse_lazy From dfe395980c6a9d181fb91dc885d35ba519e224ee Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 24 Nov 2020 15:32:28 +0100 Subject: [PATCH 2/4] bug fix --- rowers/interactiveplots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 52e56fd6..97b2bded 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1551,7 +1551,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, pass if len(data) == 0: - return pd.Series(),pd.Series(),0,pd.Series() + return '','Insufficient data' if len(data)>1: df = pd.concat(data,axis=0) From 12078c4e52ee8d5488f0aa74af7966e4d9b74db2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 24 Nov 2020 17:43:36 +0100 Subject: [PATCH 3/4] adding manually calculated fitness and fatigue and form --- rowers/forms.py | 11 ++++++++- rowers/interactiveplots.py | 43 +++++++++++++++++++++++++++++++---- rowers/views/analysisviews.py | 13 +++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 3fbc75fb..ab8c261f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -738,6 +738,13 @@ class FitnessFitForm(forms.Form): initial='rscore', label='Workload Metric') + # temporary + k1 = forms.FloatField(required=True,initial=1.0, + label='k1') + k2 = forms.FloatField(required=True,initial=1.0, + label='k2') + p0 = forms.IntegerField(required=True,initial=100,label='Unfit Performance') + mode = forms.ChoiceField(required=True, choices=modechoices, initial='rower', @@ -745,7 +752,9 @@ class FitnessFitForm(forms.Form): ) class Meta: - fields = ['startdate','enddate','mode','fitnesstest','kfitness','kfatigue','metricchoice'] + fields = ['startdate','enddate','mode','fitnesstest', + 'kfitness','kfatigue','metricchoice', + 'k1','k2','p0'] class SessionDateShiftForm(forms.Form): shiftstartdate = forms.DateField( diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 97b2bded..525c09b8 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1528,18 +1528,23 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): return [script,div,js_resources,css_resources] def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, - enddate=None,kfitness=42,kfatigue=7,fitnesstest=20): + enddate=None,kfitness=42,kfatigue=7,fitnesstest=20, + metricchoice='rscore', + k1=1,k2=1,p0=100): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' dates = [] testpower = [] + fatigues = [] + fitnesses = [] workouts = workouts.order_by('date') data = [] fitnesstestsecs = fitnesstest*60 + # create CP data for w in workouts: cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=w.id) try: @@ -1559,6 +1564,7 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, errfunc = lambda pars,x,y: fitfunc(pars,x)-y for w in workouts: + # Create CP data point for date range ids = [w.id for w in workouts.filter(date__gte=w.date-datetime.timedelta(days=kfitness), date__lte=w.date)] @@ -1590,12 +1596,32 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) testpower.append(powertest) + # create Fitness and Fatigue number + fatigue = 0 + fitness = 0 + previousworkouts = workouts.filter(date__lte=w.date) + for ww in previousworkouts: + delta = (w.date-ww.date).days + weight = getattr(ww,metricchoice) + fatigue += weight*math.exp(-delta/kfatigue) + fitness += weight*math.exp(-delta/kfitness) + + fatigues.append(fatigue) + fitnesses.append(fitness) + + df = pd.DataFrame({ 'date':dates, 'testpower':testpower, + 'fatigue':fatigues, + 'fitness':fitnesses, }) + df['fatigue'] = k2*df['fatigue'] + df['fitness'] = p0+k1*df['fitness'] + df['form'] = df['fitness']-df['fatigue'] + df.sort_values(['date'],inplace=True) @@ -1606,14 +1632,14 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, - - - source = ColumnDataSource( data = dict( testpower = df['testpower'], date = df['date'], - fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')) + fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')), + fitness = df['fitness'], + fatigue = df['fatigue'], + form = df['form'], ) ) @@ -1654,6 +1680,13 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, plot.circle('date','testpower',source=source,fill_color='green',size=10, legend_label='{fitnesstest} min power'.format(fitnesstest=fitnesstest)) + plot.line('date','fitness',source=source,color='yellow', + legend_label='fitness') + plot.line('date','fatigue',source=source,color='red', + legend_label='fatigue') + plot.line('date','form',source=source,color='green', + legend_label='form') + plot.xaxis.axis_label = 'Date' plot.yaxis.axis_label = 'Power (W)' diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 0788b16a..6537fdc9 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -1555,6 +1555,12 @@ def fitness_from_cp_view(request,userid=0,mode='rower', kfitness = 42 kfatigue = 7 fitnesstest = 20 + metricchoice = 'rscore' + + # temp fit parameters + k1 = 1 + k2 = 1 + p0 = 100. if request.method == 'POST': form = FitnessFitForm(request.POST) @@ -1565,6 +1571,11 @@ def fitness_from_cp_view(request,userid=0,mode='rower', kfitness = form.cleaned_data['kfitness'] kfatigue = form.cleaned_data['kfatigue'] fitnesstest = form.cleaned_data['fitnesstest'] + metricchoice = form.cleaned_data['metricchoice'] + # temporary manual "fit" parameters + k1 = form.cleaned_data['k1'] + k2 = form.cleaned_data['k2'] + p0 = form.cleaned_data['p0'] else: form = FitnessFitForm() @@ -1584,6 +1595,8 @@ def fitness_from_cp_view(request,userid=0,mode='rower', kfitness=kfitness, kfatigue=kfatigue, fitnesstest=fitnesstest, + metricchoice=metricchoice, + k1=k1,k2=k2,p0=p0 ) breadcrumbs = [ From a7ccc1e408d9f4b49ba0e3d8dc5289ef50d9f9c4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 24 Nov 2020 17:46:06 +0100 Subject: [PATCH 4/4] legend position --- rowers/interactiveplots.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 525c09b8..6e614247 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1690,6 +1690,8 @@ def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, plot.xaxis.axis_label = 'Date' plot.yaxis.axis_label = 'Power (W)' + plot.legend.location = "top_left" + plot.xaxis.formatter = DatetimeTickFormatter( days=["%d %B %Y"], months=["%d %B %Y"],