Initial import
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user