83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
from dataclasses import dataclass
|
|
from typing import Dict, Any, Optional
|
|
|
|
@dataclass
|
|
class DishThermalState:
|
|
food_name: str
|
|
macronutrients: Dict[str, float]
|
|
estimated_mass_g: float
|
|
initial_temp_c: float
|
|
volume_cm3: Optional[float] = None
|
|
|
|
|
|
class MicrowaveThermalEngine:
|
|
"""Calculates cook parameters based on physical properties"""
|
|
|
|
DEFAULT_EFFICIENCY = 0.70 # ~70% magnetron efficiency
|
|
COOK_TARGET_TEMP_C = 74.0 # Safe food temp for cooking/reheating
|
|
DEFROST_TARGET_TEMP_C = 4.0 # Chilled state target for defrosting
|
|
LATENT_HEAT_ICE_J_G = 334.0 # Joules required to melt 1g of ice to water
|
|
|
|
@staticmethod
|
|
def estimate_specific_heat(macros: Dict[str, float], total_weight_g: float) -> float:
|
|
"""Estimates Cp in J/(g*C) based on macro composition"""
|
|
if total_weight_g <= 0:
|
|
return 3.5
|
|
w_water = macros.get("water_g", total_weight_g * 0.7) / total_weight_g
|
|
w_protein = macros.get("protein_g", 0.0) / total_weight_g
|
|
w_fat = macros.get("fat_g", 0.0) / total_weight_g
|
|
w_carbs = macros.get("carbs_g", 0.0) / total_weight_g
|
|
|
|
return (4.184 * w_water) + (1.71 * w_protein) + (1.67 * w_fat) + (1.42 * w_carbs)
|
|
|
|
def calculate_cook_plan(
|
|
self, state: DishThermalState, microwave_wattage: int, defrost_mode: bool
|
|
) -> Dict[str, Any]:
|
|
cp = self.estimate_specific_heat(state.macronutrients, state.estimated_mass_g)
|
|
label = state.food_name.lower()
|
|
|
|
# Set target temperature based on selected mode
|
|
target_temp = self.DEFROST_TARGET_TEMP_C if defrost_mode else self.COOK_TARGET_TEMP_C
|
|
delta_t = max(0.0, target_temp - state.initial_temp_c)
|
|
|
|
# 1. Base thermal energy: Q_sensible = m * c_p * delta_t
|
|
required_joules = state.estimated_mass_g * cp * delta_t
|
|
|
|
# 2. Account for Phase Change (Ice -> Water) if food starts below 0°C
|
|
if state.initial_temp_c < 0:
|
|
water_g = state.macronutrients.get("water_g", state.estimated_mass_g * 0.7)
|
|
latent_energy_joules = water_g * self.LATENT_HEAT_ICE_J_G
|
|
required_joules += latent_energy_joules
|
|
|
|
# 3. Determine power level and duty cycle based on mode
|
|
if defrost_mode:
|
|
# Defrost mode strictly runs low power (30%) to allow heat conduction
|
|
power_level = 30 if "bread" in label or "baked" in label else 40
|
|
time_factor = 1.1 # Slight padding for thermal conductivity losses
|
|
else:
|
|
# Cook / Reheat Mode logic
|
|
if state.initial_temp_c < 0:
|
|
# Cooking from frozen needs lower power to defrost first, then cook
|
|
power_level = 50
|
|
time_factor = 1.35
|
|
elif state.estimated_mass_g > 350 and not any(w in label for w in ["soup", "beverage", "water", "tea"]):
|
|
power_level = 70
|
|
time_factor = 1.2
|
|
elif any(w in label for w in ["cheese", "cream", "sauce", "butter", "egg"]):
|
|
power_level = 60
|
|
time_factor = 1.25
|
|
else:
|
|
power_level = 100
|
|
time_factor = 1.0
|
|
|
|
# Effective power delivered to food
|
|
effective_power_watts = microwave_wattage * self.DEFAULT_EFFICIENCY * (power_level / 100.0)
|
|
total_seconds = (required_joules / effective_power_watts * time_factor) if effective_power_watts > 0 else 0
|
|
|
|
return {
|
|
"cook_time_seconds": round(total_seconds),
|
|
"recommended_power_level_pct": power_level,
|
|
"target_temp": target_temp,
|
|
"estimated_specific_heat": round(cp, 2),
|
|
"energy_joules": round(required_joules)
|
|
} |