76 lines
2.6 KiB
Python
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
|