diff --git a/rowers/forms.py b/rowers/forms.py index eff14e8a..ab8c261f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -703,6 +703,59 @@ 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') + + # 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', + label='Workout Mode' + ) + + class Meta: + fields = ['startdate','enddate','mode','fitnesstest', + 'kfitness','kfatigue','metricchoice', + 'k1','k2','p0'] + class SessionDateShiftForm(forms.Form): shiftstartdate = forms.DateField( initial=timezone.now(), diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 5923345d..6e614247 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1528,28 +1528,35 @@ 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, + metricchoice='rscore', + k1=1,k2=1,p0=100): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' dates = [] - fourminpower = [] - hourpower = [] + 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: df = pd.read_parquet(cpfile) df['workout'] = w.id + df['workoutdate'] = w.date.strftime('%d-%m-%Y') data.append(df) except: 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) @@ -1557,7 +1564,8 @@ 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), + # 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)] powerdf = df[df['workout'].isin(ids)] @@ -1577,48 +1585,61 @@ 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) + + # 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, - 'fourminpower':fourminpower, - 'hourpower':hourpower, + '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) - 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')) + fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')), + fitness = df['fitness'], + fatigue = df['fatigue'], + form = df['form'], ) ) @@ -1656,14 +1677,21 @@ 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.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)' + plot.legend.location = "top_left" + plot.xaxis.formatter = DatetimeTickFormatter( days=["%d %B %Y"], months=["%d %B %Y"], @@ -1673,7 +1701,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 +1713,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 %} -
+ 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. +
+