Files
quantum-bridge-mcp/docs/superpowers/plans/sub3-executor.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

348 lines
12 KiB
Markdown

# 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<<n, shots)` → call `.get_outcome_count()` for `HashMap<usize,usize>`
- 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<ValidationResult, BridgeError> {
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<SimulationResult, BridgeError> {
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, &params, &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<String, u64> = 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<f64>, Vec<usize>)> {
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<f64>
// let qubits = extract_qubit_indices(gc); // Vec<usize>
// 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<f32>` — 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.