Fix segment avg_speed: derive from distance/time; tighten speed bounds to reject false matches
This commit is contained in:
@@ -25,10 +25,17 @@ CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviat
|
|||||||
# Rejects false matches from long circuit rides where the track passes the
|
# Rejects false matches from long circuit rides where the track passes the
|
||||||
# segment start early and the segment end hours later.
|
# segment start early and the segment end hours later.
|
||||||
_MIN_SPEED_MS: dict[str, float] = {
|
_MIN_SPEED_MS: dict[str, float] = {
|
||||||
'cycling': 1.0, # ~3.6 km/h — slowest realistic climb
|
'cycling': 2.0, # ~7.2 km/h — below any realistic cyclist even on brutal climbs
|
||||||
'running': 0.5, # ~1.8 km/h
|
'running': 0.8, # ~2.9 km/h
|
||||||
}
|
}
|
||||||
_MIN_SPEED_DEFAULT = 0.2 # hiking / walking / unknown
|
_MIN_SPEED_DEFAULT = 0.3 # hiking / walking / unknown
|
||||||
|
|
||||||
|
# Maximum geometric speed per sport in m/s — rejects GPS glitch matches.
|
||||||
|
_MAX_SPEED_MS: dict[str, float] = {
|
||||||
|
'cycling': 30.0, # ~108 km/h
|
||||||
|
'running': 12.0, # ~43 km/h
|
||||||
|
}
|
||||||
|
_MAX_SPEED_DEFAULT = 20.0
|
||||||
|
|
||||||
# ── fast distance approximation ───────────────────────────────────────────────
|
# ── fast distance approximation ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -207,7 +214,10 @@ def _extract_effort(
|
|||||||
) -> SegmentEffort:
|
) -> SegmentEffort:
|
||||||
elapsed_s = track.times[j] - track.times[i]
|
elapsed_s = track.times[j] - track.times[i]
|
||||||
started_at = (track.started_at + timedelta(seconds=track.times[i])).replace(microsecond=0)
|
started_at = (track.started_at + timedelta(seconds=track.times[i])).replace(microsecond=0)
|
||||||
avg_speed = _avg_nonnull(track.speeds, i, j)
|
# Always derive avg speed from segment distance / elapsed time. Device-recorded
|
||||||
|
# speed is unreliable across formats (m/s vs km/h in older FIT files) and
|
||||||
|
# averaging instantaneous GPS speed over a slice gives different results anyway.
|
||||||
|
avg_speed = (seg.distance_m / elapsed_s * 3.6) if elapsed_s > 0 else None
|
||||||
avg_hr_raw = _avg_nonnull(track.hrs, i, j)
|
avg_hr_raw = _avg_nonnull(track.hrs, i, j)
|
||||||
avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None
|
avg_hr = int(round(avg_hr_raw)) if avg_hr_raw is not None else None
|
||||||
avg_pwr_raw = _avg_nonnull(track.powers, i, j)
|
avg_pwr_raw = _avg_nonnull(track.powers, i, j)
|
||||||
@@ -259,12 +269,13 @@ def detect_one(track: ActivityTrack, seg: Segment) -> list[SegmentEffort]:
|
|||||||
# No end found — no more efforts possible starting at or after start_idx.
|
# No end found — no more efforts possible starting at or after start_idx.
|
||||||
break
|
break
|
||||||
|
|
||||||
# Reject implausibly slow matches (e.g. circuit rides matching start
|
# Reject implausibly slow or fast matches.
|
||||||
# early and end hours later on a second pass through the area).
|
|
||||||
elapsed = track.times[end_idx] - track.times[start_idx]
|
elapsed = track.times[end_idx] - track.times[start_idx]
|
||||||
if elapsed > 0:
|
if elapsed > 0:
|
||||||
|
geo_speed = seg.distance_m / elapsed
|
||||||
min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT)
|
min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT)
|
||||||
if seg.distance_m / elapsed < min_speed:
|
max_speed = _MAX_SPEED_MS.get(track.sport, _MAX_SPEED_DEFAULT)
|
||||||
|
if geo_speed < min_speed or geo_speed > max_speed:
|
||||||
search_from = start_idx + 1
|
search_from = start_idx + 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user