Gyanateet Dutta
Fix Space loading: direct Streamlit, lazy imports, ReNova page, fix deps
9f8cf99
Raw
History Blame Contribute Delete
5.2 kB
"""Environment abstractions for RL-based calibration and steering.
Observation vectors are detector statistics (e.g., detector click rates), while
actions are perturbations applied to a control-parameter vector.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
import numpy as np
from surface_code_in_stem.surface_code import surface_code_circuit_string
class ControlEnvironment(Protocol):
"""Protocol for calibration environments.
Implementations should expose detector-statistic observations and accept
action vectors representing control-parameter perturbations.
"""
action_dim: int
def reset(self) -> np.ndarray:
"""Reset the environment and return the initial observation."""
def step(self, action: np.ndarray) -> tuple[np.ndarray, float, dict[str, float]]:
"""Apply an action, return (observation, reward, diagnostics)."""
@dataclass(frozen=True)
class StimCalibrationConfig:
"""Configuration for a Stim-backed calibration environment."""
distance: int = 3
rounds: int = 3
shots: int = 128
base_error_rate: float = 0.001
seed: int = 0
@dataclass(frozen=True)
class QECEnvConfig:
"""Backward-compatible config used by the Streamlit Syndrome-Net page."""
distance: int = 3
rounds: int = 3
noise: float = 0.001
shots: int = 128
seed: int = 0
parameter_dim: int = 4
def qec_environment(config: QECEnvConfig) -> "StimCalibrationEnvironment":
"""Create a Stim calibration environment from legacy QEC config."""
stim_config = StimCalibrationConfig(
distance=config.distance,
rounds=config.rounds,
shots=config.shots,
base_error_rate=config.noise,
seed=config.seed,
)
return StimCalibrationEnvironment(stim_config, parameter_dim=config.parameter_dim)
class StimCalibrationEnvironment:
"""Stim-backed environment for policy calibration.
The environment maintains a parameter vector ``theta``. Each action is added
to ``theta``. The effective circuit error rate is computed as
``clip(base_error_rate + theta.sum(), 1e-6, 0.2)``.
"""
def __init__(self, config: StimCalibrationConfig, parameter_dim: int = 4):
if parameter_dim <= 0:
raise ValueError("parameter_dim must be positive.")
self.config = config
self.action_dim = parameter_dim
self._rng = np.random.default_rng(config.seed)
self.theta = np.zeros(parameter_dim, dtype=np.float64)
def _build_sampler(self, physical_error_rate: float):
try:
import stim
except ModuleNotFoundError as exc: # pragma: no cover
raise ImportError("Stim is required for StimCalibrationEnvironment.") from exc
circuit_text = surface_code_circuit_string(
self.config.distance,
self.config.rounds,
float(physical_error_rate),
)
circuit = stim.Circuit(circuit_text)
sampler = circuit.compile_detector_sampler(seed=self.config.seed)
return sampler
def _evaluate(self) -> tuple[np.ndarray, float, dict[str, float]]:
physical_error_rate = float(
np.clip(self.config.base_error_rate + np.sum(self.theta), 1e-6, 0.2)
)
sampler = self._build_sampler(physical_error_rate)
det_samples, obs_samples = sampler.sample(
self.config.shots,
separate_observables=True,
)
detector_rates = np.mean(det_samples, axis=0, dtype=np.float64)
logical_error_rate = float(np.mean(obs_samples[:, 0], dtype=np.float64))
reward = -logical_error_rate
diagnostics = {
"logical_error_rate": logical_error_rate,
"effective_p": physical_error_rate,
}
return detector_rates, reward, diagnostics
def reset(self) -> np.ndarray:
self.theta.fill(0.0)
observation, _, _ = self._evaluate()
return observation
def step(self, action: np.ndarray) -> tuple[np.ndarray, float, dict[str, float]]:
action = np.asarray(action, dtype=np.float64)
if action.shape != (self.action_dim,):
raise ValueError(f"action must have shape ({self.action_dim},).")
self.theta = np.clip(self.theta + action, -0.05, 0.05)
return self._evaluate()
class HardwareTraceAdapter:
"""Adapter hooks for future hardware trace integration.
Users should subclass this and implement trace acquisition plus reward
extraction from experiment metadata.
"""
def observation_from_trace(self, trace: np.ndarray) -> np.ndarray:
"""Convert raw hardware traces to detector-statistic observations."""
trace = np.asarray(trace, dtype=np.float64)
if trace.ndim != 2:
raise ValueError("trace must be a 2D array [shots, detectors].")
return np.mean(trace, axis=0)
def reward_from_trace(self, trace: np.ndarray) -> float:
"""Compute reward from hardware trace.
Placeholder implementation minimizes total detector activity.
"""
obs = self.observation_from_trace(trace)
return -float(np.mean(obs))