Spaces:
Running
Running
File size: 8,625 Bytes
b5961aa 29b313e b5961aa 29b313e b5961aa 29b313e 445252b 29b313e b5961aa 29b313e b5961aa 29b313e b5961aa 29b313e b5961aa 29b313e b5961aa |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
import logging
from chatbot.agents.states.state import AgentState
import numpy as np
from scipy.optimize import minimize
# --- Cấu hình logging ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def optimize_portions_scipy(state: AgentState):
logger.info("---NODE: SCIPY OPTIMIZER (FINAL VERSION)---")
profile = state.get("user_profile", {})
menu = state.get("selected_structure", [])
if not menu:
print("⚠️ Menu rỗng, bỏ qua tối ưu hóa.")
return {"final_menu": [], "user_profile": profile}
# --- BƯỚC 1: XÁC ĐỊNH MỤC TIÊU TỐI ƯU HÓA (CRITICAL STEP) ---
daily_targets = np.array([
float(profile.get("targetcalories", 1314)),
float(profile.get("protein", 98)),
float(profile.get("totalfat", 43)),
float(profile.get("carbohydrate", 131))
])
meal_ratios = {"sáng": 0.25, "trưa": 0.40, "tối": 0.35}
generated_meals = set(d.get("assigned_meal", "").lower() for d in menu)
# Tính Target Thực Tế (Optimization Target)
# Ví dụ: Nếu chỉ có bữa Trưa -> Target = Daily * 0.4
# Nếu có Trưa + Tối -> Target = Daily * (0.4 + 0.35)
active_target = np.zeros(4)
active_ratios_sum = 0
for m in ["sáng", "trưa", "tối"]:
if m in generated_meals:
active_target += daily_targets * meal_ratios[m]
active_ratios_sum += meal_ratios[m]
# Fallback: Nếu không xác định được bữa nào, dùng Target Ngày
if np.sum(active_target) == 0:
active_target = daily_targets
logger.info(f" 🎯 Mục tiêu tối ưu hóa (Active Target): {active_target.astype(int)}")
# --- BƯỚC 2: THIẾT LẬP MA TRẬN & BOUNDS ---
matrix = []
bounds = []
meal_indices = {"sáng": [], "trưa": [], "tối": []}
# Tính target riêng từng bữa để dùng cho Distribution Loss
target_kcal_per_meal = {
k: daily_targets[0] * v for k, v in meal_ratios.items()
}
for i, dish in enumerate(menu):
nutrients = [
float(dish.get("kcal", 0)),
float(dish.get("protein", 0)),
float(dish.get("totalfat", 0)),
float(dish.get("carbs", 0))
]
matrix.append(nutrients)
# Logic Bounds Thông minh
current_kcal = nutrients[0]
t_meal_name = dish.get("assigned_meal", "").lower()
t_meal_target = target_kcal_per_meal.get(t_meal_name, 500)
# Nếu 1 món chiếm > 90% Kcal mục tiêu của bữa đó -> Phải cho giảm sâu
if current_kcal > (t_meal_target * 0.9):
bounds.append((0.3, 1.0))
elif "solver_bounds" in dish:
bounds.append(dish["solver_bounds"])
else:
bounds.append((0.5, 1.5))
if "sáng" in t_meal_name: meal_indices["sáng"].append(i)
elif "trưa" in t_meal_name: meal_indices["trưa"].append(i)
elif "tối" in t_meal_name: meal_indices["tối"].append(i)
matrix = np.array(matrix).T
n_dishes = len(menu)
initial_guess = np.ones(n_dishes)
# --- BƯỚC 3: ADAPTIVE WEIGHTS ---
optimized_portions = initial_guess
try:
# Tính dinh dưỡng tối đa có thể đạt được (nếu ăn x2.5 suất tất cả)
max_possible = matrix.dot(np.full(n_dishes, 2.5))
# Trọng số mặc định: [Kcal, P, L, C]
adaptive_weights = np.array([3.0, 2.0, 1.0, 1.0])
nutri_names = ["Kcal", "Protein", "Lipid", "Carb"]
for i in range(1, 4): # Check P, L, C
# Nếu Max khả thi vẫn < 70% Target -> Menu này quá thiếu chất đó
# -> Giảm trọng số về gần 0 để Solver không cố gắng cứu nó
if max_possible[i] < (active_target[i] * 0.7):
logger.info(f" ⚠️ Thiếu hụt {nutri_names[i]} nghiêm trọng (Max {int(max_possible[i])} < Target {int(active_target[i])}). Bỏ qua tối ưu chỉ số này.")
adaptive_weights[i] = 0.01
# --- BƯỚC 4: LOSS FUNCTION ---
def objective(portions):
# A. Loss Macro (So với Active Target)
current_macros = matrix.dot(portions)
# Dùng adaptive_weights để tránh bẫy
diff = (current_macros - active_target) / (active_target + 1e-5)
loss_macro = np.sum(adaptive_weights * (diff ** 2))
# B. Loss Phân bổ Bữa ăn (Chỉ cần thiết nếu sinh nhiều bữa)
loss_dist = 0
if active_ratios_sum > 0.5: # Chỉ tính nếu sinh > 1 bữa
kcal_row = matrix[0]
for m_type, indices in meal_indices.items():
if not indices: continue
current_meal_kcal = np.sum(kcal_row[indices] * portions[indices])
target_meal = target_kcal_per_meal.get(m_type, 0)
d = (current_meal_kcal - target_meal) / (target_meal + 1e-5)
loss_dist += (d ** 2)
return 3 * loss_macro + loss_dist
# 5. Run Optimization
logger.info("Đang tối ưu hóa phần suất món ăn...")
res = minimize(objective, initial_guess, method='SLSQP', bounds=bounds)
if res.success:
optimized_portions = res.x
else:
logger.warning(f"⚠️ Solver không hội tụ: {res.message}. Dùng portions mặc định.")
except Exception as e:
logger.error(f"🔥 LỖI CRITICAL KHI CHẠY SOLVER: {e}")
optimized_portions = np.ones(n_dishes)
# 6. Apply Results
final_menu = []
total_stats = np.zeros(4)
achieved_meal_kcal = {"sáng": 0, "trưa": 0, "tối": 0}
for i, dish in enumerate(menu):
ratio = optimized_portions[i]
final_dish = dish.copy()
final_dish["portion_scale"] = float(round(ratio, 2))
final_dish["final_kcal"] = int(dish.get("kcal", 0) * ratio)
final_dish["final_protein"] = int(dish.get("protein", 0) * ratio)
final_dish["final_totalfat"] = int(dish.get("totalfat", 0) * ratio)
final_dish["final_carbs"] = int(dish.get("carbs", 0) * ratio)
logger.info(f" - {dish['name']} ({dish['assigned_meal']}): x{final_dish['portion_scale']} suất -> {final_dish['final_kcal']}kcal, {final_dish['final_protein']}g Protein, {final_dish['final_totalfat']}g Total Fat, {final_dish['final_carbs']}g Carbs")
final_menu.append(final_dish)
total_stats += np.array([
final_dish["final_kcal"], final_dish["final_protein"],
final_dish["final_totalfat"], final_dish["final_carbs"]
])
m_type = dish.get("assigned_meal", "").lower()
if "sáng" in m_type: achieved_meal_kcal["sáng"] += final_dish["final_kcal"]
elif "trưa" in m_type: achieved_meal_kcal["trưa"] += final_dish["final_kcal"]
elif "tối" in m_type: achieved_meal_kcal["tối"] += final_dish["final_kcal"]
# --- BƯỚC 7: BÁO CÁO KẾT QUẢ ---
logger.info("\n 📊 BÁO CÁO TỐI ƯU HÓA CHI TIẾT:")
headers = ["Chỉ số", "Mục tiêu (Bữa)", "Kết quả", "Độ lệch"]
row_format = " | {:<12} | {:<15} | {:<15} | {:<15} |"
logger.info(" " + "-"*65)
logger.info(row_format.format(*headers))
logger.info(" " + "-"*65)
labels = ["Năng lượng", "Protein", "TotalFat", "Carb"]
units = ["Kcal", "g", "g", "g"]
for i in range(4):
t_val = int(active_target[i]) # So sánh với Active Target
r_val = int(total_stats[i])
diff = r_val - t_val
diff_str = f"{diff:+d} {units[i]}"
status = ""
percent_diff = abs(diff) / (t_val + 1e-5)
# Nếu weight bị giảm về 0.01 thì không cảnh báo lỗi nữa (vì đã chấp nhận bỏ qua)
if percent_diff > 0.15 and adaptive_weights[i] > 0.1: status = "⚠️"
else: status = "✅"
logger.info(row_format.format(
labels[i],
f"{t_val} {units[i]}",
f"{r_val} {units[i]}",
f"{diff_str} {status}"
))
logger.info(" " + "-"*65)
logger.info("\n ⚖️ PHÂN BỔ TỪNG BỮA (Kcal):")
for meal in ["sáng", "trưa", "tối"]:
if meal in generated_meals:
t_meal = int(target_kcal_per_meal[meal])
r_meal = int(achieved_meal_kcal[meal])
logger.info(f" - {meal.capitalize():<5}: Đạt {r_meal} / {t_meal} Kcal")
return {
"final_menu": final_menu,
"user_profile": profile
} |