From 03f68aa72e78213427e2c17bc9e4998b6a93ddae Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Tue, 4 Feb 2025 20:00:44 +0100
Subject: [PATCH] adding critical stroke rate chart
---
rowers/dataprep.py | 32 +++--
rowers/datautils.py | 5 +-
rowers/forms.py | 1 +
rowers/interactiveplots.py | 69 +++++++++
rowers/models.py | 12 ++
rowers/rowing_workout_metrics_pb2.py | 8 +-
rowers/templates/otwcr.html | 20 +++
rowers/templates/user_analysis_select.html | 154 +++++++++++++--------
rowers/views/analysisviews.py | 139 ++++++++++++++++++-
9 files changed, 364 insertions(+), 76 deletions(-)
create mode 100644 rowers/templates/otwcr.html
diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index b7043a7f..e055f8ef 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -553,7 +553,10 @@ def setcp(workout, background=False, recurrance=True):
# check dts
tarr = datautils.getlogarr(4000)
if df['delta'][0] in tarr:
- return(df, df['delta'], df['cp'])
+ try:
+ return(df, df['delta'], df['cp'], df['cr'])
+ except KeyError:
+ return(df, df['delta'], df['cp'], 0*df['cp'])
except Exception as e:
try:
os.remove(filename)
@@ -565,7 +568,7 @@ def setcp(workout, background=False, recurrance=True):
strokesdf = remove_nulls_pl(strokesdf)
if strokesdf.is_empty():
- return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
totaltime = strokesdf['time'].max()
maxt = totaltime/1000.
@@ -580,7 +583,7 @@ def setcp(workout, background=False, recurrance=True):
elif os.path.exists(csvfilename+'.gz'): # pragma: no cover
csvfile = csvfilename+'.gz'
else: # pragma: no cover
- return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
csvfile = os.path.abspath(csvfile)
with grpc.insecure_channel(
@@ -593,7 +596,7 @@ def setcp(workout, background=False, recurrance=True):
grpc.channel_ready_future(channel).result(timeout=10)
except grpc.FutureTimeoutError: # pragma: no cover
dologging('metrics.log','grpc channel time out in setcp')
- return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.DataFrame({'delta': [], 'cp': [], 'cr':[]}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
stub = metrics_pb2_grpc.MetricsStub(channel)
req = metrics_pb2.CPRequest(filename = csvfile, filetype = "CSV", tarr = logarr)
@@ -602,16 +605,18 @@ def setcp(workout, background=False, recurrance=True):
response = stub.GetCP(req, timeout=60)
except Exception as e:
dologging('metrics.log', traceback.format_exc())
- return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
delta = pl.Series(np.array(response.delta))
cpvalues = pl.Series(np.array(response.power))
powermean = response.avgpower
+ spmvalues = pl.Series(np.array(response.spm))
try:
df = pl.DataFrame({
'delta': delta,
'cp': cpvalues,
+ 'cr': spmvalues,
'id': workout.id,
})
@@ -620,7 +625,7 @@ def setcp(workout, background=False, recurrance=True):
except Exception as e:
dologging("metrics.log", "setcp: "+ str(e))
- return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
#df.to_parquet(filename, engine='fastparquet', compression='GZIP')
@@ -631,7 +636,7 @@ def setcp(workout, background=False, recurrance=True):
workout.goldmedalduration = goldmedalduration
workout.save()
- return df, delta, cpvalues
+ return df, delta, cpvalues, spmvalues # pragma: no cover
@@ -772,7 +777,7 @@ def fetchcp_new(rower, workouts):
data = []
for workout in workouts:
- df, delta, cpvalues = setcp(workout)
+ df, delta, cpvalues, cpvalues_spm = setcp(workout)
df = df.drop('id')
df = df.with_columns((pl.lit(str(workout))).alias("workout"))
df = df.with_columns((pl.lit(workout.url())).alias("url"))
@@ -780,7 +785,7 @@ def fetchcp_new(rower, workouts):
data.append(df)
if len(data) == 0:
- return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
if len(data) > 1:
df = pl.concat(data)
@@ -791,11 +796,16 @@ def fetchcp_new(rower, workouts):
pl.all().sort_by('cp').last(),
])
except (KeyError, ColumnNotFoundError): # pragma: no cover
- return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
+ return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64)
df = df.filter(pl.col("cp")>20)
- return df['delta'], df['cp'], 0, df['workout'], df['url']
+ try:
+ testje = df['cr']
+ except KeyError:
+ return df['delta'], df['cp'], 0*df['cp'], 0, df['workout'], df['url']
+
+ return df['delta'], df['cp'], df['cr'], 0, df['workout'], df['url']
diff --git a/rowers/datautils.py b/rowers/datautils.py
index 0b24d252..2eeac953 100644
--- a/rowers/datautils.py
+++ b/rowers/datautils.py
@@ -84,7 +84,10 @@ def cpfit(powerdf, fraclimit=0.0001, nmax=1000):
p1 = p0
thesecs = powerdf['Delta'].to_numpy()
- theavpower = powerdf['CP'].to_numpy()
+ try:
+ theavpower = powerdf['CP'].to_numpy()
+ except: # pragma: no cover
+ theavpower = powerdf['CR'].to_numpy()
if len(thesecs) >= 4:
diff --git a/rowers/forms.py b/rowers/forms.py
index face5583..a6a1c9d7 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -1299,6 +1299,7 @@ analysischoices = (
('stats', 'Statistics'),
('compare', 'Compare'),
('cp', 'CP chart'),
+ ('cr', 'CR chart'),
)
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 2f0d2a5d..ae3a25ae 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -1113,6 +1113,75 @@ def interactive_otwcpchart(powerdf, promember=0, rowername="", r=None,
return [script, div, p1, ratio, message]
+def interactive_otwcrchart(powerdf, promember=0, rowername="", r=None,
+ cpfit='data',
+ title='', type='water'):
+
+ powerdf2 = powerdf.filter((pl.col("Delta") > 0) & (pl.col("CR") > 0))
+
+ # plot tools
+ if (promember == 1): # pragma: no cover
+ TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair'
+ else:
+ TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair'
+
+ x_axis_type = 'log'
+
+ deltas = powerdf2['Delta'].apply(lambda x: timedeltaconv(x))
+ powerdf2 = powerdf2.with_columns(
+ ftime = deltas.apply(lambda x: strfdelta(x)),
+ Deltaminutes = pl.col("Delta")/60.
+ )
+
+ # there is no Paul's law for OTW
+
+ thesecs = powerdf2['Delta']
+ theavpower = powerdf2['CR']
+
+ p1, fitt, fitpower, ratio = datautils.cpfit(powerdf2)
+ if cpfit == 'automatic' and r is not None:
+ if type == 'water':
+ p1 = [r.r0, r.r1, r.r2, r.r3]
+ ratio = r.cpratio
+ elif type == 'erg': # pragma: no cover
+ p1 = [r.er0, r.er1, r.er2, r.er3]
+ ratio = r.ecrratio
+
+ def fitfunc(pars, x):
+ return abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3])))
+
+ fitpower = fitfunc(p1, fitt)
+
+ message = ""
+ # if len(fitpower[fitpower<0]) > 0:
+ # message = "CP model fit didn't give correct results"
+
+ deltas = fitt.apply(lambda x: timedeltaconv(x))
+ ftime = niceformat(deltas)
+
+
+
+ fit_data = pl.DataFrame(dict(
+ CR=fitpower,
+ CRmax=ratio*fitpower,
+ duration=fitt/60.,
+ ftime=ftime,
+ ))
+
+ if not title:
+ title = "Critical StrokeRate for "+rowername
+
+ chart_dict = {
+ 'data': powerdf2.to_dicts(),
+ 'fitdata': fit_data.to_dicts(),
+ 'title': title,
+ }
+
+ script, div = get_chart("/cr", chart_dict)
+
+ return [script, div, p1, ratio, message]
+
+
def interactive_agegroup_plot(df, distance=2000, duration=None,
sex='male', weightcategory='hwt'):
diff --git a/rowers/models.py b/rowers/models.py
index f1451085..ca4d864c 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -1104,6 +1104,18 @@ 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")
+ r0 = models.FloatField(default=1.0, verbose_name="CR r1")
+ r1 = models.FloatField(default=1.0, verbose_name="CR r2")
+ r2 = models.FloatField(default=1.0, verbose_name="CR r3")
+ r3 = models.FloatField(default=1.0, verbose_name="CR r4")
+ crratio = models.FloatField(default=1.0, verbose_name="CR fit ratio")
+
+ er0 = models.FloatField(default=1.0, verbose_name="erg CR r1")
+ er1 = models.FloatField(default=1.0, verbose_name="erg CR r2")
+ er2 = models.FloatField(default=1.0, verbose_name="erg CR r3")
+ er3 = models.FloatField(default=1.0, verbose_name="erg CR r4")
+ ecpratio = models.FloatField(default=1.0, verbose_name="erg CP fit ratio")
+
cprange = models.IntegerField(default=42, verbose_name="Range for calculation of breakthrough workouts and fitness (CP)",
choices=cppresets)
diff --git a/rowers/rowing_workout_metrics_pb2.py b/rowers/rowing_workout_metrics_pb2.py
index 80b29670..132e63c3 100644
--- a/rowers/rowing_workout_metrics_pb2.py
+++ b/rowers/rowing_workout_metrics_pb2.py
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1crowing-workout-metrics.proto\x12\x16rowing_workout_metrics\"\x80\x01\n\x15WorkoutMetricsRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0b\n\x03sex\x18\x02 \x01(\t\x12\x0b\n\x03\x66tp\x18\x03 \x01(\x01\x12\r\n\x05hrftp\x18\x04 \x01(\x01\x12\r\n\x05hrmax\x18\x05 \x01(\x01\x12\r\n\x05hrmin\x18\x06 \x01(\x01\x12\x0e\n\x06wpsavg\x18\x07 \x01(\x01\"\x80\x01\n\x16WorkoutMetricsResponse\x12\x0b\n\x03tss\x18\x01 \x01(\x01\x12\r\n\x05normp\x18\x02 \x01(\x01\x12\r\n\x05trimp\x18\x03 \x01(\x01\x12\r\n\x05hrtss\x18\x04 \x01(\x01\x12\r\n\x05normv\x18\x05 \x01(\x01\x12\r\n\x05normw\x18\x06 \x01(\x01\x12\x0e\n\x06spmtss\x18\x07 \x01(\x01\"=\n\tCPRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x10\n\x08\x66iletype\x18\x02 \x01(\t\x12\x0c\n\x04tarr\x18\x03 \x03(\x01\"<\n\nCPResponse\x12\r\n\x05\x64\x65lta\x18\x01 \x03(\x01\x12\r\n\x05power\x18\x02 \x03(\x01\x12\x10\n\x08\x61vgpower\x18\x03 \x01(\x01\x32\xc7\x01\n\x07Metrics\x12l\n\x0b\x43\x61lcMetrics\x12-.rowing_workout_metrics.WorkoutMetricsRequest\x1a..rowing_workout_metrics.WorkoutMetricsResponse\x12N\n\x05GetCP\x12!.rowing_workout_metrics.CPRequest\x1a\".rowing_workout_metrics.CPResponseB\x1aZ\x18./rowing-workout-metricsb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1crowing-workout-metrics.proto\x12\x16rowing_workout_metrics\"\x80\x01\n\x15WorkoutMetricsRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0b\n\x03sex\x18\x02 \x01(\t\x12\x0b\n\x03\x66tp\x18\x03 \x01(\x01\x12\r\n\x05hrftp\x18\x04 \x01(\x01\x12\r\n\x05hrmax\x18\x05 \x01(\x01\x12\r\n\x05hrmin\x18\x06 \x01(\x01\x12\x0e\n\x06wpsavg\x18\x07 \x01(\x01\"\x80\x01\n\x16WorkoutMetricsResponse\x12\x0b\n\x03tss\x18\x01 \x01(\x01\x12\r\n\x05normp\x18\x02 \x01(\x01\x12\r\n\x05trimp\x18\x03 \x01(\x01\x12\r\n\x05hrtss\x18\x04 \x01(\x01\x12\r\n\x05normv\x18\x05 \x01(\x01\x12\r\n\x05normw\x18\x06 \x01(\x01\x12\x0e\n\x06spmtss\x18\x07 \x01(\x01\"=\n\tCPRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x10\n\x08\x66iletype\x18\x02 \x01(\t\x12\x0c\n\x04tarr\x18\x03 \x03(\x01\"I\n\nCPResponse\x12\r\n\x05\x64\x65lta\x18\x01 \x03(\x01\x12\r\n\x05power\x18\x02 \x03(\x01\x12\x10\n\x08\x61vgpower\x18\x03 \x01(\x01\x12\x0b\n\x03spm\x18\x04 \x03(\x01\x32\xc7\x01\n\x07Metrics\x12l\n\x0b\x43\x61lcMetrics\x12-.rowing_workout_metrics.WorkoutMetricsRequest\x1a..rowing_workout_metrics.WorkoutMetricsResponse\x12N\n\x05GetCP\x12!.rowing_workout_metrics.CPRequest\x1a\".rowing_workout_metrics.CPResponseB\x1aZ\x18./rowing-workout-metricsb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -39,7 +39,7 @@ if not _descriptor._USE_C_DESCRIPTORS:
_globals['_CPREQUEST']._serialized_start=318
_globals['_CPREQUEST']._serialized_end=379
_globals['_CPRESPONSE']._serialized_start=381
- _globals['_CPRESPONSE']._serialized_end=441
- _globals['_METRICS']._serialized_start=444
- _globals['_METRICS']._serialized_end=643
+ _globals['_CPRESPONSE']._serialized_end=454
+ _globals['_METRICS']._serialized_start=457
+ _globals['_METRICS']._serialized_end=656
# @@protoc_insertion_point(module_scope)
diff --git a/rowers/templates/otwcr.html b/rowers/templates/otwcr.html
new file mode 100644
index 00000000..f4d7d849
--- /dev/null
+++ b/rowers/templates/otwcr.html
@@ -0,0 +1,20 @@
+
+ {{ the_div|safe }}
+
+
+
+
+ | Duration |
+ Stroke Estimate 1 |
+ Stroke Estimate 2 |
+
+
+
+
+ | {{ duration }} |
+ {{ power }} |
+ {{ upper }} |
+
+
+
+
diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html
index 853039d7..65722c5c 100644
--- a/rowers/templates/user_analysis_select.html
+++ b/rowers/templates/user_analysis_select.html
@@ -124,13 +124,13 @@