diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 0cdc2251..ff689960 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -172,7 +172,7 @@ def workout_summary_to_df( id=w.id ) csv_links.append(csv_link) - trimps.append(workout_trimp(w)) + trimps.append(workout_trimp(w)[0]) rscore = workout_rscore(w) rscores.append(int(rscore[0])) @@ -2302,17 +2302,20 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, engine.dispose() return data + def workout_trimp(workout): r = workout.user + hrftp = (r.an+r.tr)/2. df,row = getrowdata_db(id=workout.id) df = clean_df_stats(df,workstrokesonly=False) if df.empty: df,row = getrowdata_db(id=workout.id) df = clean_df_stats(df,workstrokesonly=False) - trimp = calc_trimp(df,r.sex,r.max,r.rest) + trimp,hrtss = calc_trimp(df,r.sex,r.max,r.rest,hrftp) trimp = int(trimp) + hrtss = int(hrtss) - return trimp + return trimp,hrtss def workout_rscore(w): r = w.user diff --git a/rowers/forms.py b/rowers/forms.py index 9699aedc..df48585f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -352,6 +352,31 @@ class DateRangeForm(forms.Form): class Meta: fields = ['startdate','enddate'] +class FitnessMetricForm(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') + ) + + mode = forms.ChoiceField(required=True, + choices=modechoices, + initial='rower', + label='Workout Mode' + ) + + class Meta: + fields = ['startdate','enddate','mode'] + class SessionDateShiftForm(forms.Form): shiftstartdate = forms.DateField( initial=timezone.now(), diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index a1efd629..9e652900 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -57,6 +57,7 @@ thetimezone = get_current_timezone() from scipy.stats import linregress,percentileofscore from scipy import optimize from scipy.signal import savgol_filter +from scipy.interpolate import griddata import stravastuff @@ -628,7 +629,137 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): return [script,div,js_resources,css_resources] - +def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): + + power4min = [int(m.PowerFourMin) for m in fitnessmetrics] + power2k = [int(m.PowerTwoK) for m in fitnessmetrics] + power1hr = [int(m.PowerOneHour) for m in fitnessmetrics] + dates = [m.date for m in fitnessmetrics] + mode = [m.workoutmode for m in fitnessmetrics] + + df = pd.DataFrame( + {'power4min':power4min, + 'power2k':power2k, + 'power1hr':power1hr, + 'date':dates, + 'dates':dates, + 'mode':mode + }) + + + delta = df['power4min'].astype('int').diff() + + mask = delta == 0 + + df.loc[mask,'power4min'] = np.nan + df.dropna(inplace=True,axis=0,how='any') + + + df = df[df['power2k']>0] + df = df[df['mode']==workoutmode] + + groups = df.groupby(by='date').max() + + power4min = groups['power4min'] + date = groups['dates'] + power2k = groups['power2k'] + power1hr = groups['power1hr'] + + source = ColumnDataSource( + data = dict( + power4min = power4min, + power2k = power2k, + date = date, + power1hr = power1hr, + fdate=groups['dates'].map(lambda x: x.strftime('%d-%m-%Y')) ) + ) + + + # fit + + resampled = groups.set_index('dates') + resampled.index = pd.to_datetime(resampled.index) + resampled = resampled.resample('D').interpolate( + method='linear',order=2) + + power4min = resampled['power4min'] + date = resampled.index.values + power2k = resampled['power2k'] + power1hr = resampled['power1hr'] + + source2 = ColumnDataSource( + data = dict( + power4min = power4min, + power2k = power2k, + date = date, + power1hr = power1hr, + fdate=resampled.index.map(lambda x: x.strftime('%d-%m-%Y')) ) + ) + + + + + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' + + plot = Figure(tools=TOOLS,toolbar_location="above", + toolbar_sticky=False,width=900, + x_axis_type='datetime') + + # add watermark + plot.extra_y_ranges = {"watermark": watermarkrange} + plot.extra_x_ranges = {"watermark": watermarkrange} + + plot.image_url([watermarkurl],watermarkx,watermarky, + watermarkw,watermarkh, + global_alpha=watermarkalpha, + w_units='screen', + h_units='screen', + anchor=watermarkanchor, + dilate=True, + x_range_name = "watermark", + y_range_name = "watermark", + ) + + plot.circle('date','power2k',source=source,fill_color='red',size=10, + legend='2k power') + + + plot.circle('date','power1hr',source=source,fill_color='blue',size=10, + legend='1 hr power') + + plot.circle('date','power4min',source=source,fill_color='green',size=10, + legend='4 min power') + + plot.line('date','power4min',source=source2,color='green') + plot.line('date','power2k',source=source2,color='red') + plot.line('date','power1hr',source=source2,color='blue') + + plot.xaxis.axis_label = 'Date' + plot.yaxis.axis_label = 'Power (W)' + + plot.xaxis.formatter = DatetimeTickFormatter( + days=["%d %B %Y"], + months=["%d %B %Y"], + years=["%d %B %Y"], + ) + + plot.xaxis.major_label_orientation = pi/4 + + plot.y_range = Range1d(0,1.5*max(power4min)) + plot.title.text = 'Power levels from workouts '+user.first_name + + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Power 4 minutes','@power4min'), + ('Power 2000 m','@power2k'), + ('Power 1 hour','@power1hr'), + ('Date','@fdate'), + ]) + + script,div = components(plot) + + return [script,div] def interactive_histoall(theworkouts): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' diff --git a/rowers/metrics.py b/rowers/metrics.py index 320b21bf..2de011a6 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -334,7 +334,7 @@ This value should be fairly constant across all stroke rates.""", ) -def calc_trimp(df,sex,hrmax,hrmin): +def calc_trimp(df,sex,hrmax,hrmin,hrftp): if sex == 'male': f = 1.92 else: @@ -343,10 +343,14 @@ def calc_trimp(df,sex,hrmax,hrmin): dt = df['time'].diff()/6.e4 hrr = (df['hr']-hrmin)/(hrmax-hrmin) + hrrftp = (hrftp-hrmin)/(hrmax-hrmin) + trimp1hr = 60*hrrftp*0.64*np.exp(f*hrrftp) trimpdata = dt*hrr*0.64*np.exp(f*hrr) trimp = trimpdata.sum() - return trimp + hrtss = 100*trimp/trimp1hr + + return trimp,hrtss def getagegrouprecord(age,sex='male',weightcategory='hwt', diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 6a91f51d..da26d0a5 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -111,8 +111,13 @@ def get_session_metrics(ps): for w in ws: distancev += w.distance durationv += timefield_to_seconds_duration(w.duration) - trimpv += dataprep.workout_trimp(w) - rscorev += dataprep.workout_rscore(w)[0] + thetrimp,hrtss = dataprep.workout_trimp(w) + trimpv += thetrimp + tss = dataprep.workout_rscore(w)[0] + if not np.isnan(tss) and tss != 0: + rscorev += tss + elif tss == 0: + rscorev += hrtss ratio,statusv,completiondate = is_session_complete_ws(ws,ps) try: @@ -181,11 +186,16 @@ def is_session_complete_ws(ws,ps): durationseconds = timefield_to_seconds_duration(w.duration) score += durationseconds elif ps.sessionmode == 'TRIMP': - trimp = dataprep.workout_trimp(w) + trimp,hrtss = dataprep.workout_trimp(w) score += trimp elif ps.sessionmode == 'rScore': rscore = dataprep.workout_rscore(w)[0] - score += rscore + if not np.isnan(rscore) and rscore != 0: + score += rscore + elif rscore == 0: + trimp,hrtss = dataprep.workout_trimp(w) + score += hrtss + if not completiondate and score>=cratiomin*value: completiondate = w.date diff --git a/rowers/tasks.py b/rowers/tasks.py index c5625658..08f48c49 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -743,26 +743,7 @@ def handle_updateergcp(rower_id,workoutfilenames,debug=False,**kwargs): return 1 -@app.task -def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, - **kwargs): - - powerfourmin = -1 - power2k = -1 - powerhour = -1 - - mdict = { - 'user_id': user_id, - 'PowerFourMin': powerfourmin, - 'PowerTwoK': power2k, - 'PowerOneHour': powerhour, - 'workoutmode': mode, - 'last_workout': max(workoutids), - 'date': timezone.now().strftime('%Y-%m-%d'), - } - - result = fitnessmetric_to_sql(mdict,debug=debug,doclean=False) - +def cp_from_workoutids(workoutids,debug=False): columns = ['power','workoutid','time'] df = getsmallrowdata_db(columns,ids=workoutids,debug=debug) df.dropna(inplace=True,axis=0) @@ -821,6 +802,30 @@ def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, power2k = fitfunc(p1,t3) + return powerfourmin,power2k,powerhour + + +@app.task +def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, + **kwargs): + + powerfourmin = -1 + power2k = -1 + powerhour = -1 + + mdict = { + 'user_id': user_id, + 'PowerFourMin': powerfourmin, + 'PowerTwoK': power2k, + 'PowerOneHour': powerhour, + 'workoutmode': mode, + 'last_workout': max(workoutids), + 'date': timezone.now().strftime('%Y-%m-%d'), + } + + result = fitnessmetric_to_sql(mdict,debug=debug,doclean=False) + + powerfourmin,power2k,powerhour = cp_from_workoutids(workoutids,debug=debug) mdict = { 'user_id': user_id, diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index da1726ee..f64f716b 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -126,7 +126,7 @@
{% if user|is_promember %} OTE Critical Power @@ -138,6 +138,19 @@ Analyse power vs piece duration to make predictions, for erg pieces.