smart tdee
This commit is contained in:
@@ -1,143 +1,68 @@
|
||||
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,
|
||||
}
|
||||
DEFAULT_MET = 5.0
|
||||
|
||||
WORKOUT_KCAL_PER_SESSION = 200
|
||||
KCAL_PER_STEP_PER_KG = 0.0005
|
||||
FALLBACK_KCAL_PER_MIN = 6
|
||||
MET_BY_KEYWORD: list[tuple[str, float]] = [
|
||||
("триатлон", 10.0),
|
||||
("марафон", 9.8),
|
||||
("бег", 9.8),
|
||||
("running", 9.8),
|
||||
("run", 9.0),
|
||||
("плаван", 8.0),
|
||||
("swim", 8.0),
|
||||
("велосипед", 7.5),
|
||||
("cycling", 7.5),
|
||||
("вел", 7.5),
|
||||
("hiit", 8.0),
|
||||
("кроссфит", 8.0),
|
||||
("силов", 6.0),
|
||||
("strength", 6.0),
|
||||
("зал", 5.5),
|
||||
("gym", 5.5),
|
||||
("йога", 3.0),
|
||||
("yoga", 3.0),
|
||||
("ходьб", 3.5),
|
||||
("walk", 3.5),
|
||||
("прогул", 3.5),
|
||||
]
|
||||
|
||||
|
||||
@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 infer_met(workout: dict[str, Any]) -> float | None:
|
||||
explicit = workout.get("met")
|
||||
if explicit is not None:
|
||||
return float(explicit)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
activity_type = str(workout.get("activity_type") or "").lower()
|
||||
title = str(workout.get("title") or "").lower()
|
||||
notes = str(workout.get("notes") or "").lower()
|
||||
haystack = f"{activity_type} {title} {notes}"
|
||||
|
||||
for keyword, met in MET_BY_KEYWORD:
|
||||
if keyword in haystack:
|
||||
return met
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
def estimate_workout_active_kcal(workout: dict[str, Any], *, weight_kg: float) -> float:
|
||||
active = workout.get("active_calories")
|
||||
if active is not None:
|
||||
return float(active)
|
||||
return round(float(active), 1)
|
||||
|
||||
duration = workout.get("duration_min")
|
||||
if duration:
|
||||
return float(duration) * FALLBACK_KCAL_PER_MIN
|
||||
return 0.0
|
||||
if not duration:
|
||||
return 0.0
|
||||
|
||||
met = infer_met(workout)
|
||||
if met is None:
|
||||
return 0.0
|
||||
|
||||
hours = float(duration) / 60.0
|
||||
return round(met * weight_kg * hours, 1)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def workouts_kcal_total(workouts: list[dict[str, Any]], *, weight_kg: float) -> float:
|
||||
if not workouts:
|
||||
return 0.0
|
||||
return round(sum(estimate_workout_active_kcal(w, weight_kg=weight_kg) for w in workouts), 1)
|
||||
|
||||
Reference in New Issue
Block a user