95 lines
4.3 KiB
Python
95 lines
4.3 KiB
Python
"""Non-overlapping continuous 30-second windows, persisted during import/backfill."""
|
|
|
|
import math
|
|
from itertools import pairwise
|
|
|
|
from .analytics import enrich, numeric, running_cost
|
|
|
|
SENSORS = ("heart_rate", "cadence", "power", "stride_length", "ground_contact",
|
|
"vertical_oscillation")
|
|
|
|
|
|
def pack(intervals, offset, moving_offset):
|
|
duration = sum(i["dt"] for i in intervals)
|
|
distance = sum(i["ds"] for i in intervals)
|
|
speed = distance / duration
|
|
result = {"duration": duration, "distance": distance, "elapsed": offset / 60,
|
|
"moving_offset": moving_offset / 60}
|
|
for field in SENSORS:
|
|
usable = [i for i in intervals if numeric(i.get(field)) and i[field] > 0]
|
|
time = sum(i["dt"] for i in usable)
|
|
result[field] = sum(i[field] * i["dt"] for i in usable) / time if time else None
|
|
result[field + "_coverage"] = time / duration * 100
|
|
result["sensor_coverage"] = result["heart_rate_coverage"]
|
|
hr_seconds = {}
|
|
for item in intervals:
|
|
if numeric(item.get("heart_rate")) and item["heart_rate"] > 0:
|
|
key = str(round(item["heart_rate"]))
|
|
hr_seconds[key] = hr_seconds.get(key, 0) + item["dt"]
|
|
result["hr_seconds"] = hr_seconds
|
|
result["pace_variability"] = math.sqrt(
|
|
sum((i["ds"] / i["dt"] - speed) ** 2 * i["dt"] for i in intervals) / duration
|
|
) / speed * 100
|
|
grades = [i for i in intervals if numeric(i.get("grade"))]
|
|
time = sum(i["dt"] for i in grades)
|
|
coverage = time / duration * 100
|
|
result["terrain_coverage"] = coverage
|
|
joint = [i for i in grades if numeric(i.get("heart_rate")) and i["heart_rate"] > 0]
|
|
joint_time = sum(i["dt"] for i in joint)
|
|
result["joint_coverage"] = joint_time / duration * 100
|
|
result["joint_hr"] = sum(i["heart_rate"] * i["dt"] for i in joint) / joint_time if joint_time else None
|
|
result["joint_speed"] = sum(i["ds"] for i in joint) / joint_time if joint_time else None
|
|
result["joint_grade"] = sum(i["grade"] * i["dt"] for i in joint) / joint_time if joint_time else None
|
|
mean = sum(i["grade"] * i["dt"] for i in grades) / time if time else None
|
|
result["grade"] = mean if coverage >= 80 else None
|
|
result["grade_variability"] = math.sqrt(
|
|
sum((i["grade"] - mean) ** 2 * i["dt"] for i in grades) / time
|
|
) if coverage >= 80 else None
|
|
equivalent = sum(i["ds"] * running_cost(i["grade"] / 100) / running_cost(0)
|
|
for i in grades)
|
|
result["gap"] = 1000 * time / equivalent if coverage >= 80 and equivalent > 0 else None
|
|
result["hr_artifact_percent"] = sum(i["dt"] for i in intervals if i["hr_artifact"]) / duration * 100
|
|
return result
|
|
|
|
|
|
def analyze(samples):
|
|
rows = enrich(samples)
|
|
origin = next((r["time"] for r in rows if numeric(r.get("time"))), 0)
|
|
result, pending, elapsed, moving = [], [], 0.0, 0.0
|
|
offset = moving_start = 0.0
|
|
|
|
def flush():
|
|
if pending:
|
|
result.append(pack(pending, offset, moving_start))
|
|
|
|
for before, after in pairwise(rows):
|
|
valid = all(numeric(r.get(k)) for r in (before, after) for k in ("time", "distance"))
|
|
dt = after["time"] - before["time"] if valid else 0
|
|
ds = after["distance"] - before["distance"] if valid else 0
|
|
if not (0 < dt <= 10 and 0.5 <= ds / dt <= 15):
|
|
flush()
|
|
pending, elapsed = [], 0.0
|
|
continue
|
|
hr, previous_hr = after.get("heart_rate"), before.get("heart_rate")
|
|
artifact = numeric(hr) and (not 30 <= hr <= 240 or
|
|
numeric(previous_hr) and abs(hr - previous_hr) / dt > 20)
|
|
interval = {k: after.get(k) for k in SENSORS}
|
|
interval.update(grade=after.get("grade"), hr_artifact=bool(artifact))
|
|
if artifact:
|
|
interval["heart_rate"] = None
|
|
remaining = dt
|
|
while remaining > 1e-9:
|
|
if not pending:
|
|
offset = before["time"] - origin + dt - remaining
|
|
moving_start = moving
|
|
portion = min(remaining, 30 - elapsed)
|
|
pending.append({**interval, "dt": portion, "ds": ds * portion / dt})
|
|
remaining -= portion
|
|
elapsed += portion
|
|
moving += portion
|
|
if elapsed >= 30 - 1e-9:
|
|
flush()
|
|
pending, elapsed = [], 0.0
|
|
flush()
|
|
return result
|