File size: 20,621 Bytes
b3b6505
13260f8
9174bdb
 
 
 
 
 
 
 
 
 
13260f8
 
9174bdb
e26eac9
4305a67
b3b6505
 
9174bdb
b3b6505
 
 
 
 
 
82013db
615135c
4305a67
9174bdb
b3b6505
b2a179c
 
9174bdb
a71591d
e26eac9
 
68c9302
e26eac9
28f016b
b2a179c
0fc908f
b2a179c
 
 
 
 
0fc908f
615135c
 
 
 
 
9174bdb
 
 
b3b6505
9174bdb
 
b3b6505
9174bdb
 
 
b3b6505
 
9174bdb
 
b3b6505
9d1f883
 
82013db
9d1f883
82013db
9d1f883
 
82013db
 
9d1f883
 
 
 
 
 
 
82013db
 
9d1f883
 
 
 
 
 
 
82013db
 
9d1f883
 
9174bdb
 
 
 
 
 
 
0fc908f
9174bdb
 
 
 
 
 
ee6ae39
9174bdb
 
 
 
e26eac9
 
9174bdb
e26eac9
9174bdb
68c9302
9174bdb
b3b6505
9174bdb
b3b6505
 
9174bdb
 
 
b3b6505
9174bdb
 
4305a67
 
 
 
9174bdb
4305a67
9174bdb
 
4305a67
9174bdb
4305a67
9174bdb
4305a67
 
b3b6505
9174bdb
 
4305a67
 
9174bdb
 
 
 
 
 
 
 
 
 
 
 
 
4305a67
e26eac9
9174bdb
b3b6505
9174bdb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4305a67
 
 
 
 
9174bdb
4305a67
 
9174bdb
4305a67
 
 
 
 
b3b6505
4305a67
 
 
 
9174bdb
 
 
4305a67
 
9174bdb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3b6505
 
9174bdb
 
 
 
 
 
 
 
b3b6505
9174bdb
 
 
 
 
 
 
 
 
 
 
4305a67
 
 
 
 
 
 
e26eac9
 
9174bdb
e01815f
9174bdb
 
e01815f
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9174bdb
418e7fe
e01815f
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
418e7fe
 
 
 
 
 
 
e01815f
 
 
418e7fe
e01815f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
 
 
 
 
418e7fe
e01815f
 
 
13260f8
e01815f
4305a67
e01815f
418e7fe
e01815f
9174bdb
e01815f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4305a67
9174bdb
e01815f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4305a67
9174bdb
418e7fe
 
4305a67
e01815f
4305a67
e01815f
418e7fe
e01815f
 
 
 
418e7fe
e01815f
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
418e7fe
e01815f
 
 
 
 
 
 
9174bdb
e01815f
9174bdb
 
e01815f
 
9174bdb
 
 
 
 
 
4305a67
e01815f
 
 
4305a67
1e38698
0fc908f
9174bdb
 
 
4305a67
 
ece85d8
 
0fc908f
9174bdb
b2a179c
9174bdb
b2a179c
 
b3b6505
9174bdb
b2a179c
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# -*- coding: utf-8 -*-
"""
Minimal FastAPI + Gradio server for the Quantum Circuit Optimization Environment.

Deployment spec:
  - Root endpoint:  GET /          → health check JSON
  - Gradio UI:      mounted at     /ui
  - Port:           7860  (managed externally by Docker / HF Spaces)
  - No uvicorn.run() in this file

Run locally:
  uvicorn server.app:app --host 0.0.0.0 --port 7860
"""

import json
import logging
import os
import sys

# sys.path bootstrap so this file can be run directly
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_HERE)
for _p in (_ROOT, _HERE):
    if _p not in sys.path:
        sys.path.insert(0, _p)

from fastapi import FastAPI, Body
from fastapi.responses import RedirectResponse
import gradio as gr
from pydantic import BaseModel

from openenv.core.env_server import create_fastapi_app

from models import ActionType, GateType, QuantumAction, QuantumObservation
from .my_env_environment import QuantumCircuitEnvironment

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# FastAPI base app - Includes OpenEnv WebSocket endpoints
# ---------------------------------------------------------------------------
fastapi_app = create_fastapi_app(
    lambda: QuantumCircuitEnvironment(seed=42),
    QuantumAction,
    QuantumObservation
)

@fastapi_app.get("/web")
def web_redirect():
    """Redirect /web to root Gradio UI."""
    return RedirectResponse(url="/")

@fastapi_app.get("/health")
def health():
    return {"status": "ok", "message": "Quantum Circuit Optimizer is running"}

@fastapi_app.get("/api/info")
def info():
    return {
        "name": "Quantum Circuit Optimization Environment",
        "version": "1.0.0",
        "gradio_ui": "Available at root path /"
    }

class ResetRequest(BaseModel):
    task_id: str = "easy"

# Root-level endpoints for OpenEnv validation
@fastapi_app.post("/reset")
def reset_endpoint(body: dict = Body(default={})):
    """OpenEnv-compliant reset endpoint."""
    task_id = body.get("task_id", "easy")
    temp_env = QuantumCircuitEnvironment(seed=42)
    obs = temp_env.reset(config={"task_id": task_id})
    obs_dict = obs.model_dump() if hasattr(obs, "model_dump") else obs.dict()
    return JSONResponse(content={"observation": obs_dict})

@fastapi_app.post("/step")
def step_endpoint(action: QuantumAction):
    """OpenEnv-compliant step endpoint."""
    temp_env = QuantumCircuitEnvironment(seed=42)
    temp_env.reset(config={"task_id": "easy"})
    obs = temp_env.step(action)
    obs_dict = obs.model_dump() if hasattr(obs, "model_dump") else obs.dict()
    return JSONResponse(content={"observation": obs_dict})

@fastapi_app.get("/state")
def state_endpoint():
    """OpenEnv-compliant state endpoint."""
    temp_env = QuantumCircuitEnvironment(seed=42)
    temp_env.reset(config={"task_id": "easy"})
    state = temp_env.state
    state_dict = state.model_dump() if hasattr(state, "model_dump") else state.dict()
    return JSONResponse(content={"state": state_dict})

# API endpoints (for programmatic access with global env)
@fastapi_app.post("/api/reset")
def api_reset(req: ResetRequest):
    """Programmatic endpoint to reset the environment."""
    global current_obs, step_history
    current_obs = env.reset(config={"task_id": req.task_id})
    step_history = []
    return current_obs

@fastapi_app.post("/api/step")
def api_step(action: QuantumAction):
    """Programmatic endpoint to step the environment."""
    global current_obs
    current_obs = env.step(action)
    return current_obs

@fastapi_app.get("/api/state")
def api_state():
    """Programmatic endpoint to fetch the full internal state."""
    return env.state

# ---------------------------------------------------------------------------
# Global environment instance (for Gradio UI)
# ---------------------------------------------------------------------------
env = QuantumCircuitEnvironment(seed=42)

# Gradio State
current_obs = None
step_history: list = []


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _format_circuit(obs) -> str:
    """ASCII circuit summary."""
    if not obs or not obs.circuit_gates:
        return "(empty circuit)"
    lines = []
    for i, g in enumerate(obs.circuit_gates):
        gate   = g.get("gate", "?")
        qubits = g.get("qubits", [])
        param  = g.get("parameter")
        q_str  = ",".join(str(q) for q in qubits)
        if param is not None:
            lines.append(f"  {i+1:>2}. {gate}({q_str}, θ={param:.4f})")
        else:
            lines.append(f"  {i+1:>2}. {gate}({q_str})")
    return "\n".join(lines)


def _format_scores(obs) -> str:
    """Plain-text score summary."""
    if not obs:
        return ""
    meta = obs.metadata or {}
    fid  = round(meta.get("fidelity_score",    obs.fidelity), 4)
    eff  = round(meta.get("efficiency_score",   0),            4)
    noi  = round(meta.get("noise_score",        0),            4)
    con  = round(meta.get("constraints_score",  0),            4)
    agg  = round(meta.get("aggregate_score",    obs.score),    4)
    return (
        f"Fidelity:    {fid}\n"
        f"Efficiency:  {eff}\n"
        f"Noise:       {noi}\n"
        f"Constraints: {con}\n"
        f"AGGREGATE:   {agg}"
    )


def _status(obs, extra: str = "") -> str:
    if not obs:
        return "No environment loaded — click Reset."
    s = (
        f"Task: {obs.task_id} | Qubits: {obs.num_qubits} | "
        f"Step: {obs.steps_taken}/{obs.max_steps}\n"
        f"Fidelity: {obs.fidelity:.4f} | Score: {obs.score:.4f} | "
        f"Reward: {obs.reward:+.4f}"
    )
    if obs.done:
        s += f"\n>>> EPISODE COMPLETE — final score: {obs.score:.4f} <<<"
    if extra:
        s += f"\n{extra}"
    return s


# ---------------------------------------------------------------------------
# Gradio callbacks
# ---------------------------------------------------------------------------

TASK_MAP = {
    "Bell State (Easy)":         "easy",
    "GHZ State (Medium)":        "medium",
    "Unitary Approx (Hard)":     "hard",
    "Imperfect but Efficient":   "efficient",
    "Noise-Dominant":            "noisy",
    "Budgeted Optimization":     "budget",
    "Approximate Target":        "approx",
}


def do_reset(task_name):
    global current_obs, step_history
    task_id = TASK_MAP.get(task_name, "easy")
    current_obs  = env.reset(config={"task_id": task_id})
    step_history = []
    return (
        _status(current_obs, f"Reset to: {task_name}"),
        _format_circuit(current_obs),
        _format_scores(current_obs),
        "",
    )


def do_step(action_type, gate_type, qubit_0, qubit_1, parameter):
    global current_obs, step_history

    if current_obs is None:
        return "ERROR: Reset the environment first!", "(no circuit)", "", ""
    if current_obs.done:
        return _status(current_obs), _format_circuit(current_obs), _format_scores(current_obs), "\n".join(step_history)

    try:
        at     = ActionType(action_type)
        gate   = None
        qubits = []
        param  = None

        if at == ActionType.ADD:
            gate = GateType(gate_type) if gate_type else None
            if gate in (GateType.CNOT,):
                qubits = [int(qubit_0), int(qubit_1)]
            elif gate is not None and gate.value == "SWAP":
                qubits = [int(qubit_0), int(qubit_1)]
            else:
                qubits = [int(qubit_0)]
            if gate in (GateType.RX, GateType.RZ) and parameter is not None:
                param = float(parameter)

        elif at == ActionType.SWAP:
            qubits = [int(qubit_0), int(qubit_1)]

        elif at == ActionType.PARAM:
            if parameter is not None:
                param = float(parameter)

        action      = QuantumAction(action_type=at, gate=gate, qubits=qubits, parameter=param)
        current_obs = env.step(action)

    except Exception as exc:
        logger.exception("Step error: %s", exc)
        return (
            f"ACTION ERROR: {exc}",
            _format_circuit(current_obs) if current_obs else "(no circuit)",
            _format_scores(current_obs) if current_obs else "",
            "\n".join(step_history),
        )

    gate_label = ""
    if gate:
        gate_label = f" {gate_type}({','.join(str(q) for q in qubits)})"
    entry = (
        f"Step {current_obs.steps_taken}: {action_type}{gate_label}"
        f" → fid={current_obs.fidelity:.4f}"
        f" score={current_obs.score:.4f}"
        f" reward={current_obs.reward:+.4f}"
        + (" [DONE]" if current_obs.done else "")
    )
    step_history.append(entry)

    return (
        _status(current_obs),
        _format_circuit(current_obs),
        _format_scores(current_obs),
        "\n".join(step_history),
    )


# ---------------------------------------------------------------------------
# Gradio UI — Elegant and User-Friendly
# ---------------------------------------------------------------------------

TASK_DESCRIPTIONS = {
    "Bell State (Easy)": {
        "description": "Create the famous Bell state: (|00⟩ + |11⟩)/√2",
        "difficulty": "Easy",
        "qubits": 2,
        "noise": "None",
        "connectivity": "Full",
        "goal": "Learn basic 2-qubit entanglement with H + CNOT"
    },
    "GHZ State (Medium)": {
        "description": "Build a 3-qubit GHZ state: (|000⟩ + |111⟩)/√2",
        "difficulty": "Medium",
        "qubits": 3,
        "noise": "Depolarizing",
        "connectivity": "Linear (0↔1↔2)",
        "goal": "Handle noise and limited connectivity"
    },
    "Unitary Approx (Hard)": {
        "description": "Approximate a complex unitary: Ry(π/3) ⊗ Rz(π/4) · CNOT",
        "difficulty": "Hard",
        "qubits": 2,
        "noise": "Thermal",
        "connectivity": "Restricted",
        "goal": "Precise parametric gate tuning under noise"
    },
    "Imperfect but Efficient": {
        "description": "GHZ state with emphasis on circuit efficiency",
        "difficulty": "Medium",
        "qubits": 3,
        "noise": "None",
        "connectivity": "Full",
        "goal": "Balance fidelity (60%) vs efficiency (40%)"
    },
    "Noise-Dominant": {
        "description": "Bell state optimized for noise resilience",
        "difficulty": "Medium",
        "qubits": 2,
        "noise": "Thermal (High)",
        "connectivity": "Full",
        "goal": "Minimize gate count to survive noise (40% weight)"
    },
    "Budgeted Optimization": {
        "description": "GHZ state with strict 3-gate budget",
        "difficulty": "Medium",
        "qubits": 3,
        "noise": "None",
        "connectivity": "Full",
        "goal": "Achieve high fidelity with minimal gates"
    },
    "Approximate Target": {
        "description": "4-qubit GHZ with relaxed fidelity threshold (0.80)",
        "difficulty": "Medium",
        "qubits": 4,
        "noise": "None",
        "connectivity": "Full",
        "goal": "Scale to larger circuits with approximate solutions"
    },
}

GATE_DESCRIPTIONS = {
    "H": "Hadamard — Creates superposition: |0⟩ → (|0⟩+|1⟩)/√2",
    "X": "Pauli-X — Bit flip: |0⟩ ↔ |1⟩",
    "CNOT": "Controlled-NOT — Entangles 2 qubits (requires qubit 0 & 1)",
    "RX": "X-Rotation — Rotate around X-axis by angle θ (needs parameter)",
    "RZ": "Z-Rotation — Rotate around Z-axis by angle θ (needs parameter)",
}

ACTION_DESCRIPTIONS = {
    "ADD": "Add a quantum gate to the circuit",
    "REMOVE": "Remove the last gate from the circuit",
    "SWAP": "Swap two qubits (requires qubit 0 & 1)",
    "PARAM": "Tune the parameter of the last parametric gate (RX/RZ)",
    "STOP": "End the episode and finalize the circuit",
}

def create_gradio_app() -> gr.Blocks:
    with gr.Blocks(title="Quantum Circuit Optimizer", theme=gr.themes.Soft()) as demo:

        # Header
        gr.Markdown(
            """
            # Quantum Circuit Optimizer
            ### Build quantum circuits step-by-step to maximize fidelity with target states
            
            **Goal:** Design circuits that are accurate, efficient, noise-resistant, and hardware-compliant.
            """
        )

        # Task Selection Section
        with gr.Accordion("Available Tasks & Challenges", open=False):
            gr.Markdown(
                """
                Choose a task below to start optimizing. Each task has different constraints:
                
                | Task | Difficulty | Qubits | Noise | Connectivity | Focus |
                |------|------------|--------|-------|--------------|-------|
                | **Bell State** | Easy | 2 | None | Full | Basic entanglement |
                | **GHZ State** | Medium | 3 | Depolarizing | Linear | Noise + topology |
                | **Unitary Approx** | Hard | 2 | Thermal | Restricted | Parametric tuning |
                | **Efficient** | Medium | 3 | None | Full | Minimize depth/gates |
                | **Noise-Dominant** | Medium | 2 | High thermal | Full | Noise resilience |
                | **Budgeted** | Medium | 3 | None | Full | 3-gate limit |
                | **Approximate** | Medium | 4 | None | Full | Scale to 4 qubits |
                """
            )

        with gr.Accordion("Quick Start Guide", open=False):
            gr.Markdown(
                """
                ### How to Use:
                1. **Select a Task** — Choose from 7 quantum optimization challenges
                2. **Reset Environment** — Initialize the task and clear the circuit
                3. **Build Your Circuit** — Add gates step-by-step:
                   - Use **H** and **CNOT** for entanglement
                   - Use **RX/RZ** for parametric rotations (set angle in radians)
                   - Use **SWAP** to rearrange qubits for connectivity
                4. **Monitor Scores** — Watch fidelity, efficiency, noise, and constraints
                5. **Stop** — End the episode when satisfied
                
                ### Scoring:
                - **Fidelity** (40-60%): How close to the target state
                - **Efficiency** (20%): Circuit depth and gate count
                - **Noise** (15-25%): Resilience to quantum errors
                - **Constraints** (10-15%): Hardware connectivity compliance
                """
            )

        gr.Markdown("---")

        # Step 1: Task Selection
        gr.Markdown("### Step 1: Select Task")
        with gr.Row():
            with gr.Column(scale=3):
                task_dd = gr.Dropdown(
                    choices=list(TASK_MAP.keys()),
                    value="Bell State (Easy)",
                    label="Choose a Quantum Task",
                    info="Select the optimization challenge you want to solve"
                )
            with gr.Column(scale=1):
                reset_btn = gr.Button("Reset Environment", variant="primary", size="lg")

        # Task info display
        task_info = gr.Markdown("")
        
        def update_task_info(task_name):
            info = TASK_DESCRIPTIONS.get(task_name, {})
            return f"""
            **{info.get('difficulty', '')}** | {info.get('qubits', 0)} qubits | Noise: {info.get('noise', 'N/A')} | Connectivity: {info.get('connectivity', 'N/A')}
            
            {info.get('description', '')}
            
            **Goal:** {info.get('goal', '')}
            """
        
        task_dd.change(fn=update_task_info, inputs=[task_dd], outputs=[task_info])

        gr.Markdown("---")

        # Step 2: Circuit Building
        gr.Markdown("### Step 2: Build Your Circuit")
        
        with gr.Row():
            with gr.Column():
                action_dd = gr.Dropdown(
                    choices=["ADD", "REMOVE", "SWAP", "PARAM", "STOP"],
                    value="ADD",
                    label="Action Type",
                    info="What operation do you want to perform?"
                )
                action_help = gr.Markdown(ACTION_DESCRIPTIONS["ADD"])
                action_dd.change(fn=lambda x: ACTION_DESCRIPTIONS.get(x, ""), inputs=[action_dd], outputs=[action_help])
            
            with gr.Column():
                gate_dd = gr.Dropdown(
                    choices=["H", "X", "CNOT", "RX", "RZ"],
                    value="H",
                    label="Gate Type (for ADD action)",
                    info="Which quantum gate to add?"
                )
                gate_help = gr.Markdown(GATE_DESCRIPTIONS["H"])
                gate_dd.change(fn=lambda x: GATE_DESCRIPTIONS.get(x, ""), inputs=[gate_dd], outputs=[gate_help])

        with gr.Row():
            qubit_0 = gr.Number(
                value=0,
                label="Qubit 0",
                info="Target qubit (or first qubit for 2-qubit gates)",
                precision=0
            )
            qubit_1 = gr.Number(
                value=1,
                label="Qubit 1",
                info="Second qubit (for CNOT/SWAP only)",
                precision=0
            )
            parameter = gr.Slider(
                minimum=-3.14159,
                maximum=3.14159,
                value=0,
                step=0.01,
                label="Parameter (θ in radians)",
                info="Rotation angle for RX/RZ gates"
            )

        with gr.Row():
            step_btn = gr.Button("Execute Step", variant="primary", size="lg", scale=2)
            state_btn = gr.Button("Get Full State", variant="secondary", scale=1)

        gr.Markdown("---")

        # Step 3: Monitor Progress
        gr.Markdown("### Step 3: Monitor Your Progress")
        
        with gr.Row():
            with gr.Column():
                status_out = gr.Textbox(
                    label="Current Status",
                    lines=4,
                    interactive=False,
                    placeholder="Click 'Reset Environment' to start..."
                )
                scores_out = gr.Textbox(
                    label="Score Breakdown",
                    lines=6,
                    interactive=False
                )
            
            with gr.Column():
                circuit_out = gr.Textbox(
                    label="Current Circuit",
                    lines=10,
                    interactive=False,
                    placeholder="Your circuit will appear here..."
                )

        history_out = gr.Textbox(
            label="Step History",
            lines=8,
            interactive=False,
            placeholder="Action history will be logged here..."
        )

        # Advanced: Full State
        with gr.Accordion("Advanced: Full Environment State", open=False):
            state_out = gr.JSON(label="Complete State Dump")

        # Footer
        gr.Markdown(
            """
            ---
            **Tips:**
            - Start simple: H on qubit 0, then CNOT(0,1) creates a Bell state
            - Watch the fidelity score — aim for >0.90 on easy tasks
            - Fewer gates = better efficiency and noise scores
            - Use REMOVE to undo mistakes
            - Use PARAM to fine-tune RX/RZ angles
            """
        )

        # Event Handlers
        OUTS = [status_out, circuit_out, scores_out, history_out]

        reset_btn.click(fn=do_reset, inputs=[task_dd], outputs=OUTS)
        step_btn.click(fn=do_step, inputs=[action_dd, gate_dd, qubit_0, qubit_1, parameter], outputs=OUTS)
        
        def fetch_state():
            s = env.state
            return s.dict() if hasattr(s, "dict") else s
        
        state_btn.click(fn=fetch_state, inputs=[], outputs=[state_out])

        # Initialize task info on load
        demo.load(fn=update_task_info, inputs=[task_dd], outputs=[task_info])

    return demo


# ---------------------------------------------------------------------------
# Create Gradio app and mount to FastAPI
# ---------------------------------------------------------------------------
demo = create_gradio_app()

# Mount Gradio at root
app = gr.mount_gradio_app(fastapi_app, demo, path="/")

# ---------------------------------------------------------------------------
# Entry Point
# ---------------------------------------------------------------------------
def main():
    """Entry point for 'uv run server' command."""
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)

if __name__ == "__main__":
    main()