code wiki / (root) / nx_vessel_thermal.nx

nx_vessel_thermal.nx source

↩ module page · 72 lines · 3312 B

1// nx_vessel_thermal.nx -- H0 of the VIRTUAL-PROTOTYPE / HARDWARE-IN-THE- 2// LOOP ladder. A lumped-capacitance thermal model of the PHYSICAL 3// fermentation vessel in REAL ENGINEERING UNITS, so the hardware can be 4// sized and tested in software before a dollar is spent -- the way a 5// fighter programme runs the digital twin before bending metal. 6// 7// THE PHYSICS (Newton's law of cooling + a heater): 8// C * dT/dt = P*duty - U*(T - T_ambient) 9// C = heat capacity of the contents [J/K] (= mass * specific heat) 10// P = heater power [W] 11// U = insulation conductance to ambient [W/K] (lower = better insulated) 12// Equilibrium at full duty: T_eq = T_ambient + P/U. If T_eq < setpoint 13// the heater is UNDERSIZED -- the design can NEVER reach target, caught 14// here in software instead of after you buy the part. 15// 16// Integer units (no float): temp/ambient milli-C, P milli-W, U milli-(W/K), 17// C J/K, dt seconds. Derivation: dT_mc = net_mw * dt_s / C_jk. 18// 19// THE EXCEED: R1's controller used abstract Q14 gains; H0 is PHYSICAL -- 20// real watts and J/K and W/K -- so it answers "will a 50 W heater hold 4 L 21// of milk at 43 C in a vessel that loses 1 W/K?" before building anything. 22// 23// genealogy_id: lumped_capacitance_thermal_model + newton_cooling_1701 24// + digital_twin_mbse 25 26import "nx_syscalls.nx" 27 28struct NxVesselThermal { 29 heat_capacity_jk: i64, // J/K (contents mass x specific heat) 30 heater_power_mw: i64, // milli-W 31 insulation_mwk: i64, // milli-(W/K) -- lower = better insulated 32 ambient_mc: i64, // milli-C 33} 34 35func nx_vessel_thermal_new(heat_capacity_jk: i64, heater_power_mw: i64, 36 insulation_mwk: i64, ambient_mc: i64) -> *NxVesselThermal { 37 let v: *NxVesselThermal = (sys_mmap(32)) as *NxVesselThermal 38 v.heat_capacity_jk = heat_capacity_jk 39 v.heater_power_mw = heater_power_mw 40 v.insulation_mwk = insulation_mwk 41 v.ambient_mc = ambient_mc 42 return v 43} 44 45// One timestep of dt_s seconds at the given heater duty (milli, 1000=full). 46// Returns the new temperature (milli-C). 47func nx_vessel_thermal_step(v: *NxVesselThermal, temp_mc: i64, 48 duty_milli: i64, dt_s: i64) -> i64 { 49 let heat_mw: i64 = v.heater_power_mw * duty_milli / 1000 50 let loss_mw: i64 = v.insulation_mwk * (temp_mc - v.ambient_mc) / 1000 51 let net_mw: i64 = heat_mw - loss_mw 52 let dt_mc: i64 = net_mw * dt_s / v.heat_capacity_jk 53 return temp_mc + dt_mc 54} 55 56// Equilibrium temperature at FULL duty: the hottest the heater can sustain. 57// T_eq = T_ambient + P/U. Below setpoint => the heater is UNDERSIZED. 58func nx_vessel_equilibrium_mc(v: *NxVesselThermal) -> i64 { 59 return v.ambient_mc + (v.heater_power_mw * 1000) / v.insulation_mwk 60} 61 62// Heater DESIGN check: can this hardware ever reach the setpoint? 63func nx_vessel_can_reach(v: *NxVesselThermal, setpoint_mc: i64) -> i64 { 64 if nx_vessel_equilibrium_mc(v) > setpoint_mc { return 1 } 65 return 0 66} 67 68// Steady-state heater duty (milli) needed to HOLD a setpoint: duty = U*dT/P. 69func nx_vessel_hold_duty_milli(v: *NxVesselThermal, setpoint_mc: i64) -> i64 { 70 let loss_mw: i64 = v.insulation_mwk * (setpoint_mc - v.ambient_mc) / 1000 71 return loss_mw * 1000 / v.heater_power_mw 72}