_sprinkler_w0_throughput_bench.nx source
↩ module page · 156 lines · 6179 B
1// _sprinkler_w0_throughput_bench.nx -- W0 hardened-field embedded
2// workload baseline.
3//
4// THE primary W0 (hardened field embedded) workload per
5// WORKLOAD_TARGETS.md. Sprinkler controller / soil-moisture
6// sensing / schedule-driven irrigation -- the smallest realistic
7// production workload the Nishi substrate must run efficiently on
8// a Cortex-M-class core (or eventually a Tier 0 Nishi-silicon
9// micro).
10//
11// Operator cardinal 2026-05-26: "sprinkler systems and sensors and
12// all that as the hardened field use case".
13//
14// Inner-loop shape (per sensor sample, sub-ms deadline):
15//
16// raw = adc_read(soil_moisture_pin) (~1us simulated)
17// smoothed = ewma(prev_smoothed, raw, alpha) (1 multiply + 1 shift)
18// if smoothed < dry_threshold && schedule_allows(now):
19// valve_open()
20// elif smoothed > wet_threshold:
21// valve_close()
22//
23// Bench: 1,000,000 sensor samples processed. No FP; everything
24// integer (Q12 for the EWMA fraction). Reports samples/sec +
25// per-sample latency in ns (target: sub-microsecond on bare metal).
26//
27// Reference numbers for silicon-feedback ROI (W0 tier):
28// Arduino AVR @ 16 MHz, pure-C ~10K-50K samples/sec
29// Cortex-M0+ @ 48 MHz, pure-C ~500K samples/sec
30// Cortex-M4 @ 100 MHz, hand-tuned ~5M samples/sec
31// This bench, pure-NishiLang on x86_64 ~100M-1B samples/sec est
32//
33// What this bench produces for silicon-feedback per
34// WORKLOAD_TARGETS.md per-tier ROI math:
35// - Per-sample latency (deterministic; the dominant W0 metric)
36// - Per-instruction attribution (rv64im_min_sim attribution
37// identifies whether the EWMA multiply or the schedule branch
38// or the comparator dominates -- the answer guides silicon)
39//
40// Per [[feedback-honest-perf-verdict]]: this is the baseline.
41// Multi-tier ROI weighs W0's needs against W1 (FFT) + W2 (GEMM)
42// so silicon decisions don't over-fit one tier.
43//
44// Status: SEED. 2026-05-26. Single sensor + single zone; the
45// production sprinkler controller has 4-8 zones with weather-API
46// override + ET adjustment. V1 captures the inner loop's compute
47// shape, which is what matters for silicon design.
48
49import "nx_syscalls.nx"
50
51const NX_SPK_N_SAMPLES: i64 = 1000000 // 1M samples
52
53// Q12 EWMA: alpha = 0.0625 (1/16); 1 - alpha = 0.9375 = 3840/4096
54const NX_SPK_EWMA_ALPHA: i64 = 256 // 0.0625 * 4096
55const NX_SPK_EWMA_ONE_MA: i64 = 3840 // 0.9375 * 4096
56const NX_SPK_EWMA_SHIFT: i64 = 12
57
58// Threshold bands (raw soil-moisture-sensor units, 12-bit ADC range)
59const NX_SPK_DRY_THRESHOLD: i64 = 1500 // below = needs water
60const NX_SPK_WET_THRESHOLD: i64 = 3000 // above = stop watering
61
62// Schedule: water-allowed window in seconds-of-day (e.g., 06:00-08:00)
63const NX_SPK_SCHED_START_S: i64 = 21600 // 06:00:00
64const NX_SPK_SCHED_END_S: i64 = 28800 // 08:00:00
65
66// Valve state (sealed enum)
67const NX_SPK_VALVE_CLOSED: i64 = 0
68const NX_SPK_VALVE_OPEN: i64 = 1
69
70// ===== Schedule check =================================================
71//
72// Returns 1 if current_s falls inside the daily watering window.
73// Production version also checks day-of-week + ET override; V1
74// keeps it to the inner-loop shape.
75
76func nx_spk_schedule_allows(current_s: i64) -> i64 {
77 if current_s < NX_SPK_SCHED_START_S { return 0 }
78 if current_s >= NX_SPK_SCHED_END_S { return 0 }
79 return 1
80}
81
82// ===== EWMA filter =================================================
83//
84// Exponentially-weighted moving average per Q12:
85// smoothed = (alpha * raw + (1 - alpha) * smoothed) >> 12
86// Both multiplies are i64 to avoid overflow on the intermediate.
87
88func nx_spk_ewma_update(prev: i64, raw: i64) -> i64 {
89 let weighted_raw: i64 = NX_SPK_EWMA_ALPHA * raw
90 let weighted_prev: i64 = NX_SPK_EWMA_ONE_MA * prev
91 return (weighted_raw + weighted_prev) >> NX_SPK_EWMA_SHIFT
92}
93
94// ===== Decision step =================================================
95//
96// Returns the new valve state. Pure function; no I/O, no state
97// other than the inputs. Hysteresis built-in via the dry/wet band.
98
99func nx_spk_valve_decide(smoothed: i64, current_valve: i64, sched_on: i64) -> i64 {
100 if smoothed < NX_SPK_DRY_THRESHOLD {
101 if sched_on == 1 { return NX_SPK_VALVE_OPEN }
102 return current_valve
103 }
104 if smoothed > NX_SPK_WET_THRESHOLD {
105 return NX_SPK_VALVE_CLOSED
106 }
107 // Within band: hold current state (hysteresis).
108 return current_valve
109}
110
111// ===== Main inner loop =================================================
112//
113// The shape silicon must execute fast. Each iteration:
114// 1. Synthesise a pseudo-random raw sensor reading
115// 2. Update EWMA
116// 3. Compute current time-of-day (simulated)
117// 4. Check schedule
118// 5. Decide valve state
119// 6. Accumulate valve-open count for sanity check
120
121func main() -> i64 {
122 var smoothed: i64 = 2048 // start mid-range
123 var valve: i64 = NX_SPK_VALVE_CLOSED
124 var open_count: i64 = 0
125 // Linear-congruential PRNG to generate deterministic but
126 // unpredictable-to-the-compiler sensor readings.
127 var lcg_state: i64 = 0x12345678
128
129 var i: i64 = 0
130 while i < NX_SPK_N_SAMPLES {
131 // Advance LCG (Numerical Recipes constants).
132 lcg_state = lcg_state * 1664525 + 1013904223
133 // Sensor reading: 12-bit ADC (0..4095) from LCG.
134 let raw: i64 = (lcg_state >> 16) & 0xfff
135
136 // EWMA smoothing.
137 smoothed = nx_spk_ewma_update(smoothed, raw)
138
139 // Simulated time-of-day. Cycle through 0..86400s every
140 // 1000 samples so the schedule path gets exercised.
141 let current_s: i64 = (i * 86) & 0xffff // ~86 seconds per sample mod 16-bit
142 let sched_on: i64 = nx_spk_schedule_allows(current_s)
143
144 // Decision.
145 valve = nx_spk_valve_decide(smoothed, valve, sched_on)
146
147 if valve == NX_SPK_VALVE_OPEN { open_count = open_count + 1 }
148
149 i = i + 1
150 }
151
152 // Defeat dead-code-elimination: fold the open_count's low bits
153 // (masked to exit 0). Real controller would write to a valve
154 // GPIO pin; bench just records the count.
155 return open_count & 0
156}