169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
from typing import Any
|
||
|
||
from app.fitness.activity_budget import workouts_kcal_total
|
||
|
||
DEFAULT_NEAT_KCAL = 200.0
|
||
NEAT_KCAL_MIN = 200.0
|
||
NEAT_KCAL_MAX = 300.0
|
||
KCAL_PER_STEP_REF = 0.04 / 86 # ~0.04 kcal/step at 86 kg
|
||
WATER_ML_PER_KG = 33 # middle of 30–35 ml/kg range
|
||
|
||
GOAL_CALORIE_ADJUST = {
|
||
"lose": -500,
|
||
"maintain": 0,
|
||
"gain": 300,
|
||
}
|
||
|
||
PROTEIN_G_PER_KG = {
|
||
"lose": 2.2,
|
||
"maintain": 1.8,
|
||
"gain": 1.8,
|
||
}
|
||
FAT_G_PER_KG = 1.0
|
||
|
||
|
||
def bmr_mifflin(*, sex: str, weight_kg: float, height_cm: float, age: int) -> float:
|
||
base = 10 * weight_kg + 6.25 * height_cm - 5 * age
|
||
if sex.lower() in ("m", "male", "м", "мужской"):
|
||
return base + 5
|
||
return base - 161
|
||
|
||
|
||
def neat_base_kcal(profile: dict[str, Any]) -> float:
|
||
raw = profile.get("neat_base_kcal")
|
||
if raw is not None:
|
||
return max(NEAT_KCAL_MIN, min(NEAT_KCAL_MAX, float(raw)))
|
||
return DEFAULT_NEAT_KCAL
|
||
|
||
|
||
def steps_kcal(*, steps: int, weight_kg: float) -> float:
|
||
if steps <= 0:
|
||
return 0.0
|
||
return round(steps * weight_kg * KCAL_PER_STEP_REF, 1)
|
||
|
||
|
||
def bmi(weight_kg: float, height_cm: float) -> float:
|
||
if height_cm <= 0:
|
||
return 0.0
|
||
h = height_cm / 100
|
||
return weight_kg / (h * h)
|
||
|
||
|
||
def water_target_l(weight_kg: float) -> float:
|
||
return round(weight_kg * WATER_ML_PER_KG / 1000, 1)
|
||
|
||
|
||
def macro_targets(
|
||
calorie_target: float,
|
||
weight_kg: float,
|
||
goal: str = "maintain",
|
||
) -> dict[str, float]:
|
||
protein_g = round(weight_kg * PROTEIN_G_PER_KG.get(goal, 1.8), 0)
|
||
fat_g = round(weight_kg * FAT_G_PER_KG, 0)
|
||
protein_cal = protein_g * 4
|
||
fat_cal = fat_g * 9
|
||
carbs_g = max(0, round((calorie_target - protein_cal - fat_cal) / 4, 0))
|
||
return {"protein_g": protein_g, "fat_g": fat_g, "carbs_g": carbs_g}
|
||
|
||
|
||
def one_rep_max(weight_kg: float, reps: int) -> float:
|
||
if reps <= 0:
|
||
return weight_kg
|
||
if reps == 1:
|
||
return weight_kg
|
||
return round(weight_kg * (1 + reps / 30), 1)
|
||
|
||
|
||
def _profile_fields(profile: dict[str, Any]) -> tuple[float, float, int, str, str]:
|
||
weight = float(profile.get("weight_kg") or 70)
|
||
height = float(profile.get("height_cm") or 170)
|
||
age = int(profile.get("age") or 30)
|
||
sex = str(profile.get("sex") or "male")
|
||
goal = str(profile.get("goal") or "maintain")
|
||
return weight, height, age, sex, goal
|
||
|
||
|
||
def compute_tdee(
|
||
profile: dict[str, Any],
|
||
*,
|
||
steps_total: int = 0,
|
||
workouts: list[dict[str, Any]] | None = None,
|
||
) -> dict[str, float]:
|
||
weight, height, age, sex, _ = _profile_fields(profile)
|
||
bmr = bmr_mifflin(sex=sex, weight_kg=weight, height_cm=height, age=age)
|
||
neat = neat_base_kcal(profile)
|
||
s_kcal = steps_kcal(steps=steps_total, weight_kg=weight)
|
||
w_kcal = workouts_kcal_total(workouts or [], weight_kg=weight)
|
||
tdee_val = bmr + neat + s_kcal + w_kcal
|
||
return {
|
||
"bmr": round(bmr, 0),
|
||
"neat_kcal": round(neat, 0),
|
||
"steps_kcal": s_kcal,
|
||
"workout_kcal": w_kcal,
|
||
"tdee": round(tdee_val, 0),
|
||
}
|
||
|
||
|
||
def compute_daily_targets(
|
||
profile: dict[str, Any],
|
||
*,
|
||
steps_total: int = 0,
|
||
workouts: list[dict[str, Any]] | None = None,
|
||
) -> dict[str, Any]:
|
||
weight, height, age, sex, goal = _profile_fields(profile)
|
||
breakdown = compute_tdee(profile, steps_total=steps_total, workouts=workouts)
|
||
calorie_target = round(breakdown["tdee"] + GOAL_CALORIE_ADJUST.get(goal, 0), 0)
|
||
macros = macro_targets(calorie_target, weight, goal)
|
||
water = water_target_l(weight)
|
||
|
||
return {
|
||
**breakdown,
|
||
"calorie_target": calorie_target,
|
||
"protein_g": macros["protein_g"],
|
||
"fat_g": macros["fat_g"],
|
||
"carbs_g": macros["carbs_g"],
|
||
"water_l": water,
|
||
"bmi": round(bmi(weight, height), 1),
|
||
"steps": steps_total,
|
||
}
|
||
|
||
|
||
def targets_to_api(daily: dict[str, Any]) -> dict[str, float]:
|
||
return {
|
||
"calories": daily["calorie_target"],
|
||
"protein_g": daily["protein_g"],
|
||
"fat_g": daily["fat_g"],
|
||
"carbs_g": daily["carbs_g"],
|
||
"water_ml": round(daily["water_l"] * 1000),
|
||
}
|
||
|
||
|
||
def tdee_breakdown_to_api(daily: dict[str, Any]) -> dict[str, Any]:
|
||
return {
|
||
"bmr": daily["bmr"],
|
||
"neat_kcal": daily["neat_kcal"],
|
||
"steps_kcal": daily["steps_kcal"],
|
||
"workout_kcal": daily["workout_kcal"],
|
||
"tdee": daily["tdee"],
|
||
"calorie_target": daily["calorie_target"],
|
||
"steps": daily.get("steps", 0),
|
||
}
|
||
|
||
|
||
def compute_targets(profile: dict[str, Any]) -> dict[str, Any]:
|
||
"""Rest-day targets (BMR + NEAT, no steps/workouts) for profile storage."""
|
||
daily = compute_daily_targets(profile, steps_total=0, workouts=[])
|
||
return {
|
||
"bmr": daily["bmr"],
|
||
"tdee": daily["tdee"],
|
||
"bmi": daily["bmi"],
|
||
"neat_kcal": daily["neat_kcal"],
|
||
"steps_kcal": 0,
|
||
"workout_kcal": 0,
|
||
"calorie_target": daily["calorie_target"],
|
||
"protein_g": daily["protein_g"],
|
||
"fat_g": daily["fat_g"],
|
||
"carbs_g": daily["carbs_g"],
|
||
"water_l": daily["water_l"],
|
||
}
|