TwoQuarks commited on
Commit
cc4fd03
·
verified ·
1 Parent(s): c06ab31

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -492
app.py CHANGED
@@ -1,539 +1,210 @@
1
  import os
2
  import sys
3
- import time
4
- import uuid
5
- import json
6
  from pathlib import Path
7
-
8
  import gradio as gr
9
- import numpy as np
10
- import matplotlib.pyplot as plt
 
 
11
 
12
  ROOT = Path(__file__).resolve().parent
13
 
14
- # Make local quark folders importable
15
- for p in [ROOT / 'Down', ROOT / 'Strange', ROOT / 'Charm']:
16
- if p.exists() and str(p) not in sys.path:
17
- sys.path.insert(0, str(p))
18
-
19
- RUNTIME_DIR = Path('/tmp/twoquarks_runs')
20
- RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
21
-
22
-
23
- def _run_id() -> str:
24
- return str(uuid.uuid4())[:8]
25
-
26
-
27
- def _save_csv(path: Path, header: list[str], rows: list[list[object]]) -> None:
28
- path.parent.mkdir(parents=True, exist_ok=True)
29
- with path.open('w', encoding='utf-8') as f:
30
- f.write(','.join(header) + '\n')
31
- for row in rows:
32
- f.write(','.join(map(str, row)) + '\n')
33
-
34
-
35
- def _plot_series(y, title, xlabel='step', ylabel='value'):
36
- fig, ax = plt.subplots()
37
- ax.plot(y)
38
- ax.set_title(title)
39
- ax.set_xlabel(xlabel)
40
- ax.set_ylabel(ylabel)
41
- ax.grid(True, alpha=0.2)
42
- return fig
43
-
44
-
45
- def _save_fig(fig, path: Path) -> str:
46
- """Save a matplotlib figure as PNG and return the filepath (string)."""
47
- path.parent.mkdir(parents=True, exist_ok=True)
48
- fig.savefig(path, dpi=160, bbox_inches='tight')
49
- return str(path)
50
-
51
-
52
- # =========================
53
- # DOWN / AntiDown
54
- # =========================
55
-
56
- def run_down(episodes_per_phase: int, seed: int):
57
- """Runs Down paradox tabular experiment (HFLevo vs LevoParadoxIsomer)."""
58
- from down.exp.run_paradox_tabular import run_experiment
59
-
60
- run_id = _run_id()
61
- out_dir = RUNTIME_DIR / f'down_{run_id}'
62
- out_csv = out_dir / 'down_paradox_tabular_results.csv'
63
-
64
- run_experiment(out_csv=out_csv, episodes_per_phase=int(episodes_per_phase), seed=int(seed))
65
-
66
- # Load and summarize
67
- import csv
68
- rows = []
69
- with out_csv.open('r', encoding='utf-8') as f:
70
- reader = csv.DictReader(f)
71
- for r in reader:
72
- rows.append(r)
73
-
74
- # Aggregate mean reward by phase+agent
75
- phases = sorted({int(r['phase']) for r in rows})
76
- agents = sorted({r['agent'] for r in rows})
77
- summary = {}
78
- for a in agents:
79
- summary[a] = {p: [] for p in phases}
80
- for r in rows:
81
- summary[r['agent']][int(r['phase'])].append(float(r['episode_reward']))
82
-
83
- table_lines = []
84
- for a in agents:
85
- for p in phases:
86
- vals = summary[a][p]
87
- table_lines.append({
88
- 'agent': a,
89
- 'phase': p,
90
- 'mean_reward': float(np.mean(vals)) if vals else float('nan'),
91
- 'std_reward': float(np.std(vals)) if vals else float('nan'),
92
- 'n': len(vals),
93
- })
94
-
95
- # Plot: mean reward per phase (bar-ish via line)
96
- fig, ax = plt.subplots()
97
- for a in agents:
98
- means = [np.mean(summary[a][p]) if summary[a][p] else np.nan for p in phases]
99
- ax.plot(phases, means, marker='o', label=a)
100
- ax.set_title('DOWN: Mean reward by phase')
101
- ax.set_xlabel('phase')
102
- ax.set_ylabel('mean episode reward')
103
- ax.grid(True, alpha=0.2)
104
- ax.legend()
105
 
106
- meta = {
107
- 'run_id': run_id,
108
- 'timestamp_utc': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()),
109
- 'seed': int(seed),
110
- 'episodes_per_phase': int(episodes_per_phase),
111
- 'artifact_csv': str(out_csv),
112
- }
113
 
114
- # Save plot(s) to PNG for the UI carousel
115
- graphics_dir = out_dir / 'graphics'
116
- mean_plot = _save_fig(fig, graphics_dir / 'down_mean_reward_by_phase.png')
117
- carousel = [mean_plot]
118
-
119
- headers = ['agent', 'phase', 'mean_reward', 'std_reward', 'n']
120
- rows = [[row.get(h) for h in headers] for row in table_lines]
121
- return json.dumps(meta, indent=2), rows, fig, str(out_csv), carousel
122
-
123
-
124
- def run_antidown(n_episodes: int, base_seed: int):
125
- """Runs AntiDown corrupted valley tabular experiment with adjustable n_episodes."""
126
- # Import pieces from the script
127
- from AntiDown.envs.corrupted_valley import CorruptedValleyEnv
128
- from AntiDown.levo.levo_q_tabular import LevoQTabularAgent
129
- from AntiDown.levo.levo_thinking_ensemble import LevoThinkingEnsembleAgent
130
- from AntiDown.exp.run_corrupted_valley_tabular import EpsGreedyQAgent, SoftmaxBoltzmannAgent, run_phase
131
- from AntiDown.utils.logging import CSVLogger
132
-
133
- run_id = _run_id()
134
- out_dir = RUNTIME_DIR / f'antidown_{run_id}'
135
- out_csv = out_dir / 'antidown_corrupted_valley_tabular.csv'
136
-
137
- base_seed = int(base_seed)
138
- seed_offset = 1000
139
-
140
- probe_env = CorruptedValleyEnv(seed=base_seed, phase=1)
141
- n_states = probe_env.n_states
142
- n_actions = probe_env.n_actions
143
-
144
- agents = [
145
- EpsGreedyQAgent(n_states, n_actions, epsilon=0.1, name='EpsGreedy'),
146
- SoftmaxBoltzmannAgent(n_states, n_actions, tau=0.5, name='Softmax'),
147
- LevoQTabularAgent(n_states, n_actions, A=0.5, omega=0.05, ent_weight=0.0, name='LevoQ'),
148
- LevoThinkingEnsembleAgent(n_states, n_actions, n_heads=5, A=0.5, omega=0.05, lambda_var=0.5, name='LevoThinking'),
149
- ]
150
 
151
- logger = CSVLogger(
152
- str(out_csv),
153
- fieldnames=['phase', 'phase_tag', 'episode', 'agent', 'total_reward', 'valley_visits'],
154
- )
155
 
156
- n_episodes = int(n_episodes)
157
-
158
- # 3 phases
159
- run_phase(base_seed, seed_offset, 1, n_episodes, agents, logger, noisy_heads_spec=None, noise_std=0.0)
160
- run_phase(base_seed, seed_offset, 2, n_episodes, agents, logger, noisy_heads_spec=None, noise_std=0.0)
161
- run_phase(base_seed, seed_offset, 3, n_episodes, agents, logger, noisy_heads_spec=None, noise_std=0.1)
162
-
163
- # Summarize
164
- import csv
165
- rows = []
166
- with out_csv.open('r', encoding='utf-8') as f:
167
- reader = csv.DictReader(f)
168
- for r in reader:
169
- rows.append(r)
170
-
171
- phases = sorted({int(r['phase']) for r in rows})
172
- agents_names = sorted({r['agent'] for r in rows})
173
- summary = {a: {p: [] for p in phases} for a in agents_names}
174
- for r in rows:
175
- summary[r['agent']][int(r['phase'])].append(float(r['total_reward']))
176
-
177
- table_lines = []
178
- for a in agents_names:
179
- for p in phases:
180
- vals = summary[a][p]
181
- table_lines.append({
182
- 'agent': a,
183
- 'phase': p,
184
- 'mean_total_reward': float(np.mean(vals)) if vals else float('nan'),
185
- 'std_total_reward': float(np.std(vals)) if vals else float('nan'),
186
- 'mean_valley_visits': float(np.mean([float(rr['valley_visits']) for rr in rows if rr['agent']==a and int(rr['phase'])==p])) if vals else float('nan'),
187
- 'n': len(vals),
188
- })
189
-
190
- fig, ax = plt.subplots()
191
- for a in agents_names:
192
- means = [np.mean(summary[a][p]) if summary[a][p] else np.nan for p in phases]
193
- ax.plot(phases, means, marker='o', label=a)
194
- ax.set_title('AntiDown: Mean total reward by phase')
195
- ax.set_xlabel('phase')
196
- ax.set_ylabel('mean episode total reward')
197
- ax.grid(True, alpha=0.2)
198
- ax.legend()
199
 
200
- meta = {
201
- 'run_id': run_id,
202
- 'timestamp_utc': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()),
203
- 'base_seed': base_seed,
204
- 'episodes_per_phase': n_episodes,
205
- 'artifact_csv': str(out_csv),
206
- }
207
 
208
- # Save plot(s) to PNG for the UI carousel
209
- graphics_dir = out_dir / 'graphics'
210
- mean_plot = _save_fig(fig, graphics_dir / 'antidown_mean_total_reward_by_phase.png')
211
- carousel = [mean_plot]
212
 
213
- headers = ['agent', 'phase', 'mean_total_reward', 'std_total_reward', 'mean_valley_visits', 'n']
214
- rows = [[row.get(h) for h in headers] for row in table_lines]
215
- return json.dumps(meta, indent=2), rows, fig, str(out_csv), carousel
216
 
 
 
 
217
 
218
- # =========================
219
- # STRANGE / AntiStrange
220
- # =========================
 
221
 
222
- def run_strange(n_episodes: int, max_steps: int, seed: int, mode: str):
223
- if mode == 'Strange':
224
- from strange.exp.run_strange_hypothesis_lab import run_experiment
225
- out_name = 'strange_hypothesis_lab.csv'
226
- else:
227
- from AntiStrange.exp.run_antistrange_hypothesis_lab import run_antistrange as run_experiment
228
- out_name = 'antistrange_hypothesis_lab.csv'
229
-
230
- run_id = _run_id()
231
- out_dir = RUNTIME_DIR / f"{mode.lower()}_{run_id}"
232
- out_csv = out_dir / out_name
233
-
234
- run_experiment(n_episodes=int(n_episodes), max_steps=int(max_steps), seed=int(seed), out_path=str(out_csv))
235
-
236
- # Load CSV and make curves
237
- import csv
238
- rewards_by_ep = {}
239
- with out_csv.open('r', encoding='utf-8') as f:
240
- reader = csv.DictReader(f)
241
- for r in reader:
242
- ep = int(r['episode'])
243
- rewards_by_ep.setdefault(ep, 0.0)
244
- rewards_by_ep[ep] += float(r['reward'])
245
-
246
- eps = sorted(rewards_by_ep.keys())
247
- ep_returns = [rewards_by_ep[e] for e in eps]
248
-
249
- fig = _plot_series(ep_returns, f'{mode}: return per episode', xlabel='episode', ylabel='return')
250
-
251
- metrics = {
252
- 'run_id': run_id,
253
- 'timestamp_utc': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()),
254
- 'mode': mode,
255
- 'seed': int(seed),
256
- 'n_episodes': int(n_episodes),
257
- 'max_steps': int(max_steps),
258
- 'return_mean_last_20': float(np.mean(ep_returns[-20:])) if len(ep_returns) >= 20 else float(np.mean(ep_returns)),
259
- 'return_std_last_20': float(np.std(ep_returns[-20:])) if len(ep_returns) >= 20 else float(np.std(ep_returns)),
260
- 'artifact_csv': str(out_csv),
261
- }
262
 
263
- # Save plot(s) to PNG for the UI carousel
264
- graphics_dir = out_dir / 'graphics'
265
- plot_path = _save_fig(fig, graphics_dir / f"{mode.lower()}_return_per_episode.png")
266
- carousel = [plot_path]
 
 
267
 
268
- return json.dumps(metrics, indent=2), fig, str(out_csv), carousel
 
 
 
 
 
269
 
 
270
 
271
- # =========================
272
- # CHARM
273
- # =========================
 
 
 
 
274
 
275
- def run_charm(n_episodes: int, seed: int):
276
- from charm.levo.charm import train_charm_enchanted_valley
277
 
278
- run_id = _run_id()
279
- out_dir = RUNTIME_DIR / f'charm_{run_id}'
280
- out_dir.mkdir(parents=True, exist_ok=True)
281
 
282
- stats = train_charm_enchanted_valley(n_episodes=int(n_episodes), seed=int(seed))
 
 
283
 
284
- rewards = stats['rewards']
285
- lam = stats['lambda']
286
- rho = stats['rho_mean']
 
287
 
288
- fig_r = _plot_series(rewards, 'Charm: reward per episode', xlabel='episode', ylabel='reward')
289
- fig_l = _plot_series(lam, 'Charm: lambda (meta-control) per episode', xlabel='episode', ylabel='lambda')
290
- fig_rho = _plot_series(rho, 'Charm: rho_mean per episode', xlabel='episode', ylabel='rho_mean')
291
 
292
- # Save CSV artifact
293
- out_csv = out_dir / 'charm_timeseries.csv'
294
- header = ['episode', 'reward', 'lambda', 'rho_mean']
295
- rows = [[i, float(rewards[i]), float(lam[i]), float(rho[i])] for i in range(len(rewards))]
296
- _save_csv(out_csv, header, rows)
297
 
298
- metrics = {
299
- 'run_id': run_id,
300
- 'timestamp_utc': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()),
301
- 'seed': int(seed),
302
- 'n_episodes': int(n_episodes),
303
- 'reward_mean_last_50': float(np.mean(rewards[-50:])) if len(rewards) >= 50 else float(np.mean(rewards)),
304
- 'reward_std_last_50': float(np.std(rewards[-50:])) if len(rewards) >= 50 else float(np.std(rewards)),
305
- 'artifact_csv': str(out_csv),
306
- }
307
 
308
- # Save plots to PNG for the UI carousel
309
- graphics_dir = out_dir / 'graphics'
310
- p_r = _save_fig(fig_r, graphics_dir / 'charm_reward.png')
311
- p_l = _save_fig(fig_l, graphics_dir / 'charm_lambda.png')
312
- p_rho = _save_fig(fig_rho, graphics_dir / 'charm_rho_mean.png')
313
- carousel = [p_r, p_l, p_rho]
314
 
315
- return json.dumps(metrics, indent=2), fig_r, fig_l, fig_rho, str(out_csv), carousel
 
 
 
 
 
316
 
 
317
 
318
- # =========================
 
319
  # UI
320
- # =========================
321
 
322
  def build_ui():
323
- css_path = ROOT / "style.css"
324
- css = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
325
-
326
- NAV_HTML = """
327
- <nav id="tqNav">
328
- <div class="nav-inner">
329
- <div class="brand">TwoQuarks</div>
330
- <div class="nav-links">
331
- <a href="#down" onclick="return false;">DOWN</a>
332
- <a href="#strange" onclick="return false;">STRANGE</a>
333
- <a href="#top" onclick="return false;">TOP</a>
334
- <a href="#charm" onclick="return false;">CHARM</a>
335
- <a href="#up" onclick="return false;">UP</a>
336
- <a href="#bottom" onclick="return false;">BOTTOM</a>
337
- </div>
338
- <div class="spacer"></div>
339
- <div class="menu-btn" id="menuBtn" title="Menu"><span></span></div>
340
- <div class="menu" id="siteMenu">
341
- <a href="https://twoquarks.com/#about" target="_blank" rel="noopener">ABOUT</a>
342
- <a href="https://twoquarks.com/quarkslab.html" target="_blank" rel="noopener">QuarksLab</a>
343
- <div class="sep"></div>
344
- <a href="https://twoquarks.com/quarks/bottom/resume.pdf" target="_blank" rel="noopener">Resume</a>
345
- <a href="https://twoquarks.com/summary.pdf" target="_blank" rel="noopener">Summary</a>
346
- </div>
347
- </div>
348
- </nav>
349
- """
350
 
351
- head = """
352
- <link rel="preconnect" href="https://fonts.googleapis.com">
353
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
354
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
355
-
356
- <canvas id="quantumField"></canvas>
357
-
358
- <script>
359
- (function(){
360
- // Hamburger menu toggle
361
- window.addEventListener("load", () => {
362
- const menuBtn = document.getElementById('menuBtn');
363
- const siteMenu = document.getElementById('siteMenu');
364
- if(menuBtn && siteMenu){
365
- menuBtn.addEventListener('click', (e)=>{ e.stopPropagation(); siteMenu.classList.toggle('open'); });
366
- document.addEventListener('click', ()=> siteMenu.classList.remove('open'));
367
- siteMenu.addEventListener('click', (e)=> e.stopPropagation());
368
- }
369
- });
370
-
371
- // Starfield (ported from your site canvas pattern)
372
- const canvas = document.getElementById('quantumField');
373
- if(!canvas) return;
374
- const ctx = canvas.getContext('2d');
375
-
376
- function resize(){
377
- canvas.width = innerWidth;
378
- canvas.height = innerHeight;
379
- }
380
- resize();
381
- addEventListener('resize', resize);
382
-
383
- let particles = [];
384
- const count = 680;
385
-
386
- for(let i=0;i<count;i++){
387
- particles.push({
388
- x:(Math.random()-0.5)*canvas.width,
389
- y:(Math.random()-0.5)*canvas.height,
390
- z:Math.random()*canvas.width,
391
- });
392
- }
393
-
394
- function render(){
395
- ctx.clearRect(0,0,canvas.width,canvas.height);
396
- for(const p of particles){
397
- p.z -= 2.2;
398
- if(p.z < 1){
399
- p.x=(Math.random()-0.7)*canvas.width;
400
- p.y=(Math.random()-0.7)*canvas.height;
401
- p.z=canvas.width;
402
- }
403
- const k=128/p.z;
404
- const px=p.x*k+canvas.width/2;
405
- const py=p.y*k+canvas.height/2;
406
- const size=(1-p.z/canvas.width)*1.29;
407
- ctx.beginPath();
408
- ctx.fillStyle="rgba(140,180,255,0.85)";
409
- ctx.arc(px,py,size,0,Math.PI*2);
410
- ctx.fill();
411
- }
412
- requestAnimationFrame(render);
413
- }
414
- render();
415
- })();
416
- </script>
417
- """
418
 
419
- with gr.Blocks(title='TwoQuarks - QuarksLab (Interactive)', css=css, head=head) as demo:
420
- gr.HTML(NAV_HTML)
421
-
422
- with gr.Column(elem_id='tqApp'):
423
- gr.HTML(
424
- """
425
- <div class="tq-hero">
426
- <div class="brand">TwoQuarks - QuarksLab (Interactive)</div>
427
- <div class="desc">Real runs, real knobs: seed/episodes/steps. Short-bounded experiments (public CPU).</div>
428
- </div>
429
- """
430
- )
431
-
432
- with gr.Tab('DOWN / AntiDown'):
433
- gr.Markdown('Tabular experiments (fast).')
434
-
435
- with gr.Row():
436
- down_eps = gr.Slider(50, 800, value=200, step=50, label='DOWN: episodes_per_phase')
437
- down_seed = gr.Number(value=2025, precision=0, label='DOWN: seed')
438
- run_down_btn = gr.Button('Run DOWN')
439
-
440
- down_meta = gr.Code(label='DOWN: run meta (JSON)', language='json')
441
- down_table = gr.Dataframe(
442
- headers=['agent', 'phase', 'mean_reward', 'std_reward', 'n'],
443
- label='DOWN: summary (mean/std by phase+agent)',
444
- interactive=False,
445
- )
446
- down_plot = gr.Plot(label='DOWN: mean reward by phase')
447
- down_carousel = gr.Gallery(label='DOWN: results (carousel)', columns=1, height=520)
448
- down_file = gr.File(label='DOWN: results CSV')
449
-
450
- run_down_btn.click(
451
- fn=run_down,
452
- inputs=[down_eps, down_seed],
453
- outputs=[down_meta, down_table, down_plot, down_file, down_carousel],
454
- )
455
-
456
- gr.Markdown('---')
457
-
458
- with gr.Row():
459
- ad_eps = gr.Slider(20, 400, value=120, step=20, label='AntiDown: episodes_per_phase')
460
- ad_seed = gr.Number(value=1234, precision=0, label='AntiDown: base seed')
461
- run_ad_btn = gr.Button('Run AntiDown')
462
-
463
- ad_meta = gr.Code(label='AntiDown: run meta (JSON)', language='json')
464
- ad_table = gr.Dataframe(
465
- headers=['agent', 'phase', 'mean_total_reward', 'std_total_reward', 'mean_valley_visits', 'n'],
466
- label='AntiDown: summary',
467
- interactive=False,
468
- )
469
- ad_plot = gr.Plot(label='AntiDown: mean total reward by phase')
470
- ad_carousel = gr.Gallery(label='AntiDown: results (carousel)', columns=1, height=520)
471
- ad_file = gr.File(label='AntiDown: results CSV')
472
-
473
- run_ad_btn.click(
474
- fn=run_antidown,
475
- inputs=[ad_eps, ad_seed],
476
- outputs=[ad_meta, ad_table, ad_plot, ad_file, ad_carousel],
477
- )
478
-
479
- with gr.Tab('STRANGE / AntiStrange'):
480
- gr.Markdown('HypothesisLab: swarm dynamics with hidden regime shifts.')
481
- mode = gr.Radio(['Strange', 'AntiStrange'], value='Strange', label='Mode')
482
-
483
- with gr.Row():
484
- seps = gr.Slider(50, 500, value=200, step=50, label='Episodes')
485
- ssteps = gr.Slider(10, 120, value=50, step=5, label='Max steps per episode')
486
- sseed = gr.Number(value=0, precision=0, label='Seed')
487
-
488
- run_s_btn = gr.Button('Run')
489
- s_meta = gr.Code(label='Run meta (JSON)', language='json')
490
- s_plot = gr.Plot(label='Return per episode')
491
- s_carousel = gr.Gallery(label='Results (carousel)', columns=1, height=520)
492
- s_file = gr.File(label='Results CSV')
493
-
494
- run_s_btn.click(
495
- fn=run_strange,
496
- inputs=[seps, ssteps, sseed, mode],
497
- outputs=[s_meta, s_plot, s_file, s_carousel],
498
- )
499
-
500
- with gr.Tab('CHARM'):
501
- gr.Markdown('Enchanted Valley: non-stationary graph + CharmField meta-control.')
502
-
503
- with gr.Row():
504
- ceps = gr.Slider(50, 600, value=300, step=50, label='Episodes')
505
- cseed = gr.Number(value=0, precision=0, label='Seed')
506
- run_c_btn = gr.Button('Run CHARM')
507
-
508
- c_meta = gr.Code(label='Run meta (JSON)', language='json')
509
- c_plot_r = gr.Plot(label='Reward')
510
- c_plot_l = gr.Plot(label='Lambda')
511
- c_plot_rho = gr.Plot(label='Rho_mean')
512
- c_carousel = gr.Gallery(label='CHARM: results (carousel)', columns=1, height=520)
513
- c_file = gr.File(label='Timeseries CSV')
514
-
515
- run_c_btn.click(
516
- fn=run_charm,
517
- inputs=[ceps, cseed],
518
- outputs=[c_meta, c_plot_r, c_plot_l, c_plot_rho, c_file, c_carousel],
519
- )
520
-
521
- gr.Markdown(
522
- """### Notes
523
- - This Space runs bounded experiments on shared CPU.
524
- - For heavy runs, keep episodes modest and use the CSV artifact to reproduce locally.
525
  """
526
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  return demo
529
 
530
 
 
 
 
 
531
  demo = build_ui()
532
 
533
- if __name__ == '__main__':
534
  demo.launch(
535
- server_name='0.0.0.0',
536
- server_port=int(os.getenv('PORT', '7860')),
537
  show_api=False,
538
  )
539
-
 
1
  import os
2
  import sys
3
+ import subprocess
 
 
4
  from pathlib import Path
 
5
  import gradio as gr
6
+
7
+ # ============================================================
8
+ # Paths
9
+ # ============================================================
10
 
11
  ROOT = Path(__file__).resolve().parent
12
 
13
+ DOWN_ROOT = ROOT / "Down"
14
+ STRANGE_ROOT = ROOT / "Strange"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ DOWN_GRAPHICS = DOWN_ROOT / "graphics"
17
+ STRANGE_GRAPHICS = STRANGE_ROOT / "graphics"
 
 
 
 
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # ============================================================
21
+ # Helpers
22
+ # ============================================================
 
23
 
24
+ def _clean_pngs(folder: Path):
25
+ if folder.exists():
26
+ for f in folder.glob("*.png"):
27
+ f.unlink()
28
+ else:
29
+ folder.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
 
 
 
 
 
 
31
 
32
+ def _collect_pngs(folder: Path):
33
+ return sorted(str(p) for p in folder.glob("*.png"))
 
 
34
 
 
 
 
35
 
36
+ # ============================================================
37
+ # DOWN (Unified: Down + AntiDown)
38
+ # ============================================================
39
 
40
+ def run_down_all(episodes_per_phase: int):
41
+ run_all = DOWN_ROOT / "run_all.py"
42
+ if not run_all.exists():
43
+ raise RuntimeError("Down/run_all.py not found")
44
 
45
+ _clean_pngs(DOWN_GRAPHICS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ cmd = [
48
+ sys.executable,
49
+ "run_all.py",
50
+ "--episodes",
51
+ str(int(episodes_per_phase)),
52
+ ]
53
 
54
+ subprocess.run(
55
+ cmd,
56
+ cwd=str(DOWN_ROOT),
57
+ env=os.environ.copy(),
58
+ check=True,
59
+ )
60
 
61
+ images = _collect_pngs(DOWN_GRAPHICS)
62
 
63
+ meta = {
64
+ "status": "completed",
65
+ "pipeline": "DOWN + AntiDown (unified)",
66
+ "episodes_per_phase": int(episodes_per_phase),
67
+ "graphics_count": len(images),
68
+ "graphics_dir": str(DOWN_GRAPHICS),
69
+ }
70
 
71
+ return meta, images
 
72
 
 
 
 
73
 
74
+ # ============================================================
75
+ # STRANGE (Unified: Strange + AntiStrange)
76
+ # ============================================================
77
 
78
+ def run_strange_all():
79
+ run_all = STRANGE_ROOT / "run_all.py"
80
+ if not run_all.exists():
81
+ raise RuntimeError("Strange/run_all.py not found")
82
 
83
+ _clean_pngs(STRANGE_GRAPHICS)
 
 
84
 
85
+ cmd = [sys.executable, "run_all.py"]
 
 
 
 
86
 
87
+ subprocess.run(
88
+ cmd,
89
+ cwd=str(STRANGE_ROOT),
90
+ env=os.environ.copy(),
91
+ check=True,
92
+ )
 
 
 
93
 
94
+ images = _collect_pngs(STRANGE_GRAPHICS)
 
 
 
 
 
95
 
96
+ meta = {
97
+ "status": "completed",
98
+ "pipeline": "STRANGE + AntiStrange (unified)",
99
+ "graphics_count": len(images),
100
+ "graphics_dir": str(STRANGE_GRAPHICS),
101
+ }
102
 
103
+ return meta, images
104
 
105
+
106
+ # ============================================================
107
  # UI
108
+ # ============================================================
109
 
110
  def build_ui():
111
+ with gr.Blocks(
112
+ title="TwoQuarks QuarksLab (Interactive)",
113
+ ) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ gr.Markdown(
116
+ """
117
+ # TwoQuarks QuarksLab (Interactive)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ **Real runs. Real artifacts. No duplicated models.**
120
+ Each section launches a **single unified pipeline** and displays the **actual plots generated**.
121
+ """
122
+ )
123
+
124
+ # ----------------------------
125
+ # DOWN
126
+ # ----------------------------
127
+ gr.Markdown("## DOWN / AntiDown")
128
+ gr.Markdown(
129
+ """
130
+ Unified tabular paradox experiment.
131
+ This runs **one pipeline** that includes both quark and antiquark.
132
+ """
133
+ )
134
+
135
+ down_eps = gr.Slider(
136
+ minimum=50,
137
+ maximum=800,
138
+ step=50,
139
+ value=200,
140
+ label="Episodes per phase",
141
+ )
142
+
143
+ run_down_btn = gr.Button("Run DOWN (unified)")
144
+
145
+ down_meta = gr.JSON(label="Run status")
146
+ down_gallery = gr.Gallery(
147
+ label="Generated plots",
148
+ columns=2,
149
+ height="auto",
150
+ )
151
+
152
+ run_down_btn.click(
153
+ fn=run_down_all,
154
+ inputs=[down_eps],
155
+ outputs=[down_meta, down_gallery],
156
+ )
157
+
158
+ gr.Markdown("---")
159
+
160
+ # ----------------------------
161
+ # STRANGE
162
+ # ----------------------------
163
+ gr.Markdown("## STRANGE / AntiStrange")
164
+ gr.Markdown(
165
+ """
166
+ Unified hypothesis lab (swarm dynamics + regime shifts).
167
+ Runs **Strange + AntiStrange + dual comparison** as a single experiment.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  """
169
+ )
170
+
171
+ run_strange_btn = gr.Button("Run STRANGE (unified)")
172
+
173
+ strange_meta = gr.JSON(label="Run status")
174
+ strange_gallery = gr.Gallery(
175
+ label="Generated plots",
176
+ columns=2,
177
+ height="auto",
178
+ )
179
+
180
+ run_strange_btn.click(
181
+ fn=run_strange_all,
182
+ inputs=[],
183
+ outputs=[strange_meta, strange_gallery],
184
+ )
185
+
186
+ gr.Markdown(
187
+ """
188
+ ---
189
+ ### Notes
190
+ - All experiments are executed via their respective **`run_all.py`**.
191
+ - Gradio does not import or orchestrate internal model logic.
192
+ - All visualizations are read directly from the `graphics/` folders.
193
+ """
194
+ )
195
 
196
  return demo
197
 
198
 
199
+ # ============================================================
200
+ # Launch
201
+ # ============================================================
202
+
203
  demo = build_ui()
204
 
205
+ if __name__ == "__main__":
206
  demo.launch(
207
+ server_name="0.0.0.0",
208
+ server_port=int(os.getenv("PORT", "7860")),
209
  show_api=False,
210
  )