Files
Stride/app/effort_segments.py
T

99 lines
3.7 KiB
Python

"""Compact five-minute moving segments; gaps over ten seconds are excluded."""
import math
from itertools import pairwise
from .analytics import numeric
SEGMENT_SECONDS = 300
def summary(intervals):
duration = sum(item["dt"] for item in intervals)
distance = sum(item["ds"] for item in intervals)
hr = [(item["hr"], item["dt"]) for item in intervals if numeric(item["hr"]) and item["hr"] > 0]
hr_time = sum(dt for _, dt in hr)
mean_speed = distance / duration if duration else None
variation = (
math.sqrt(
sum((item["ds"] / item["dt"] - mean_speed) ** 2 * item["dt"] for item in intervals)
/ duration
)
/ mean_speed
* 100
if duration and mean_speed
else None
)
drift = None
if duration >= 240 and hr_time >= duration * 0.8:
halves = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
elapsed = 0.0
for item in intervals:
for index, begin, end in ((0, 0, duration / 2), (1, duration / 2, duration)):
dt = max(0, min(elapsed + item["dt"], end) - max(elapsed, begin))
if dt and numeric(item["hr"]) and item["hr"] > 0:
halves[index][0] += item["ds"] * dt / item["dt"]
halves[index][1] += item["hr"] * dt
halves[index][2] += dt
elapsed += item["dt"]
if all(part[2] >= duration * 0.4 and part[0] > 0 and part[1] > 0 for part in halves):
efficiency = [ds / hr_total for ds, hr_total, _ in halves]
drift = (efficiency[0] - efficiency[1]) / efficiency[0] * 100
return {
"distance": distance,
"duration": duration,
"heart_rate": sum(value * dt for value, dt in hr) / hr_time if hr_time else None,
"elevation": sum(item["ascent"] for item in intervals)
if duration
and sum(item["dt"] for item in intervals if item["has_elevation"]) >= duration * 0.8
else None,
"pace_variability": variation,
"heart_rate_drift": drift,
"sensor_coverage": hr_time / duration * 100 if duration else None,
}
def analyze(samples):
intervals, segments, pending, elapsed, offset = [], [], [], 0.0, 0.0
for before, after in pairwise(samples):
if not all(
numeric(row.get(key)) for row in (before, after) for key in ("time", "distance")
):
continue
dt, ds = after["time"] - before["time"], after["distance"] - before["distance"]
if not 0 < dt <= 10 or not 0.5 <= ds / dt <= 15:
continue
ascent = (
max(0, after["elevation"] - before["elevation"])
if all(numeric(row.get("elevation")) for row in (before, after))
else None
)
original = {
"dt": dt,
"ds": ds,
"hr": after.get("heart_rate"),
"ascent": ascent or 0,
"has_elevation": ascent is not None,
}
intervals.append(original)
remaining = dt
while remaining > 1e-9:
portion = min(remaining, SEGMENT_SECONDS - elapsed)
pending.append(
{
**original,
"dt": portion,
"ds": ds * portion / dt,
"ascent": (ascent or 0) * portion / dt,
}
)
elapsed += portion
remaining -= portion
if elapsed >= SEGMENT_SECONDS - 1e-9:
segments.append({**summary(pending), "offset": offset})
offset += elapsed
pending, elapsed = [], 0.0
if pending:
segments.append({**summary(pending), "offset": offset})
return summary(intervals), segments