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}