code wiki / (root) / _sprinkler_w0_throughput_bench.nx

_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}