Spaces:
Sleeping
Sleeping
| """Qubit-Medic FastAPI server. | |
| Built on **openenv-core** ``create_fastapi_app`` so the canonical OpenEnv | |
| routes (``/reset``, ``/step``, ``/state``, ``/health``, ``/schema``, | |
| ``/metadata``, ``/mcp``) are wired automatically by the framework. | |
| We add a few extras on top: | |
| * ``GET /healthz`` - the Day-0 deployment-substrate liveness probe | |
| (returns Stim/PyMatching/openenv versions). Used by the recurring | |
| 4-hour HF Spaces wakeup ping. | |
| * ``POST /decode`` - PyMatching baseline demo: takes a hand-crafted | |
| syndrome and returns the matching-decoder's prediction. Useful for | |
| the Gradio playground. | |
| Run with ``python -m qubit_medic.server.app`` or | |
| ``uvicorn qubit_medic.server.app:app --host 0.0.0.0 --port 7860``. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import sys | |
| from typing import Optional | |
| from fastapi import Body, HTTPException | |
| from fastapi.responses import HTMLResponse | |
| from openenv.core import create_fastapi_app | |
| from qubit_medic.config import DEFAULT_HOST, DEFAULT_PORT | |
| from qubit_medic.server.environment import DecoderEnvironment | |
| from qubit_medic.server.openenv_adapter import ( | |
| QubitMedicAction, | |
| QubitMedicEnvironment, | |
| QubitMedicObservation, | |
| ) | |
| logger = logging.getLogger("qubit_medic.server") | |
| logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO")) | |
| # --------------------------------------------------------------------------- # | |
| # Build the OpenEnv-compliant FastAPI app # | |
| # --------------------------------------------------------------------------- # | |
| app = create_fastapi_app( | |
| env=QubitMedicEnvironment, | |
| action_cls=QubitMedicAction, | |
| observation_cls=QubitMedicObservation, | |
| ) | |
| app.title = "Qubit-Medic OpenEnv" | |
| app.version = os.getenv("QUBIT_MEDIC_VERSION", "1.0.0") | |
| app.description = ( | |
| "RL training environment for LLM-based quantum error-correction " | |
| "decoders. Built on Stim + PyMatching with five independent verifiable " | |
| "rewards (logical correction, syndrome consistency, Hamming overlap, " | |
| "format compliance, PyMatching beat-rate). Wraps " | |
| "qubit_medic.server.environment.DecoderEnvironment in " | |
| "openenv.core.Environment - see /metadata, /schema, /docs." | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # Day-0 + demo extras # | |
| # --------------------------------------------------------------------------- # | |
| # Lazy-built *legacy* DecoderEnvironment for /decode demos. The OpenEnv | |
| # adapter has its own per-instance DecoderEnvironment; we keep this one | |
| # around for the simple synchronous `/decode` baseline endpoint. | |
| _legacy_env: Optional[DecoderEnvironment] = None | |
| def _get_legacy_env() -> DecoderEnvironment: | |
| global _legacy_env | |
| if _legacy_env is None: | |
| _legacy_env = DecoderEnvironment() | |
| _legacy_env._cache_for("L1_warmup") # noqa: SLF001 | |
| _legacy_env._cache_for("L2_target") # noqa: SLF001 | |
| return _legacy_env | |
| # --------------------------------------------------------------------------- # | |
| # Compliance Section 2 (audit 2026-04): POST /state and POST /close. # | |
| # --------------------------------------------------------------------------- # | |
| # OpenEnv's create_fastapi_app already mounts GET /state and (via the | |
| # canonical contract) does not expose /close at all. The participant-guide | |
| # audit explicitly requires POST /state and POST /close, so we surface | |
| # both as additional routes that delegate to the legacy DecoderEnvironment | |
| # singleton (the same one /decode already uses). The OpenEnv-canonical | |
| # GET /state route is preserved untouched. | |
| # --------------------------------------------------------------------------- # | |
| def post_state() -> dict: | |
| """POST mirror of the OpenEnv GET /state route. | |
| Returns a JSON-serialisable snapshot of env state. Uses the inner | |
| :meth:`DecoderEnvironment.state` (added in Section 1 compliance work) | |
| which excludes ground-truth fields by construction. | |
| """ | |
| return _get_legacy_env().state() | |
| def post_close() -> dict: | |
| """POST /close: drop in-flight episodes on the legacy env singleton. | |
| The singleton is rebuilt lazily on the next /reset, so calling /close | |
| repeatedly is idempotent. Returns a small JSON dict so the caller can | |
| confirm the request landed. | |
| """ | |
| _get_legacy_env().close() | |
| return {"ok": True, "closed": True} | |
| def healthz() -> dict: | |
| """Lightweight liveness probe (Day-0 deployment-substrate test). | |
| Returns the versions of Stim, PyMatching, and openenv so curl-ing | |
| this in a browser or from Colab proves both that networking works | |
| AND that the heavy quantum + RL deps actually loaded. Used by the | |
| recurring 4-hour HF Spaces wakeup ping. | |
| """ | |
| import stim | |
| try: | |
| import pymatching as _pm | |
| pm_v = getattr(_pm, "__version__", "unknown") | |
| except Exception as exc: # pragma: no cover - defensive | |
| pm_v = f"import-error: {exc}" | |
| try: | |
| import openenv as _oe | |
| oe_v = getattr(_oe, "__version__", "unknown") | |
| except Exception as exc: # pragma: no cover - defensive | |
| oe_v = f"import-error: {exc}" | |
| return { | |
| "ok": True, | |
| "service": "qubit-medic", | |
| "version": app.version, | |
| "stim_version": stim.__version__, | |
| "pymatching_version": pm_v, | |
| "openenv_version": oe_v, | |
| "python_version": sys.version.split()[0], | |
| } | |
| def decode( | |
| syndrome: list[int] = Body(..., embed=True), | |
| level: str = Body("L2_target", embed=True), | |
| ) -> dict: | |
| """Decode an arbitrary syndrome with PyMatching (baseline) and return | |
| its predicted Pauli frame and observable flip. | |
| Intended for the live Gradio demo: a notebook or web page can POST a | |
| hand-crafted syndrome here and visualise the matching-decoder result. | |
| """ | |
| import numpy as np | |
| env = _get_legacy_env() | |
| cache = env._cache_for(level) # noqa: SLF001 | |
| arr = np.asarray(syndrome, dtype=np.uint8) | |
| if arr.shape[0] != cache.layout.num_detectors: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"syndrome length {arr.shape[0]} != " | |
| f"{cache.layout.num_detectors} expected for {level}", | |
| ) | |
| from qubit_medic.server.physics import ( | |
| predicted_observable_flip, | |
| pymatching_predicted_pauli_frame, | |
| ) | |
| pm_obs = int(cache.matching.decode(arr)[0]) | |
| px, pz = pymatching_predicted_pauli_frame(cache.matching, arr, cache.layout) | |
| return { | |
| "level": level, | |
| "syndrome": syndrome, | |
| "pymatching_observable_flip": pm_obs, | |
| "pymatching_x_errors": px, | |
| "pymatching_z_errors": pz, | |
| "implied_observable_from_x_errors": predicted_observable_flip( | |
| px, cache.layout | |
| ), | |
| } | |
| # --------------------------------------------------------------------------- # | |
| # Root landing page # | |
| # --------------------------------------------------------------------------- # | |
| def root() -> HTMLResponse: | |
| """HTML landing page shown in the HF Spaces App tab.""" | |
| html = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Qubit-Medic — OpenEnv server</title> | |
| <style> | |
| body { font-family: sans-serif; max-width: 680px; margin: 60px auto; color: #e0e0e0; background: #0d1117; } | |
| h1 { font-size: 1.5rem; } | |
| a { color: #58a6ff; } | |
| code { background: #161b22; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; } | |
| ul { line-height: 2; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Qubit-Medic — OpenEnv server</h1> | |
| <p> | |
| This Space exposes a <strong>JSON API</strong> for the quantum | |
| error-decoding environment (Stim + PyMatching, OpenEnv contract). | |
| Use the links below to interact with it. | |
| </p> | |
| <ul> | |
| <li><a href="/docs">Interactive API docs (Swagger)</a></li> | |
| <li><a href="/redoc">ReDoc</a></li> | |
| <li><a href="/healthz">Liveness <code>GET /healthz</code></a> — versions probe</li> | |
| <li><a href="/metadata">OpenEnv <code>GET /metadata</code></a></li> | |
| </ul> | |
| <p> | |
| Typical flow: <code>POST /reset</code> then <code>POST /step</code> | |
| with the model’s text action — see the schema in <a href="/docs">/docs</a>. | |
| </p> | |
| </body> | |
| </html>""" | |
| return HTMLResponse(content=html) | |
| # --------------------------------------------------------------------------- # | |
| # Local entry point # | |
| # --------------------------------------------------------------------------- # | |
| def _main() -> None: | |
| import uvicorn | |
| uvicorn.run( | |
| "qubit_medic.server.app:app", | |
| host=os.getenv("QUBIT_MEDIC_HOST", DEFAULT_HOST), | |
| port=int(os.getenv("QUBIT_MEDIC_PORT", str(DEFAULT_PORT))), | |
| log_level=os.getenv("LOG_LEVEL", "info").lower(), | |
| ) | |
| if __name__ == "__main__": | |
| _main() | |