Files
sport-platform/backend/app/services/zones.py
2026-03-16 14:46:20 +03:00

76 lines
2.6 KiB
Python

import numpy as np
from backend.app.models.activity import DataPoint
# Coggan 7-zone power model (% of FTP)
POWER_ZONES = [
{"zone": 1, "name": "Active Recovery", "min_pct": 0, "max_pct": 55},
{"zone": 2, "name": "Endurance", "min_pct": 55, "max_pct": 75},
{"zone": 3, "name": "Tempo", "min_pct": 75, "max_pct": 90},
{"zone": 4, "name": "Threshold", "min_pct": 90, "max_pct": 105},
{"zone": 5, "name": "VO2max", "min_pct": 105, "max_pct": 120},
{"zone": 6, "name": "Anaerobic", "min_pct": 120, "max_pct": 150},
{"zone": 7, "name": "Neuromuscular", "min_pct": 150, "max_pct": 10000},
]
# 5-zone HR model (% of LTHR)
HR_ZONES = [
{"zone": 1, "name": "Recovery", "min_pct": 0, "max_pct": 81},
{"zone": 2, "name": "Aerobic", "min_pct": 81, "max_pct": 90},
{"zone": 3, "name": "Tempo", "min_pct": 90, "max_pct": 95},
{"zone": 4, "name": "Threshold", "min_pct": 95, "max_pct": 100},
{"zone": 5, "name": "Anaerobic", "min_pct": 100, "max_pct": 10000},
]
def calculate_power_zones(
data_points: list[DataPoint],
ftp: float,
) -> list[dict]:
"""Calculate time-in-zone distribution for power."""
powers = np.array([dp.power for dp in data_points if dp.power is not None], dtype=float)
if len(powers) == 0 or ftp <= 0:
return []
total = len(powers)
result = []
for z in POWER_ZONES:
low = ftp * z["min_pct"] / 100
high = ftp * z["max_pct"] / 100
seconds = int(np.sum((powers >= low) & (powers < high)))
result.append({
"zone": z["zone"],
"name": z["name"],
"min_watts": round(low),
"max_watts": round(high) if z["max_pct"] < 10000 else None,
"seconds": seconds,
"percentage": round(seconds / total * 100, 1) if total > 0 else 0,
})
return result
def calculate_hr_zones(
data_points: list[DataPoint],
lthr: int,
) -> list[dict]:
"""Calculate time-in-zone distribution for heart rate."""
hrs = np.array([dp.heart_rate for dp in data_points if dp.heart_rate is not None], dtype=float)
if len(hrs) == 0 or lthr <= 0:
return []
total = len(hrs)
result = []
for z in HR_ZONES:
low = lthr * z["min_pct"] / 100
high = lthr * z["max_pct"] / 100
seconds = int(np.sum((hrs >= low) & (hrs < high)))
result.append({
"zone": z["zone"],
"name": z["name"],
"min_bpm": round(low),
"max_bpm": round(high) if z["max_pct"] < 10000 else None,
"seconds": seconds,
"percentage": round(seconds / total * 100, 1) if total > 0 else 0,
})
return result