render: add --recompute-vam to backfill climbing_time_s into existing activities
Reads each activity's timeseries, re-runs the VAM algorithm (which now returns both climbing_vam_mh and climbing_time_s), and patches activities/*.json and index.json in-place. Run once after upgrading to the new schema so NerdCorner can filter and opacity-encode existing data.
This commit is contained in:
@@ -377,6 +377,71 @@ def _link_data(site: Path, data: Path) -> None:
|
|||||||
console.print(f"Linked data: [cyan]{target}[/cyan] → [cyan]{public_data}[/cyan]")
|
console.print(f"Linked data: [cyan]{target}[/cyan] → [cyan]{public_data}[/cyan]")
|
||||||
|
|
||||||
|
|
||||||
|
def _recompute_vam(data: Path, handle: str | None = None) -> None:
|
||||||
|
"""Recompute climbing_vam_mh and climbing_time_s for all activities.
|
||||||
|
|
||||||
|
Reads the stored timeseries, re-runs the VAM algorithm, and patches both
|
||||||
|
activities/*.json and index.json in-place. Run once after adding
|
||||||
|
climbing_time_s to the schema so the NerdCorner VAM chart can filter short
|
||||||
|
climbs and opacity-encode confidence.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from bincio.extract.metrics import _VAM_SPORTS, _build_ele_1hz, _vam_from_ele_1hz
|
||||||
|
|
||||||
|
targets = [data / handle] if handle else _user_dirs(data)
|
||||||
|
for user_dir in targets:
|
||||||
|
acts_dir = user_dir / "activities"
|
||||||
|
index_path = user_dir / "index.json"
|
||||||
|
if not acts_dir.exists() or not index_path.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
index_data = json.loads(index_path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
index_by_id = {s["id"]: s for s in index_data.get("activities", [])}
|
||||||
|
updated = 0
|
||||||
|
|
||||||
|
for act_path in sorted(acts_dir.glob("*.json")):
|
||||||
|
if act_path.stem.endswith((".timeseries", ".geojson")):
|
||||||
|
continue
|
||||||
|
ts_path = acts_dir / f"{act_path.stem}.timeseries.json"
|
||||||
|
if not ts_path.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
detail = json.loads(act_path.read_text(encoding="utf-8"))
|
||||||
|
if detail.get("sport") not in _VAM_SPORTS:
|
||||||
|
continue
|
||||||
|
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||||
|
t_vals = ts.get("t", [])
|
||||||
|
e_vals = ts.get("elevation_m", [])
|
||||||
|
sparse: dict[int, float | None] = {int(t): e for t, e in zip(t_vals, e_vals)}
|
||||||
|
ele_1hz = _build_ele_1hz(sparse)
|
||||||
|
result = _vam_from_ele_1hz(ele_1hz) if ele_1hz else None
|
||||||
|
new_vam, new_climb_t = result if result else (None, None)
|
||||||
|
if (new_vam == detail.get("climbing_vam_mh") and
|
||||||
|
new_climb_t == detail.get("climbing_time_s")):
|
||||||
|
continue
|
||||||
|
detail["climbing_vam_mh"] = new_vam
|
||||||
|
detail["climbing_time_s"] = new_climb_t
|
||||||
|
act_path.write_text(
|
||||||
|
json.dumps(detail, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
summary = index_by_id.get(act_path.stem)
|
||||||
|
if summary is not None:
|
||||||
|
summary["climbing_vam_mh"] = new_vam
|
||||||
|
summary["climbing_time_s"] = new_climb_t
|
||||||
|
updated += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
index_path.write_text(
|
||||||
|
json.dumps(index_data, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
console.print(f" [cyan]{user_dir.name}[/cyan]: {updated} VAM(s) recomputed")
|
||||||
|
|
||||||
|
|
||||||
def _backfill_vam_summary(data: Path, handle: str | None = None) -> None:
|
def _backfill_vam_summary(data: Path, handle: str | None = None) -> None:
|
||||||
"""Copy climbing_vam_mh from detail JSONs into index.json summaries.
|
"""Copy climbing_vam_mh from detail JSONs into index.json summaries.
|
||||||
|
|
||||||
@@ -475,6 +540,9 @@ def _backfill_speed(data: Path, handle: str | None = None) -> None:
|
|||||||
@click.option("--recompute-elevation", "recompute_elevation", is_flag=True,
|
@click.option("--recompute-elevation", "recompute_elevation", is_flag=True,
|
||||||
help="Recompute elevation_gain_m/loss_m for all activities from stored timeseries "
|
help="Recompute elevation_gain_m/loss_m for all activities from stored timeseries "
|
||||||
"(run once after upgrading the dropout-skip fix).")
|
"(run once after upgrading the dropout-skip fix).")
|
||||||
|
@click.option("--recompute-vam", "recompute_vam", is_flag=True,
|
||||||
|
help="Recompute climbing_vam_mh and climbing_time_s for all activities from stored "
|
||||||
|
"timeseries (run once after adding climbing_time_s to the schema).")
|
||||||
@click.option("--backfill-vam-summary", "backfill_vam_summary", is_flag=True,
|
@click.option("--backfill-vam-summary", "backfill_vam_summary", is_flag=True,
|
||||||
help="Copy climbing_vam_mh from detail JSONs into index.json summaries "
|
help="Copy climbing_vam_mh from detail JSONs into index.json summaries "
|
||||||
"(run once after the VAM curve → summary migration).")
|
"(run once after the VAM curve → summary migration).")
|
||||||
@@ -492,6 +560,7 @@ def render(
|
|||||||
no_build: bool,
|
no_build: bool,
|
||||||
recompute_climbs: bool,
|
recompute_climbs: bool,
|
||||||
recompute_elevation: bool,
|
recompute_elevation: bool,
|
||||||
|
recompute_vam: bool,
|
||||||
backfill_vam_summary: bool,
|
backfill_vam_summary: bool,
|
||||||
backfill_speed: bool,
|
backfill_speed: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -511,6 +580,10 @@ def render(
|
|||||||
console.print("Recomputing elevation gain/loss from timeseries…")
|
console.print("Recomputing elevation gain/loss from timeseries…")
|
||||||
_recompute_elevation(data, handle=handle)
|
_recompute_elevation(data, handle=handle)
|
||||||
|
|
||||||
|
if recompute_vam:
|
||||||
|
console.print("Recomputing VAM and climbing time from timeseries…")
|
||||||
|
_recompute_vam(data, handle=handle)
|
||||||
|
|
||||||
if backfill_vam_summary:
|
if backfill_vam_summary:
|
||||||
console.print("Backfilling climbing_vam_mh into summaries…")
|
console.print("Backfilling climbing_vam_mh into summaries…")
|
||||||
_backfill_vam_summary(data, handle=handle)
|
_backfill_vam_summary(data, handle=handle)
|
||||||
|
|||||||
Reference in New Issue
Block a user