Rankine–Hugoniot Front Tokens / front_tokens_experiment.py
Failed on benchmark
1"""Toy verification of Rankine--Hugoniot front tokens on Burgers' equation.
2
3Run with the prescribed Python interpreter. Outputs a concise JSON report and
4an optional CSV-like result file in this directory.
5"""
6import json, math
7from pathlib import Path
8import numpy as np
9
10SEED = 7
11rng = np.random.default_rng(SEED)
12
13
14def flux(u):
15 return 0.5 * u * u
16
17
18def rh_speed(ul, ur):
19 den = ul - ur
20 return (flux(ul) - flux(ur)) / den if abs(den) > 1e-12 else ul
21
22
23def godunov_flux(ul, ur):
24 # Exact scalar Burgers Godunov flux for the Riemann problem.
25 if ul <= ur: # rarefaction
26 if ul >= 0: return flux(ul)
27 if ur <= 0: return flux(ur)
28 return 0.0
29 # shock
30 s = rh_speed(ul, ur)
31 return flux(ul) if s >= 0 else flux(ur)
32
33
34def godunov_step(u, dx, dt):
35 # Fixed exterior states equal to the nearest interior value (shock stays away).
36 ext = np.concatenate(([u[0]], u, [u[-1]]))
37 f = np.array([godunov_flux(ext[i], ext[i+1]) for i in range(len(ext)-1)])
38 return u - dt / dx * (f[1:] - f[:-1])
39
40
41def exact_shock(x, xfront, ul=1.0, ur=0.0):
42 return np.where(x < xfront, ul, ur)
43
44
45def detect_front(u, x, threshold=0.35):
46 jumps = np.abs(np.diff(u))
47 ids = np.flatnonzero(jumps >= threshold)
48 if len(ids) == 0:
49 return []
50 # For this toy, adjacent flagged edges are one cluster; code also handles many.
51 clusters = []
52 start = prev = int(ids[0])
53 for q in ids[1:]:
54 q = int(q)
55 if q > prev + 1:
56 clusters.append((start, prev)); start = q
57 prev = q
58 clusters.append((start, prev))
59 out = []
60 for a, b in clusters:
61 # traces are pooled from the cells immediately outside the flagged cluster
62 li = max(0, a)
63 ri = min(len(u)-1, b+1)
64 out.append({"x": float(0.5*(x[a]+x[b+1])),
65 "ul": float(u[li]), "ur": float(u[ri])})
66 return out
67
68
69def token_rollout(u0, x, dt, nsteps):
70 """Propagate detected fronts analytically and render the piecewise field."""
71 tokens = detect_front(u0, x)
72 # In the intended module this is where attention/residual prediction enters;
73 # for a clean shock the exact traces are already the sufficient local state.
74 xs = []
75 fields = []
76 u = u0.copy()
77 for _ in range(nsteps + 1):
78 if tokens:
79 t = tokens[0]
80 fields.append(exact_shock(x, t["x"], t["ul"], t["ur"]))
81 xs.append(t["x"])
82 else:
83 fields.append(u.copy()); xs.append(float("nan"))
84 if tokens:
85 for t in tokens:
86 t["x"] += dt * rh_speed(t["ul"], t["ur"])
87 return np.asarray(fields), np.asarray(xs)
88
89
90def crossing_position(u, x, level=0.5):
91 # Linear interpolation of the numerical shock crossing.
92 k = np.flatnonzero((u[:-1] >= level) & (u[1:] < level))
93 if len(k) == 0: return float("nan")
94 i = int(k[0]); den = u[i] - u[i+1]
95 return float(x[i] + (level-u[i]) * (x[i+1]-x[i]) / den) if den else float(x[i])
96
97
98def main():
99 # Core mathematical check over random traces and x-independent flux.
100 pairs = rng.uniform(-1.0, 1.0, (100, 2))
101 pairs = pairs[np.abs(pairs[:,0]-pairs[:,1]) > .1]
102 speed_err = []
103 for ul, ur in pairs:
104 s = rh_speed(ul, ur)
105 speed_err.append(abs((flux(ul)-flux(ur))/(ul-ur)-s))
106 math_check = {"max_rh_identity_error": float(max(speed_err)),
107 "mean_rh_identity_error": float(np.mean(speed_err)),
108 "identity_pass": bool(max(speed_err) < 1e-12)}
109
110 # Clean shock benchmark at three resolutions, same physical horizon.
111 records = []
112 for N in (64, 128, 256):
113 dx = 1.0/N; x = (np.arange(N)+.5)*dx
114 x0 = .25; ul, ur = 1., 0.; s = rh_speed(ul, ur)
115 u0 = exact_shock(x, x0, ul, ur)
116 # Conservative baseline has a standard CFL timestep.
117 dt = .35*dx/max(abs(ul), abs(ur)); steps = int(.20/dt)
118 dt = .20/steps
119 ub = u0.copy(); base_pos=[]
120 for _ in range(steps):
121 ub = godunov_step(ub, dx, dt)
122 base_pos.append(crossing_position(ub, x))
123 tf, token_pos = token_rollout(u0, x, dt, steps)
124 times = np.arange(steps+1)*dt
125 true_pos = x0 + s*times
126 # compare only positions available at the grid output times
127 bpos = np.asarray(base_pos)
128 bpos_err = float(np.nanmean(np.abs(bpos-true_pos[1:])))
129 tpos_err = float(np.mean(np.abs(token_pos-true_pos)))
130 bfield = float(np.mean(np.abs(ub-exact_shock(x,true_pos[-1],ul,ur))))
131 tfield = float(np.mean(np.abs(tf[-1]-exact_shock(x,true_pos[-1],ul,ur))))
132 records.append({"N":N,"dt":dt,"steps":steps,
133 "baseline_front_L1":bpos_err,"idea_front_L1":tpos_err,
134 "baseline_field_L1":bfield,"idea_field_L1":tfield})
135
136 # One-step propagation error vs dt: discretization of the initial location is
137 # held fixed, so the analytic kinematic update should have no time integration error.
138 N=128; dx=1/N; x=(np.arange(N)+.5)*dx; x0=.25; u0=exact_shock(x,x0)
139 tok=detect_front(u0,x)[0]; xdet=tok['x']; s=rh_speed(tok['ul'],tok['ur'])
140 scales=[]
141 for dt in (0.002, 0.004, 0.008):
142 pred=xdet+dt*s; truth=x0+dt*s
143 scales.append({"dt":dt,"abs_position_error":abs(pred-truth)})
144 report={"seed":SEED,"math_check":math_check,"records":records,
145 "one_step_scaling":scales,
146 "note":"Front-token field uses exact local traces on a clean Riemann shock; no learned residual is needed in this deliberately minimal case."}
147 Path("results.json").write_text(json.dumps(report, indent=2))
148 print(json.dumps(report, indent=2))
149
150if __name__ == '__main__': main()