144 lines
4.2 KiB
Python
144 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any
|
|
|
|
BASELINE_STEPS_BY_LEVEL: dict[str, int] = {
|
|
"sedentary": 5000,
|
|
"light": 7000,
|
|
"moderate": 9000,
|
|
"active": 11000,
|
|
"very_active": 13000,
|
|
}
|
|
|
|
WORKOUT_KCAL_PER_SESSION = 200
|
|
KCAL_PER_STEP_PER_KG = 0.0005
|
|
FALLBACK_KCAL_PER_MIN = 6
|
|
|
|
|
|
@dataclass
|
|
class ActivityBonus:
|
|
steps: int
|
|
steps_baseline: int
|
|
steps_bonus_kcal: float
|
|
workout_active_kcal: float
|
|
workout_baseline_kcal: float
|
|
workout_bonus_kcal: float
|
|
total_bonus_kcal: float
|
|
scale_factor: float
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
def baseline_steps(profile: dict[str, Any]) -> int:
|
|
override = profile.get("baseline_steps")
|
|
if override is not None:
|
|
return int(override)
|
|
level = str(profile.get("activity_level") or "moderate")
|
|
return BASELINE_STEPS_BY_LEVEL.get(level, 9000)
|
|
|
|
|
|
def baseline_workout_kcal_day(profile: dict[str, Any]) -> float:
|
|
override = profile.get("baseline_workout_kcal")
|
|
if override is not None:
|
|
return float(override)
|
|
weekly = int(profile.get("weekly_workouts") or 3)
|
|
return round(weekly * WORKOUT_KCAL_PER_SESSION / 7, 1)
|
|
|
|
|
|
def estimate_workout_active_kcal(workout: dict[str, Any]) -> float:
|
|
active = workout.get("active_calories")
|
|
if active is not None:
|
|
return float(active)
|
|
duration = workout.get("duration_min")
|
|
if duration:
|
|
return float(duration) * FALLBACK_KCAL_PER_MIN
|
|
return 0.0
|
|
|
|
|
|
def steps_bonus_kcal(*, steps: int, baseline_steps: int, weight_kg: float) -> float:
|
|
extra_steps = max(0, steps - baseline_steps)
|
|
return round(extra_steps * weight_kg * KCAL_PER_STEP_PER_KG, 1)
|
|
|
|
|
|
def compute_activity_bonus(
|
|
profile: dict[str, Any],
|
|
*,
|
|
steps_total: int,
|
|
workouts: list[dict[str, Any]],
|
|
) -> ActivityBonus:
|
|
weight_kg = float(profile.get("weight_kg") or 70)
|
|
steps_base = baseline_steps(profile)
|
|
workout_base = baseline_workout_kcal_day(profile)
|
|
|
|
s_bonus = steps_bonus_kcal(steps=steps_total, baseline_steps=steps_base, weight_kg=weight_kg)
|
|
workout_active = round(sum(estimate_workout_active_kcal(w) for w in workouts), 1)
|
|
w_bonus = max(0.0, round(workout_active - workout_base, 1))
|
|
total_bonus = round(s_bonus + w_bonus, 1)
|
|
|
|
base_cal = float(profile.get("calorie_target") or 2000)
|
|
scale_factor = 1.0 if base_cal <= 0 else round((base_cal + total_bonus) / base_cal, 4)
|
|
|
|
return ActivityBonus(
|
|
steps=steps_total,
|
|
steps_baseline=steps_base,
|
|
steps_bonus_kcal=s_bonus,
|
|
workout_active_kcal=workout_active,
|
|
workout_baseline_kcal=workout_base,
|
|
workout_bonus_kcal=w_bonus,
|
|
total_bonus_kcal=total_bonus,
|
|
scale_factor=scale_factor,
|
|
)
|
|
|
|
|
|
def _targets_dict(
|
|
*,
|
|
calories: float,
|
|
protein_g: float,
|
|
fat_g: float,
|
|
carbs_g: float,
|
|
water_ml: float,
|
|
) -> dict[str, float]:
|
|
return {
|
|
"calories": round(calories),
|
|
"protein_g": round(protein_g),
|
|
"fat_g": round(fat_g),
|
|
"carbs_g": round(carbs_g),
|
|
"water_ml": round(water_ml),
|
|
}
|
|
|
|
|
|
def build_base_targets(profile: dict[str, Any]) -> dict[str, float]:
|
|
water_l = float(profile.get("water_l") or 2.5)
|
|
return _targets_dict(
|
|
calories=float(profile.get("calorie_target") or 2000),
|
|
protein_g=float(profile.get("protein_g") or 140),
|
|
fat_g=float(profile.get("fat_g") or 65),
|
|
carbs_g=float(profile.get("carbs_g") or 200),
|
|
water_ml=water_l * 1000,
|
|
)
|
|
|
|
|
|
def scale_targets(
|
|
base_targets: dict[str, float],
|
|
bonus_kcal: float,
|
|
) -> tuple[dict[str, float], dict[str, float]]:
|
|
"""Return (effective_targets, targets_base). Water is not scaled."""
|
|
targets_base = dict(base_targets)
|
|
base_cal = float(base_targets["calories"])
|
|
|
|
if bonus_kcal <= 0 or base_cal <= 0:
|
|
return dict(base_targets), targets_base
|
|
|
|
scale = (base_cal + bonus_kcal) / base_cal
|
|
effective = _targets_dict(
|
|
calories=base_cal + bonus_kcal,
|
|
protein_g=float(base_targets["protein_g"]) * scale,
|
|
fat_g=float(base_targets["fat_g"]) * scale,
|
|
carbs_g=float(base_targets["carbs_g"]) * scale,
|
|
water_ml=float(base_targets["water_ml"]),
|
|
)
|
|
return effective, targets_base
|
|
|