diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 0ea6635a..1f6a27ba 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1242,7 +1242,7 @@ def fetchcp(rower,theworkouts,table='cpdata'): def create_row_df(r,distance,duration,startdatetime,workouttype='rower', avghr=None,avgpwr=None,avgspm=None, rankingpiece = False, - duplicate=False, + duplicate=False,rpe=-1, title='Manual entry',notes='',weightcategory='hwt', adaptiveclass='None'): @@ -1351,6 +1351,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', workoutsource='unknown', notes='', totaldist=0, totaltime=0, rankingpiece=False, + rpe=-1, duplicate=False, summary='', makeprivate=False, @@ -1583,6 +1584,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', workoutsource=workoutsource, rankingpiece=rankingpiece, forceunit=forceunit, + rpe=rpe, csvfilename=f2, notes=notes, summary=summary, maxhr=maxhr, averagehr=averagehr, startdatetime=workoutstartdatetime, @@ -1810,6 +1812,7 @@ def new_workout_from_file(r, f2, workoutsource=None, title='Workout', boattype='1x', + rpe=-1, makeprivate=False, notes='', uploadoptions={'boattype':'1x','workouttype':'rower'}): @@ -1944,6 +1947,7 @@ def new_workout_from_file(r, f2, dosummary=dosummary, workoutsource=workoutsource, summary=summary, + rpe=rpe, inboard=inboard, oarlength=oarlength, title=title, forceunit='N', diff --git a/rowers/datautils.py b/rowers/datautils.py index 7f8c1af4..c19e9b6b 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -13,6 +13,20 @@ from rowers.mytypes import otwtypes,otetypes,rowtypes #p0 = [500,350,10,8000] p0 = [190,200,33,16000] +# RPE to TSS +rpetotss = { + 1:20, + 2:30, + 3:40, + 4:50, + 5:60, + 6:70, + 7:80, + 8:100, + 9:120, + 10:140, +} + def updatecp(delta,cpvalues,r,workouttype='water'): if workouttype in otwtypes: p0 = r.p0 diff --git a/rowers/forms.py b/rowers/forms.py index 26defb73..6d074114 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -245,17 +245,23 @@ class StandardsForm(forms.Form): # The form used for uploading files class DocumentsForm(forms.Form): + rpechoices = Workout.rpechoices + rpechoices = tuple([(-1,'---')]+list(rpechoices)) title = forms.CharField(required=False) file = forms.FileField(required=False, validators=[validate_file_extension]) workouttype = forms.ChoiceField(required=True, - choices=Workout.workouttypes) + choices=Workout.workouttypes, + label='Workout Type') boattype = forms.ChoiceField(required=True, choices=mytypes.boattypes, label = "Boat Type") + rpe = forms.ChoiceField(required=False, + choices=rpechoices, + label='Rate of Perceived Exertion',initial=-1) notes = forms.CharField(required=False, widget=forms.Textarea) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 376e6c43..14711cc9 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -97,7 +97,7 @@ import rowers.c2stuff as c2stuff from rowers.metrics import axes,axlabels,yaxminima,yaxmaxima,get_yaxminima,get_yaxmaxima from rowers.utils import lbstoN -from rowers.datautils import p0 +from rowers.datautils import p0,rpetotss import rowers.datautils as datautils from pandas.core.groupby.groupby import DataError @@ -1650,18 +1650,30 @@ def getfatigues( if metricchoice == 'rscore': factor = 2.0 - for i in range(nrdays): + for i in range(nrdays+1): date = startdate+datetime.timedelta(days=i) ws = Workout.objects.filter(user=user.rower,date=date,duplicate=False) weight = 0 for w in ws: - weight += factor*getattr(w,metricchoice) - if getattr(w,metricchoice) == 0: - if metricchoice == 'rscore' and w.hrtss != 0: + if getattr(w,metricchoice) > 0: + weight += factor*getattr(w,metricchoice) + if getattr(w,metricchoice) <= 0: + if metricchoice == 'rscore' and w.hrtss > 0: weight+= factor*w.hrtss - else: + elif metricchoice == 'rscore' and w.hrtss <= 0: trimp,hrtss = dataprep.workout_trimp(w) rscore,normp = dataprep.workout_rscore(w) + if w.rpe and w.rpe > 0: + dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second + dd = dd/3600 + weight += factor*rpetotss[w.rpe]*dd + elif metricchoice == 'trimp' and w.trimp <= 0: + trimp,hrtss = dataprep.workout_trimp(w) + rscore,normp = dataprep.workout_rscore(w) + if w.rpe and w.rpe > 0: + dd = 3600*w.duration.hour+60*w.duration.minute+w.duration.second + dd = dd/3600 + weight += 2*rpetotss[w.rpe]*dd impulses.append(weight) @@ -1680,6 +1692,7 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, metricchoice='trimp',doform=False,dofatigue=False): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + TOOLS2 = 'box_zoom,hover' fatigues = [] @@ -1713,7 +1726,9 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, 'impulse':impulses, }) - + endfitness = fitnesses[-1] + endfatigue = fatigues[-1] + endform = endfitness-endfatigue if modelchoice == 'banister': df['fatigue'] = k2*df['fatigue'] @@ -1777,18 +1792,18 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, fitlabel = 'PTE (fitness)' fatiguelabel = 'NTE (fatigue)' formlabel = 'Performance' - rightaxlabel = 'NTE' - if doform: - yaxlabel = 'PTE/Performance' + rightaxlabel = 'Performance' + if dofatigue: + yaxlabel = 'PTE/NTE' else: yaxlabel = 'PTE' else: fitlabel = 'Fitness' fatiguelabel = 'Fatigue' formlabel = 'Freshness' - rightaxlabel = 'Fatigue' - if doform: - yaxlabel = 'Fitness/Freshness' + rightaxlabel = 'Freshness' + if dofatigue: + yaxlabel = 'Fitness/Fatigue' else: yaxlabel = 'Fitness' @@ -1829,13 +1844,6 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, plot.legend.location = "top_left" - #plot.xaxis.formatter = DatetimeTickFormatter( - # days=["%d %B %Y"], - # months=["%d %B %Y"], - # years=["%d %B %Y"], - # ) - - #plot.xaxis.major_label_orientation = pi/4 plot.sizing_mode = 'scale_both' #plot.y_range = Range1d(0,1.5*max(df['testpower'])) @@ -1854,19 +1862,26 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, hover = plot.select(dict(type=HoverTool)) + linked_crosshair = CrosshairTool(dimensions='height') + hover.tooltips = OrderedDict([ #(legend_label,'@testpower'), ('Date','@fdate'), - (fitlabel,'@fitness'), - (fatiguelabel,'@fatigue'), - (formlabel,'@form') + (fitlabel,'@fitness{int}'), + (fatiguelabel,'@fatigue{int}'), + (formlabel,'@form{int}'), + ('Impulse','@impulse{int}') ]) - plot2 = Figure(tools=TOOLS,x_axis_type='datetime', + + + plot2 = Figure(tools=TOOLS2,x_axis_type='datetime', plot_width=900,plot_height=150, toolbar_location=None, toolbar_sticky=False) + + plot2.x_range = xrange plot2.y_range = Range1d(0,df['impulse'].max()) @@ -1876,6 +1891,9 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, plot2.yaxis.axis_label = 'Impulse' plot2.xaxis.axis_label = 'Date' + plot.add_tools(linked_crosshair) + plot2.add_tools(linked_crosshair) + layout = layoutcolumn([plot,plot2]) layout.sizing_mode = 'stretch_both' @@ -1892,7 +1910,7 @@ def performance_chart(user,startdate=None,enddate=None,kfitness=42,kfatigue=7, ) ) - return [script,div] + return [script,div,endfitness,endfatigue,endform] def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, diff --git a/rowers/models.py b/rowers/models.py index f38ccc04..8a6663a3 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -881,11 +881,17 @@ class Rower(models.Model): ep3 = models.FloatField(default=1.0,verbose_name="erg CP p4") ecpratio = models.FloatField(default=1.0,verbose_name="erg CP fit ratio") - cprange = models.IntegerField(default=42,verbose_name="Range for calculation breakthrough workouts and fitness (CP)", + cprange = models.IntegerField(default=42,verbose_name="Range for calculation of breakthrough workouts and fitness (CP)", choices=cppresets) otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack") + # performance manager stuff + kfit = models.IntegerField(default=42,verbose_name='Fitness Time Decay Constant (days)') + kfatigue = models.IntegerField(default=7,verbose_name='Fatigue Time Decay Constant (days)') + showfit = models.BooleanField(default=False) + showfresh = models.BooleanField(default=False) + pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power") pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power") pw_at = models.IntegerField(default=203,verbose_name="AT Power") @@ -2898,6 +2904,19 @@ class Workout(models.Model): privacychoices = mytypes.privacychoices adaptivetypes = mytypes.adaptivetypes boatbrands = mytypes.boatbrands + rpechoices = ( + (0,'Not Specified'), + (1,'1 Very Easy (a walk in the park)'), # 20 TSS / hour + (2,'2 Easy (You breathe normally, it feels comfortable)'), # 30 TSS / hour + (3,'3 Somewhat easy (You can talk easily but did you notice the beautiful clouds?)'), + (4,'4 Moderate (You can talk in short spurts, breathing more labored, this feels just right)'), # 50 TSS/hour + (5,"5 (It's not that painful, you just don't want to be here all day.)"), + (6,'6 Somewhat Hard (You can say a few words if you need to)'), # 70 TSS / hour + (7,'7 Vigorous (This is starting to get painful)'), + (8,"8 Hard (You can barely talk, breathing heavily, hoping you won't have to this that long)"), # 100 TSS / hour + (9,'9 Very Hard (My goodness, please make it stop)'), # 120 TSS / hour + (10,'10 Max Effort (You can barely remember your name, you would rather rip out your toenails than go through this)') # 140 TSS / hour + ) user = models.ForeignKey(Rower,on_delete=models.CASCADE) team = models.ManyToManyField(Team,blank=True) @@ -2925,12 +2944,18 @@ class Workout(models.Model): distance = models.IntegerField(default=0,blank=True) duration = models.TimeField(blank=True) dragfactor = models.IntegerField(default=0,blank=True) + + # scores trimp = models.IntegerField(default=-1,blank=True) rscore = models.IntegerField(default=-1,blank=True) hrtss = models.IntegerField(default=-1,blank=True) normp = models.IntegerField(default=-1,blank=True) normv = models.FloatField(default=-1,blank=True) normw = models.FloatField(default=-1,blank=True) + goldmedalstandard = models.FloatField(default=-1,blank=True,verbose_name='Gold Medal Standard') + rpe = models.IntegerField(default=0,blank=True,choices=rpechoices, + verbose_name='Rate of Perceived Exertion') + weightcategory = models.CharField( default="hwt", max_length=10, @@ -2958,7 +2983,6 @@ class Workout(models.Model): inboard = models.FloatField(default=0.88) oarlength = models.FloatField(default=2.89) - notes = models.CharField(blank=True,null=True,max_length=1000) summary = models.TextField(blank=True) privacy = models.CharField(default='visible',max_length=30, @@ -3541,6 +3565,7 @@ class WorkoutForm(ModelForm): 'dragfactor', 'weightcategory', 'adaptiveclass', + 'rpe', 'notes', 'rankingpiece', 'duplicate', @@ -3618,7 +3643,7 @@ class RowerPowerForm(ModelForm): class RowerCPForm(ModelForm): class Meta: model = Rower - fields = ['cprange'] + fields = ['cprange','kfit','kfatigue'] # Form to set rower's Power zones, including test routines # to enable consistency diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index 593b55ce..e5f11295 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -11,20 +11,29 @@ diff --git a/rowers/templates/menu_analytics.html b/rowers/templates/menu_analytics.html index 28cc9eb4..021950b7 100644 --- a/rowers/templates/menu_analytics.html +++ b/rowers/templates/menu_analytics.html @@ -26,6 +26,16 @@  Fitness