348 lines
12 KiB
Markdown
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, ¶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<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.
|