nx_pendulum.nx source
↩ module page · 140 lines · 7173 B
1// nx_pendulum.nx -- Simulates a nonlinear pendulum using integer arithmetic and symplectic integration for accurate physical behavior.
2const O_MAGIC_3141593: i64 = 3141593
3const O_MAGIC_6283185: i64 = 6283185
4const O_MAGIC_1570796: i64 = 1570796
5const O_MAGIC_1047198: i64 = 1047198
6const O_MAGIC_4096: i64 = 4096
7// nx_pendulum.nx -- BROWSER-RUNNABLE real pendulum simulator (base-relative, integer/no-float, no JS logic). SAME
8// code runs native (base = mmap -> PNG) and in the browser (base = 0 = sovereign WASM memory). Integrates the TRUE
9// nonlinear pendulum theta'' = -(g/L) sin(theta) with a symplectic (semi-implicit) integer step and our own Taylor
10// sine. The real, experimentally-established pendulum laws emerge: (a) Galileo's T proportional to sqrt(L), (b)
11// small-amplitude ISOCHRONISM (period ~ independent of amplitude), (c) large-amplitude period GROWTH (~+18% at 90
12// deg) -- the nonlinear effect a LINEARIZED fake (-k theta) cannot reproduce. Restoring strength GLN encodes g/L
13// (GLN=980 => g=9.8, L=1; period ~2007 steps at dt=1ms). nx_wasm auto-exports every func + the linear memory.
14// license_tier: ORIGINAL
15const W: i64 = 256
16const H: i64 = 256
17const O_FB: i64 = 0
18const O_TH: i64 = 524288 // theta (micro-rad, x1e6)
19const O_OM: i64 = 524296 // omega (micro-rad / s)
20const O_GLN: i64 = 524304 // restoring numerator = 980/L (g/L * dt * 1e5, g=9.8 dt=1ms)
21const O_AMP: i64 = 524312 // initial amplitude (micro-rad)
22const O_LIN: i64 = 524320 // 0 = true sin pendulum, 1 = linear approximation (for the liar-kill)
23const O_STEP: i64 = 524328
24const O_THEAD: i64 = 524336
25const O_TCNT: i64 = 524344
26const O_TR: i64 = 524352 // bob trail (display px,py)
27const TRAILN: i64 = 120
28const SC: i64 = 1000000 // micro-unit scale
29const DT_DIV: i64 = 1000 // theta += omega/1000 (dt = 1ms)
30const GL_DIV: i64 = 100000 // omega -= GLN*sin(theta)/1e5
31const CX: i64 = W/2 // pivot sits on the frame's centre line, by definition
32const PIVY: i64 = 40
33const BOBR: i64 = 7 // bob disc radius; render_impl draws the bob at exactly this size
34// ★DERIVED FROM THE FRAME, NOT PICKED. This was a hardcoded 155 while CX is 128, so the moment the
35// swing passed asin(128/155) = 55.7 degrees the bob left the picture ENTIRELY -- the pendulum's own
36// visualisation losing its subject. MEASURED 2026-08-14 by nx_pendulum_gate's new element tooth: at
37// theta = -0.9898 rad the bob sat at x = -1 on a 256-wide frame, and the raster row passed anyway
38// because it only counted non-background pixels (the rod and trail alone cleared its bar).
39// The rod can be at most the half-width less the bob's own radius, or the extreme of the swing
40// clips: bx_max = CX + LPX must satisfy bx_max + BOBR <= W-1. Written as that arithmetic so it stays
41// correct if the frame or the bob is ever resized, instead of needing this note to be re-read.
42const LPX: i64 = W - 1 - CX - BOBR
43const STEPS_PER_TICK: i64 = 16
44
45func rgb(r: i64, g: i64, b: i64) -> i64 { return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16) }
46func sin_fixed(Xin: i64) -> i64 {
47 let PI: i64=O_MAGIC_3141593; let TWO_PI: i64=O_MAGIC_6283185; let HALF: i64=O_MAGIC_1570796
48 var sign: i64=1; var X: i64=Xin
49 if X<0 { X=0-X; sign=0-1 }
50 X = X % TWO_PI
51 if X > PI { X = TWO_PI - X; sign = 0-sign }
52 if X > HALF { X = PI - X }
53 let x2: i64 = X*X/SC
54 var term: i64=X; var sum: i64=X; var k: i64=1
55 while k<=8 { term = (0-term)*x2/SC/((2*k)*(2*k+1)); sum=sum+term; k=k+1 }
56 return sum*sign
57}
58func cos_fixed(X: i64) -> i64 { return sin_fixed(X+O_MAGIC_1570796) }
59
60func putpx(base: i64, px: i64, py: i64, c: i64) -> i64 {
61 if px<0 { return 0 } if px>=W { return 0 } if py<0 { return 0 } if py>=H { return 0 }
62 let fb: *i64 = (base + O_FB) as *i64; fb[py*W+px]=c; return 0
63}
64func disc(base: i64, px: i64, py: i64, r: i64, c: i64) -> i64 {
65 var dy: i64=0-r
66 while dy<=r { var dx: i64=0-r
67 while dx<=r { if dx*dx+dy*dy <= r*r { putpx(base, px+dx, py+dy, c) } dx=dx+1 }
68 dy=dy+1 }
69 return 0
70}
71func line(base: i64, x0: i64, y0: i64, x1: i64, y1: i64, c: i64) -> i64 {
72 var dx: i64=x1-x0; if dx<0 { dx=0-dx }
73 var dy: i64=y1-y0; if dy<0 { dy=0-dy }
74 var n: i64=dx; if dy>n { n=dy }
75 if n==0 { putpx(base,x0,y0,c); return 0 }
76 var i: i64=0
77 while i<=n { putpx(base, x0+(x1-x0)*i/n, y0+(y1-y0)*i/n, c); i=i+1 }
78 return 0
79}
80func clear_fb(base: i64, c: i64) -> i64 { let fb: *i64=(base+O_FB) as *i64; var i: i64=0; while i<W*H { fb[i]=c; i=i+1 } return 0 }
81
82func init_impl(base: i64) -> i64 {
83 let TH: *i64=(base+O_TH) as *i64; let OM: *i64=(base+O_OM) as *i64; let GLN: *i64=(base+O_GLN) as *i64
84 let AMP: *i64=(base+O_AMP) as *i64; let LIN: *i64=(base+O_LIN) as *i64
85 GLN[0]=980; AMP[0]=O_MAGIC_1047198; LIN[0]=0 // L=1m, amplitude 60deg, true pendulum
86 TH[0]=AMP[0]; OM[0]=0
87 let stp: *i64=(base+O_STEP) as *i64; stp[0]=0
88 let th2: *i64=(base+O_THEAD) as *i64; th2[0]=0
89 let tc: *i64=(base+O_TCNT) as *i64; tc[0]=0
90 return 0
91}
92// symplectic (semi-implicit Euler) step: update omega using theta_n, then theta using omega_{n+1}
93func step_one_impl(base: i64) -> i64 {
94 let TH: *i64=(base+O_TH) as *i64; let OM: *i64=(base+O_OM) as *i64; let GLN: *i64=(base+O_GLN) as *i64; let LIN: *i64=(base+O_LIN) as *i64
95 var th: i64=TH[0]; var om: i64=OM[0]
96 var rest: i64=0
97 if LIN[0]==1 { rest = GLN[0]*th/GL_DIV } else { rest = GLN[0]*sin_fixed(th)/GL_DIV }
98 om = om - rest
99 th = th + om/DT_DIV
100 TH[0]=th; OM[0]=om
101 return 0
102}
103func record_trail(base: i64) -> i64 {
104 let TH: *i64=(base+O_TH) as *i64; let TR: *i64=(base+O_TR) as *i64
105 let th: i64=TH[0]
106 let bx: i64=CX + LPX*sin_fixed(th)/SC; let by: i64=PIVY + LPX*cos_fixed(th)/SC
107 let h0: *i64=(base+O_THEAD) as *i64; let tc: *i64=(base+O_TCNT) as *i64
108 let h: i64=h0[0]; TR[h*2]=bx; TR[h*2+1]=by
109 h0[0]=(h+1)%TRAILN; if tc[0]<TRAILN { tc[0]=tc[0]+1 }
110 return 0
111}
112func tick_impl(base: i64, cmd: i64) -> i64 {
113 if cmd==3 { init_impl(base); return 0 }
114 var s: i64=0; while s<STEPS_PER_TICK { step_one_impl(base); s=s+1 }
115 record_trail(base)
116 let stp: *i64=(base+O_STEP) as *i64; stp[0]=stp[0]+1
117 return 0
118}
119func render_impl(base: i64) -> i64 {
120 let TH: *i64=(base+O_TH) as *i64; let TR: *i64=(base+O_TR) as *i64; let tc: *i64=(base+O_TCNT) as *i64
121 clear_fb(base, rgb(10,12,20))
122 let th: i64=TH[0]
123 let bx: i64=CX + LPX*sin_fixed(th)/SC; let by: i64=PIVY + LPX*cos_fixed(th)/SC
124 // bob trail
125 var t: i64=0
126 while t<tc[0] { putpx(base, TR[t*2], TR[t*2+1], rgb(60,90,120)); t=t+1 }
127 disc(base, CX, PIVY, 3, rgb(180,180,190)) // pivot
128 line(base, CX, PIVY, bx, by, rgb(150,160,180)) // rod
129 disc(base, bx, by, BOBR, rgb(90,200,235)) // bob
130 return 0
131}
132
133// ---- wasm interface ----
134func ww() -> i64 { return W }
135func hh() -> i64 { return H }
136func fb_off() -> i64 { return O_FB }
137func init() -> i64 { return init_impl(0) }
138func tick(cmd: i64) -> i64 { return tick_impl(0, cmd) }
139func render() -> i64 { return render_impl(0) }
140func mem_bytes() -> i64 { return O_TR + TRAILN*2*8 + O_MAGIC_4096 }