Records: apply sidecars before computing; fix best_climb_m for long mountain climbs
- _rebuild_athlete_json now applies sidecar edits (sub_sport, sport, etc.) in-memory before passing summaries to write_athlete_json, so activities marked indoor via sidecar are correctly excluded from records. - _best_climb now runs Kadane's over cumulative distance (not 1Hz dense time) so recording pauses don't create None gaps that falsely reset the climbing window. Grappa: 811m→1603m; Nivolet: 311m→2009m. - Add bincio render --recompute-climbs to backfill existing activities from their stored timeseries.
This commit is contained in:
+30
-20
@@ -231,7 +231,23 @@ def compute_best_efforts(
|
||||
|
||||
best_climb_m: Optional[float] = None
|
||||
if sport == "cycling":
|
||||
best_climb_m = _best_climb(ele_1hz)
|
||||
# Use cumulative device distance as the x-axis so recording pauses
|
||||
# (where distance doesn't increase) don't create gaps that reset the window.
|
||||
# Fall back to elapsed-time ordering when no device distance is recorded.
|
||||
dist_ele = sorted(
|
||||
(p.distance_m, p.elevation_m)
|
||||
for p in pts
|
||||
if p.distance_m is not None and p.elevation_m is not None
|
||||
)
|
||||
if not dist_ele:
|
||||
dist_ele = sorted(
|
||||
(int((p.timestamp - started_at).total_seconds()), p.elevation_m)
|
||||
for p in pts
|
||||
if p.elevation_m is not None
|
||||
and int((p.timestamp - started_at).total_seconds()) >= 0
|
||||
)
|
||||
if len(dist_ele) >= 2:
|
||||
best_climb_m = _best_climb(dist_ele)
|
||||
|
||||
return best_efforts, best_climb_m
|
||||
|
||||
@@ -261,32 +277,26 @@ def _fastest_time_for_distance(speed_1hz: list[float], target_km: float) -> Opti
|
||||
return best_s
|
||||
|
||||
|
||||
def _best_climb(ele_1hz: list[Optional[float]]) -> Optional[float]:
|
||||
"""Maximum net elevation gain over any contiguous window (Kadane's on deltas).
|
||||
def _best_climb(pts_sorted: list[tuple[float, float]]) -> Optional[float]:
|
||||
"""Maximum net elevation gain over any contiguous uphill window (Kadane's).
|
||||
|
||||
None samples are treated as breaks between segments — the Kadane window is
|
||||
reset to 0 at each gap so non-contiguous elevation data is never joined.
|
||||
Returns None if fewer than two non-None samples exist.
|
||||
pts_sorted: list of (x, elevation_m) pairs sorted by x, where x is
|
||||
cumulative distance (m) or elapsed time (s). Using cumulative distance
|
||||
means recording pauses (x doesn't increase while stopped) don't create
|
||||
gaps that falsely reset the climbing window.
|
||||
"""
|
||||
non_null = sum(1 for e in ele_1hz if e is not None)
|
||||
if non_null < 2:
|
||||
if len(pts_sorted) < 2:
|
||||
return None
|
||||
|
||||
max_gain = 0.0
|
||||
current = 0.0
|
||||
prev: Optional[float] = None
|
||||
prev_e = pts_sorted[0][1]
|
||||
|
||||
for e in ele_1hz:
|
||||
if e is None:
|
||||
# Gap — reset window so we don't bridge the discontinuity
|
||||
current = 0.0
|
||||
prev = None
|
||||
continue
|
||||
if prev is not None:
|
||||
current = max(0.0, current + (e - prev))
|
||||
if current > max_gain:
|
||||
max_gain = current
|
||||
prev = e
|
||||
for _, e in pts_sorted[1:]:
|
||||
current = max(0.0, current + (e - prev_e))
|
||||
if current > max_gain:
|
||||
max_gain = current
|
||||
prev_e = e
|
||||
|
||||
return round(max_gain, 1) if max_gain > 0 else None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user