Spaces:
Running
Running
| 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 | |
| } |