"""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