segments: reject false efforts via geometric speed check
Long circuit rides were matching a segment START early and finding the segment END hours later on a second pass, producing effort times of ~17000s on a 4.7km segment. The conformance check passed because the full-circuit track covers all interior points within 50m over 5 hours. Add a per-sport minimum geometric speed (segment_distance / elapsed_s): cycling ≥ 1.0 m/s, running ≥ 0.5 m/s, default ≥ 0.2 m/s. When the check fails, advance past the current start candidate and retry, so a legitimate later match (e.g. a second lap done at real speed) is still detected.
This commit is contained in:
@@ -21,6 +21,15 @@ MATCH_RADIUS_M = 25 # max distance to segment start/end to open/close a
|
|||||||
CONFORMANCE_MAX_DEV_M = 50 # max allowed deviation for each interior segment point
|
CONFORMANCE_MAX_DEV_M = 50 # max allowed deviation for each interior segment point
|
||||||
CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviate
|
CONFORMANCE_MAX_FRAC = 0.30 # max fraction of interior points allowed to deviate
|
||||||
|
|
||||||
|
# Minimum geometric speed (segment_distance / elapsed_s) per sport, in m/s.
|
||||||
|
# Rejects false matches from long circuit rides where the track passes the
|
||||||
|
# segment start early and the segment end hours later.
|
||||||
|
_MIN_SPEED_MS: dict[str, float] = {
|
||||||
|
'cycling': 1.0, # ~3.6 km/h — slowest realistic climb
|
||||||
|
'running': 0.5, # ~1.8 km/h
|
||||||
|
}
|
||||||
|
_MIN_SPEED_DEFAULT = 0.2 # hiking / walking / unknown
|
||||||
|
|
||||||
# ── fast distance approximation ───────────────────────────────────────────────
|
# ── fast distance approximation ───────────────────────────────────────────────
|
||||||
|
|
||||||
_R = 6_371_000.0 # Earth radius in metres
|
_R = 6_371_000.0 # Earth radius in metres
|
||||||
@@ -250,6 +259,15 @@ 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
|
||||||
|
# early and end hours later on a second pass through the area).
|
||||||
|
elapsed = track.times[end_idx] - track.times[start_idx]
|
||||||
|
if elapsed > 0:
|
||||||
|
min_speed = _MIN_SPEED_MS.get(track.sport, _MIN_SPEED_DEFAULT)
|
||||||
|
if seg.distance_m / elapsed < min_speed:
|
||||||
|
search_from = start_idx + 1
|
||||||
|
continue
|
||||||
|
|
||||||
if _conformance_ok(track, seg, start_idx, end_idx):
|
if _conformance_ok(track, seg, start_idx, end_idx):
|
||||||
efforts.append(_extract_effort(track, seg, start_idx, end_idx))
|
efforts.append(_extract_effort(track, seg, start_idx, end_idx))
|
||||||
search_from = end_idx + 1
|
search_from = end_idx + 1
|
||||||
|
|||||||
Reference in New Issue
Block a user