| """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: |
| 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)) |
|
|