code wiki / (root) / nx_ferment_thermal.nx

nx_ferment_thermal.nx source

↩ module page · 79 lines · 3732 B

1// nx_ferment_thermal.nx -- R1 hardware rung: vessel temperature hold. 2// 3// Composes nx_pid (control loop) + nx_ferment_safety (the never-poison 4// envelope CLAMPS every commanded setpoint, so the loop can NEVER 5// target a temperature above culture-kill). Heat-only actuator model 6// (a yogurt/cheese vessel heats; cooling is passive loss to ambient) -- 7// the cheapest real hardware. 8// 9// TWO independent safety layers, so no PID mistuning can cook a culture: 10// 1. setpoint CLAMP at construction (kill - margin), and 11// 2. a hard thermal INTERLOCK in every step: at/above the ceiling the 12// heater command is forced to 0 regardless of the controller. 13// With a bounded per-step heat (heater_max * heat_gain) smaller than the 14// margin, the vessel can never cross culture-kill in a single step. 15// 16// Proven by the gate: unsafe setpoints clamp; the command is heat-only 17// and bounded; a room-temperature vessel is driven up into the setpoint 18// neighborhood; and temperature NEVER reaches culture-kill across the 19// whole run (the ceiling holds DYNAMICALLY, not just at config time). 20// 21// genealogy_id: ziegler_nichols_1942_pid (via nx_pid) 22// + nishi_ferment_safety_envelope_2026 (via nx_ferment_safety) 23 24import "nx_syscalls.nx" 25import "nx_pid.nx" 26import "nx_ferment_safety.nx" 27 28const NX_FT_SAFE_MARGIN_MILLI_C: i64 = 1000 // hold setpoint + interlock 1 C below kill 29 30struct NxFermentThermal { 31 pid: *PID, 32 setpoint_milli_c: i64, // clamped-safe setpoint 33 kill_milli_c: i64, // culture-kill ceiling from the envelope 34 heater_max: i64, 35} 36 37// Clamp a requested setpoint so it never reaches culture-kill. 38func nx_ferment_thermal_safe_setpoint(env: *NxFermentSafetyEnvelope, requested_milli_c: i64) -> i64 { 39 let kill: i64 = env.culture_kill_temp_milli_c as i64 40 let ceiling: i64 = kill - NX_FT_SAFE_MARGIN_MILLI_C 41 if requested_milli_c > ceiling { return ceiling } 42 return requested_milli_c 43} 44 45func nx_ferment_thermal_new(env: *NxFermentSafetyEnvelope, 46 requested_setpoint_milli_c: i64, 47 kp: i64, ki: i64, kd: i64, i_limit: i64, 48 heater_max: i64) -> *NxFermentThermal { 49 let t: *NxFermentThermal = (sys_mmap(32)) as *NxFermentThermal 50 let p: *PID = (sys_mmap(56)) as *PID 51 nx_pid_init(p, kp, ki, kd, i_limit) 52 t.pid = p 53 t.setpoint_milli_c = nx_ferment_thermal_safe_setpoint(env, requested_setpoint_milli_c) 54 t.kill_milli_c = env.culture_kill_temp_milli_c as i64 55 t.heater_max = heater_max 56 return t 57} 58 59// One control step: measured temperature -> clamped heater command. 60// - HARD INTERLOCK: at/above (kill - margin) force 0 (never add heat). 61// - heat-only: a negative PID output (vessel above setpoint) -> 0. 62// - never exceeds heater_max. 63func nx_ferment_thermal_step(t: *NxFermentThermal, measured_milli_c: i64) -> i64 { 64 if measured_milli_c >= t.kill_milli_c - NX_FT_SAFE_MARGIN_MILLI_C { return 0 } 65 var cmd: i64 = nx_pid_update(t.pid, t.setpoint_milli_c, measured_milli_c) 66 if cmd < 0 { cmd = 0 } 67 if cmd > t.heater_max { cmd = t.heater_max } 68 return cmd 69} 70 71// First-order thermal plant (Newton cooling): the vessel gains heat from 72// the heater command and loses heat to ambient. Q14 coefficients. Used 73// by the gate to exercise the closed loop; also a real predictive model. 74func nx_ferment_plant_step(temp_milli_c: i64, heater_cmd: i64, ambient_milli_c: i64, 75 heat_gain_q14: i64, loss_q14: i64) -> i64 { 76 let heat: i64 = (heater_cmd * heat_gain_q14) / NX_PID_Q 77 let loss: i64 = ((temp_milli_c - ambient_milli_c) * loss_q14) / NX_PID_Q 78 return temp_milli_c + heat - loss 79}