# Sub-plan 3 — LocalSimulator / Executor (Task 5) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** Implement `LocalSimulator` — the spinoza statevector engine. Walks the oq3_semantics AST to extract gate ops, applies them to a spinoza `State`, samples measurement counts, and optionally returns the full statevector. **Starting state (after sub-plan 2):** - All of sub-plan 1 files present - `src/validator.rs` — `CircuitValidator` fully implemented and tested - `src/main.rs` declares `mod validator;` - `cargo test` passes (validator tests green) **Deliverable:** `cargo test` passes with 7 executor unit tests green. One commit added. **Key APIs:** - `spinoza::core::{apply, c_apply, cc_apply, reservoir_sampling, State}` - `spinoza::gates::Gate` — variants: `H, X, Y, Z, P(f64), RX(f64), RY(f64), RZ(f64), SWAP(usize,usize)` - No `S/T/Sdg/Tdg` variant — use `Gate::P(PI/2.0)`, `Gate::P(PI/4.0)`, etc. - `reservoir_sampling(&state, 1<` - Endianness warning: if Bell test sees `"01"/"10"`, reverse qubit index: `num_qubits - 1 - raw_idx` **Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Task 5. --- ## Task 5: LocalSimulator — spinoza executor **Files:** - Modify: `src/executor.rs` (add `LocalSimulator` + helpers below the trait definitions) ### 5a — Write failing tests first - [ ] **Step 1: Replace the mock test block in src/executor.rs with real tests** Replace the entire `#[cfg(test)]` block at the bottom of `src/executor.rs` with: ```rust #[cfg(test)] mod tests { use super::*; use crate::types::{CircuitSource, ShotCount}; const BELL_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;"#; const X_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; x q[0]; c = measure q;"#; const H_CIRCUIT: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; h q[0]; c = measure q;"#; fn sim() -> LocalSimulator { LocalSimulator::new() } #[test] fn bell_circuit_only_produces_00_and_11() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); assert_eq!(result.shots, 10_000); let total: u64 = result.counts.values().sum(); assert_eq!(total, 10_000); for key in result.counts.keys() { assert!(key == "00" || key == "11", "unexpected bitstring: {key}"); } } #[test] fn bell_circuit_is_roughly_balanced() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); let count_00 = result.counts.get("00").copied().unwrap_or(0) as f64; let ratio = count_00 / 10_000.0; assert!((0.45..=0.55).contains(&ratio), "ratio_00={ratio:.3}"); } #[test] fn x_gate_always_produces_one() { let result = sim() .run(&CircuitSource(X_CIRCUIT.to_string()), ShotCount(1_000), false) .unwrap(); assert_eq!(result.counts.get("1").copied().unwrap_or(0), 1_000); } #[test] fn h_gate_produces_roughly_balanced_single_qubit() { let result = sim() .run(&CircuitSource(H_CIRCUIT.to_string()), ShotCount(10_000), false) .unwrap(); let count_0 = result.counts.get("0").copied().unwrap_or(0) as f64; let ratio = count_0 / 10_000.0; assert!((0.45..=0.55).contains(&ratio), "ratio_0={ratio:.3}"); } #[test] fn run_returns_statevector_when_requested() { let result = sim() .run(&CircuitSource(BELL_CIRCUIT.to_string()), ShotCount(100), true) .unwrap(); let sv = result.statevector.expect("statevector should be present"); assert_eq!(sv.len(), 4); // 2^2 let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum(); assert!((norm - 1.0).abs() < 1e-9, "norm={norm}"); } #[test] fn local_simulator_name_is_local_simulator() { assert_eq!(sim().name(), "local_simulator"); } #[test] fn local_simulator_max_qubits_is_28() { assert_eq!(sim().max_qubits(), MAX_LOCAL_QUBITS); } } ``` - [ ] **Step 2: Confirm tests fail (LocalSimulator undefined)** ```bash cargo test executor::tests 2>&1 | tail -5 ``` Expected: compile error `cannot find struct LocalSimulator`. ### 5b — Implementation - [ ] **Step 3: Add imports and LocalSimulator struct above the #[cfg(test)] block** Insert this block between the trait definitions and the `#[cfg(test)]` block in `src/executor.rs`: ```rust use std::collections::HashMap; use std::f64::consts::PI; use std::time::Instant; use oq3_semantics::syntax_to_semantics::parse_source_string; use spinoza::core::{apply, c_apply, cc_apply, reservoir_sampling, State}; use spinoza::gates::Gate; pub struct LocalSimulator; impl LocalSimulator { pub fn new() -> Self { Self } } impl CanIntrospect for LocalSimulator { fn name(&self) -> &str { "local_simulator" } fn max_qubits(&self) -> usize { MAX_LOCAL_QUBITS } fn supported_gates(&self) -> &[&str] { SUPPORTED_GATES } } impl CanValidate for LocalSimulator { fn validate(&self, circuit: &CircuitSource) -> Result { use crate::validator::CircuitValidator; CircuitValidator::new(MAX_LOCAL_QUBITS).validate(circuit) } } impl CanExecute for LocalSimulator { fn run( &self, circuit: &CircuitSource, shots: ShotCount, return_statevector: bool, ) -> Result { let start = Instant::now(); let parse_result = parse_source_string(&circuit.0, Some("circuit.qasm"), None::<&[&str]>); let program = parse_result.program(); let num_qubits = count_qubits(program); if num_qubits == 0 { return Err(BridgeError::Simulation("circuit declares no qubits".to_string())); } if num_qubits > MAX_LOCAL_QUBITS { return Err(BridgeError::QubitLimitExceeded { requested: num_qubits, limit: MAX_LOCAL_QUBITS, }); } let mut state = State::new(num_qubits); for (gate_name, params, qubits) in extract_gate_ops(program) { apply_gate(&gate_name, ¶ms, &qubits, &mut state)?; } let statevector = if return_statevector { Some( state.reals.iter() .zip(state.imags.iter()) .map(|(&r, &i)| (r as f64, i as f64)) .collect(), ) } else { None }; let num_states = 1usize << num_qubits; let mut reservoir = reservoir_sampling(&state, num_states, shots.0 as usize); let raw_counts = reservoir.get_outcome_count(); let counts: HashMap = raw_counts .into_iter() .map(|(idx, cnt)| (format!("{:0>width$b}", idx, width = num_qubits), cnt as u64)) .collect(); Ok(SimulationResult { counts, shots: shots.0, execution_time_ms: start.elapsed().as_secs_f64() * 1000.0, statevector, }) } } impl Backend for LocalSimulator {} ``` - [ ] **Step 4: Add apply_gate function** ```rust fn apply_gate( gate_name: &str, params: &[f64], qubits: &[usize], state: &mut State, ) -> Result<(), BridgeError> { let supported = SUPPORTED_GATES.join(", "); match (gate_name, qubits) { ("h", &[t]) => apply(Gate::H, state, t), ("x", &[t]) => apply(Gate::X, state, t), ("y", &[t]) => apply(Gate::Y, state, t), ("z", &[t]) => apply(Gate::Z, state, t), ("s", &[t]) => apply(Gate::P(PI / 2.0), state, t), ("t", &[t]) => apply(Gate::P(PI / 4.0), state, t), ("sdg", &[t]) => apply(Gate::P(-PI / 2.0), state, t), ("tdg", &[t]) => apply(Gate::P(-PI / 4.0), state, t), ("rx", &[t]) if params.len() == 1 => apply(Gate::RX(params[0]), state, t), ("ry", &[t]) if params.len() == 1 => apply(Gate::RY(params[0]), state, t), ("rz", &[t]) if params.len() == 1 => apply(Gate::RZ(params[0]), state, t), ("cx", &[ctrl, tgt]) => c_apply(Gate::X, state, ctrl, tgt), ("cz", &[ctrl, tgt]) => c_apply(Gate::Z, state, ctrl, tgt), ("swap",&[a, b]) => apply(Gate::SWAP(a, b), state, a), ("ccx", &[c0, c1, tgt]) => cc_apply(Gate::X, state, c0, c1, tgt), ("measure", _) => {} // sampling handles measurement _ => return Err(BridgeError::UnsupportedGate { gate: gate_name.to_string(), line: 0, supported, }), } Ok(()) } ``` - [ ] **Step 5: Add extract_gate_ops and count_qubits** These functions walk the AST. Use the variant names you identified in **sub-plan 2, Task 4 Step 3**. The skeletons below must be replaced with real match arms: ```rust use oq3_semantics::asg::Stmt; /// Extracts (gate_name_lowercase, float_params, qubit_indices) for every gate call. fn extract_gate_ops(program: &oq3_semantics::asg::Program) -> Vec<(String, Vec, Vec)> { let mut ops = Vec::new(); for stmt in program.stmts() { // ← adjust method name if needed // Fill in from AST exploration (sub-plan 2): // if let Stmt::GateCall(gc) = stmt { // let name = resolve_gate_name(gc).to_lowercase(); // let params = extract_float_params(gc); // Vec // let qubits = extract_qubit_indices(gc); // Vec // ops.push((name, params, qubits)); // } let _ = stmt; } ops } /// Counts total declared qubits (sum of all qubit register widths). fn count_qubits(program: &oq3_semantics::asg::Program) -> usize { let mut total = 0; for stmt in program.stmts() { // ← adjust method name if needed // Fill in from AST exploration (sub-plan 2): // if let Stmt::QubitDeclaration(qd) = stmt { // total += qd.width().unwrap_or(1); // } let _ = stmt; } total } ``` - [ ] **Step 6: Run tests and iterate until all 7 pass** ```bash cargo test executor::tests 2>&1 ``` **Debugging guide:** | Failing test | Likely cause | Fix | |---|---|---| | `bell_circuit_only_produces_00_and_11` sees `"01"` or `"10"` | spinoza qubit index convention reversed vs QASM3 | In `extract_gate_ops`, flip qubit index: `num_qubits - 1 - raw_idx` | | `bell_circuit_only_produces_00_and_11` sees all zeros | `extract_gate_ops` returning empty (match arm wrong) | Re-check variant name from exploration | | `x_gate_always_produces_one` fails | Same empty ops issue | Same fix | | `run_returns_statevector_when_requested` norm ≠ 1 | `state.reals`/`state.imags` indexing issue | Verify spinoza `State` fields are `Vec` — cast with `as f64` | | `local_simulator_max_qubits_is_28` fails | `MAX_LOCAL_QUBITS` not 28 | Check `src/executor.rs` constant | - [ ] **Step 7: Run full test suite** ```bash cargo test 2>&1 ``` Expected: all tests pass (validator + executor). - [ ] **Step 8: Commit** ```bash git add src/executor.rs git commit -m "feat: implement LocalSimulator with spinoza statevector engine" ``` --- ## Final verification ```bash cargo fmt --check && cargo clippy -- -D warnings && cargo test ``` Expected: all green. Sub-plan 3 complete — hand off to sub-plan 4.