"""Shared utilities for dynamic surface code circuit builders. These helpers centralize the Stim string assembly, coordinate bookkeeping, and noisy layer construction used by the dynamic variants inspired by Morvan et al. (2025). Each builder returns a Stim circuit string with qubit coordinates, noisy entangling layers, detectors that stitch time-adjacent measurements, and a terminal logical observable constructed from a boundary measurement. """ from __future__ import annotations from dataclasses import dataclass from typing import Dict, Iterable, List, Optional, Sequence, Tuple from surface_code_in_stem.noise_models import NoiseModel, resolve_noise_model from surface_code_in_stem.surface_code import adjacent_coords, prepare_coords Coord = Tuple[float, float] @dataclass class DynamicLayout: """Container for the square-lattice surface code coordinates used here.""" datas: List[Coord] x_measures: List[Coord] z_measures: List[Coord] coord_to_index: Dict[Coord, int] @classmethod def build(cls, distance: int) -> "DynamicLayout": datas, x_measures, z_measures, coord_to_index = prepare_coords(distance) return cls(datas=datas, x_measures=x_measures, z_measures=z_measures, coord_to_index=coord_to_index) class StimStringBuilder: """Utility for assembling Stim circuit strings with detector bookkeeping.""" def __init__(self, coord_lookup: Dict[int, Coord]): self.lines: List[str] = [] self.measurement_count = 0 self.coord_lookup = coord_lookup def add(self, line: str) -> None: if line.strip(): self.lines.append(line.rstrip()) def measure(self, qubits: Sequence[int]) -> List[int]: if not qubits: return [] targets = " ".join(map(str, qubits)) self.add(f"M {targets}") indices = list(range(self.measurement_count, self.measurement_count + len(qubits))) self.measurement_count += len(qubits) return indices def detector(self, coord: Coord, rec_indices: Sequence[int]) -> None: offsets = [self.measurement_count - idx for idx in rec_indices] rec_terms = " ".join(f"rec[-{offset}]" for offset in offsets) self.add(f"DETECTOR({coord[0]}, {coord[1]}) {rec_terms}") def observable(self, qubit_measure_indices: Sequence[int]) -> None: offsets = [self.measurement_count - idx for idx in qubit_measure_indices] rec_terms = " ".join(f"rec[-{offset}]" for offset in offsets) self.add(f"OBSERVABLE_INCLUDE(0) {rec_terms}") def qubit_coords(self) -> None: for index, coord in self.coord_lookup.items(): self.add(f"QUBIT_COORDS({coord[0]},{coord[1]}) {index}") def build(self) -> str: return "\n".join(self.lines) + "\n" def index_string(coords: Iterable[Coord], c2i: Dict[Coord, int]) -> str: return " ".join(str(c2i[c]) for c in coords) def noisy_layer( builder: StimStringBuilder, gate: str, pairs: List[int], idle_qubits: Sequence[int], noise_model: NoiseModel, layer_id: str, ) -> None: if not pairs: return targets = " ".join(map(str, pairs)) builder.add(f"{gate} {targets}") for line in noise_model.gate_noise( gate=gate, pair_targets=pairs, idle_targets=idle_qubits, layer_id=layer_id ): builder.add(line) builder.add("TICK") def orientation_pairs( measures: List[Coord], orient: int, c2i: Dict[Coord, int], reorder: Sequence[int] | None = None ) -> List[int]: pairs: List[int] = [] for measure in measures: adj = adjacent_coords(measure) if reorder is not None: adj = [adj[i] for i in reorder] if orient < len(adj) and adj[orient] in c2i: pairs.extend([c2i[measure], c2i[adj[orient]]]) return pairs def stabilizer_cycle( builder: StimStringBuilder, layout: DynamicLayout, p: float, orientations: Sequence[int], gate: str, noise_model: Optional[NoiseModel] = None, reset_data: bool = False, measure_data: bool = False, prev_meas: Dict[Coord, int] | None = None, ) -> Dict[Coord, int]: prev_meas = prev_meas or {} datas, x_measures, z_measures, c2i = ( layout.datas, layout.x_measures, layout.z_measures, layout.coord_to_index, ) noise_model = resolve_noise_model(p, noise_model) measure_qubits = x_measures + z_measures all_qubits = datas + measure_qubits reset_targets = measure_qubits + (datas if reset_data else []) if reset_targets: builder.add(f"R {index_string(reset_targets, c2i)}") for line in noise_model.reset_noise(qubits=[c2i[q] for q in reset_targets], layer_id="dynamic_reset"): builder.add(line) builder.add("TICK") builder.add(f"H {index_string(x_measures, c2i)}") for line in noise_model.gate_noise(gate="H", pair_targets=[], idle_targets=[c2i[q] for q in all_qubits], layer_id="dynamic_h_pre"): builder.add(line) builder.add("TICK") reorder = [0, 2, 1, 3] for orient in orientations: cz_pairs = orientation_pairs(z_measures, orient, c2i) cx_pairs = orientation_pairs(x_measures, orient, c2i, reorder=reorder) active_pair_targets = set(cz_pairs) | set(cx_pairs) idle_targets = [c2i[q] for q in all_qubits if c2i[q] not in active_pair_targets] noisy_layer(builder, gate, cz_pairs, idle_targets, noise_model, f"dynamic_z_orient_{orient}") noisy_layer(builder, gate, cx_pairs, idle_targets, noise_model, f"dynamic_x_orient_{orient}") builder.add(f"H {index_string(x_measures, c2i)}") for line in noise_model.gate_noise(gate="H", pair_targets=[], idle_targets=[c2i[q] for q in all_qubits], layer_id="dynamic_h_post"): builder.add(line) builder.add("TICK") measurement_targets = measure_qubits + (datas if measure_data else []) for line in noise_model.measurement_noise(qubits=[c2i[q] for q in measurement_targets], layer_id="dynamic_measure"): builder.add(line) record_indices = builder.measure([c2i[q] for q in measurement_targets]) coord_order = measure_qubits + (datas if measure_data else []) coord_to_rec = {coord: idx for coord, idx in zip(coord_order, record_indices)} for coord in measure_qubits: current = coord_to_rec[coord] if coord in prev_meas: builder.detector(coord, [prev_meas[coord], current]) else: builder.detector(coord, [current]) return coord_to_rec