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}