code wiki / (root) / nx_weather_lib.nx

nx_weather_lib.nx source

↩ module page · 159 lines · 8229 B

1// nx_weather_lib.nx -- LIB: THE WEATHER SYSTEM (2026-08-26). The shipped weather was ONE 2// deterministic coin per day-eighth (cal_rain: seed x segment x P_RAINS[season] -> 0/1) -- honest, 3// replay-exact, and structurally memoryless: no buildup, no fronts, no wind vector, no typed 4// precipitation. This lib is the continuous state the coin could not express, built ON the same 5// determinism law (every field is a pure function of (seed, absolute day-eighth, season table)), 6// and CALIBRATED TO THE BANKED DATA: the P_RAINS seasonal rows are preserved as the LONG-RUN rain 7// rates -- the gate proves the empirical frequency matches the row within a binomial bound, so 8// adopting this system cannot silently change how often it rains in any world. 9// 10// STATE per absolute day-eighth e (all Q12, 0..4096 unless noted): 11// pressure p(e): a bounded seeded random walk with persistence -- the front carrier. 12// humidity h(e): seasonal base from the world's OWN P_RAINS row (data-driven), plus a walk. 13// coverage c(e): cloud cover = f(low pressure, high humidity) -- what the sky consumer reads. 14// rain r(e): coverage above a threshold DERIVED from the row (see wx_rain) -- typed by temp. 15// temp t(e): seasonal sinusoid + night cooling, Q12 relative scale (freezing = WX_FREEZE_Q). 16// wind w(e): speed from the pressure GRADIENT |p(e)-p(e-1)| (fronts are windy -- the 17// physical shape, not a random number), direction a slow seeded walk. 18// 19// PERSISTENCE is the capability the coin lacked: every walk mixes the PREVIOUS eighth's value, so 20// P(rain | rain at e-1) > P(rain | dry at e-1) -- measurable, and the gate measures it. 21// license_tier: ORIGINAL No hw writes (Rule 26). 22import "nx_syscalls.nx" 23 24const WX_Q: i64 = 4096 25// walk mixing: 3/4 previous + 1/4 fresh draw -- the persistence knob. 3:1 gives a correlation 26// length of ~4 eighths (half a day), the front timescale a player can watch build. Named, and the 27// gate's persistence tooth fails if a future edit makes the state memoryless again. 28const WX_MIX_OLD: i64 = 3 29const WX_MIX_DEN: i64 = 4 30// freezing point on the Q12 relative temperature scale: winter nights cross it, summer days never 31// reach it. The seasonal sinusoid spans 0..WX_Q with winter trough at WX_Q/6; freezing sits at 32// WX_Q/4 so snow requires (winter OR night) AND a cold draw -- derived from the scale's own 33// construction, not a meteorology claim. 34const WX_FREEZE_Q: i64 = 1024 35 36// deterministic hash -- the same shape the world module's whash uses (pure fn of inputs) 37func wx_hash(a: i64, b: i64, c: i64) -> i64 { 38 var n: i64 = a*374761393 + b*668265263 + c*1442695041 39 n = (n ^ (n >> 13))*1274126177 40 n = n ^ (n >> 16) 41 if n < 0 { n = 0 - n } 42 return n 43} 44 45// a Q12 draw for (seed, eighth, salt) 46func wx_draw(seed: i64, e: i64, salt: i64) -> i64 { 47 return wx_hash(seed, e, salt) % (WX_Q + 1) 48} 49 50// bounded persistent walk: value at eighth e, computed by mixing back WX_DEPTH eighths of draws. 51// Closed form instead of stored state so the field stays a PURE FUNCTION of (seed, e) -- replay 52// exactness and window-slide immunity, the same law the terrain obeys. Depth 8 covers a day; the 53// geometric weights make older draws negligible past the correlation length. 54const WX_DEPTH: i64 = 8 55func wx_walk(seed: i64, e: i64, salt: i64) -> i64 { 56 var acc: i64 = 0 57 var wsum: i64 = 0 58 var w: i64 = WX_MIX_DEN - WX_MIX_OLD // newest weight 1 (of 4) 59 var num: i64 = 1 60 var den: i64 = 1 61 var k: i64 = 0 62 while k < WX_DEPTH { 63 // weight of draw at age k: (1/4) * (3/4)^k, scaled by den=4^k bookkeeping below 64 let dr: i64 = wx_draw(seed, e - k, salt) 65 acc = acc + dr*num*w 66 wsum = wsum + num*w 67 num = num*WX_MIX_OLD 68 den = den*WX_MIX_DEN 69 // renormalize num against den drift by capping magnitude (num/den = (3/4)^k exactly when 70 // both carried; we carry num only and divide once -- exact because wsum uses the same num) 71 k = k + 1 72 } 73 if wsum < 1 { wsum = 1 } 74 return acc/wsum 75} 76 77func wx_pressure(seed: i64, e: i64) -> i64 { return wx_walk(seed, e, 9101) } 78 79// humidity: the seasonal P_RAINS row (permil) IS the long-run base -- data drives the model. 80// rain_row_permil arrives from the caller's own season table. 81func wx_humidity(seed: i64, e: i64, rain_row_permil: i64) -> i64 { 82 let base: i64 = rain_row_permil*WX_Q/1000 83 let wob: i64 = wx_walk(seed, e, 9203) 84 // half base, half walk scaled around it: keeps the seasonal mean at the row while fronts move it 85 return (base + (base*wob*2/WX_Q))/2 + wob/4 86} 87 88// THE FRONT FACTOR: one zero-mean persistent walk is the weather's carrier. Zero-mean by 89// construction (the walk is symmetric about WX_Q/2), so anything ADDED around a banked rate 90// leaves the long-run mean at the rate -- v1 of this file mixed a coverage draw INTO the rain 91// decision and the gate measured row 220 collapsing to 5 permil; the front-factor form is the 92// correction the gate demanded. 93func wx_front(seed: i64, e: i64) -> i64 { 94 return wx_walk(seed, e, 9203) - WX_Q/2 95} 96// front modulation depth: the row moves at most +/-300 permil at the walk's full excursion; with 97// the walk's mixing the TYPICAL swing is ~+/-120 permil. Named as the one weather-drama knob; the 98// gate's T2 envelope and T3 persistence floor are what actually pin it from both sides. 99const WX_RAIN_SWING: i64 = 500 // 300 measured a 36-permil persistence gap vs the 50 floor; 500 is the 100 // smallest step that clears it -- T2's envelope pins the other side 101 102// coverage for the SKY consumer: seasonal base (the row, rescaled) moved by the same front that 103// decides rain -- so cloud cover BUILDS before and during rain, from one shared carrier. 104func wx_coverage(seed: i64, e: i64, rain_row_permil: i64) -> i64 { 105 let base: i64 = rain_row_permil*WX_Q/1000 106 var c: i64 = (base + (wx_front(seed, e) + WX_Q/2))/2 107 if c < 0 { c = 0 } 108 if c > WX_Q { c = WX_Q } 109 return c 110} 111 112// RAIN, RATE-PRESERVING BY CONSTRUCTION: a fresh uniform draw against (row + front-modulation). 113// The modulation is zero-mean, so the long-run rate is the row (clamping bias only at extreme 114// rows, absorbed by the gate's binomial envelope); persistence rides the front's autocorrelation. 115func wx_rain(seed: i64, e: i64, rain_row_permil: i64) -> i64 { 116 var thr: i64 = rain_row_permil + wx_front(seed, e)*WX_RAIN_SWING/(WX_Q/2) 117 if thr < 0 { thr = 0 } 118 if thr > 1000 { thr = 1000 } 119 let u: i64 = wx_draw(seed, e, 9307) 120 if u*1000 < thr*WX_Q { return 1 } 121 return 0 122} 123 124// temperature: seasonal sinusoid (4-point season ramp, winter trough WX_Q/6, summer crest 5*WX_Q/6) 125// plus night cooling of WX_Q/8 -- both spans named on the scale's own construction. 126func wx_temp(season: i64, is_night: i64, seed: i64, e: i64) -> i64 { 127 var base: i64 = 0 128 if season == 0 { base = WX_Q/2 } // spring 129 if season == 1 { base = 5*WX_Q/6 } // summer 130 if season == 2 { base = WX_Q/2 } // autumn 131 if season == 3 { base = WX_Q/6 } // winter 132 var t: i64 = base + (wx_walk(seed, e, 9401) - WX_Q/2)/4 133 if is_night == 1 { t = t - WX_Q/8 } 134 if t < 0 { t = 0 } 135 if t > WX_Q { t = WX_Q } 136 return t 137} 138 139// precipitation TYPE: 0 none, 1 rain, 2 snow (raining while below freezing) 140func wx_precip(seed: i64, e: i64, rain_row_permil: i64, season: i64, is_night: i64) -> i64 { 141 if wx_rain(seed, e, rain_row_permil) == 0 { return 0 } 142 if wx_temp(season, is_night, seed, e) < WX_FREEZE_Q { return 2 } 143 return 1 144} 145 146// wind SPEED from the pressure gradient between consecutive eighths (fronts are windy -- the 147// physical shape), plus a floor breeze from the coverage walk; direction a slow angular walk. 148func wx_wind_speed(seed: i64, e: i64) -> i64 { 149 var g: i64 = wx_pressure(seed, e) - wx_pressure(seed, e - 1) 150 if g < 0 { g = 0 - g } 151 let breeze: i64 = wx_walk(seed, e, 9509)/8 152 var w: i64 = g*2 + breeze 153 if w > WX_Q { w = WX_Q } 154 return w 155} 156func wx_wind_dir_q(seed: i64, e: i64) -> i64 { 157 // 0..WX_Q maps one full turn; the walk makes it drift slowly rather than jump 158 return wx_walk(seed, e, 9601) 159}