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
    }