everydaytok commited on
Commit
9c039a0
Β·
verified Β·
1 Parent(s): 07a5296

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +958 -408
app.py CHANGED
@@ -1,442 +1,992 @@
1
  """
2
- corrected_bridge.py
3
- ────────────────────────────────────────────────────────────────
4
- The honest architecture:
5
-
6
- 1. Perspective Engine β†’ finds C (the hidden numbers). Pure math.
7
- 2. StructuredBridge β†’ turns C into a clean, rich context string.
8
- 3. DeepSeek / any LLM β†’ reads that string and reasons in language.
9
-
10
- There is NO tensor injection into BART / DeepSeek.
11
- That approach failed because the LatentProjector was never trained
12
- with a language objective β€” it only ever minimised noise-prediction loss.
13
-
14
- Instead, the bridge is a structured formatter. This is not a compromise:
15
- it IS the correct separation of concerns. The engine's job ends at numbers.
16
- The LLM's job starts at language. They should not share a tensor space
17
- unless both sides are jointly trained on a language reconstruction loss
18
- (a separate, significant fine-tuning task we have NOT done yet).
19
-
20
- If you want to run the full loop:
21
- pip install torch transformers requests gradio fastapi uvicorn
22
-
23
- Run:
24
- python corrected_bridge.py
 
 
 
25
  """
26
 
27
- import torch
28
- import torch.nn as nn
29
- import torch.optim as optim
30
- import math, time, threading, random, json
31
- import gradio as gr
32
-
33
- # ─────────────────────────────────────────────────────────────
34
- # 0. DEVICE
35
- # ─────────────────────────────────────────────────────────────
36
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
37
- USE_AMP = torch.cuda.is_available()
38
-
39
- # ─────────────────────────────────────────────────────────────
40
- # 1. DOMAINS (same physics as always)
41
- # ─────────────────────────────────────────────────────────────
42
- DOMAINS = {
43
- 0: {
44
- "name": "Ecology",
45
- "labels": ("Fox Population", "Rabbit Population"),
46
- "c_min": [5.0, 50.0],
47
- "c_max": [50.0, 500.0],
48
- "b_norm": 300.0,
49
- "unit": "surviving rabbits",
50
- "sim": lambda c0, c1: c1 * math.exp(-0.05 * c0),
51
- "sim_t": lambda c0, c1: c1 * torch.exp(-0.05 * c0),
52
- "rule_text": (
53
- "Ecosystem predator-prey dynamics. "
54
- "Rabbit survival decays exponentially with fox population. "
55
- "Rule: survivors = rabbits Γ— e^(βˆ’0.05 Γ— foxes)"
56
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  },
58
- 1: {
59
- "name": "Market",
60
- "labels": ("Supply (units)", "Demand (units)"),
61
- "c_min": [10.0, 10.0],
62
- "c_max": [100.0, 100.0],
 
63
  "b_norm": 1200.0,
64
- "unit": "price ($)",
65
- "sim": lambda s, d: (d ** 2 / s) * 1.2,
66
- "sim_t": lambda s, d: (d ** 2 / s) * 1.2,
67
- "rule_text": (
68
- "Non-linear commodity market. "
69
- "Price scales with demand squared and inversely with supply. "
70
- "Rule: price = (demandΒ² / supply) Γ— 1.2"
71
- ),
72
  },
73
- 2: {
74
- "name": "Physics",
75
- "labels": ("Launch Velocity (m/s)", "Launch Angle (Β°)"),
76
- "c_min": [10.0, 5.0],
77
- "c_max": [120.0, 85.0],
78
- "b_norm": 600.0,
79
- "unit": "range (m)",
80
- "sim": lambda v, a: ((v**2 * math.sin(math.radians(2*a))) / 9.81) * math.exp(-v/100),
81
- "sim_t": lambda v, a: ((v**2 * torch.sin(2*a*math.pi/180.0)) / 9.81) * torch.exp(-v/100),
82
- "rule_text": (
83
- "Projectile with atmospheric drag. "
84
- "Range follows ballistic formula modulated by exponential drag. "
85
- "Rule: range = (vΒ² Γ— sin(2ΞΈ) / 9.81) Γ— e^(βˆ’v/100)"
86
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  },
88
- }
89
-
90
- TEST_SUITE = [
91
- (0, 120.0),
92
- (0, 55.0),
93
- (1, 450.0),
94
- (1, 900.0),
95
- (2, 320.0),
96
- (2, 150.0),
97
- (2, 500.0),
98
  ]
99
 
100
- # ─────────────────────────────────────────────────────────────
101
- # 2. DIFFUSION SCHEDULE
102
- # ─────────────────────────────────────────────────────────────
103
- T_STEPS = 60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  def make_schedule(T, s=0.008):
106
- x = torch.linspace(0, T, T+1, device=DEVICE)
107
- f = torch.cos(((x/T)+s)/(1+s)*math.pi/2)**2
108
  acp = f / f[0]
109
  betas = torch.clamp(1.0 - acp[1:]/acp[:-1], 1e-4, 0.999)
110
  return torch.cumprod(1.0 - betas, dim=0)
111
 
112
  ACP = make_schedule(T_STEPS)
113
 
114
- # ─────────────────────────────────────────────────────────────
115
- # 3. NETWORK (unchanged β€” this part was always correct)
116
- # ─────────────────────────────────────────────────────────────
117
- HIDDEN = 256
118
- DEPTH = 5
119
 
120
- class ResBlock(nn.Module):
121
- def __init__(self, d):
122
- super().__init__()
123
- self.net = nn.Sequential(nn.Linear(d, d*2), nn.GELU(), nn.Linear(d*2, d))
124
- self.norm = nn.LayerNorm(d)
125
- def forward(self, x): return self.norm(x + self.net(x))
126
 
127
- class PerspectiveEngine(nn.Module):
128
- def __init__(self):
129
- super().__init__()
130
- self.proj = nn.Linear(7, HIDDEN) # c_t(2)+t(1)+a_oh(3)+b(1)
131
- self.blocks = nn.ModuleList([ResBlock(HIDDEN) for _ in range(DEPTH)])
132
- self.head = nn.Sequential(nn.LayerNorm(HIDDEN), nn.Linear(HIDDEN, 2))
133
- def forward(self, c_t, t, a, b):
134
- x = self.proj(torch.cat([c_t, t, a, b], dim=-1))
135
- for blk in self.blocks: x = blk(x)
136
- return self.head(x)
137
-
138
- # ─────────────────────────────────────────────────────────────
139
- # 4. DATA + TRAINING
140
- # ─────────────────────────────────────────────────────────────
141
- N_TRAIN = 20_000
142
- EPOCHS = 1_000
143
- BATCH = 512
144
- LR = 3e-3
145
-
146
- _engine: PerspectiveEngine = None
147
- _train_log = []
148
- _phase = "idle"
149
-
150
- def log(msg):
151
- stamp = time.strftime("%H:%M:%S")
152
- line = f"[{stamp}] {msg}"
153
- print(line)
154
- _train_log.append(line)
155
- if len(_train_log) > 300: _train_log.pop(0)
156
-
157
- def generate_data(seed=42):
158
- torch.manual_seed(seed)
159
- all_A, all_B, all_C = [], [], []
160
- per = N_TRAIN // len(DOMAINS)
161
- for did, dom in DOMAINS.items():
162
- C_n = torch.rand(per, 2, device=DEVICE)
163
- c0 = C_n[:,0]*(dom["c_max"][0]-dom["c_min"][0])+dom["c_min"][0]
164
- c1 = C_n[:,1]*(dom["c_max"][1]-dom["c_min"][1])+dom["c_min"][1]
165
- B_r = dom["sim_t"](c0, c1)
166
- B_n = (B_r/dom["b_norm"]).clamp(0,1).unsqueeze(1)
167
- A_oh = nn.functional.one_hot(
168
- torch.full((per,), did, dtype=torch.long, device=DEVICE), 3
169
- ).float()
170
- all_A.append(A_oh); all_B.append(B_n); all_C.append(C_n)
171
- return torch.cat(all_A), torch.cat(all_B), torch.cat(all_C)
172
-
173
- def run_training(seed=42):
174
- global _engine, _phase, ACP
175
- _phase = "generating"
176
- log(f"Generating {N_TRAIN} samples (seed={seed})…")
177
- A, B, C = generate_data(seed)
178
- ACP = make_schedule(T_STEPS)
179
-
180
- _phase = "training"
181
- model = PerspectiveEngine().to(DEVICE)
182
- opt = optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)
183
- sched = optim.lr_scheduler.CosineAnnealingLR(opt, EPOCHS)
184
- scaler = torch.cuda.amp.GradScaler(enabled=USE_AMP)
185
- n = len(A)
186
- t0 = time.time()
187
-
188
- for ep in range(1, EPOCHS+1):
189
- perm = torch.randperm(n, device=DEVICE)
190
- ep_loss = 0.0; nb = 0
191
- for i in range(0, n, BATCH):
192
- idx = perm[i:i+BATCH]
193
- a_b, b_b, c_b = A[idx], B[idx], C[idx]
194
- t_int = torch.randint(0, T_STEPS, (len(a_b),), device=DEVICE)
195
- t_norm = (t_int.float()/T_STEPS).unsqueeze(1)
196
- acp_t = ACP[t_int].unsqueeze(1)
197
- noise = torch.randn_like(c_b)
198
- c_noisy = acp_t.sqrt()*c_b + (1-acp_t).sqrt()*noise
199
-
200
- with torch.cuda.amp.autocast(enabled=USE_AMP):
201
- loss = nn.functional.mse_loss(model(c_noisy, t_norm, a_b, b_b), noise)
202
-
203
- opt.zero_grad()
204
- scaler.scale(loss).backward()
205
- scaler.unscale_(opt)
206
- nn.utils.clip_grad_norm_(model.parameters(), 1.0)
207
- scaler.step(opt); scaler.update()
208
- ep_loss += loss.item(); nb += 1
209
-
210
- sched.step()
211
- if ep % 100 == 0:
212
- log(f"Epoch {ep:>4}/{EPOCHS} loss={ep_loss/nb:.5f}")
213
-
214
- log(f"Training done in {time.time()-t0:.1f}s")
215
- _engine = model.eval()
216
- _phase = "ready"
217
- run_tests()
218
-
219
- # ─────────────────────────────────────────────────────────────
220
- # 5. INFERENCE
221
- # ─────────────────────────────────────────────────────────────
222
 
223
  @torch.no_grad()
224
- def retrace(domain_id: int, target_b: float):
225
- dom = DOMAINS[domain_id]
226
- b_norm = target_b / dom["b_norm"]
227
- A = nn.functional.one_hot(torch.tensor([domain_id], device=DEVICE), 3).float()
228
- B = torch.tensor([[b_norm]], device=DEVICE)
229
- c_t = torch.randn(1, 2, device=DEVICE)
 
 
230
 
231
  for t in reversed(range(T_STEPS)):
232
  t_n = torch.tensor([[t/T_STEPS]], device=DEVICE)
233
- eps_pred = _engine(c_t, t_n, A, B)
234
  acp_t = ACP[t]
235
  acp_prev = ACP[t-1] if t > 0 else torch.tensor(1.0, device=DEVICE)
236
- x0 = ((c_t - (1-acp_t).sqrt()*eps_pred) / acp_t.sqrt()).clamp(0,1)
237
  c_t = acp_prev.sqrt()*x0 + (1-acp_prev).sqrt()*eps_pred
238
 
239
- cn = c_t.clamp(0,1).squeeze().cpu().tolist()
240
- c0 = cn[0]*(dom["c_max"][0]-dom["c_min"][0])+dom["c_min"][0]
241
- c1 = cn[1]*(dom["c_max"][1]-dom["c_min"][1])+dom["c_min"][1]
242
- verified = dom["sim"](c0, c1)
243
- err = abs(verified - target_b) / max(abs(target_b), 1e-6) * 100
244
- return c0, c1, verified, err
245
-
246
- # ─────────────────────────────────────────────────────────────
247
- # 6. STRUCTURED BRIDGE
248
- # This is where the engine's numbers become language-ready.
249
- # NOT tensor injection. Honest, clean structured text.
250
- # The LLM (DeepSeek / any model) reads this as its system context.
251
- # ─────────────────────────────────────────────────────────────
252
-
253
- def build_llm_context(domain_id: int, target_b: float,
254
- c0: float, c1: float,
255
- verified: float, err: float) -> dict:
256
- """
257
- Builds a structured context dict that an LLM can receive
258
- either as a system prompt injection or as a JSON API payload.
259
-
260
- This replaces the failed LatentProjector β†’ BART decoder approach.
261
- The engine's job is to find the numbers. The LLM's job is language.
262
- """
263
- dom = DOMAINS[domain_id]
264
- status = "SETTLED" if err < 2.0 else ("APPROXIMATE" if err < 5.0 else "UNSTABLE")
265
- confidence = max(0.0, round(100.0 - err, 2))
266
-
267
- # System prompt the LLM receives (as plain text β€” no tensor magic needed)
268
- system_prompt = f"""You are an analytical reasoning assistant.
269
- The Perspective Engine (a reverse-diffusion constraint solver) has finished
270
- navigating the geometry of a {dom['name']} system and settled on hidden variables.
271
-
272
- DOMAIN: {dom['name']}
273
- GOVERNING RULE: {dom['rule_text']}
274
-
275
- OBSERVED BYPRODUCT (what we saw): {target_b:.2f} {dom['unit']}
276
- ENGINE STATUS: {status} (convergence error: {err:.3f}%, confidence: {confidence}%)
277
-
278
- RETRACED HIDDEN VARIABLES:
279
- {dom['labels'][0]}: {c0:.3f}
280
- {dom['labels'][1]}: {c1:.3f}
281
-
282
- FORWARD VERIFICATION: plugging those back into the rule gives {verified:.3f} {dom['unit']}
283
-
284
- Your task: explain, in plain language, what these hidden variables mean,
285
- why this combination produces the observed outcome, and what it implies
286
- about the state of the system. Be precise but conversational."""
287
-
288
- return {
289
- "domain": dom["name"],
290
- "target_b": target_b,
291
- "unit": dom["unit"],
292
- "c0_label": dom["labels"][0],
293
- "c0_value": round(c0, 3),
294
- "c1_label": dom["labels"][1],
295
- "c1_value": round(c1, 3),
296
- "verified_b": round(verified, 3),
297
- "error_pct": round(err, 4),
298
- "status": status,
299
- "confidence": confidence,
300
- "system_prompt": system_prompt, # β†’ inject directly into DeepSeek's context
301
- "rule": dom["rule_text"],
302
- }
303
-
304
- # ───────────────────────���─────────────────────────────────────
305
- # 7. OPTIONAL: Ask DeepSeek to reason over the context
306
- # Only runs if DEEPSEEK_URL is set in environment.
307
- # Falls back to showing the raw structured context if not.
308
- # ─────────────────────────────────────────────────────────────
309
- import os, requests as _req
310
-
311
- DEEPSEEK_URL = os.getenv("DEEPSEEK_URL", "https://everydaytok-small-llm.hf.space") # e.g. http://localhost:7860
312
-
313
- def ask_deepseek(context: dict) -> str:
314
- """
315
- Sends the structured context to your DeepSeek server as a plain text
316
- system injection. The LLM reasons in language; the engine reasoned in math.
317
- """
318
- if not DEEPSEEK_URL:
319
- return (
320
- "DeepSeek not connected (set DEEPSEEK_URL env var).\n\n"
321
- "Here is the raw structured context the LLM would receive:\n\n"
322
- + context["system_prompt"]
323
- )
324
- try:
325
- payload = {
326
- "message": (
327
- f"Given the Perspective Engine's findings above, "
328
- f"explain what a {context['c0_label']} of {context['c0_value']} "
329
- f"and a {context['c1_label']} of {context['c1_value']} means "
330
- f"in the context of the {context['domain']} system, "
331
- f"and why it produces {context['target_b']} {context['unit']}."
332
- ),
333
- "system": context["system_prompt"],
334
- }
335
- r = _req.post(f"{DEEPSEEK_URL}/chat", json=payload, timeout=60)
336
- r.raise_for_status()
337
- return r.json().get("response", str(r.json()))
338
- except Exception as e:
339
- return f"DeepSeek call failed: {e}\n\nRaw context:\n{context['system_prompt']}"
340
 
341
- # ─────────────────────────────────────────────────────────────
342
- # 8. AUTO TEST SUITE
343
- # ─────────────────────────────────────────────────────────────
344
- _test_results = []
345
 
346
- def run_tests():
347
- global _phase, _test_results
348
- _phase = "testing"
349
  results = []
350
- for did, target in TEST_SUITE:
351
- dom = DOMAINS[did]
352
- t0 = time.time()
353
- c0, c1, verified, err = retrace(did, target)
354
- ctx = build_llm_context(did, target, c0, c1, verified, err)
355
- ms = round((time.time()-t0)*1000, 1)
356
- tick = "βœ…" if err < 5.0 else "⚠️"
357
- log(f"{tick} {dom['name']:8s} | target={target:>6.1f} | "
358
- f"{dom['labels'][0]}={c0:.2f} {dom['labels'][1]}={c1:.2f} | "
359
- f"verified={verified:.2f} | err={err:.3f}% | {ms}ms")
360
- results.append({**ctx, "ms": ms})
361
- _test_results = results
362
- _phase = "done"
363
- log("All tests complete.")
364
-
365
- # ─────────────────────────────────────────────────────────────
366
- # 9. LAUNCH ON BOOT
367
- # ─────────────────────────────────────────────────────────────
368
- threading.Thread(
369
- target=run_training, kwargs={"seed": 42}, daemon=True
370
- ).start()
371
-
372
- # ─────────────────────────────────────────────────────────────
373
- # 10. GRADIO UI
374
- # ─────────────────────────────────────────────────────────────
375
- DOMAIN_NAMES = {v["name"]: k for k, v in DOMAINS.items()}
376
-
377
- def ui_solve(domain_name, target_b):
378
- if _phase != "ready" and _phase != "done" and _phase != "testing":
379
- return f"Engine is still {_phase}. Wait for training to finish.", ""
380
- did = DOMAIN_NAMES[domain_name]
381
- c0, c1, verified, err = retrace(did, float(target_b))
382
- ctx = build_llm_context(did, float(target_b), c0, c1, verified, err)
383
- llm_out = ask_deepseek(ctx)
384
- summary = json.dumps({k: v for k, v in ctx.items() if k != "system_prompt"}, indent=2)
385
- return summary, llm_out
386
-
387
- def get_log(): return "\n".join(_train_log[-60:])
388
-
389
- def get_table():
390
- if not _test_results: return []
391
- return [
392
- [r["domain"], r["target_b"], r["unit"],
393
- f"{r['c0_label']}: {r['c0_value']}",
394
- f"{r['c1_label']}: {r['c1_value']}",
395
- f"{r['verified_b']}",
396
- f"{r['error_pct']}% {'βœ…' if r['error_pct']<5 else '⚠️'}"]
397
- for r in _test_results
398
- ]
399
-
400
- def get_phase_md():
401
- icons = {"idle":"⏸","generating":"🌱","training":"🧠",
402
- "testing":"πŸ”¬","ready":"βœ…","done":"βœ…","error":"❌"}
403
- return f"## {icons.get(_phase,'❓')} Phase: **{_phase.upper()}**"
404
-
405
- with gr.Blocks(title="Perspective Engine β€” Corrected Bridge", theme=gr.themes.Monochrome()) as demo:
406
- gr.Markdown(
407
- "# 🧠 Perspective Engine β€” Corrected Architecture\n"
408
- "The engine finds hidden variables. Language is handled separately. No tensor injection."
409
- )
410
 
411
- phase_md = gr.Markdown(get_phase_md())
412
-
413
- with gr.Tabs():
414
- with gr.Tab("πŸ“Š Auto Test Results"):
415
- results_table = gr.Dataframe(
416
- headers=["Domain","Target","Unit","Hidden Var 1","Hidden Var 2","Verified","Error"],
417
- value=get_table(), interactive=False, wrap=True
418
- )
419
-
420
- with gr.Tab("πŸ” Manual Solve + LLM Context"):
421
- with gr.Row():
422
- domain_dd = gr.Dropdown(
423
- choices=list(DOMAIN_NAMES.keys()), value="Market", label="Domain"
424
- )
425
- target_sl = gr.Slider(50, 1000, value=450, label="Target B (observed byproduct)")
426
- solve_btn = gr.Button("Retrace Hidden Variables", variant="primary")
427
- with gr.Row():
428
- raw_json = gr.Code(label="Structured Context (sent to LLM)", language="json", lines=14)
429
- llm_out = gr.Textbox(label="LLM Explanation (if DeepSeek connected)", lines=14)
430
- solve_btn.click(ui_solve, [domain_dd, target_sl], [raw_json, llm_out])
431
-
432
- with gr.Tab("πŸ“‹ Live Log"):
433
- log_box = gr.Textbox(value=get_log(), lines=20, interactive=False, autoscroll=True)
434
-
435
- timer = gr.Timer(value=2)
436
- timer.tick(
437
- fn=lambda: (get_phase_md(), get_log(), get_table()),
438
- outputs=[phase_md, log_box, results_table]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  )
 
 
440
 
441
- if __name__ == "__main__":
442
- demo.launch(share=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ universal_constraint_engine_v2.py
3
+ ══════════════════════════════════════════════════════════════════════════════
4
+ Universal Constraint Engine β€” v2 (Single Script)
5
+
6
+ Theory being validated:
7
+ A = constraint / rule (text β†’ frozen sentence embedding)
8
+ B = observed outcome (normalised float)
9
+ C = hidden variables (what the engine retraces via reverse diffusion)
10
+
11
+ What this script does on a single run:
12
+ 1. FETCH β€” stream real triples from 3 sources:
13
+ (a) Synthetic formula families (physics, economics, chemistry)
14
+ (b) Executable Python function templates β†’ sample C, run, get B
15
+ (c) HuggingFace C4 stream β†’ regex-extract explicit variable=value patterns
16
+ 2. ENCODE β€” embed every A string with frozen sentence-transformers/all-MiniLM-L6-v2
17
+ 3. TRAIN β€” cross-attention diffusion network (~5–8M params)
18
+ 4. EVALUATE β€” self-test suite across all domains, log every number
19
+ 5. BRIDGE β€” send structured JSON context to external LLM via /chat endpoint
20
+
21
+ Run anywhere:
22
+ pip install torch transformers sentence-transformers datasets requests gradio
23
+ python universal_constraint_engine_v2.py
24
+
25
+ Or set LLM_CHAT_URL env var to point at your DeepSeek server:
26
+ LLM_CHAT_URL=http://your-server:7860 python universal_constraint_engine_v2.py
27
+ ══════════════════════════════════════════════════════════════════════════════
28
  """
29
 
30
+ # ─────────────────────────────────────────────────────────────────────────────
31
+ # 0. BOOTSTRAP β€” catch import errors loudly so the user knows exactly what's missing
32
+ # ─────────────────────────────────────────────────────────────────────────────
33
+ import sys, os, time, math, re, json, random, threading, traceback
34
+ from datetime import datetime
35
+
36
+ def ts():
37
+ return datetime.now().strftime("%H:%M:%S.%f")[:-3]
38
+
39
+ def log(msg, level="INFO"):
40
+ prefix = {"INFO": " ", "WARN": "⚠ ", "ERROR": "βœ– ", "OK": "βœ” ", "HEAD": "━━"}.get(level, " ")
41
+ line = f"[{ts()}] {prefix} {msg}"
42
+ print(line, flush=True)
43
+ _LOG_LINES.append(line)
44
+ if len(_LOG_LINES) > 500:
45
+ _LOG_LINES.pop(0)
46
+
47
+ _LOG_LINES = []
48
+
49
+ log("Importing core libraries…", "HEAD")
50
+ try:
51
+ import torch
52
+ import torch.nn as nn
53
+ import torch.optim as optim
54
+ import torch.nn.functional as F
55
+ log(f"torch {torch.__version__} CUDA={torch.cuda.is_available()}", "OK")
56
+ except ImportError as e:
57
+ print(f"FATAL: torch not found β€” {e}"); sys.exit(1)
58
+
59
+ try:
60
+ from sentence_transformers import SentenceTransformer
61
+ log("sentence-transformers ready", "OK")
62
+ SENT_OK = True
63
+ except ImportError:
64
+ log("sentence-transformers not installed β€” A embeddings will use random fallback", "WARN")
65
+ SENT_OK = False
66
+
67
+ try:
68
+ from datasets import load_dataset
69
+ log("HuggingFace datasets ready", "OK")
70
+ HF_OK = True
71
+ except ImportError:
72
+ log("datasets not installed β€” C4 stream phase will be skipped", "WARN")
73
+ HF_OK = False
74
+
75
+ try:
76
+ import requests as _req
77
+ log("requests ready", "OK")
78
+ except ImportError:
79
+ log("requests not installed β€” LLM bridge will be skipped", "WARN")
80
+ _req = None
81
+
82
+ try:
83
+ import gradio as gr
84
+ log("gradio ready", "OK")
85
+ GR_OK = True
86
+ except ImportError:
87
+ log("gradio not installed β€” will print results to console only", "WARN")
88
+ GR_OK = False
89
+
90
+ # ─────────────────────────────────────────────────────────────────────────────
91
+ # 1. GLOBAL CONFIG
92
+ # ─────────────────────────────────────────────────────────────────────────────
93
+ log("Loading config…", "HEAD")
94
+
95
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
96
+ USE_AMP = torch.cuda.is_available()
97
+ log(f"Compute device: {DEVICE} AMP: {USE_AMP}", "OK")
98
+
99
+ # Sentence encoder (frozen β€” never updated)
100
+ SENT_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
101
+ A_DIM = 384 # all-MiniLM output dimension
102
+ A_CTX = 128 # projected down for engine cross-attention keys/values
103
+
104
+ # Diffusion
105
+ T_STEPS = 80
106
+ LATENT_D = 2 # hidden variables per problem (normalised to [0,1])
107
+
108
+ # Network
109
+ HIDDEN = 256
110
+ N_HEADS = 4
111
+ DEPTH = 8 # DiT blocks β€” tune down to 6 if memory is tight
112
+ HEAD_DIM = HIDDEN // N_HEADS # 64
113
+
114
+ # Training
115
+ EPOCHS = 600
116
+ BATCH = 256
117
+ LR = 2e-3
118
+
119
+ # Data budgets (per source)
120
+ N_SYNTHETIC = 40_000
121
+ N_CODE = 15_000
122
+ N_C4 = 10_000 # streamed; actual yield may be lower
123
+ C4_SCAN_CAP = 50_000 # max C4 documents to scan for regex matches
124
+
125
+ # LLM bridge
126
+ LLM_CHAT_URL = os.getenv("LLM_CHAT_URL", "") # e.g. http://localhost:7860
127
+ log(f"LLM_CHAT_URL: '{LLM_CHAT_URL}' ({'set' if LLM_CHAT_URL else 'not set β€” bridge skipped'})")
128
+
129
+ # ─────────────────────────────────────────────────────────────────────────────
130
+ # 2. SENTENCE ENCODER (load once, freeze forever)
131
+ # ─────────────────────────────────────────────────────────────────────────────
132
+ log("Loading sentence encoder…", "HEAD")
133
+ _sent_enc = None
134
+
135
+ def get_sent_enc():
136
+ global _sent_enc
137
+ if _sent_enc is None:
138
+ if SENT_OK:
139
+ try:
140
+ log(f"Downloading {SENT_MODEL_NAME} (22M params, ~90MB)…")
141
+ _sent_enc = SentenceTransformer(SENT_MODEL_NAME, device=str(DEVICE))
142
+ for p in _sent_enc.parameters():
143
+ p.requires_grad_(False)
144
+ log(f"Sentence encoder loaded on {DEVICE}", "OK")
145
+ except Exception as e:
146
+ log(f"Sentence encoder load failed: {e}", "ERROR")
147
+ log("Falling back to random 384-dim embeddings", "WARN")
148
+ _sent_enc = None
149
+ else:
150
+ log("sentence-transformers unavailable β€” using random embeddings", "WARN")
151
+ return _sent_enc
152
+
153
+ def encode_texts(texts: list[str]) -> torch.Tensor:
154
+ """Returns [N, A_DIM] float32 tensor."""
155
+ enc = get_sent_enc()
156
+ if enc is None:
157
+ # Deterministic random fallback: same text β†’ same vector via hash
158
+ vecs = []
159
+ for t in texts:
160
+ rng = random.Random(hash(t) & 0xFFFFFFFF)
161
+ vecs.append([rng.gauss(0, 1) for _ in range(A_DIM)])
162
+ return torch.tensor(vecs, dtype=torch.float32, device=DEVICE)
163
+ with torch.no_grad():
164
+ emb = enc.encode(texts, convert_to_tensor=True,
165
+ show_progress_bar=False, batch_size=256)
166
+ return emb.to(dtype=torch.float32, device=DEVICE)
167
+
168
+ # ─────────────────────────────────────────────────────────────────────────────
169
+ # 3. DATA PIPELINE
170
+ # ─────────────────────────────────────────────────────────────────────────────
171
+ log("Defining data pipeline…", "HEAD")
172
+
173
+ # ── 3a. Synthetic formula families ──────────────────────────────────────────
174
+
175
+ FORMULA_FAMILIES = [
176
+ {
177
+ "name": "projectile_drag",
178
+ "rule_text": "Projectile range with exponential atmospheric drag. "
179
+ "Formula: range = (v^2 * sin(2*theta) / 9.81) * exp(-v/100). "
180
+ "Variables: v=launch velocity m/s, theta=angle degrees.",
181
+ "c_ranges": [(10, 120), (5, 85)],
182
+ "b_norm": 600.0,
183
+ "forward": lambda c: ((c[0]**2 * math.sin(2*c[1]*math.pi/180)) / 9.81) * math.exp(-c[0]/100),
184
  },
185
+ {
186
+ "name": "market_price",
187
+ "rule_text": "Non-linear commodity market pricing. "
188
+ "Formula: price = (demand^2 / supply) * 1.2. "
189
+ "Variables: supply=units available, demand=units requested.",
190
+ "c_ranges": [(10, 100), (10, 100)],
191
  "b_norm": 1200.0,
192
+ "forward": lambda c: (c[1]**2 / c[0]) * 1.2,
 
 
 
 
 
 
 
193
  },
194
+ {
195
+ "name": "predator_prey",
196
+ "rule_text": "Lotka-Volterra predator suppression. "
197
+ "Formula: survivors = prey * exp(-0.05 * predators). "
198
+ "Variables: predators=fox count, prey=rabbit population.",
199
+ "c_ranges": [(5, 50), (50, 500)],
200
+ "b_norm": 300.0,
201
+ "forward": lambda c: c[1] * math.exp(-0.05 * c[0]),
202
+ },
203
+ {
204
+ "name": "compound_interest",
205
+ "rule_text": "Compound interest accumulation. "
206
+ "Formula: amount = principal * (1 + rate)^years. "
207
+ "Variables: principal=initial dollars, rate=annual fraction.",
208
+ "c_ranges": [(100, 10000), (0.01, 0.20)],
209
+ "b_norm": 50000.0,
210
+ "forward": lambda c: c[0] * (1 + c[1]) ** 10,
211
+ },
212
+ {
213
+ "name": "ohms_power",
214
+ "rule_text": "Electrical power dissipation. "
215
+ "Formula: power = voltage^2 / resistance. "
216
+ "Variables: voltage=volts, resistance=ohms.",
217
+ "c_ranges": [(1, 240), (1, 1000)],
218
+ "b_norm": 60000.0,
219
+ "forward": lambda c: (c[0]**2) / c[1],
220
+ },
221
+ {
222
+ "name": "fluid_flow",
223
+ "rule_text": "Hagen-Poiseuille laminar flow rate. "
224
+ "Formula: flow = pi * radius^4 * pressure / (8 * viscosity * length). "
225
+ "Variables: radius=pipe radius m, pressure=pressure diff Pa.",
226
+ "c_ranges": [(0.001, 0.05), (100, 100000)],
227
+ "b_norm": 1.0,
228
+ "forward": lambda c: math.pi * (c[0]**4) * c[1] / (8 * 0.001 * 1.0),
229
+ },
230
+ {
231
+ "name": "chemical_rate",
232
+ "rule_text": "Arrhenius reaction rate law. "
233
+ "Formula: rate = A * exp(-Ea / (R * T)) where R=8.314, A=1e6. "
234
+ "Variables: Ea=activation energy J/mol, T=temperature Kelvin.",
235
+ "c_ranges": [(5000, 80000), (200, 1000)],
236
+ "b_norm": 1e6,
237
+ "forward": lambda c: 1e6 * math.exp(-c[0] / (8.314 * c[1])),
238
+ },
239
+ {
240
+ "name": "population_growth",
241
+ "rule_text": "Logistic population growth model. "
242
+ "Formula: final = K / (1 + ((K - N0) / N0) * exp(-r * t)) where t=20, K=10000. "
243
+ "Variables: N0=initial population, r=growth rate.",
244
+ "c_ranges": [(10, 1000), (0.01, 0.5)],
245
+ "b_norm": 10000.0,
246
+ "forward": lambda c: 10000 / (1 + ((10000 - c[0]) / max(c[0], 1)) * math.exp(-c[1] * 20)),
247
+ },
248
+ ]
249
+
250
+ def generate_synthetic(n=N_SYNTHETIC, seed=42) -> tuple:
251
+ """Returns (A_texts, B_vals, C_norm_vals) as Python lists."""
252
+ random.seed(seed)
253
+ A_texts, B_vals, C_norms = [], [], []
254
+ per = n // len(FORMULA_FAMILIES)
255
+ total_ok = 0; total_skip = 0
256
+
257
+ for fam in FORMULA_FAMILIES:
258
+ ok = 0; skip = 0
259
+ for _ in range(per * 3): # oversample to account for out-of-range B
260
+ if ok >= per: break
261
+ c_raw = [random.uniform(*r) for r in fam["c_ranges"]]
262
+ try:
263
+ b_raw = fam["forward"](c_raw)
264
+ except (ZeroDivisionError, ValueError, OverflowError):
265
+ skip += 1; continue
266
+
267
+ if not math.isfinite(b_raw) or b_raw <= 0:
268
+ skip += 1; continue
269
+
270
+ b_norm = b_raw / fam["b_norm"]
271
+ if not (0.0 < b_norm < 1.0):
272
+ skip += 1; continue
273
+
274
+ c_norm = [(c_raw[i] - fam["c_ranges"][i][0]) /
275
+ (fam["c_ranges"][i][1] - fam["c_ranges"][i][0])
276
+ for i in range(2)]
277
+ c_norm = [max(0.0, min(1.0, v)) for v in c_norm]
278
+
279
+ A_texts.append(fam["rule_text"])
280
+ B_vals.append(b_norm)
281
+ C_norms.append(c_norm)
282
+ ok += 1
283
+
284
+ total_ok += ok; total_skip += skip
285
+ log(f" {fam['name']:25s}: {ok:>5} triples ({skip} skipped)")
286
+
287
+ log(f"Synthetic: {total_ok} total triples ({total_skip} rejected)", "OK")
288
+ return A_texts, B_vals, C_norms
289
+
290
+
291
+ # ── 3b. Executable Python function templates ────────────────────────────────
292
+
293
+ CODE_TEMPLATES = [
294
+ {
295
+ "text": "def score(accuracy, recall):\n return 2*(accuracy*recall)/(accuracy+recall+1e-9)",
296
+ "ranges": [(0.1, 1.0), (0.1, 1.0)],
297
+ "b_norm": 1.0,
298
+ "fn": lambda c: 2*(c[0]*c[1])/(c[0]+c[1]+1e-9),
299
+ },
300
+ {
301
+ "text": "def revenue(price, quantity):\n elasticity = -1.5\n return price * quantity * (1 + elasticity * (price/50 - 1))",
302
+ "ranges": [(10, 100), (100, 10000)],
303
+ "b_norm": 1_000_000.0,
304
+ "fn": lambda c: c[0] * c[1] * (1 + (-1.5) * (c[0]/50 - 1)),
305
+ },
306
+ {
307
+ "text": "def signal_snr(power, noise):\n import math\n return 10 * math.log10(power / max(noise, 1e-9))",
308
+ "ranges": [(0.001, 1000), (0.001, 10)],
309
+ "b_norm": 60.0,
310
+ "fn": lambda c: 10 * math.log10(c[0] / max(c[1], 1e-9)),
311
+ },
312
+ {
313
+ "text": "def bond_duration(coupon_rate, yield_rate):\n T = 10\n return sum(t * coupon_rate * (1+yield_rate)**-t for t in range(1,T+1)) + T*(1+yield_rate)**-T",
314
+ "ranges": [(0.01, 0.15), (0.01, 0.20)],
315
+ "b_norm": 15.0,
316
+ "fn": lambda c: sum(t*c[0]*(1+c[1])**-t for t in range(1,11)) + 10*(1+c[1])**-10,
317
+ },
318
+ {
319
+ "text": "def mixing_entropy(p1, p2):\n import math\n p3 = max(1e-9, 1-p1-p2)\n return -(p1*math.log(p1+1e-9)+p2*math.log(p2+1e-9)+p3*math.log(p3))",
320
+ "ranges": [(0.05, 0.60), (0.05, 0.60)],
321
+ "b_norm": 2.0,
322
+ "fn": lambda c: -(c[0]*math.log(c[0]+1e-9) + c[1]*math.log(c[1]+1e-9) +
323
+ max(1e-9, 1-c[0]-c[1])*math.log(max(1e-9, 1-c[0]-c[1]))),
324
+ },
325
+ {
326
+ "text": "def satellite_orbit(mass, radius):\n G = 6.674e-11; M = 5.972e24\n return math.sqrt(G * M / max(radius, 1)) * mass / 1e6",
327
+ "ranges": [(100, 5000), (6.4e6, 4.2e7)],
328
+ "b_norm": 50.0,
329
+ "fn": lambda c: math.sqrt(6.674e-11 * 5.972e24 / max(c[1], 1)) * c[0] / 1e6,
330
  },
 
 
 
 
 
 
 
 
 
 
331
  ]
332
 
333
+ def generate_code_triples(n=N_CODE, seed=99) -> tuple:
334
+ random.seed(seed)
335
+ A_texts, B_vals, C_norms = [], [], []
336
+ per = n // len(CODE_TEMPLATES)
337
+ total_ok = 0; total_skip = 0
338
+
339
+ for tmpl in CODE_TEMPLATES:
340
+ ok = 0; skip = 0
341
+ for _ in range(per * 5):
342
+ if ok >= per: break
343
+ c_raw = [random.uniform(*r) for r in tmpl["ranges"]]
344
+ try:
345
+ b_raw = tmpl["fn"](c_raw)
346
+ except Exception:
347
+ skip += 1; continue
348
+ if not math.isfinite(b_raw):
349
+ skip += 1; continue
350
+ b_norm = b_raw / tmpl["b_norm"]
351
+ if not (0.001 < b_norm < 0.999):
352
+ skip += 1; continue
353
+ c_norm = [(c_raw[i] - tmpl["ranges"][i][0]) /
354
+ (tmpl["ranges"][i][1] - tmpl["ranges"][i][0])
355
+ for i in range(2)]
356
+ c_norm = [max(0.0, min(1.0, v)) for v in c_norm]
357
+ A_texts.append(tmpl["text"])
358
+ B_vals.append(b_norm)
359
+ C_norms.append(c_norm)
360
+ ok += 1
361
+
362
+ total_ok += ok; total_skip += skip
363
+ log(f" code_tmpl '{tmpl['text'][:40]}…': {ok} triples")
364
+
365
+ log(f"Code templates: {total_ok} total ({total_skip} rejected)", "OK")
366
+ return A_texts, B_vals, C_norms
367
+
368
+
369
+ # ── 3c. C4 stream β€” extract explicit variable=value patterns ────────────────
370
+
371
+ # Patterns that look like: "with v=90 and theta=53, range=320"
372
+ # or "x1=0.4, x2=0.7 yields output=1.23"
373
+ C4_REGEX = re.compile(
374
+ r'(?P<rule>[^.]{20,120}(?:formula|equation|law|rule|function|model)[^.]{0,60})\.'
375
+ r'|'
376
+ r'(?:where|with|given|using|when)\s+'
377
+ r'(?P<var1>[a-zA-Z_]\w{0,15})\s*[=β‰ˆ]\s*(?P<val1>-?\d+\.?\d*(?:e[+-]?\d+)?)'
378
+ r'[,\s]+(?:and\s+)?'
379
+ r'(?P<var2>[a-zA-Z_]\w{0,15})\s*[=β‰ˆ]\s*(?P<val2>-?\d+\.?\d*(?:e[+-]?\d+)?)'
380
+ r'[,\s]*(?:,\s*(?:the\s+)?(?P<out_var>[a-zA-Z_]\w{0,15})\s*'
381
+ r'(?:=|is|equals|becomes|gives)\s*(?P<out_val>-?\d+\.?\d*(?:e[+-]?\d+)?))?',
382
+ re.IGNORECASE
383
+ )
384
+
385
+ def stream_c4_triples(max_triples=N_C4, scan_cap=C4_SCAN_CAP) -> tuple:
386
+ """
387
+ Streams C4 and extracts (rule_sentence, B, C) triples.
388
+ B and C are extracted from explicit numeric variable=value patterns.
389
+ Falls back to empty lists if datasets not available.
390
+ """
391
+ if not HF_OK:
392
+ log("datasets unavailable β€” C4 phase skipped", "WARN")
393
+ return [], [], []
394
+
395
+ log(f"Streaming C4 (scan up to {scan_cap} docs for {max_triples} triples)…")
396
+ A_texts, B_vals, C_norms = [], [], []
397
+ scanned = 0; found = 0; parse_errors = 0
398
+
399
+ try:
400
+ ds = load_dataset("allenai/c4", "en", split="train", streaming=True,
401
+ trust_remote_code=True)
402
+ except Exception as e:
403
+ log(f"C4 load failed: {e}", "ERROR")
404
+ log("Trying fallback dataset: wikitext-103-raw-v1", "WARN")
405
+ try:
406
+ ds = load_dataset("wikitext", "wikitext-103-raw-v1",
407
+ split="train", streaming=True)
408
+ except Exception as e2:
409
+ log(f"Fallback also failed: {e2}", "ERROR")
410
+ return [], [], []
411
+
412
+ try:
413
+ for doc in ds:
414
+ if scanned >= scan_cap or found >= max_triples:
415
+ break
416
+ scanned += 1
417
+ text = doc.get("text", "")
418
+ if len(text) < 40:
419
+ continue
420
+
421
+ for m in C4_REGEX.finditer(text):
422
+ try:
423
+ v1 = float(m.group("val1"))
424
+ v2 = float(m.group("val2"))
425
+ out = m.group("out_val")
426
+ if out is None:
427
+ continue
428
+ b_raw = float(out)
429
+ if b_raw <= 0 or not math.isfinite(b_raw):
430
+ continue
431
+
432
+ # Build rule text from surrounding sentence
433
+ start = max(0, m.start() - 80)
434
+ rule_sentence = text[start: m.end() + 80].replace('\n', ' ').strip()
435
+ rule_sentence = rule_sentence[:200]
436
+
437
+ # Normalise: use per-sample scale (store as b_norm=b_raw, C as ratio)
438
+ b_norm_val = b_raw / (abs(b_raw) * 2 + 1e-9) # crude [0,1]
439
+ c1_norm = abs(v1) / (abs(v1) * 2 + 1e-9)
440
+ c2_norm = abs(v2) / (abs(v2) * 2 + 1e-9)
441
+
442
+ if not (0.01 < b_norm_val < 0.99):
443
+ continue
444
+
445
+ A_texts.append(rule_sentence)
446
+ B_vals.append(b_norm_val)
447
+ C_norms.append([c1_norm, c2_norm])
448
+ found += 1
449
+
450
+ if found % 500 == 0:
451
+ log(f" C4 progress: {found} triples ({scanned} docs scanned)")
452
+
453
+ except (ValueError, TypeError) as pe:
454
+ parse_errors += 1
455
+ continue
456
+
457
+ except Exception as e:
458
+ log(f"C4 stream interrupted: {e}", "WARN")
459
+ log(traceback.format_exc(), "ERROR")
460
+
461
+ log(f"C4 scan complete: {found} triples from {scanned} docs "
462
+ f"({parse_errors} parse errors)", "OK")
463
+ return A_texts, B_vals, C_norms
464
+
465
+
466
+ # ── 3d. Combine and tensorise ────────────────────────────────────────────────
467
+
468
+ _dataset_cache = None # (A_emb, B_tensor, C_tensor)
469
+
470
+ def build_dataset(seed=42) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
471
+ global _dataset_cache
472
+ if _dataset_cache is not None:
473
+ return _dataset_cache
474
+
475
+ log("Building combined dataset…", "HEAD")
476
+
477
+ log("Phase A: synthetic formula families")
478
+ sA, sB, sC = generate_synthetic(N_SYNTHETIC, seed=seed)
479
+
480
+ log("Phase B: executable code templates")
481
+ cA, cB, cC = generate_code_triples(N_CODE, seed=seed+1)
482
+
483
+ log("Phase C: C4 web stream")
484
+ wA, wB, wC = stream_c4_triples(N_C4, C4_SCAN_CAP)
485
+
486
+ all_A = sA + cA + wA
487
+ all_B = sB + cB + wB
488
+ all_C = sC + cC + wC
489
+
490
+ log(f"Total triples before embedding: {len(all_A)}", "OK")
491
+ log(f" Synthetic: {len(sA)}")
492
+ log(f" Code templates: {len(cA)}")
493
+ log(f" C4 web: {len(wA)}")
494
+
495
+ if len(all_A) == 0:
496
+ log("FATAL: zero triples collected β€” check data pipeline", "ERROR")
497
+ sys.exit(1)
498
+
499
+ # Embed all A strings in one batched pass
500
+ log(f"Encoding {len(all_A)} constraint texts β†’ {A_DIM}-dim embeddings…")
501
+ t0 = time.time()
502
+ A_emb = encode_texts(all_A) # [N, A_DIM]
503
+ log(f"Encoding done in {time.time()-t0:.1f}s shape={tuple(A_emb.shape)}", "OK")
504
+
505
+ B_tensor = torch.tensor(all_B, dtype=torch.float32, device=DEVICE).unsqueeze(1)
506
+ C_tensor = torch.tensor(all_C, dtype=torch.float32, device=DEVICE)
507
+
508
+ log(f"Final dataset tensors: A{tuple(A_emb.shape)} B{tuple(B_tensor.shape)} C{tuple(C_tensor.shape)}", "OK")
509
+
510
+ _dataset_cache = (A_emb, B_tensor, C_tensor)
511
+ return _dataset_cache
512
+
513
+
514
+ # ─────────────────────────────────────────────────────────────────────────────
515
+ # 4. NETWORK ARCHITECTURE (~5–8M parameters)
516
+ # ─────────────────────────────────────────────────────────────────────────────
517
+ log("Defining network architecture…", "HEAD")
518
+
519
+ class ConstraintProjector(nn.Module):
520
+ """Compresses frozen sentence embedding A [B, A_DIM] β†’ [B, A_CTX] for cross-attention."""
521
+ def __init__(self):
522
+ super().__init__()
523
+ self.net = nn.Sequential(
524
+ nn.Linear(A_DIM, 256), nn.GELU(),
525
+ nn.Linear(256, A_CTX), nn.LayerNorm(A_CTX)
526
+ )
527
+ def forward(self, a): return self.net(a).unsqueeze(1) # [B, 1, A_CTX]
528
+
529
 
530
+ class DiTBlock(nn.Module):
531
+ """
532
+ Diffusion Transformer block.
533
+ Self-attention on the latent c_t.
534
+ Cross-attention: c_t queries A_ctx (the constraint memory).
535
+ """
536
+ def __init__(self, hidden, n_heads, ctx_dim, shared_kv=None):
537
+ super().__init__()
538
+ self.norm1 = nn.LayerNorm(hidden)
539
+ self.norm2 = nn.LayerNorm(hidden)
540
+ self.norm3 = nn.LayerNorm(hidden)
541
+
542
+ self.self_attn = nn.MultiheadAttention(hidden, n_heads, batch_first=True)
543
+
544
+ # Cross-attention: Q from hidden, K/V from ctx_dim
545
+ self.cross_q = nn.Linear(hidden, hidden)
546
+ # Shared KV across all blocks saves parameters
547
+ self.shared_kv = shared_kv # nn.Linear(ctx_dim, hidden*2) passed in
548
+ self.cross_out = nn.Linear(hidden, hidden)
549
+
550
+ self.ffn = nn.Sequential(
551
+ nn.Linear(hidden, hidden * 4), nn.GELU(),
552
+ nn.Linear(hidden * 4, hidden)
553
+ )
554
+
555
+ def forward(self, x, a_ctx):
556
+ # x: [B, 1, HIDDEN] (single-token latent, treated as sequence of 1)
557
+ # a_ctx: [B, 1, A_CTX]
558
+
559
+ # Self-attention
560
+ x2, _ = self.self_attn(self.norm1(x), self.norm1(x), self.norm1(x))
561
+ x = x + x2
562
+
563
+ # Cross-attention
564
+ q = self.cross_q(self.norm2(x)) # [B, 1, HIDDEN]
565
+ kv = self.shared_kv(a_ctx) # [B, 1, HIDDEN*2]
566
+ k, v = kv.chunk(2, dim=-1)
567
+ # Manual scaled dot-product (cross-dim attention: q is HIDDEN, k/v are HIDDEN)
568
+ scale = (HEAD_DIM) ** -0.5
569
+ attn_w = torch.softmax((q @ k.transpose(-2, -1)) * scale, dim=-1)
570
+ x2 = self.cross_out(attn_w @ v)
571
+ x = x + x2
572
+
573
+ # FFN
574
+ x = x + self.ffn(self.norm3(x))
575
+ return x
576
+
577
+
578
+ class UniversalConstraintEngine(nn.Module):
579
+ """
580
+ Input:
581
+ c_t [B, LATENT_D] β€” noisy latent at timestep t
582
+ t [B, 1] β€” normalised timestep
583
+ a_emb [B, A_DIM] β€” frozen sentence embedding of constraint A
584
+ b [B, 1] β€” normalised observed outcome B
585
+ Output:
586
+ eps_pred [B, LATENT_D] β€” predicted noise (standard diffusion objective)
587
+ """
588
+ def __init__(self):
589
+ super().__init__()
590
+ in_dim = LATENT_D + 1 + 1 # c_t + t + b (A enters via cross-attn)
591
+
592
+ self.a_proj = ConstraintProjector()
593
+ self.shared_kv = nn.Linear(A_CTX, HIDDEN * 2) # shared across all blocks
594
+ self.input_proj = nn.Linear(in_dim, HIDDEN)
595
+
596
+ self.blocks = nn.ModuleList([
597
+ DiTBlock(HIDDEN, N_HEADS, A_CTX, self.shared_kv)
598
+ for _ in range(DEPTH)
599
+ ])
600
+
601
+ self.head = nn.Sequential(
602
+ nn.LayerNorm(HIDDEN),
603
+ nn.Linear(HIDDEN, LATENT_D)
604
+ )
605
+
606
+ def forward(self, c_t, t, a_emb, b):
607
+ a_ctx = self.a_proj(a_emb) # [B, 1, A_CTX]
608
+ x = self.input_proj(torch.cat([c_t, t, b], dim=-1)).unsqueeze(1) # [B, 1, HIDDEN]
609
+ for blk in self.blocks:
610
+ x = blk(x, a_ctx)
611
+ return self.head(x.squeeze(1)) # [B, LATENT_D]
612
+
613
+
614
+ def count_params(model):
615
+ total = sum(p.numel() for p in model.parameters())
616
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
617
+ return total, trainable
618
+
619
+ # ─────────────────────────────────────────────────────────────────────────────
620
+ # 5. DIFFUSION SCHEDULE
621
+ # ─────────────────────────────────────────────────────────────────────────────
622
  def make_schedule(T, s=0.008):
623
+ x = torch.linspace(0, T, T+1, device=DEVICE)
624
+ f = torch.cos(((x/T)+s)/(1+s)*math.pi/2)**2
625
  acp = f / f[0]
626
  betas = torch.clamp(1.0 - acp[1:]/acp[:-1], 1e-4, 0.999)
627
  return torch.cumprod(1.0 - betas, dim=0)
628
 
629
  ACP = make_schedule(T_STEPS)
630
 
631
+ # ─────────────────────────────────────────────────────────────────────────────
632
+ # 6. TRAINING LOOP
633
+ # ─────────────────────────────────────────────────────────────────────────────
634
+ _engine : UniversalConstraintEngine = None
635
+ _train_state = {"phase": "idle", "epoch": 0, "loss": None, "elapsed": None}
636
 
637
+ def run_full_pipeline(seed=42):
638
+ global _engine, ACP, _train_state
 
 
 
 
639
 
640
+ try:
641
+ # ── Data ──────────────────────────────────────────────────────────────
642
+ _train_state["phase"] = "fetching"
643
+ A_emb, B_tensor, C_tensor = build_dataset(seed)
644
+ n = len(A_emb)
645
+
646
+ # ── Model ──────────────────────────────────────────────────────────────
647
+ _train_state["phase"] = "training"
648
+ log("Instantiating UniversalConstraintEngine…", "HEAD")
649
+ model = UniversalConstraintEngine().to(DEVICE)
650
+ total, trainable = count_params(model)
651
+ log(f"Parameters: {total:,} total | {trainable:,} trainable", "OK")
652
+
653
+ opt = optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)
654
+ sched = optim.lr_scheduler.CosineAnnealingLR(opt, EPOCHS)
655
+ scaler = torch.cuda.amp.GradScaler(enabled=USE_AMP)
656
+ ACP = make_schedule(T_STEPS)
657
+
658
+ log(f"Training: {EPOCHS} epochs | batch={BATCH} | n={n} | device={DEVICE}", "HEAD")
659
+ t0 = time.time()
660
+
661
+ for ep in range(1, EPOCHS+1):
662
+ model.train()
663
+ perm = torch.randperm(n, device=DEVICE)
664
+ ep_loss = 0.0; nb = 0
665
+
666
+ for i in range(0, n, BATCH):
667
+ idx = perm[i:i+BATCH]
668
+ a_b = A_emb[idx]
669
+ b_b = B_tensor[idx]
670
+ c_b = C_tensor[idx]
671
+
672
+ t_int = torch.randint(0, T_STEPS, (len(a_b),), device=DEVICE)
673
+ t_norm = (t_int.float()/T_STEPS).unsqueeze(1)
674
+ acp_t = ACP[t_int].unsqueeze(1)
675
+ noise = torch.randn_like(c_b)
676
+ c_noisy = acp_t.sqrt()*c_b + (1-acp_t).sqrt()*noise
677
+
678
+ with torch.cuda.amp.autocast(enabled=USE_AMP):
679
+ eps_pred = model(c_noisy, t_norm, a_b, b_b)
680
+ loss = F.mse_loss(eps_pred, noise)
681
+
682
+ opt.zero_grad()
683
+ scaler.scale(loss).backward()
684
+ scaler.unscale_(opt)
685
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
686
+ scaler.step(opt); scaler.update()
687
+ ep_loss += loss.item(); nb += 1
688
+
689
+ sched.step()
690
+ avg = ep_loss / nb
691
+ _train_state.update({"epoch": ep, "loss": round(avg, 5)})
692
+
693
+ if ep % 50 == 0:
694
+ elapsed = time.time() - t0
695
+ log(f"Epoch {ep:>4}/{EPOCHS} loss={avg:.5f} "
696
+ f"lr={sched.get_last_lr()[0]:.2e} elapsed={elapsed:.0f}s")
697
+
698
+ elapsed = round(time.time()-t0, 1)
699
+ log(f"Training complete: {elapsed}s final_loss={avg:.5f}", "OK")
700
+ _train_state["elapsed"] = elapsed
701
+
702
+ model.eval()
703
+ _engine = model
704
+
705
+ # ── Evaluation ────────────────────────────────────────────────────────
706
+ _train_state["phase"] = "evaluating"
707
+ run_self_evaluation()
708
+
709
+ _train_state["phase"] = "done"
710
+ log("Pipeline finished.", "OK")
711
+
712
+ except Exception as e:
713
+ _train_state["phase"] = "error"
714
+ log(f"Pipeline crashed: {e}", "ERROR")
715
+ log(traceback.format_exc(), "ERROR")
716
+
717
+
718
+ # ─────────────────────────────────────────────────────────────────────────────
719
+ # 7. INFERENCE β€” REVERSE DIFFUSION
720
+ # ─────────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
 
722
  @torch.no_grad()
723
+ def retrace(rule_text: str, b_norm: float) -> tuple[float, float, torch.Tensor]:
724
+ """
725
+ Given a text rule and normalised B, run reverse diffusion.
726
+ Returns (c0_norm, c1_norm, c_final_tensor).
727
+ """
728
+ a_emb = encode_texts([rule_text]) # [1, A_DIM]
729
+ B = torch.tensor([[b_norm]], device=DEVICE)
730
+ c_t = torch.randn(1, LATENT_D, device=DEVICE)
731
 
732
  for t in reversed(range(T_STEPS)):
733
  t_n = torch.tensor([[t/T_STEPS]], device=DEVICE)
734
+ eps_pred = _engine(c_t, t_n, a_emb, B)
735
  acp_t = ACP[t]
736
  acp_prev = ACP[t-1] if t > 0 else torch.tensor(1.0, device=DEVICE)
737
+ x0 = ((c_t - (1-acp_t).sqrt()*eps_pred) / acp_t.sqrt()).clamp(0, 1)
738
  c_t = acp_prev.sqrt()*x0 + (1-acp_prev).sqrt()*eps_pred
739
 
740
+ c_final = c_t.clamp(0, 1)
741
+ cn = c_final.squeeze().cpu().tolist()
742
+ return cn[0], cn[1], c_final
743
+
744
+
745
+ # ─────────────────────────────────────────────────────────────────────────────
746
+ # 8. SELF-EVALUATION SUITE
747
+ # ─────────────────────────────────────────────────────────────────────────────
748
+
749
+ EVAL_CASES = [
750
+ # (rule_text, c0_true_raw, c1_true_raw, family_index_in FORMULA_FAMILIES)
751
+ # We compute B from the known C, then ask the engine to retrace C from B.
752
+ (0, 90.0, 53.0), # projectile: v=90, ΞΈ=53 β†’ ~320m
753
+ (0, 60.0, 30.0), # projectile: v=60, ΞΈ=30
754
+ (1, 25.0, 90.0), # market: supply=25, demand=90
755
+ (1, 70.0, 50.0), # market: supply=70, demand=50
756
+ (2, 15.0, 200.0), # predator-prey: foxes=15, rabbits=200
757
+ (2, 30.0, 400.0), # predator-prey: foxes=30, rabbits=400
758
+ (3, 1000.0, 0.07), # compound interest: $1000 @ 7%
759
+ (4, 120.0, 50.0), # ohms: 120V / 50Ξ©
760
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
+ _eval_results = []
 
 
 
763
 
764
+ def run_self_evaluation():
765
+ global _eval_results
766
+ log("Self-evaluation suite starting…", "HEAD")
767
  results = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
 
769
+ for fam_idx, c0_raw, c1_raw in EVAL_CASES:
770
+ fam = FORMULA_FAMILIES[fam_idx]
771
+ try:
772
+ b_raw = fam["forward"]([c0_raw, c1_raw])
773
+ except Exception as e:
774
+ log(f" Forward sim failed for {fam['name']}: {e}", "ERROR"); continue
775
+
776
+ if not math.isfinite(b_raw) or b_raw <= 0:
777
+ log(f" Skipping {fam['name']} (b_raw={b_raw})", "WARN"); continue
778
+
779
+ b_norm = b_raw / fam["b_norm"]
780
+ if not (0.0 < b_norm < 1.0):
781
+ log(f" Skipping {fam['name']} (b_norm={b_norm:.3f} out of range)", "WARN"); continue
782
+
783
+ # Normalise true C for comparison
784
+ c0_norm_true = (c0_raw - fam["c_ranges"][0][0]) / (fam["c_ranges"][0][1] - fam["c_ranges"][0][0])
785
+ c1_norm_true = (c1_raw - fam["c_ranges"][1][0]) / (fam["c_ranges"][1][1] - fam["c_ranges"][1][0])
786
+
787
+ t0 = time.time()
788
+ c0_pred, c1_pred, _ = retrace(fam["rule_text"], b_norm)
789
+ ms = round((time.time()-t0)*1000, 1)
790
+
791
+ # Denormalise prediction
792
+ c0_pred_raw = c0_pred*(fam["c_ranges"][0][1]-fam["c_ranges"][0][0]) + fam["c_ranges"][0][0]
793
+ c1_pred_raw = c1_pred*(fam["c_ranges"][1][1]-fam["c_ranges"][1][0]) + fam["c_ranges"][1][0]
794
+
795
+ # Forward-verify with predicted C
796
+ try:
797
+ b_pred = fam["forward"]([c0_pred_raw, c1_pred_raw])
798
+ except Exception:
799
+ b_pred = float("nan")
800
+
801
+ err = abs(b_pred - b_raw) / max(abs(b_raw), 1e-6) * 100 if math.isfinite(b_pred) else 999.0
802
+ tick = "βœ…" if err < 5.0 else ("⚠️" if err < 20.0 else "❌")
803
+
804
+ log(
805
+ f" {tick} {fam['name']:20s} | "
806
+ f"target_B={b_raw:.3f} | "
807
+ f"pred({fam['c_ranges'][0]}: {c0_pred_raw:.2f}, "
808
+ f"{fam['c_ranges'][1]}: {c1_pred_raw:.2f}) | "
809
+ f"verified_B={b_pred:.3f} | err={err:.3f}% | {ms}ms"
810
+ )
811
+
812
+ results.append({
813
+ "formula": fam["name"],
814
+ "b_target": round(b_raw, 4),
815
+ "b_norm": round(b_norm, 4),
816
+ "c0_true": c0_raw, "c0_pred": round(c0_pred_raw, 3),
817
+ "c1_true": c1_raw, "c1_pred": round(c1_pred_raw, 3),
818
+ "b_verified": round(b_pred, 4) if math.isfinite(b_pred) else None,
819
+ "error_pct": round(err, 4),
820
+ "passed": err < 5.0,
821
+ "ms": ms,
822
+ })
823
+
824
+ n_pass = sum(1 for r in results if r["passed"])
825
+ log(f"Evaluation complete: {n_pass}/{len(results)} passed (<5% error)", "OK")
826
+ log(f"Pass rate: {100*n_pass/max(len(results),1):.1f}%",
827
+ "OK" if n_pass/max(len(results),1) > 0.6 else "WARN")
828
+ _eval_results = results
829
+
830
+
831
+ # ─────────────────────────────────────────────────────────────────────────────
832
+ # 9. LLM BRIDGE (calls external /chat endpoint)
833
+ # ─────────────────────────────────────────────────────────────────────────────
834
+
835
+ def build_context(fam_idx: int, c0_raw: float, c1_raw: float,
836
+ b_raw: float, err: float, b_norm: float) -> dict:
837
+ fam = FORMULA_FAMILIES[fam_idx]
838
+ status = "SETTLED" if err < 2 else ("APPROXIMATE" if err < 5 else "UNSTABLE")
839
+
840
+ prompt = (
841
+ f"You are an analytical assistant. "
842
+ f"A constraint-diffusion engine has retraced hidden variables from an observed outcome.\n\n"
843
+ f"DOMAIN: {fam['name']}\n"
844
+ f"RULE: {fam['rule_text']}\n\n"
845
+ f"OBSERVED OUTCOME (B): {b_raw:.4f}\n"
846
+ f"ENGINE STATUS: {status} (verification error: {err:.3f}%)\n\n"
847
+ f"RETRACED HIDDEN VARIABLES (C):\n"
848
+ f" {fam['c_ranges'][0]}: {c0_raw:.3f}\n"
849
+ f" {fam['c_ranges'][1]}: {c1_raw:.3f}\n\n"
850
+ f"Explain in plain language what these hidden variables mean in context, "
851
+ f"why they produce the observed outcome, and what the result implies about the system state."
852
  )
853
+ return {"rule": fam["rule_text"], "b": b_raw, "c0": c0_raw, "c1": c1_raw,
854
+ "error_pct": err, "status": status, "prompt": prompt}
855
 
856
+
857
+ def call_llm(context: dict) -> str:
858
+ if not LLM_CHAT_URL or _req is None:
859
+ return (
860
+ "LLM not connected (set LLM_CHAT_URL env var).\n\n"
861
+ "Context that would be sent:\n\n" + context["prompt"]
862
+ )
863
+ try:
864
+ log(f"Calling LLM at {LLM_CHAT_URL}/chat…")
865
+ resp = _req.post(
866
+ f"{LLM_CHAT_URL}/chat",
867
+ json={"message": context["prompt"]},
868
+ timeout=90
869
+ )
870
+ resp.raise_for_status()
871
+ data = resp.json()
872
+ text = data.get("response") or data.get("text") or data.get("content") or str(data)
873
+ log(f"LLM responded ({len(text)} chars)", "OK")
874
+ return text
875
+ except Exception as e:
876
+ log(f"LLM call failed: {e}", "ERROR")
877
+ return f"LLM call failed: {e}\n\nContext:\n{context['prompt']}"
878
+
879
+
880
+ def run_llm_on_eval_results():
881
+ """After self-eval, send the best settled result to the LLM for interpretation."""
882
+ if not _eval_results:
883
+ log("No eval results to send to LLM", "WARN"); return
884
+ settled = [r for r in _eval_results if r["passed"]]
885
+ if not settled:
886
+ log("No settled results to send to LLM", "WARN"); return
887
+
888
+ best = min(settled, key=lambda r: r["error_pct"])
889
+ fam_idx = next(i for i,f in enumerate(FORMULA_FAMILIES) if f["name"] == best["formula"])
890
+ ctx = build_context(fam_idx, best["c0_pred"], best["c1_pred"],
891
+ best["b_target"], best["error_pct"], best["b_norm"])
892
+ log(f"Sending best result ({best['formula']}, err={best['error_pct']}%) to LLM…")
893
+ response = call_llm(ctx)
894
+ log("── LLM RESPONSE ──────────────────────────────────", "HEAD")
895
+ for line in response.split("\n"):
896
+ log(f" {line}")
897
+ log("──────────────────────────────────────────────────", "HEAD")
898
+
899
+
900
+ # ─────────────────────────────────────────────────────────────────────────────
901
+ # 10. LAUNCH β€” background thread, then Gradio or console
902
+ # ─────────────────────────────────────────────────────────────────────────────
903
+
904
+ def pipeline_thread():
905
+ run_full_pipeline(seed=42)
906
+ run_llm_on_eval_results()
907
+
908
+ log("Launching pipeline in background thread…", "HEAD")
909
+ threading.Thread(target=pipeline_thread, daemon=True).start()
910
+
911
+
912
+ # ─────────────────────────────────────────────────────────────────────────────
913
+ # 11. GRADIO UI
914
+ # ─────────────────────────────────────────────────────────────────────────────
915
+
916
+ if GR_OK:
917
+ def get_phase_md():
918
+ p = _train_state["phase"]
919
+ icons = {"idle":"⏸","fetching":"🌐","training":"🧠",
920
+ "evaluating":"πŸ”¬","done":"βœ…","error":"❌"}
921
+ ep = _train_state.get("epoch", 0)
922
+ loss = _train_state.get("loss")
923
+ l = f" loss={loss}" if loss else ""
924
+ return f"## {icons.get(p,'❓')} **{p.upper()}** epoch={ep}/{EPOCHS}{l}"
925
+
926
+ def get_log_str():
927
+ return "\n".join(_LOG_LINES[-80:])
928
+
929
+ def get_table():
930
+ if not _eval_results: return []
931
+ return [[r["formula"], r["b_target"],
932
+ f"{r['c0_pred']} (true {r['c0_true']})",
933
+ f"{r['c1_pred']} (true {r['c1_true']})",
934
+ r["b_verified"], f"{r['error_pct']}%",
935
+ "βœ…" if r["passed"] else "❌"] for r in _eval_results]
936
+
937
+ def ui_query(rule, b_val):
938
+ if _engine is None:
939
+ return "Engine not ready yet.", ""
940
+ try:
941
+ fam = next((f for f in FORMULA_FAMILIES if f["name"] in rule.lower()), FORMULA_FAMILIES[1])
942
+ fam_idx = FORMULA_FAMILIES.index(fam)
943
+ b_norm = float(b_val) / fam["b_norm"]
944
+ b_norm = max(0.01, min(0.99, b_norm))
945
+ c0, c1, _ = retrace(rule, b_norm)
946
+ c0r = c0*(fam["c_ranges"][0][1]-fam["c_ranges"][0][0])+fam["c_ranges"][0][0]
947
+ c1r = c1*(fam["c_ranges"][1][1]-fam["c_ranges"][1][0])+fam["c_ranges"][1][0]
948
+ b_v = fam["forward"]([c0r, c1r])
949
+ err = abs(b_v - float(b_val)) / max(abs(float(b_val)), 1e-6) * 100
950
+ ctx = build_context(fam_idx, c0r, c1r, float(b_val), err, b_norm)
951
+ raw = json.dumps({k:v for k,v in ctx.items() if k!="prompt"}, indent=2)
952
+ llm = call_llm(ctx)
953
+ return raw, llm
954
+ except Exception as e:
955
+ return f"Error: {e}\n{traceback.format_exc()}", ""
956
+
957
+ with gr.Blocks(title="Universal Constraint Engine v2", theme=gr.themes.Monochrome()) as demo:
958
+ gr.Markdown("# 🧠 Universal Constraint Engine v2\nA=constraint text, B=observed, C=retraced hidden vars")
959
+ phase_md = gr.Markdown(get_phase_md())
960
+
961
+ with gr.Tabs():
962
+ with gr.Tab("πŸ“Š Evaluation Results"):
963
+ tbl = gr.Dataframe(
964
+ headers=["Formula","B target","C0 pred","C1 pred","B verified","Error","Pass"],
965
+ value=get_table(), interactive=False, wrap=True)
966
+
967
+ with gr.Tab("πŸ” Live Query"):
968
+ rule_box = gr.Textbox(label="Rule / Constraint text", value=FORMULA_FAMILIES[1]["rule_text"], lines=3)
969
+ b_box = gr.Number(label="Observed B (raw, not normalised)", value=450.0)
970
+ go_btn = gr.Button("Retrace β†’", variant="primary")
971
+ with gr.Row():
972
+ raw_out = gr.Code(label="Structured context (JSON)", language="json", lines=14)
973
+ llm_out = gr.Textbox(label="LLM interpretation", lines=14)
974
+ go_btn.click(ui_query, [rule_box, b_box], [raw_out, llm_out])
975
+
976
+ with gr.Tab("πŸ“‹ Live Log"):
977
+ log_box = gr.Textbox(value=get_log_str(), lines=28, interactive=False, autoscroll=True)
978
+
979
+ timer = gr.Timer(value=3)
980
+ timer.tick(fn=lambda: (get_phase_md(), get_log_str(), get_table()),
981
+ outputs=[phase_md, log_box, tbl])
982
+
983
+ if __name__ == "__main__":
984
+ demo.launch(share=False)
985
+
986
+ else:
987
+ # No Gradio β€” block main thread so the daemon pipeline thread keeps running
988
+ if __name__ == "__main__":
989
+ log("Gradio not available β€” watching pipeline in console. Ctrl+C to stop.")
990
+ while _train_state["phase"] not in ("done", "error"):
991
+ time.sleep(5)
992
+ log("Pipeline finished. Final log above.")