| |
| |
| |
| |
| |
|
|
| """ |
| Quantum Circuit Optimization Environment Implementation. |
| |
| A noise-aware, hardware-constrained reinforcement learning environment |
| for quantum circuit design and optimization. |
| |
| Physics backend: Qiskit Statevector (stateless — no Aer, no noise models). |
| RL logic: Pure Python / NumPy (unchanged from original design). |
| |
| Design principle: |
| Environment = decision + RL logic |
| Qiskit = stateless physics engine |
| """ |
|
|
| |
| |
| |
| |
| import os |
| import sys |
|
|
| _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) |
|
|
| import logging |
| import math |
| from typing import Any, Dict, List, Optional, Tuple |
| from uuid import uuid4 |
|
|
| logger = logging.getLogger(__name__) |
|
|
| import numpy as np |
|
|
| |
| |
| |
| from qiskit import QuantumCircuit |
| from qiskit.quantum_info import Statevector |
|
|
| from openenv.core.env_server.interfaces import Environment |
|
|
| |
| |
| try: |
| from ..models import ActionType, GateType, QuantumAction, QuantumObservation, QuantumState |
| from ..graders import ( |
| AggregateGrader, |
| ConstraintsGrader, |
| EfficiencyGrader, |
| FidelityGrader, |
| NoiseGrader, |
| UnitaryGrader, |
| ) |
| except ImportError: |
| from models import ActionType, GateType, QuantumAction, QuantumObservation, QuantumState |
| from graders import ( |
| AggregateGrader, |
| ConstraintsGrader, |
| EfficiencyGrader, |
| FidelityGrader, |
| NoiseGrader, |
| UnitaryGrader, |
| ) |
|
|
| try: |
| from .tasks import TASK_REGISTRY |
| except ImportError: |
| from tasks import TASK_REGISTRY |
|
|
|
|
| |
| |
| |
| MAX_QUBITS: int = 4 |
| MAX_STEPS: int = 15 |
| MAX_DEPTH: int = 20 |
|
|
|
|
| |
| |
| |
|
|
| def build_qiskit_circuit(circuit_gates: List[Dict[str, Any]], num_qubits: int) -> QuantumCircuit: |
| """ |
| Convert a list of gate dicts into a Qiskit QuantumCircuit. |
| |
| Supports: H, X, CNOT, SWAP, RX, RZ. |
| All other gate names are silently skipped. |
| |
| Args: |
| circuit_gates: List of dicts with keys ``gate``, ``qubits``, ``parameter``. |
| num_qubits: Number of qubits in the circuit. |
| |
| Returns: |
| A Qiskit :class:`QuantumCircuit` with the gates applied in order. |
| """ |
| qc = QuantumCircuit(num_qubits) |
|
|
| for g in circuit_gates: |
| name = g["gate"] |
| q = g["qubits"] |
| p = g.get("parameter") |
|
|
| if name == "H": |
| qc.h(q[0]) |
| elif name == "X": |
| qc.x(q[0]) |
| elif name == "CNOT": |
| qc.cx(q[0], q[1]) |
| elif name == "SWAP": |
| |
| |
| qc.swap(q[0], q[1]) |
| elif name == "RX": |
| qc.rx(float(p) if p is not None else 0.0, q[0]) |
| elif name == "RZ": |
| qc.rz(float(p) if p is not None else 0.0, q[0]) |
| |
|
|
| return qc |
|
|
|
|
| |
| |
| |
|
|
| def compute_statevector(circuit_gates: List[Dict[str, Any]], num_qubits: int) -> np.ndarray: |
| """ |
| Compute the statevector of a circuit using Qiskit. |
| |
| Starts from the |00...0> ground state (Qiskit default). |
| Does NOT store any Qiskit objects — returns a plain NumPy complex128 array. |
| |
| Args: |
| circuit_gates: Gate list as produced by the environment's _gates field. |
| num_qubits: Number of qubits. |
| |
| Returns: |
| Complex NumPy array of shape (2**num_qubits,). |
| """ |
| if not circuit_gates: |
| |
| sv = np.zeros(2 ** num_qubits, dtype=np.complex128) |
| sv[0] = 1.0 |
| return sv |
|
|
| qc = build_qiskit_circuit(circuit_gates, num_qubits) |
| sv = np.asarray(Statevector.from_instruction(qc).data, dtype=np.complex128) |
|
|
| |
| norm = np.linalg.norm(sv) |
| if norm > 0: |
| sv = sv / norm |
|
|
| |
| assert len(sv) == 2 ** num_qubits, ( |
| f"Statevector length {len(sv)} != 2^{num_qubits}={2**num_qubits}" |
| ) |
| return sv |
|
|
|
|
| |
| |
| |
|
|
| def compute_circuit_depth(gates: List[Dict[str, Any]], num_qubits: int) -> int: |
| """ |
| Compute circuit depth (longest path through any qubit). |
| |
| Each gate occupies one time-step on each of its qubits. |
| Depth = max over all qubits of the number of gate layers touching that qubit. |
| """ |
| if not gates: |
| return 0 |
|
|
| qubit_depths = [0] * num_qubits |
| for gate_info in gates: |
| qubits = gate_info["qubits"] |
| if len(qubits) == 1: |
| qubit_depths[qubits[0]] += 1 |
| elif len(qubits) >= 2: |
| |
| max_d = max(qubit_depths[q] for q in qubits) |
| for q in qubits: |
| qubit_depths[q] = max_d + 1 |
|
|
| return max(qubit_depths) if qubit_depths else 0 |
|
|
|
|
| |
| |
| |
|
|
| class QuantumCircuitEnvironment(Environment): |
| """ |
| RL environment for noise-aware, hardware-constrained quantum circuit optimization. |
| |
| The agent builds a quantum circuit step by step to match a target quantum state |
| while minimising depth, gate count, noise impact, and SWAP overhead. |
| |
| Physics backend: Qiskit Statevector (stateless — called per step, not stored). |
| Supports concurrent WebSocket sessions. |
| """ |
|
|
| SUPPORTS_CONCURRENT_SESSIONS: bool = True |
|
|
| def __init__(self, seed: Optional[int] = None): |
| """ |
| Initialise the environment. |
| |
| Args: |
| seed: Optional RNG seed for reproducibility. |
| """ |
| self._seed = seed or 42 |
| self._rng = np.random.RandomState(self._seed) |
|
|
| |
| self._task_id: str = "easy" |
| self._task_config: Dict[str, Any] = {} |
| self._target_sv: Optional[np.ndarray] = None |
| self._target_unitary: Optional[np.ndarray] = None |
|
|
| |
| self._gates: List[Dict[str, Any]] = [] |
| self._num_qubits: int = 2 |
|
|
| |
| self._step_count: int = 0 |
| self._max_steps: int = 20 |
| self._done: bool = False |
| self._prev_score: float = 0.0 |
| self._prev_fidelity: float = 0.0 |
| self._prev_depth: int = 0 |
| self._prev_action: Optional[ActionType] = None |
| self._last_reward: float = 0.0 |
| self._episode_id: str = str(uuid4()) |
| self._swap_count: int = 0 |
| self._prev_penalty: float = 0.0 |
|
|
| |
| self._aggregate_grader = AggregateGrader() |
|
|
| |
| |
| |
|
|
| def reset(self, config: Optional[Dict] = None) -> QuantumObservation: |
| """ |
| Reset the environment for a new episode. |
| |
| Args: |
| config: Optional configuration dictionary containing task_id. |
| |
| Returns: |
| Initial QuantumObservation. |
| """ |
| |
| task_id = config.get("task_id", "easy") if config else "easy" |
|
|
| |
| task_cls = TASK_REGISTRY.get(task_id, TASK_REGISTRY["easy"]) |
| self._task_config = task_cls.config() |
| self._task_id = self._task_config["task_id"] |
| self._num_qubits = min(self._task_config["num_qubits"], MAX_QUBITS) |
| self._max_steps = self._task_config["max_steps"] |
|
|
| |
| if self._task_config["num_qubits"] > MAX_QUBITS: |
| logger.warning( |
| "Task '%s' requests %d qubits; clamped to MAX_QUBITS=%d.", |
| self._task_id, self._task_config["num_qubits"], MAX_QUBITS, |
| ) |
|
|
| |
| if hasattr(task_cls, "target_unitary"): |
| self._target_unitary = task_cls.target_unitary() |
| self._target_sv = None |
| logger.info("Loaded task '%s' (%d qubits) with UNITARY target.", self._task_id, self._num_qubits) |
| else: |
| self._target_sv = task_cls.target_statevector() |
| self._target_unitary = None |
| logger.info("Loaded task '%s' (%d qubits) with STATEVECTOR target.", self._task_id, self._num_qubits) |
|
|
| |
| weights = self._task_config.get("grader_weights") |
| if weights: |
| self._aggregate_grader = AggregateGrader(weights=weights) |
|
|
| |
| self._gates = [] |
| self._step_count = 0 |
| self._done = False |
| self._prev_action = None |
| self._last_reward = 0.0 |
| self._episode_id = str(uuid4()) |
| self._swap_count = 0 |
|
|
| |
| initial_scores = self._compute_all_scores() |
| self._prev_score = initial_scores["aggregate"] |
| self._prev_fidelity = initial_scores["fidelity"] |
| self._prev_depth = initial_scores["depth"] |
| self._prev_penalty = ( |
| 0.5 * (1.0 - initial_scores["efficiency"]) + |
| 0.3 * (1.0 - initial_scores["noise"]) + |
| 0.2 * (1.0 - initial_scores["constraints"]) |
| ) |
|
|
| return self._build_observation(reward=0.0) |
|
|
| def step(self, action: QuantumAction) -> QuantumObservation: |
| """ |
| Execute one step in the environment. |
| |
| STEP 6: Each step: |
| 1. Applies action to circuit_gates |
| 2. Computes statevector via Qiskit |
| 3. Computes reward from state |
| 4. Returns updated observation |
| |
| Args: |
| action: QuantumAction specifying the gate operation. |
| |
| Returns: |
| QuantumObservation with updated metrics and shaped reward. |
| """ |
| if self._done: |
| return self._build_observation(reward=0.0) |
|
|
| self._step_count += 1 |
|
|
| |
| if action.action_type == ActionType.STOP: |
| self._done = True |
| reward = self._compute_terminal_reward() |
|
|
| if self._prev_score > 0.8: |
| reward += 0.2 |
| else: |
| reward -= 0.2 |
| |
| |
| reward = max(-1.0, min(1.0, reward)) |
|
|
| logger.info( |
| "[STOP] task=%s qubits=%d step=%d reward=%.4f", |
| self._task_id, self._num_qubits, self._step_count, reward, |
| ) |
| return self._build_observation(reward=reward) |
|
|
| |
| valid, penalty_msg = self._validate_action(action) |
| if not valid: |
| self._check_done() |
| logger.info("[INVALID] task=%s step=%d error=%s", self._task_id, self._step_count, penalty_msg) |
| return self._build_observation(reward=-0.1, error=penalty_msg) |
|
|
| |
| pre_action_fidelity = self._prev_fidelity |
| pre_action_depth = self._prev_depth |
|
|
| |
| |
| self._apply_action(action) |
|
|
| |
| scores = self._compute_all_scores() |
| current_score = scores["aggregate"] |
| current_fidelity = scores["fidelity"] |
| current_depth = scores["depth"] |
|
|
| |
| |
| |
|
|
| |
| current_penalty = ( |
| 0.5 * (1.0 - scores["efficiency"]) + |
| 0.3 * (1.0 - scores["noise"]) + |
| 0.2 * (1.0 - scores["constraints"]) |
| ) |
|
|
| |
| if len(self._gates) >= 2: |
| prev_gate = self._gates[-2] |
| curr_gate = self._gates[-1] |
| if prev_gate.get("gate") == curr_gate.get("gate") and prev_gate.get("qubits") == curr_gate.get("qubits"): |
| current_penalty += 0.005 |
|
|
| |
| if action.action_type == ActionType.PARAM: |
| current_penalty += 0.003 |
|
|
| |
| if action.action_type == ActionType.SWAP: |
| current_penalty += 0.02 |
|
|
| |
| fidelity_delta = current_fidelity - self._prev_fidelity |
| penalty_delta = current_penalty - getattr(self, '_prev_penalty', 0.0) |
|
|
| reward = fidelity_delta * 3.0 - penalty_delta * 0.5 |
|
|
| |
| if action.action_type == ActionType.ADD and fidelity_delta < 0: |
| reward += 0.15 |
|
|
| |
| if action.action_type == ActionType.REMOVE: |
| |
| if self._last_reward != 0: |
| reward = min(reward, abs(self._last_reward) * 0.5) |
| |
| |
| if current_depth < pre_action_depth and abs(current_fidelity - pre_action_fidelity) < 1e-6: |
| reward = min(reward + 0.005, 0.01) |
| elif current_fidelity < pre_action_fidelity: |
| reward -= 0.05 |
|
|
| |
| if action.action_type == ActionType.REMOVE and self._prev_action == ActionType.ADD: |
| reward -= 0.05 |
|
|
| |
| if self._prev_action == ActionType.REMOVE: |
| reward -= 0.02 |
|
|
| |
| reward += 0.02 |
|
|
| |
| optimal_depth = self._task_config.get("optimal_depth", 5) |
| if current_fidelity > 0.95 and current_depth > optimal_depth: |
| reward -= 0.1 |
|
|
| |
| reward = np.tanh(reward) |
|
|
| |
| self._prev_action = action.action_type |
| self._last_reward = reward |
|
|
| |
| if self._step_count >= self._max_steps: |
| self._done = True |
| reward -= 0.1 |
| reward = max(-1.0, min(1.0, reward)) |
|
|
| |
| self._prev_score = current_score |
| self._prev_fidelity = current_fidelity |
| self._prev_depth = current_depth |
| self._prev_penalty = current_penalty |
|
|
| logger.info( |
| "[STEP] task=%s qubits=%d step=%d | " |
| "fid=%.4f eff=%.4f noise=%.4f cstr=%.4f agg=%.4f | " |
| "depth=%d gates=%d swaps=%d reward=%.4f", |
| self._task_id, self._num_qubits, self._step_count, |
| scores["fidelity"], scores["efficiency"], |
| scores["noise"], scores["constraints"], scores["aggregate"], |
| current_depth, scores["gate_count"], self._swap_count, reward, |
| ) |
|
|
| self._check_done() |
| return self._build_observation(reward=reward) |
|
|
| @property |
| def state(self) -> QuantumState: |
| """Return the full internal state.""" |
| scores = self._compute_all_scores() |
| return QuantumState( |
| episode_id=self._episode_id, |
| step_count=self._step_count, |
| circuit_gates=list(self._gates), |
| target_description=self._task_config.get("description", ""), |
| task_id=self._task_id, |
| max_steps=self._max_steps, |
| noise_model_name=self._task_config.get("noise_model", "none"), |
| current_fidelity=scores.get("fidelity", 0.0), |
| current_score=scores.get("aggregate", 0.0), |
| ) |
|
|
| |
| |
| |
|
|
| def _validate_action(self, action: QuantumAction) -> Tuple[bool, Optional[str]]: |
| """Validate that the action is legal.""" |
| at = action.action_type |
|
|
| if at == ActionType.ADD: |
| if action.gate is None: |
| return False, "ADD action requires a gate type" |
| if not action.qubits: |
| return False, "ADD action requires target qubits" |
| for q in action.qubits: |
| if q < 0 or q >= self._num_qubits: |
| return False, f"Qubit index {q} out of range [0, {self._num_qubits - 1}]" |
| if action.gate == GateType.CNOT and len(action.qubits) != 2: |
| return False, "CNOT requires exactly 2 qubits" |
| if action.gate in (GateType.H, GateType.X, GateType.RX, GateType.RZ): |
| if len(action.qubits) != 1: |
| return False, f"{action.gate.value} requires exactly 1 qubit" |
| if action.gate in (GateType.RX, GateType.RZ) and action.parameter is None: |
| return False, f"{action.gate.value} requires a parameter (angle)" |
|
|
| elif at == ActionType.REMOVE: |
| if not self._gates: |
| return False, "Cannot REMOVE from an empty circuit" |
|
|
| elif at == ActionType.SWAP: |
| if len(action.qubits) != 2: |
| return False, "SWAP requires exactly 2 qubits" |
| for q in action.qubits: |
| if q < 0 or q >= self._num_qubits: |
| return False, f"Qubit index {q} out of range [0, {self._num_qubits - 1}]" |
| if action.qubits[0] == action.qubits[1]: |
| return False, "SWAP qubit indices must be distinct" |
|
|
| elif at == ActionType.PARAM: |
| if not self._gates: |
| return False, "Cannot PARAM-tune with empty circuit" |
| if action.parameter is None: |
| return False, "PARAM action requires a parameter value" |
|
|
| return True, None |
|
|
| |
| |
| |
|
|
| def _apply_action(self, action: QuantumAction) -> None: |
| """ |
| Apply a validated action to the circuit gate list. |
| |
| IMPORTANT: No Qiskit objects are stored — only plain Python dicts. |
| State recomputation happens lazily in _compute_all_scores(). |
| """ |
| at = action.action_type |
|
|
| if at == ActionType.ADD: |
| gate_dict: Dict[str, Any] = { |
| "gate": action.gate.value if action.gate else "H", |
| "qubits": list(action.qubits), |
| } |
| if action.parameter is not None: |
| gate_dict["parameter"] = action.parameter |
| self._gates.append(gate_dict) |
|
|
| elif at == ActionType.REMOVE: |
| |
| if self._gates: |
| self._gates.pop() |
|
|
| elif at == ActionType.SWAP: |
| |
| |
| self._gates.append({ |
| "gate": "SWAP", |
| "qubits": list(action.qubits), |
| }) |
| self._swap_count += 1 |
|
|
| elif at == ActionType.PARAM: |
| |
| for i in range(len(self._gates) - 1, -1, -1): |
| if self._gates[i]["gate"] in ("RX", "RZ"): |
| self._gates[i]["parameter"] = action.parameter |
| break |
|
|
| |
| |
| |
|
|
| def _compute_all_scores(self) -> Dict[str, float]: |
| """ |
| Compute all grader scores for the current circuit. |
| |
| Calls compute_statevector() → Qiskit for the statevector path. |
| UnitaryGrader uses its own matrix math (unchanged). |
| """ |
| |
| if self._target_unitary is not None: |
| fidelity_score = UnitaryGrader.grade(self._gates, self._target_unitary, self._num_qubits) |
| |
| current_sv = np.zeros(2 ** self._num_qubits, dtype=np.complex128) |
| current_sv[0] = 1.0 |
| else: |
| |
| current_sv = compute_statevector(self._gates, self._num_qubits) |
|
|
| |
| expected_dim = 2 ** self._num_qubits |
| assert current_sv.shape[0] == expected_dim, ( |
| f"Statevector dim {current_sv.shape[0]} != 2^{self._num_qubits}={expected_dim}" |
| ) |
| if self._target_sv is not None: |
| assert self._target_sv.shape[0] == expected_dim, ( |
| f"Target dim {self._target_sv.shape[0]} != 2^{self._num_qubits}={expected_dim}" |
| ) |
|
|
| fidelity_score = FidelityGrader.grade(current_sv, self._target_sv) |
|
|
| |
| depth = compute_circuit_depth(self._gates, self._num_qubits) |
| gate_count = len(self._gates) |
| efficiency_score = EfficiencyGrader.grade( |
| depth, gate_count, |
| self._task_config.get("max_depth", 10), |
| self._task_config.get("max_gate_count", 15), |
| ) |
|
|
| |
| noise_model = self._task_config.get("noise_model", "none") |
| noise_score, noise_estimate = NoiseGrader.grade(self._gates, noise_model) |
|
|
| |
| connectivity = self._task_config.get("connectivity", []) |
| conn_tuples = [tuple(c) for c in connectivity] if connectivity else [] |
| constraints_score = ConstraintsGrader.grade(self._gates, conn_tuples, self._num_qubits) |
|
|
| |
| if self._target_unitary is not None: |
| aggregate = 0.7 * fidelity_score + 0.3 * efficiency_score |
| else: |
| aggregate = self._aggregate_grader.grade( |
| fidelity_score, efficiency_score, noise_score, constraints_score |
| ) |
|
|
| |
| if 0.7 < fidelity_score < 0.95: |
| scale = 0.8 + 0.2 * (fidelity_score - 0.7) / 0.25 |
| aggregate *= scale |
|
|
| |
| target_tolerance = self._task_config.get("target_tolerance") |
| if target_tolerance is not None and fidelity_score > target_tolerance: |
| aggregate += 0.2 * (fidelity_score - target_tolerance) / (1.0 - target_tolerance) |
|
|
| |
| noise_penalty = (1.0 - noise_score) * 2.0 |
| aggregate -= noise_penalty |
|
|
| |
| aggregate -= depth * 0.005 |
|
|
| |
| aggregate -= 0.02 * self._swap_count |
|
|
| |
| if depth > MAX_DEPTH: |
| aggregate -= 0.1 * (depth - MAX_DEPTH) |
|
|
| |
| strict_budget = self._task_config.get("strict_gate_budget") |
| if strict_budget is not None and gate_count > strict_budget: |
| overflow = gate_count - strict_budget |
| penalty = overflow / strict_budget |
| aggregate *= max(0.3, 1.0 - penalty) |
|
|
| |
| aggregate = max(0.0001, min(0.9999, aggregate)) |
| assert 0.0 <= aggregate <= 1.0, f"Aggregate score out of range: {aggregate}" |
|
|
| return { |
| "fidelity": fidelity_score, |
| "efficiency": efficiency_score, |
| "noise": noise_score, |
| "noise_estimate": noise_estimate, |
| "constraints": constraints_score, |
| "aggregate": aggregate, |
| "depth": depth, |
| "gate_count": gate_count, |
| } |
|
|
| def _compute_terminal_reward(self) -> float: |
| """Compute bonus/penalty at episode end based on aggregate quality.""" |
| scores = self._compute_all_scores() |
| aggregate = scores["aggregate"] |
| return 0.2 if aggregate > 0.8 else -0.2 |
|
|
| |
| |
| |
|
|
| def _check_done(self) -> None: |
| """Check if episode should terminate.""" |
| if self._step_count >= self._max_steps: |
| self._done = True |
|
|
| |
| |
| |
|
|
| def _build_observation( |
| self, reward: float, error: Optional[str] = None |
| ) -> QuantumObservation: |
| """Build and return a QuantumObservation.""" |
| scores = self._compute_all_scores() |
| depth = int(scores.get("depth", 0)) |
| gate_count = int(scores.get("gate_count", 0)) |
|
|
| |
| self._prev_fidelity = scores["fidelity"] |
| self._prev_depth = depth |
|
|
| return QuantumObservation( |
| circuit_gates=list(self._gates), |
| fidelity=scores["fidelity"], |
| depth=depth, |
| gate_count=gate_count, |
| noise_estimate=scores.get("noise_estimate", 0.0), |
| valid_actions=self._get_valid_actions(), |
| score=scores["aggregate"], |
| task_id=self._task_id, |
| num_qubits=self._num_qubits, |
| max_steps=self._max_steps, |
| steps_taken=self._step_count, |
| target_description=self._task_config.get("description", ""), |
| done=self._done, |
| reward=reward, |
| metadata={ |
| "fidelity_score": scores["fidelity"], |
| "efficiency_score": scores["efficiency"], |
| "noise_score": scores["noise"], |
| "constraints_score": scores["constraints"], |
| "aggregate_score": scores["aggregate"], |
| "error": error, |
| }, |
| ) |
|
|
| def _get_valid_actions(self) -> List[str]: |
| """Return list of valid action type strings.""" |
| actions = ["ADD", "STOP"] |
| if self._gates: |
| actions.append("REMOVE") |
| has_param_gates = any(g["gate"] in ("RX", "RZ") for g in self._gates) |
| if has_param_gates: |
| actions.append("PARAM") |
| if self._num_qubits >= 2: |
| actions.append("SWAP") |
| return actions |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.INFO, format="%(name)s | %(message)s") |
| print("=== Quantum Circuit Optimization Environment -- Qiskit Smoke Test ===") |
| print() |
|
|
| env = QuantumCircuitEnvironment(seed=42) |
|
|
| |
| print("--- Test 1: Bell State (H + CNOT) ---") |
| obs = env.reset(config={"task_id": "easy"}) |
| print(f" Initial fidelity: {obs.fidelity:.4f}, score: {obs.score:.4f}") |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0])) |
| print(f" After H(0): fid={obs.fidelity:.4f} score={obs.score:.4f} reward={obs.reward:.4f}") |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1])) |
| print(f" After CNOT(0,1): fid={obs.fidelity:.4f} score={obs.score:.4f} reward={obs.reward:.4f}") |
| obs = env.step(QuantumAction(action_type=ActionType.STOP)) |
| bell_score = obs.score |
| assert bell_score > 0.5, f"FAIL: Bell correct circuit scored {bell_score:.4f}, expected > 0.5" |
| print(f" PASS: Bell score = {bell_score:.4f}") |
| print() |
|
|
| |
| print("--- Test 2: GHZ State (INCOMPLETE: H+CNOT, missing CNOT(1,2)) ---") |
| obs = env.reset(config={"task_id": "medium"}) |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0])) |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1])) |
| obs = env.step(QuantumAction(action_type=ActionType.STOP)) |
| ghz_incomplete = obs.score |
| print(f" STOP: score={ghz_incomplete:.4f} reward={obs.reward:.4f}") |
| assert ghz_incomplete < 0.5, f"FAIL: GHZ incomplete scored {ghz_incomplete:.4f}, expected < 0.5" |
| print(f" PASS: GHZ incomplete score = {ghz_incomplete:.4f}") |
| print() |
|
|
| |
| print("--- Test 3: GHZ State (CORRECT: H+CNOT(0,1)+CNOT(1,2)) ---") |
| obs = env.reset(config={"task_id": "medium"}) |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0])) |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1])) |
| obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[1, 2])) |
| obs = env.step(QuantumAction(action_type=ActionType.STOP)) |
| ghz_correct = obs.score |
| print(f" STOP: score={ghz_correct:.4f} reward={obs.reward:.4f}") |
| assert ghz_correct > 0.5, f"FAIL: GHZ correct scored {ghz_correct:.4f}, expected > 0.5" |
| print(f" PASS: GHZ correct score = {ghz_correct:.4f}") |
| print() |
|
|
| |
| print("--- Test 4: SWAP — qubit routing effect ---") |
| |
| |
| |
| |
| gates_before_swap = [{"gate": "X", "qubits": [0]}] |
| sv_before = compute_statevector(gates_before_swap, 2) |
| gates_after_swap = [{"gate": "X", "qubits": [0]}, {"gate": "SWAP", "qubits": [0, 1]}] |
| sv_after = compute_statevector(gates_after_swap, 2) |
|
|
| |
| idx_before = int(np.argmax(np.abs(sv_before))) |
| idx_after = int(np.argmax(np.abs(sv_after))) |
| print(f" Before SWAP: dominant index = {idx_before} (amplitude={abs(sv_before[idx_before]):.4f})") |
| print(f" After SWAP: dominant index = {idx_after} (amplitude={abs(sv_after[idx_after]):.4f})") |
|
|
| |
| assert abs(sv_before[idx_before]) > 0.99, f"Before SWAP not a pure state: {sv_before}" |
| assert abs(sv_after[idx_after]) > 0.99, f"After SWAP not a pure state: {sv_after}" |
| |
| assert idx_before != idx_after, ( |
| f"SWAP did not change basis state index: before={idx_before}, after={idx_after}" |
| ) |
| print(" PASS: SWAP physically swapped qubit states") |
| print() |
|
|
| |
| print("--- Test 5: REMOVE — state reverts ---") |
| obs = env.reset(config={"task_id": "easy"}) |
| fid_empty = obs.fidelity |
| obs_h = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0])) |
| fid_after_h = obs_h.fidelity |
| obs_rm = env.step(QuantumAction(action_type=ActionType.REMOVE)) |
| fid_after_remove = obs_rm.fidelity |
| print(f" Fidelity (empty): {fid_empty:.6f}") |
| print(f" Fidelity (H(0)): {fid_after_h:.6f}") |
| print(f" Fidelity (REMOVE→ reverted): {fid_after_remove:.6f}") |
| assert abs(fid_after_remove - fid_empty) < 1e-6, ( |
| f"FAIL: state did not revert after REMOVE " |
| f"(empty={fid_empty:.6f}, after_remove={fid_after_remove:.6f})" |
| ) |
| print(" PASS: REMOVE reverted circuit state correctly") |
| print() |
|
|
| |
| print("=" * 50) |
| print("SCORE DISCRIMINATION SUMMARY:") |
| print(f" Bell correct: {bell_score:.4f} (should be > 0.5)") |
| print(f" GHZ incomplete: {ghz_incomplete:.4f} (should be < 0.5)") |
| print(f" GHZ correct: {ghz_correct:.4f} (should be > 0.5)") |
| assert bell_score > ghz_incomplete, "FAIL: Bell should score higher than incomplete GHZ" |
| assert ghz_correct > ghz_incomplete, "FAIL: Correct GHZ should score higher than incomplete" |
| print("\n ALL SMOKE TESTS PASSED!") |
|
|