nx_vessel_faults.nx source
↩ module page · 74 lines · 3279 B
1// nx_vessel_faults.nx -- H2 of the HARDWARE-IN-THE-LOOP ladder: FAULT
2// INJECTION (the "iron bird" / FMEA rig). Aerospace proves a system fails
3// safe by deliberately breaking it in simulation -- stuck actuators, dead
4// sensors, lost power -- before it ever flies. H2 does the same for the
5// fermenter: inject each hardware failure and prove the design contains
6// the hazard or flags the batch, never silently overheating or shipping
7// unsafe food.
8//
9// THE LAST LINE OF DEFENSE is an INDEPENDENT THERMAL CUTOFF -- a bimetallic
10// thermostat / thermal fuse that acts on the TRUE temperature, wired around
11// the controller. It catches the two failures the controller cannot: a
12// heater stuck on (the command is irrelevant) and a dead sensor (the
13// controller is blind). This is exactly how real appliances are made safe.
14//
15// genealogy_id: fmea_fault_injection + independent_thermal_cutoff
16// + nishi_ferment_safety_r0
17
18import "nx_syscalls.nx"
19import "nx_ferment_safety.nx"
20import "nx_ferment_thermal.nx"
21import "nx_vessel_thermal.nx"
22import "nx_vessel_io.nx"
23
24const NX_VFAULT_NONE: i64 = 0
25const NX_VFAULT_HEATER_STUCK_ON: i64 = 1
26const NX_VFAULT_SENSOR_STUCK_LOW:i64 = 2
27const NX_VFAULT_POWER_LOSS: i64 = 3
28
29// Independent hardware over-temp cutoff (thermal fuse) on TRUE temperature.
30// cutoff_mc = 0 disables it (to demonstrate it is necessary).
31func nx_thermal_cutoff(true_temp_mc: i64, cutoff_mc: i64, requested_duty: i64) -> i64 {
32 if cutoff_mc <= 0 { return requested_duty }
33 if true_temp_mc >= cutoff_mc { return 0 }
34 return requested_duty
35}
36
37// Inject a fault into the realized heater duty.
38func nx_fault_heater_duty(fault: i64, controller_duty: i64) -> i64 {
39 if fault == NX_VFAULT_HEATER_STUCK_ON { return 1000 }
40 if fault == NX_VFAULT_POWER_LOSS { return 0 }
41 return controller_duty
42}
43
44// Inject a fault into the sensor reading the controller sees.
45func nx_fault_sensor_read(fault: i64, measured: i64, stuck_value: i64) -> i64 {
46 if fault == NX_VFAULT_SENSOR_STUCK_LOW { return stuck_value }
47 return measured
48}
49
50// Run a complete fault scenario through the full stack (controller + sensor
51// + thermal twin + cutoff). Returns the MAXIMUM true temperature reached
52// (the physical-hazard metric); writes the FINAL true temperature to
53// out_final[0] (the did-it-ferment metric).
54func nx_vessel_fault_run(env: *NxFermentSafetyEnvelope, twin: *NxVesselThermal,
55 fault: i64, cutoff_mc: i64, steps: i64,
56 out_final: *i64) -> i64 {
57 let ctrl: *NxFermentThermal = nx_ferment_thermal_new(env, 43000, 700, 4, 0, 4000000, 1000)
58 let sens: *NxSensor = nx_sensor_new(20000, 300, 100, 100)
59 var truet: i64 = 20000
60 var maxt: i64 = truet
61 var step: i64 = 0
62 while step < steps {
63 var meas: i64 = nx_sensor_update(sens, truet, step)
64 meas = nx_fault_sensor_read(fault, meas, 20000)
65 let cmd: i64 = nx_ferment_thermal_step(ctrl, meas)
66 var duty: i64 = nx_fault_heater_duty(fault, cmd)
67 duty = nx_thermal_cutoff(truet, cutoff_mc, duty)
68 truet = nx_vessel_thermal_step(twin, truet, duty, 60)
69 if truet > maxt { maxt = truet }
70 step = step + 1
71 }
72 out_final[0] = truet
73 return maxt
74}