From 9af114e391c42b2463932f93d31e6ba1261e4877 Mon Sep 17 00:00:00 2001 From: Vincent Bourdon Date: Tue, 9 Jun 2026 16:14:55 +0200 Subject: [PATCH] Initial import --- .gitignore | 25 + CLAUDE.md | 255 ++ Cargo.lock | 2429 +++++++++++++++ Cargo.toml | 38 + README.md | 157 + benches/simulation.rs | 57 + curriculum/curriculum.json | 348 +++ .../plans/2026-04-28-quantum-bridge-mcp-v1.md | 2612 +++++++++++++++++ .../plans/2026-04-29-quantum-tutor.md | 65 + .../00-prerequisites.md | 73 + .../01-curriculum-types.md | 223 ++ .../02-curriculum-loader.md | 144 + .../03-progress-store.md | 174 ++ .../04-circuit-analyzer.md | 119 + .../05-exercise-checker.md | 222 ++ .../2026-04-29-quantum-tutor/06-get-lesson.md | 157 + .../07-check-exercise.md | 152 + .../08-explain-result.md | 194 ++ .../09-get-progress.md | 138 + .../2026-04-29-quantum-tutor/10-wire-tools.md | 157 + .../11-curriculum-module-2.md | 92 + .../12-curriculum-module-3.md | 89 + .../13-curriculum-module-4.md | 89 + .../14-curriculum-module-5.md | 94 + .../15-curriculum-module-6.md | 77 + .../16-curriculum-module-7.md | 88 + .../17-integration-tests.md | 222 ++ .../plans/2026-04-29-quantum-tutor/README.md | 52 + docs/superpowers/plans/sub1-foundation.md | 308 ++ docs/superpowers/plans/sub2-validator.md | 358 +++ docs/superpowers/plans/sub3-executor.md | 347 +++ docs/superpowers/plans/sub4-tools-server.md | 608 ++++ docs/superpowers/plans/sub5-quality-ci.md | 802 +++++ .../2026-04-28-quantum-bridge-mcp-design.md | 236 ++ .../specs/2026-04-29-quantum-tutor-design.md | 264 ++ src/circuit_analyzer.rs | 47 + src/error.rs | 79 + src/executor.rs | 581 ++++ src/lib.rs | 10 + src/main.rs | 25 + src/progress.rs | 124 + src/tools/list_backends.rs | 49 + src/tools/mod.rs | 200 ++ src/tools/run_circuit.rs | 112 + src/tools/tutor_tools.rs | 495 ++++ src/tools/validate_circuit.rs | 97 + src/tutor.rs | 386 +++ src/types.rs | 71 + src/validator.rs | 302 ++ tests/mcp_protocol.rs | 170 ++ tests/proptest_invariants.rs | 67 + tests/reference/tutor/exercises_fail.jsonl | 3 + tests/reference/tutor/exercises_pass.jsonl | 9 + tests/tutor_integration.rs | 159 + vendor/spinoza/Cargo.toml | 36 + vendor/spinoza/benches/benchmark.rs | 188 ++ vendor/spinoza/examples/ccx.rs | 37 + vendor/spinoza/examples/circuits.rs | 40 + vendor/spinoza/examples/expectation.rs | 30 + vendor/spinoza/examples/h.rs | 29 + vendor/spinoza/examples/measurement.rs | 25 + vendor/spinoza/examples/multicontrol.rs | 24 + vendor/spinoza/examples/p.rs | 29 + vendor/spinoza/examples/pairs.rs | 92 + vendor/spinoza/examples/pauli.rs | 34 + vendor/spinoza/examples/qcbm.rs | 55 + vendor/spinoza/examples/rx.rs | 29 + vendor/spinoza/examples/ry.rs | 29 + vendor/spinoza/examples/rz.rs | 29 + vendor/spinoza/examples/sampling.rs | 42 + vendor/spinoza/examples/u.rs | 29 + vendor/spinoza/examples/unitaries.rs | 16 + vendor/spinoza/examples/value_encoding.rs | 37 + vendor/spinoza/examples/x.rs | 29 + vendor/spinoza/examples/y.rs | 29 + vendor/spinoza/examples/z.rs | 29 + vendor/spinoza/src/circuit.rs | 1251 ++++++++ vendor/spinoza/src/config.rs | 81 + vendor/spinoza/src/consts.rs | 46 + vendor/spinoza/src/core.rs | 340 +++ vendor/spinoza/src/gates.rs | 2237 ++++++++++++++ vendor/spinoza/src/lib.rs | 142 + vendor/spinoza/src/math.rs | 79 + vendor/spinoza/src/measurement.rs | 247 ++ vendor/spinoza/src/openqasm.rs | 305 ++ vendor/spinoza/src/unitaries.rs | 374 +++ vendor/spinoza/src/utils.rs | 378 +++ 87 files changed, 20848 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 benches/simulation.rs create mode 100644 curriculum/curriculum.json create mode 100644 docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/00-prerequisites.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/01-curriculum-types.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/02-curriculum-loader.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/03-progress-store.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/04-circuit-analyzer.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/05-exercise-checker.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/06-get-lesson.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/07-check-exercise.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/08-explain-result.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/09-get-progress.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/10-wire-tools.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/11-curriculum-module-2.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/12-curriculum-module-3.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/13-curriculum-module-4.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/14-curriculum-module-5.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/15-curriculum-module-6.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/16-curriculum-module-7.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/17-integration-tests.md create mode 100644 docs/superpowers/plans/2026-04-29-quantum-tutor/README.md create mode 100644 docs/superpowers/plans/sub1-foundation.md create mode 100644 docs/superpowers/plans/sub2-validator.md create mode 100644 docs/superpowers/plans/sub3-executor.md create mode 100644 docs/superpowers/plans/sub4-tools-server.md create mode 100644 docs/superpowers/plans/sub5-quality-ci.md create mode 100644 docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md create mode 100644 docs/superpowers/specs/2026-04-29-quantum-tutor-design.md create mode 100644 src/circuit_analyzer.rs create mode 100644 src/error.rs create mode 100644 src/executor.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/progress.rs create mode 100644 src/tools/list_backends.rs create mode 100644 src/tools/mod.rs create mode 100644 src/tools/run_circuit.rs create mode 100644 src/tools/tutor_tools.rs create mode 100644 src/tools/validate_circuit.rs create mode 100644 src/tutor.rs create mode 100644 src/types.rs create mode 100644 src/validator.rs create mode 100644 tests/mcp_protocol.rs create mode 100644 tests/proptest_invariants.rs create mode 100644 tests/reference/tutor/exercises_fail.jsonl create mode 100644 tests/reference/tutor/exercises_pass.jsonl create mode 100644 tests/tutor_integration.rs create mode 100644 vendor/spinoza/Cargo.toml create mode 100644 vendor/spinoza/benches/benchmark.rs create mode 100644 vendor/spinoza/examples/ccx.rs create mode 100644 vendor/spinoza/examples/circuits.rs create mode 100644 vendor/spinoza/examples/expectation.rs create mode 100644 vendor/spinoza/examples/h.rs create mode 100644 vendor/spinoza/examples/measurement.rs create mode 100644 vendor/spinoza/examples/multicontrol.rs create mode 100644 vendor/spinoza/examples/p.rs create mode 100644 vendor/spinoza/examples/pairs.rs create mode 100644 vendor/spinoza/examples/pauli.rs create mode 100644 vendor/spinoza/examples/qcbm.rs create mode 100644 vendor/spinoza/examples/rx.rs create mode 100644 vendor/spinoza/examples/ry.rs create mode 100644 vendor/spinoza/examples/rz.rs create mode 100644 vendor/spinoza/examples/sampling.rs create mode 100644 vendor/spinoza/examples/u.rs create mode 100644 vendor/spinoza/examples/unitaries.rs create mode 100644 vendor/spinoza/examples/value_encoding.rs create mode 100644 vendor/spinoza/examples/x.rs create mode 100644 vendor/spinoza/examples/y.rs create mode 100644 vendor/spinoza/examples/z.rs create mode 100644 vendor/spinoza/src/circuit.rs create mode 100644 vendor/spinoza/src/config.rs create mode 100644 vendor/spinoza/src/consts.rs create mode 100644 vendor/spinoza/src/core.rs create mode 100644 vendor/spinoza/src/gates.rs create mode 100644 vendor/spinoza/src/lib.rs create mode 100644 vendor/spinoza/src/math.rs create mode 100644 vendor/spinoza/src/measurement.rs create mode 100644 vendor/spinoza/src/openqasm.rs create mode 100644 vendor/spinoza/src/unitaries.rs create mode 100644 vendor/spinoza/src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fff15e --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# --- Rust / Cargo build artifacts --- +/target/ +**/*.rs.bk +*.pdb + +# Criterion benchmark output +/criterion/ + +# --- AI / agent working directories --- +/.omc/ +/work/ +.claude/ +*.session + +# --- Cross-validation (Python) --- +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# --- Editor / OS --- +.DS_Store +*.swp +.idea/ +.vscode/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0a8d975 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,255 @@ +# quantum-bridge-mcp — Development Rules + +## Project + +Pure Rust MCP quantum simulator. See `docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md` for the full spec. + +Stack: `rmcp` (MCP SDK) + `oq3_semantics` (QASM3 parser) + `spinoza` (statevector engine) + `tokio`. + +## Bootstrap (greenfield project) + +`src/` and `Cargo.toml` do not exist yet. To initialize: + +```bash +cargo new --name quantum-bridge-mcp . # creates src/main.rs + Cargo.toml +``` + +Key dependencies to add to `Cargo.toml` (check current versions on crates.io): + +```toml +[dependencies] +rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk" } +oq3_semantics = "0.1" # official Qiskit OpenQASM3 parser in Rust +spinoza = "2" # pure Rust statevector engine +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "1" +tracing = "0.1" +tracing-subscriber = "0.3" + +[dev-dependencies] +proptest = "1" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "simulation" +harness = false +``` + +## Architecture + +``` +src/ +├── main.rs # Entry point: MCP stdio server (rmcp), reads from stdin, writes to stdout +├── tools/ +│ ├── mod.rs # MCP tool registration +│ ├── list_backends.rs # list_backends handler +│ ├── validate_circuit.rs # validate_circuit handler +│ └── run_circuit.rs # run_circuit handler +├── validator.rs # oq3_semantics wrapper → structured diagnostics +├── executor.rs # OpenQASM 3 AST → Spinoza gates → counts +└── error.rs # thiserror error types +tests/ +├── integration/ # MCP JSON-RPC roundtrip, tool schema conformance +├── reference/ # QASM3 circuits + expected statevectors (generated by scripts/gen_reference.py) +│ └── invalid/ # Invalid circuits + exact expected error messages (golden files) +└── proptest/ # Property-based tests (unitarity, normalization) +benches/ +└── simulation.rs # Criterion benchmarks (Bell < 5 ms, 20 qubits < 500 ms) +scripts/ +└── gen_reference.py # Prerequisites: Python 3.11 + pip install qiskit qiskit-aer +.github/workflows/ +├── ci.yml # Rust tests + Qiskit cross-validation +└── release.yml # cargo-dist (precompiled binaries) +``` + +Central trait: `Backend` (executor.rs). `LocalSimulator` implements `Backend` in V1. +MCP tools depend on `Backend`, never on `LocalSimulator` directly. + +--- + +## 1. Test-Driven Development — absolute rule + +**Red → Green → Refactor. In that order. Always.** + +1. Write the failing test first. +2. Write the minimum code to make it pass. +3. Refactor without breaking the tests. + +Never ship functional code without a test covering it. A feature without a test does not exist. + +### Types of tests to write (see spec §8) + +- **Unit**: individual gates, isolated behaviors — inside the module (`#[cfg(test)]`) +- **Statistical**: probabilistic counts with chi² test (N=10k shots, fixed seed) +- **Qiskit cross-validation**: `tests/reference/` — QASM3 circuits + expected statevectors generated by `scripts/gen_reference.py` +- **Property-based**: quantum invariants with `proptest` (unitarity, normalization) +- **Golden files**: invalid circuits → exact error messages in `tests/reference/invalid/` +- **MCP protocol**: JSON-RPC roundtrip, schema conformance +- **Benchmarks**: Criterion in `benches/`, SLA Bell < 5 ms, 20 qubits < 500 ms + +### Test rules + +- Tests must be deterministic. Use a fixed seed for any probabilistic simulation. +- One test = one behavioral assertion. Do not test multiple things in a single test. +- Test names describe behavior: `apply_h_to_zero_gives_equal_superposition`, not `test_h`. +- Never comment out a failing test — fix it or remove the feature. + +--- + +## 2. Clean Code + +- Expressive names: `parse_circuit` not `parse`, `num_qubits` not `nq`, `QubitIndex` not `usize`. +- One function = one responsibility. If the name contains "and", it's two functions. +- Max 3 parameters; beyond that: config struct. No boolean parameters: use an enum or two functions. +- Comments = *why* only (hidden constraint, non-obvious invariant). Not "what". +- No commented-out dead code. No `TODO`/`FIXME` committed to main. + +--- + +## 3. SOLID Principles (project applications) + +- **SRP**: `validator.rs` = parse+validate only. `executor.rs` = translate AST+simulate only. Tools = orchestration only. +- **OCP**: adding IBM (V1.5) does not touch `executor.rs` — `Backend` trait is mandatory. +- **LSP**: every `impl Backend` respects the contract: `run()` never panics, returns `Result`. +- **ISP**: separate traits `CanValidate` / `CanExecute` / `CanIntrospect`. No fat trait. +- **DIP**: tools depend on `&dyn Backend`, not on `LocalSimulator`. Gates injected via `&dyn GateSet`. + +--- + +## 4. Rust Architecture + +### Error handling + +- **No `unwrap()` or `expect()` in production code.** Only in tests (if truly justified). +- All errors propagate via `Result` with an explicit error type (not `Box`). +- Use `thiserror` to define error types. Each variant has a clear message. +- Errors exposed via MCP include: structured code, human-readable message, circuit position if applicable. + +```rust +// Good +#[derive(thiserror::Error, Debug)] +pub enum ValidationError { + #[error("gate '{gate}' not supported (line {line})")] + UnsupportedGate { gate: String, line: usize }, +} + +// Forbidden +fn parse(input: &str) -> bool { ... } // error swallowed +``` + +### Modules and visibility + +- Everything is private by default. `pub` is intentional, not the default. +- A module's public API = its contract. Minimize it. +- No cascading `pub use` without intent. Re-export only what belongs to the public interface. + +### Types and newtypes + +- Use newtypes for domain concepts: `QubitIndex(usize)`, `ShotCount(u32)` rather than bare `usize` everywhere. +- Do not pass raw primitives where a domain type exists. +- `Config` structs are separate from `State` structs. Do not mix configuration and mutable state. + +### No magic numbers + +```rust +// Forbidden +if qubits > 28 { ... } + +// Good +const MAX_LOCAL_QUBITS: usize = 28; +if qubits > MAX_LOCAL_QUBITS { ... } +``` + +--- + +## 5. Refactoring Rules + +- Refactor **after** tests pass, never before. +- A refactoring does not change observable behavior. If the tests change, it's no longer a refactoring. +- The rule of three: first time, just code. Second time, code and grimace. Third time, abstract. +- Do not abstract prematurely. Three similar lines are not necessarily an abstraction. + +--- + +## 6. Git & Commits + +- **One commit = one coherent intent.** Do not mix feature + fix + refactoring in the same commit. +- Commit messages in English, conventional format: `feat:`, `fix:`, `test:`, `refactor:`, `chore:`, `docs:`. +- Tests and the production code they cover go in the same commit. +- Never commit code that breaks tests (`cargo test` must pass). +- Never commit code that does not compile. +- Prefer readable atomic commits over a single large WIP commit. + +--- + +## 7. Development Commands + +```bash +# Bootstrap (first setup) — verifies dependencies compile +cargo build + +# Run the MCP server (stdio) locally +cargo run + +# f32 mode: +30% speed, +1 qubit at equal RAM (negligible precision loss) +cargo run -- --f32 + +# Claude Desktop config (~/.config/claude/claude_desktop_config.json) +# { "mcpServers": { "quantum-bridge": { "command": "/path/to/quantum-bridge-mcp" } } } + +# Tests (all) +cargo test + +# Tests + Qiskit cross-validation +cargo test && python3 scripts/gen_reference.py --validate + +# Benchmarks +cargo bench + +# Strict lint +cargo clippy -- -D warnings + +# Format +cargo fmt --check + +# Full check (before commit) +cargo fmt --check && cargo clippy -- -D warnings && cargo test +``` + +CI rejects any code that fails any of these checks. + +--- + +## 8. Quantum Gotchas + +- **Endianness / bit-ordering**: Qiskit and Spinoza may use opposite qubit-ordering conventions. Cross-validation (`tests/reference/`) is there to catch this. Never assume — verify against golden files. +- **Probabilities vs counts**: `run_circuit` returns counts (integers), not probabilities. Normalize on the Claude side if needed. +- **Fixed seed in tests**: any probabilistic simulation in tests uses a fixed seed. Without this, statistical tests are flaky. +- **Qubit limit**: statevector = `2^n × 16 bytes`. 25 qubits = 512 MB. Validate `num_qubits ≤ MAX_LOCAL_QUBITS` before simulation. + +--- + +## 9. Environment + +Environment variables: +- `RUST_LOG=debug` — enables tracing logs during development +- `IBM_QUANTUM_TOKEN` — IBM token (V1.5 only, not used in V1) + +Prerequisites for cross-validation (CI only): +```bash +python3 -m pip install qiskit qiskit-aer # Python 3.11+ +python3 scripts/gen_reference.py # generates tests/reference/ +``` + +--- + +## 10. What Is Forbidden + +- `panic!()`, `unwrap()`, `expect()` in production code +- `todo!()`, `unimplemented!()` committed to main +- `unsafe` without a comment block justifying why and proving safety +- Dependencies added without justification in the commit message +- Disabled tests (`#[ignore]`) without a linked issue and resolution date +- Dead code (`#[allow(dead_code)]`) without justification diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9c96df0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2429 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ariadne" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd" +dependencies = [ + "concolor", + "unicode-width 0.1.14", + "yansi", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "boolenum" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c8abd585d7026df20a9ae12982127ba5e81cc7a09397b957e71659da8c5de8" +dependencies = [ + "proc-macro-error", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "concolor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b946244a988c390a94667ae0e3958411fa40cc46ea496a929b263d883f5f9c3" +dependencies = [ + "bitflags 1.3.2", + "concolor-query", + "is-terminal", +] + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cov-mark" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90863d8442510cddf7f46618c4f92413774635771a3e80830c8b30d183420b14" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex 1.12.3", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex 1.12.3", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "evalexpr" +version = "11.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aff27af350e7b53e82aac3e5ab6389abd8f280640ac034508dff0608c4c7e5" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4851161a11d3ad0bf9402d90ffc3967bf231768bfd7aeb61755ad06dbf1a142" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a74ddee9e0c27d2578323c13905793e91622148f138ba29738f9dddb835e90" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "target-features", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "oq3_lexer" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27bbc91e3e9d6193a44aac8f5d62c1507c41669af71a4e7e0ef66fd6470e960" +dependencies = [ + "unicode-properties", + "unicode-xid", +] + +[[package]] +name = "oq3_parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a72022fcb414e8a0912920a1cf46417b6aa95f19d4b38778df7450f8a3c17fa" +dependencies = [ + "drop_bomb", + "oq3_lexer", + "ra_ap_limit", +] + +[[package]] +name = "oq3_semantics" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72dffd869f3548190c705828d030fbb7fca94e519dcfa6a489227e5c3ffd777" +dependencies = [ + "boolenum", + "hashbrown 0.12.3", + "oq3_source_file", + "oq3_syntax", + "rowan", +] + +[[package]] +name = "oq3_source_file" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8c03f1f92c7a8f0b5249664b526169ceb8f925cb314ff93d3b27d8a4afb78c" +dependencies = [ + "ariadne", + "oq3_syntax", +] + +[[package]] +name = "oq3_syntax" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c754ce1d9da28d6c0334c212d64b521288fe8c7cf16e9727d45dcf661ff084" +dependencies = [ + "cov-mark", + "either", + "indexmap", + "itertools", + "once_cell", + "oq3_lexer", + "oq3_parser", + "rowan", + "rustc-hash", + "rustversion", + "smol_str", + "triomphe", + "xshell", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.11.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.10", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "qasm" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25b59a14eea9f3ab832595bdf051c25fe331f225b2fb1b7b06165235342714e" +dependencies = [ + "regex 0.2.11", +] + +[[package]] +name = "quantum-bridge-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "criterion", + "oq3_semantics", + "proptest", + "rand 0.8.6", + "rmcp", + "schemars 0.8.22", + "serde", + "serde_json", + "spinoza", + "tempfile", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "ra_ap_limit" +version = "0.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d33758724f997689f84146e5401e28d875a061804f861f113696f44f5232aa" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local 0.3.6", + "utf8-ranges", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick 1.1.4", + "memchr", + "regex-automata", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick 1.1.4", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rmcp" +version = "1.5.0" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#fffe138ec424e2e9e21781c7b3878405994a6082" +dependencies = [ + "async-trait", + "base64", + "chrono", + "futures", + "pastey", + "pin-project-lite", + "rmcp-macros", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "rmcp-macros" +version = "1.5.0" +source = "git+https://github.com/modelcontextprotocol/rust-sdk#fffe138ec424e2e9e21781c7b3878405994a6082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "rowan" +version = "0.15.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a58fa8a7ccff2aec4f39cc45bf5f985cec7125ab271cf681c279fd00192b49" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "memoffset", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive 1.2.1", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spinoza" +version = "0.5.1" +dependencies = [ + "arrayvec", + "clap", + "comfy-table", + "env_logger", + "evalexpr", + "multiversion", + "qasm", + "rand 0.8.6", + "rand_distr", + "rayon", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local 1.1.9", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xshell" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e56941 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "quantum-bridge-mcp" +version = "0.1.0" +edition = "2021" +description = "Zero-dependency MCP server for local OpenQASM 3.0 quantum circuit simulation" +license = "MIT" + +[lib] +name = "quantum_bridge_mcp" +path = "src/lib.rs" + +[dependencies] +rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features = ["server", "transport-io", "macros"] } +oq3_semantics = "0.7" +spinoza = { git = "https://github.com/QuState/spinoza", rev = "f900971cd31ce764ea02a62fa23a5ea861e3e430" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "0.8" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" +rand = "0.8" + +[dev-dependencies] +proptest = "1" +criterion = { version = "0.5", features = ["html_reports"] } +tempfile = "3" + +[[bench]] +name = "simulation" +harness = false + +# spinoza uses #![feature(avx512_target_feature)] which now requires nightly; +# this local patch removes that line (the feature is stable since Rust 1.89). +[patch."https://github.com/QuState/spinoza"] +spinoza = { path = "vendor/spinoza" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..390d2d8 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# quantum-bridge-mcp + +A zero-dependency MCP server for local OpenQASM 3.0 quantum circuit simulation. Runs as a single Rust binary — no Python, no cloud account required. Includes an interactive quantum-computing tutor with a built-in curriculum. + +## Tools + +### Simulation + +| Tool | Description | +|------|-------------| +| `list_backends` | List available simulation backends and their capabilities | +| `validate_circuit` | Parse and validate an OpenQASM 3.0 circuit, returning structured diagnostics with line/column | +| `run_circuit` | Execute a circuit on the local statevector simulator, returning measurement counts and optionally the full statevector | + +### Tutor + +| Tool | Description | +|------|-------------| +| `get_lesson` | Get a quantum-computing lesson with concept, example circuit, and the next pending exercise | +| `check_exercise` | Verify a submitted circuit against a curriculum exercise; returns pass/fail with feedback and records progress on success | +| `explain_result` | Analyze a circuit and its measurement results into structured pedagogical data (gate breakdown, key concept, outcome stats) | +| `get_progress` | Report the learner's progress through the curriculum (module status, current lesson, percent complete) | + +## Usage with Claude Desktop + +Add to `~/.config/claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "quantum-bridge": { + "command": "/path/to/quantum-bridge-mcp" + } + } +} +``` + +Then ask Claude things like: + +> "Simulate a Bell state circuit and show me the measurement counts." +> "Validate this OpenQASM circuit and explain any errors." +> "What backends are available for quantum simulation?" +> "Give me the next quantum-computing lesson and an exercise to try." +> "Check my circuit against the exercise and tell me how I did." + +## Example circuit + +```qasm +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` + +## Supported gates + +`h`, `x`, `y`, `z`, `s`, `t`, `sdg`, `tdg`, `rx`, `ry`, `rz`, `cx`, `cz`, `swap`, `ccx`, `measure` + +Maximum: **28 qubits** (statevector memory = 2ⁿ × 16 bytes; 25 qubits ≈ 512 MB). + +## Build + +Requires Rust stable (1.70+). + +```bash +cargo build --release +``` + +The binary is at `target/release/quantum-bridge-mcp`. + +### Optional: f32 mode + +30% faster, supports one extra qubit at equal RAM, negligible precision loss: + +```bash +cargo run --release -- --f32 +``` + +## Test + +```bash +# Unit + integration tests +cargo test + +# Property-based tests (slow — runs 256 cases per property) +cargo test --test proptest_invariants + +# All checks (required before commit) +cargo fmt --check && cargo clippy -- -D warnings && cargo test +``` + +## Benchmark + +```bash +cargo bench +``` + +Baseline timings on a modern x86_64 (Criterion median): +- `bell_1024_shots` — ~74 µs +- `10_qubits_1k_shots` — ~169 µs +- `20_qubits_100_shots` — ~18.7 ms + +All comfortably within the SLA targets (Bell < 5 ms, 20 qubits < 500 ms). + +## Architecture + +``` +src/ + main.rs — tokio async entry point, rmcp stdio server + lib.rs — public crate API + error.rs — BridgeError (thiserror) + types.rs — QubitIndex, ShotCount, CircuitSource, ValidationResult, SimulationResult + executor.rs — Backend traits + LocalSimulator (spinoza statevector engine) + validator.rs — CircuitValidator (oq3_semantics OpenQASM 3 parser) + circuit_analyzer.rs — AST-based gate breakdown + concept extraction for explain_result + tutor.rs — CurriculumLoader, lesson/exercise model (curriculum/curriculum.json) + progress.rs — ProgressStore (sandboxed JSON, QB_PROGRESS_PATH override) + tools/ + mod.rs — QuantumBridgeServer (rmcp tool_router) + list_backends.rs + validate_circuit.rs + run_circuit.rs + tutor_tools.rs — get_lesson / check_exercise / explain_result / get_progress handlers +curriculum/ + curriculum.json — 7 modules of lessons + exercises (embedded at compile time) +tests/ + mcp_protocol.rs — JSON-RPC roundtrip integration tests + proptest_invariants.rs — quantum invariants (normalisation, statevector norm, bitstring length) + tutor_integration.rs — tutor tool roundtrips, progress persistence + reference/tutor/ — golden pass/fail exercise submissions (JSONL) +benches/ + simulation.rs — Criterion benchmarks +``` + +**Backend trait:** `executor.rs` defines `CanIntrospect + CanValidate + CanExecute`. Tools depend on `&dyn Backend`, never on `LocalSimulator` directly — adding an IBM backend in V1.5 requires no changes to tool code. + +## Tech stack + +- [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk) — MCP SDK (stdio transport) +- [`oq3_semantics`](https://crates.io/crates/oq3_semantics) — Official Qiskit OpenQASM 3 parser in Rust +- [`spinoza`](https://github.com/QuState/spinoza) — Pure Rust statevector simulator (vendored, stable Rust patch) +- `tokio`, `serde`, `thiserror`, `tracing` + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `RUST_LOG=debug` | Enable debug tracing to stderr | +| `QB_PROGRESS_PATH` | Override the tutor progress file (default: `~/.config/quantum-bridge-mcp/progress.json`) | +| `IBM_QUANTUM_TOKEN` | IBM Quantum token (V1.5 only, unused in V1) | + +## License + +MIT diff --git a/benches/simulation.rs b/benches/simulation.rs new file mode 100644 index 0000000..40403e4 --- /dev/null +++ b/benches/simulation.rs @@ -0,0 +1,57 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::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;"#; + +fn make_h_circuit(n_qubits: usize, n_shots: u32) -> (CircuitSource, ShotCount) { + let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect(); + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n" + ); + (CircuitSource(circuit), ShotCount(n_shots)) +} + +fn bench_bell_1024_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + c.bench_function("bell_1024_shots", |b| { + b.iter(|| { + sim.run( + &CircuitSource(BELL_CIRCUIT.to_string()), + ShotCount(1024), + false, + ) + .unwrap() + }) + }); +} + +fn bench_10_qubits_1k_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circuit, shots) = make_h_circuit(10, 1_000); + c.bench_function("10_qubits_1k_shots", |b| { + b.iter(|| sim.run(&circuit, shots, false).unwrap()) + }); +} + +fn bench_20_qubits_100_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circuit, shots) = make_h_circuit(20, 100); + c.bench_function("20_qubits_100_shots", |b| { + b.iter(|| sim.run(&circuit, shots, false).unwrap()) + }); +} + +criterion_group!( + benches, + bench_bell_1024_shots, + bench_10_qubits_1k_shots, + bench_20_qubits_100_shots, +); +criterion_main!(benches); diff --git a/curriculum/curriculum.json b/curriculum/curriculum.json new file mode 100644 index 0000000..55a3b55 --- /dev/null +++ b/curriculum/curriculum.json @@ -0,0 +1,348 @@ +{ + "version": "1.0", + "gate_descriptions": { + "h": {"short": "Hadamard — crée une superposition égale", "effect_on_zero": "Transforme |0⟩ en (|0⟩+|1⟩)/√2", "effect_on_one": "Transforme |1⟩ en (|0⟩-|1⟩)/√2"}, + "x": {"short": "Pauli-X — NOT quantique", "effect_on_zero": "Transforme |0⟩ en |1⟩", "effect_on_one": "Transforme |1⟩ en |0⟩"}, + "y": {"short": "Pauli-Y — rotation π autour de Y", "effect_on_zero": "Transforme |0⟩ en i|1⟩", "effect_on_one": "Transforme |1⟩ en -i|0⟩"}, + "z": {"short": "Pauli-Z — flip de phase", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en -|1⟩"}, + "s": {"short": "Phase S — rotation π/2 autour de Z", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en i|1⟩"}, + "sdg": {"short": "S† — rotation -π/2 autour de Z (inverse de S)", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en -i|1⟩"}, + "t": {"short": "Phase T — rotation π/4 autour de Z", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en e^(iπ/4)|1⟩"}, + "tdg": {"short": "T† — rotation -π/4 autour de Z (inverse de T)", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en e^(-iπ/4)|1⟩"}, + "cx": {"short": "CNOT — flip la cible si le contrôle est |1⟩", "effect_on_zero": "Si contrôle=|0⟩, ne fait rien à la cible"}, + "cz": {"short": "CZ — flip de phase sur la cible si contrôle=|1⟩"}, + "swap": {"short": "SWAP — échange l'état de deux qubits"}, + "ccx": {"short": "Toffoli — flip la cible si les deux contrôles sont |1⟩"}, + "rx": {"short": "Rotation autour de l'axe X d'un angle θ"}, + "ry": {"short": "Rotation autour de l'axe Y d'un angle θ"}, + "rz": {"short": "Rotation autour de l'axe Z d'un angle θ"} + }, + "modules": [ + { + "id": 1, + "title": "Le qubit", + "lessons": [ + { + "id": 1, + "title": "États |0⟩ et |1⟩", + "concept": "Un qubit peut être dans l'état |0⟩ ou |1⟩, exactement comme un bit classique — mais aussi dans une superposition des deux. Sans rien faire, il part de |0⟩. La porte X (NOT quantique) bascule |0⟩ en |1⟩.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;", + "what_to_observe": "Lance run_circuit avec 100 shots. Tu devrais voir uniquement '1' dans les counts.", + "exercises": [ + { + "id": "1-1-a", + "prompt": "Écris un circuit OpenQASM 3.0 avec 1 qubit qui produit toujours '1' quand on le mesure. Utilise la porte X.", + "hint": "La porte X s'applique avec 'x q[0];' avant la mesure.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Parfait ! La porte X bascule |0⟩ en |1⟩.", + "feedback_fail": "Ton circuit ne produit pas toujours '1'." + }, + { + "id": "1-1-b", + "prompt": "Écris un circuit avec 1 qubit sans aucune porte, juste la mesure.", + "hint": "Un qubit non touché part de |0⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "0", "min_ratio": 0.99}], + "forbidden_outcomes": ["1"] + }, + "feedback_pass": "Sans aucune porte, le qubit reste dans |0⟩.", + "feedback_fail": "Ton circuit produit des '1' alors qu'il ne devrait pas." + } + ] + } + ] + }, + { + "id": 2, + "title": "Superposition", + "lessons": [ + { + "id": 1, + "title": "La porte Hadamard et l'état |+⟩", + "concept": "La porte H (Hadamard) transforme |0⟩ en une superposition égale de |0⟩ et |1⟩. Quand tu mesures, tu obtiens 0 ou 1 avec 50% de probabilité chacun — pas parce que c'est aléatoire au sens classique, mais parce que la mesure fait 'choisir' le qubit.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;", + "what_to_observe": "Lance run_circuit avec 1000 shots. Tu devrais voir ~500 '0' et ~500 '1'. Relance plusieurs fois : la distribution change légèrement à chaque fois.", + "exercises": [ + { + "id": "2-1-a", + "prompt": "Écris un circuit qui met un qubit en superposition parfaite (50/50) et le mesure.", + "hint": "La porte H est la seule dont tu as besoin.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.4}, + {"bitstring": "1", "min_ratio": 0.4} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Parfait ! La porte H crée bien une superposition équilibrée.", + "feedback_fail": "Tu dois voir à la fois des '0' et des '1' en quantités comparables." + }, + { + "id": "2-1-b", + "prompt": "Applique H deux fois de suite sur le même qubit. Quel est le résultat à la mesure ?", + "hint": "H est son propre inverse : H·H = Identité.", + "criteria": { + "required_outcomes": [{"bitstring": "0", "min_ratio": 0.99}], + "forbidden_outcomes": ["1"] + }, + "feedback_pass": "Excellent ! H·H = Identité : appliquer Hadamard deux fois ramène au point de départ.", + "feedback_fail": "Deux portes H d'affilée doivent ramener à |0⟩ avec certitude." + }, + { + "id": "2-1-c", + "prompt": "Pars de |1⟩ (utilise X d'abord), puis applique H. Que vois-tu ?", + "hint": "H|1⟩ = (|0⟩-|1⟩)/√2 = |−⟩. La mesure donne toujours 50/50.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.4}, + {"bitstring": "1", "min_ratio": 0.4} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Bien vu ! H|1⟩ donne aussi 50/50 — la différence (|−⟩ vs |+⟩) est dans la phase.", + "feedback_fail": "Tu devrais voir ~50/50. Utilise X puis H." + } + ] + } + ] + }, + { + "id": 3, + "title": "Interférence et phase", + "lessons": [ + { + "id": 1, + "title": "La phase quantique et l'interférence", + "concept": "La phase d'un état quantique est invisible à la mesure directe, mais deux états en superposition peuvent interférer : leurs amplitudes s'additionnent ou s'annulent. C'est le moteur de l'avantage quantique.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nz q[0];\nh q[0];\nc = measure q;", + "what_to_observe": "Ce circuit (H·Z·H) donne toujours '1'. Compare avec un simple X.", + "exercises": [ + { + "id": "3-1-a", + "prompt": "Vérifie que H·Z·H = X. Écris le circuit H·Z·H et mesure.", + "hint": "Applique h, puis z, puis h, puis mesure.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Parfait ! H·Z·H = X — l'interférence transforme un flip de phase en flip de bit.", + "feedback_fail": "Assure-toi d'appliquer h, z, h dans cet ordre." + }, + { + "id": "3-1-b", + "prompt": "Applique H, puis Rz(π), puis H. Que vois-tu ?", + "hint": "rz(pi) q[0]; — la constante pi est disponible.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Exact ! Rz(π) est équivalent à Z (à une phase globale près).", + "feedback_fail": "Vérifie ta syntaxe pour rz(pi) q[0];" + }, + { + "id": "3-1-c", + "prompt": "Crée un circuit H·S·H et mesure. Que devient la distribution ?", + "hint": "S introduit une phase de 90° qui modifie l'interférence.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.2}, + {"bitstring": "1", "min_ratio": 0.2} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Bien joué ! H·S·H produit une superposition non triviale.", + "feedback_fail": "Tu dois voir les deux outcomes. Applique h, s, h dans cet ordre." + } + ] + } + ] + }, + { + "id": 4, + "title": "2 qubits et intrication", + "lessons": [ + { + "id": 1, + "title": "Le circuit de Bell et l'intrication", + "concept": "Deux qubits peuvent être 'intriqués' : mesurer l'un détermine instantanément le résultat de l'autre. L'état de Bell est le prototype de l'intrication.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;", + "what_to_observe": "Lance avec 1000 shots. Tu dois voir uniquement '00' et '11' en quantités égales — jamais '01' ou '10'.", + "exercises": [ + { + "id": "4-1-a", + "prompt": "Crée un état de Bell entre 2 qubits : la mesure doit donner uniquement '00' ou '11'.", + "hint": "H sur q[0], puis CX q[0]→q[1].", + "criteria": { + "required_outcomes": [ + {"bitstring": "00", "min_ratio": 0.4}, + {"bitstring": "11", "min_ratio": 0.4} + ], + "forbidden_outcomes": ["01", "10"] + }, + "feedback_pass": "Excellent ! Tu as créé l'état de Bell.", + "feedback_fail": "Tu dois voir uniquement '00' et '11'. Utilise H q[0] puis CX q[0],q[1]." + }, + { + "id": "4-1-b", + "prompt": "CNOT sans superposition : pars de |00⟩ et applique CX. Que vois-tu ?", + "hint": "Sans H, q[0] reste |0⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "00", "min_ratio": 0.99}], + "forbidden_outcomes": ["01", "10", "11"] + }, + "feedback_pass": "Correct ! CNOT avec contrôle=|0⟩ ne change rien.", + "feedback_fail": "Tu dois obtenir '00' systématiquement." + }, + { + "id": "4-1-c", + "prompt": "Prépare q[0]=|1⟩ avec X, puis applique CX. Que voit-on sur q[1] ?", + "hint": "CNOT flip la cible quand le contrôle est |1⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "11", "min_ratio": 0.99}], + "forbidden_outcomes": ["00", "01", "10"] + }, + "feedback_pass": "Parfait ! |10⟩ devient |11⟩ après CX.", + "feedback_fail": "Avec q[0]=|1⟩, le CNOT doit flipper q[1]." + } + ] + } + ] + }, + { + "id": 5, + "title": "Circuits multi-qubits", + "lessons": [ + { + "id": 1, + "title": "GHZ, Toffoli et superposition uniforme", + "concept": "On peut intriqué N qubits en même temps. L'état GHZ généralise Bell à 3+ qubits. La porte Toffoli (CCX) est un AND quantique réversible.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nc = measure q;", + "what_to_observe": "Avec 3 qubits GHZ, tu dois voir uniquement '000' et '111'.", + "exercises": [ + { + "id": "5-1-a", + "prompt": "Crée l'état GHZ à 3 qubits.", + "hint": "H sur q[0], puis CX q[0]→q[1], puis CX q[0]→q[2].", + "criteria": { + "required_outcomes": [ + {"bitstring": "000", "min_ratio": 0.4}, + {"bitstring": "111", "min_ratio": 0.4} + ], + "forbidden_outcomes": ["001", "010", "011", "100", "101", "110"] + }, + "feedback_pass": "Bravo ! L'état GHZ à 3 qubits est créé.", + "feedback_fail": "L'état GHZ doit produire uniquement '000' et '111'." + }, + { + "id": "5-1-b", + "prompt": "Toffoli : prépare q[0]=|1⟩ et q[1]=|1⟩, puis applique CCX.", + "hint": "CCX flip q[2] seulement si q[0] ET q[1] sont à |1⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "111", "min_ratio": 0.99}], + "forbidden_outcomes": ["000", "001", "010", "011", "100", "101", "110"] + }, + "feedback_pass": "Toffoli est un AND quantique : il flip q[2] seulement si q[0]=q[1]=|1⟩.", + "feedback_fail": "Utilise x q[0]; x q[1]; ccx q[0],q[1],q[2];" + }, + { + "id": "5-1-c", + "prompt": "Crée une superposition uniforme de tous les états à 2 qubits (~25% chacun).", + "hint": "Applique H sur chaque qubit indépendamment.", + "criteria": { + "required_outcomes": [ + {"bitstring": "00", "min_ratio": 0.15}, + {"bitstring": "01", "min_ratio": 0.15}, + {"bitstring": "10", "min_ratio": 0.15}, + {"bitstring": "11", "min_ratio": 0.15} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Avec H sur chaque qubit tu explores 4 états simultanément.", + "feedback_fail": "Tu dois voir les 4 états chacun avec ~25%." + } + ] + } + ] + }, + { + "id": 6, + "title": "Premiers algorithmes quantiques", + "lessons": [ + { + "id": 1, + "title": "Bernstein-Vazirani et phase kickback", + "concept": "L'algorithme de Bernstein-Vazirani retrouve une chaîne secrète en un seul appel à l'oracle. Le phase kickback est le mécanisme central : faire 'remonter' une phase d'un qubit cible vers un qubit de contrôle en superposition.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nx q[1];\nh q[0];\nh q[1];\ncx q[0], q[1];\nh q[0];\nc = measure q;", + "what_to_observe": "Bernstein-Vazirani pour s='1' : le bit q[0] (à droite) vaut toujours 1 (bitstrings '01' ou '11', jamais '00' ni '10').", + "exercises": [ + { + "id": "6-1-a", + "prompt": "Bernstein-Vazirani pour s='1' : prépare l'ancilla en |−⟩ (X+H), mets q[0] en superposition (H), applique CX q[0]→q[1] (oracle pour s=1), puis H sur q[0]. Mesure les deux qubits avec bit[2] c; c = measure q;.", + "hint": "X q[1]; H q[1]; H q[0]; CX q[0],q[1]; H q[0]; puis c = measure q;. Le bit de droite (q[0]) doit toujours valoir 1.", + "criteria": { + "required_outcomes": [ + {"bitstring": "01", "min_ratio": 0.4}, + {"bitstring": "11", "min_ratio": 0.4} + ], + "forbidden_outcomes": ["00", "10"] + }, + "feedback_pass": "Excellent ! BV retrouve s='1' en un seul appel à l'oracle : q[0] (bit de droite) vaut toujours 1.", + "feedback_fail": "Le bit q[0] (à droite) doit toujours valoir 1. Tu devrais ne voir que '01' et '11'." + }, + { + "id": "6-1-b", + "prompt": "Phase kickback : prépare q[1] en |1⟩ (X), mets q[0] en superposition (H), applique CZ q[0],q[1]. Puis H q[0]. Mesure les deux qubits avec bit[2] c; c = measure q;.", + "hint": "Le CZ avec q[1]=|1⟩ ajoute une phase -1 quand q[0]=|1⟩ — la phase remonte sur q[0]. État final |11⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "11", "min_ratio": 0.99}], + "forbidden_outcomes": ["00", "01", "10"] + }, + "feedback_pass": "Parfait ! La phase de q[1] s'est 'kickback' sur q[0]. État final |11⟩, c'est la clé des algorithmes quantiques.", + "feedback_fail": "Tu dois obtenir uniquement '11'. Vérifie : x q[1]; h q[0]; cz q[0],q[1]; h q[0]; c = measure q;." + } + ] + } + ] + }, + { + "id": 7, + "title": "Algorithme de Grover", + "lessons": [ + { + "id": 1, + "title": "Recherche quantique : amplification d'amplitude", + "concept": "L'algorithme de Grover retrouve un élément marqué dans N éléments en O(√N) appels — quadratiquement plus rapide que la recherche classique. Il utilise un oracle de phase + un opérateur de diffusion.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[0];\nh q[1];\nx q[0];\nx q[1];\ncz q[0], q[1];\nx q[0];\nx q[1];\nh q[0];\nh q[1];\nc = measure q;", + "what_to_observe": "Grover sur 2 qubits avec marqueur '11' produit '11' avec ~100% après 1 itération.", + "exercises": [ + { + "id": "7-1-a", + "prompt": "Implémente Grover sur 2 qubits pour trouver l'état marqué '11'. 1 itération suffit.", + "hint": "H H | CZ (oracle) | H H X X CZ X X H H (diffuseur).", + "criteria": { + "required_outcomes": [{"bitstring": "11", "min_ratio": 0.85}], + "forbidden_outcomes": [] + }, + "feedback_pass": "Magnifique ! Grover trouve '11' en une itération avec haute probabilité.", + "feedback_fail": "L'état '11' doit dominer (>85%). Vérifie oracle et diffuseur." + }, + { + "id": "7-1-b", + "prompt": "Grover pour l'état '00'. Oracle = X X CZ X X.", + "hint": "X q[0]; X q[1]; CZ q[0],q[1]; X q[0]; X q[1]; puis diffuseur.", + "criteria": { + "required_outcomes": [{"bitstring": "00", "min_ratio": 0.85}], + "forbidden_outcomes": [] + }, + "feedback_pass": "Excellent ! En changeant l'oracle, Grover trouve n'importe quel état marqué.", + "feedback_fail": "L'état '00' doit dominer (>85%). Ton oracle doit marquer '00'." + } + ] + } + ] + } + ] +} diff --git a/docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md b/docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md new file mode 100644 index 0000000..f5d60bc --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md @@ -0,0 +1,2612 @@ +# quantum-bridge-mcp V1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a zero-dependency MCP server (single Rust binary) that exposes three tools — `list_backends`, `validate_circuit`, `run_circuit` — for local OpenQASM 3.0 quantum circuit simulation. + +**Architecture:** An rmcp stdio server wires three tool handlers; each handler delegates to either `CircuitValidator` (wrapping `oq3_semantics`) or `LocalSimulator` (wrapping `spinoza`). The tools depend on `Backend` traits, never on concrete types, to allow IBM backend addition in V1.5 without touching tool code. + +**Tech Stack:** Rust 2021, `rmcp` (git, MCP SDK), `oq3_semantics 0.7` (OpenQASM3 parser), `spinoza 0.5` (statevector simulator), `tokio`, `serde`/`schemars`, `thiserror`, `tracing`. + +--- + +## File Map + +``` +Cargo.toml +src/ + main.rs — rmcp stdio server, wires QuantumBridgeServer + error.rs — BridgeError (thiserror) + types.rs — QubitIndex, ShotCount, CircuitSource, ValidationResult, SimulationResult + executor.rs — Backend traits + LocalSimulator (spinoza) + validator.rs — CircuitValidator (oq3_semantics) + tools/ + mod.rs — QuantumBridgeServer struct + tool_router impl + list_backends.rs — list_backends handler + validate_circuit.rs — validate_circuit handler + run_circuit.rs — run_circuit handler +tests/ + integration/ + mcp_protocol.rs — JSON-RPC roundtrip, schema conformance + proptest/ + invariants.rs — unitarity, normalisation + reference/ + bell.qasm — valid Bell circuit + bell_counts.json — expected count distribution + ghz3.qasm — 3-qubit GHZ circuit + ghz3_counts.json + invalid/ + unsupported_gate.qasm + unsupported_gate.error.txt + undeclared_qubit.qasm + undeclared_qubit.error.txt + syntax_error.qasm + syntax_error.error.txt + qubit_limit.qasm + qubit_limit.error.txt +benches/ + simulation.rs — Criterion: Bell < 5 ms, 20 qubits < 500 ms +scripts/ + gen_reference.py — generates tests/reference/ via Qiskit Aer +.github/workflows/ + ci.yml + release.yml +``` + +--- + +## Task 1: Bootstrap — Cargo project + dependencies + +**Files:** +- Create: `Cargo.toml` +- Modify: `src/main.rs` (created by `cargo new`) + +- [ ] **Step 1: Initialise the cargo project** + +```bash +cd /home/vincent/src/misc/quantum-bridge-mcp +cargo new --name quantum-bridge-mcp . +``` + +Expected: `src/main.rs` and `Cargo.toml` created. Git will show them as untracked. + +- [ ] **Step 2: Write Cargo.toml** + +> **Note:** `spinoza` publishes as `0.5.x` on crates.io (not `2.x`). Run `cargo search spinoza` and `cargo search oq3_semantics` to confirm latest patch versions before pinning. + +```toml +[package] +name = "quantum-bridge-mcp" +version = "0.1.0" +edition = "2021" +description = "Zero-dependency MCP server for local OpenQASM 3.0 quantum circuit simulation" +license = "MIT" + +[dependencies] +rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features = ["server", "transport-io", "macros"] } +oq3_semantics = "0.7" +spinoza = "0.5" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "0.8" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" + +[dev-dependencies] +proptest = "1" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "simulation" +harness = false +``` + +- [ ] **Step 3: Write minimal src/main.rs** + +```rust +fn main() { + println!("quantum-bridge-mcp"); +} +``` + +- [ ] **Step 4: Verify all dependencies resolve and compile** + +```bash +cargo build +``` + +Expected: compiles successfully (first run downloads ~200 MB of deps). If `spinoza` or `oq3_semantics` version constraint fails, run `cargo search ` and update the version in Cargo.toml. + +- [ ] **Step 5: Commit** + +```bash +git add Cargo.toml Cargo.lock src/main.rs +git commit -m "chore: bootstrap project with all V1 dependencies" +``` + +--- + +## Task 2: Foundation — Error types and domain newtypes + +**Files:** +- Create: `src/error.rs` +- Create: `src/types.rs` +- Modify: `src/main.rs` + +- [ ] **Step 1: Write src/error.rs** + +```rust +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BridgeError { + #[error("parse error at line {line}, col {col}: {message}")] + Parse { line: usize, col: usize, message: String }, + + #[error("gate '{gate}' is not supported at line {line} — supported: {supported}")] + UnsupportedGate { gate: String, line: usize, supported: String }, + + #[error("qubit index {index} is out of range (circuit declares {declared} qubits)")] + QubitOutOfRange { index: usize, declared: usize }, + + #[error("circuit requires {requested} qubits, exceeds the local simulator limit of {limit}")] + QubitLimitExceeded { requested: usize, limit: usize }, + + #[error("simulation failed: {0}")] + Simulation(String), + + #[error("measure at line {line} maps to an undeclared classical bit")] + MeasurementMapping { line: usize }, +} +``` + +- [ ] **Step 2: Write src/types.rs** + +```rust +use std::collections::HashMap; + +/// Newtype for qubit indices — prevents mixing with plain usizes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct QubitIndex(pub usize); + +/// Newtype for shot count — enforces range and intent at type level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ShotCount(pub u32); + +impl ShotCount { + pub const DEFAULT: ShotCount = ShotCount(1024); + pub const MAX: ShotCount = ShotCount(100_000); +} + +/// Wraps an OpenQASM 3.0 source string. +#[derive(Debug, Clone)] +pub struct CircuitSource(pub String); + +#[derive(Debug, Clone)] +pub enum DiagnosticSeverity { + Error, + Warning, +} + +#[derive(Debug, Clone)] +pub struct ValidationDiagnostic { + pub line: usize, + pub column: usize, + pub message: String, + pub severity: DiagnosticSeverity, +} + +#[derive(Debug, Clone)] +pub struct ValidationResult { + pub is_valid: bool, + pub diagnostics: Vec, + pub num_qubits: Option, + pub num_gates: Option, +} + +#[derive(Debug, Clone)] +pub struct SimulationResult { + /// Bitstring → count, e.g. `{"00": 512, "11": 512}`. + pub counts: HashMap, + pub shots: u32, + pub execution_time_ms: f64, + /// Optional full statevector as (real, imag) pairs per basis state. + pub statevector: Option>, +} +``` + +- [ ] **Step 3: Declare modules in src/main.rs** + +```rust +mod error; +mod types; + +fn main() { + println!("quantum-bridge-mcp"); +} +``` + +- [ ] **Step 4: Verify compilation** + +```bash +cargo build +``` + +Expected: compiles without warnings. + +- [ ] **Step 5: Commit** + +```bash +git add src/error.rs src/types.rs src/main.rs +git commit -m "feat: add error types and domain newtypes" +``` + +--- + +## Task 3: Backend traits + +**Files:** +- Create: `src/executor.rs` (traits only — implementation comes in Task 5) + +- [ ] **Step 1: Write the failing compilation test** + +Add to the bottom of `src/executor.rs` (create the file): + +```rust +use crate::error::BridgeError; +use crate::types::{CircuitSource, ShotCount, SimulationResult, ValidationResult}; + +pub const MAX_LOCAL_QUBITS: usize = 28; + +pub const SUPPORTED_GATES: &[&str] = &[ + "h", "x", "y", "z", "s", "t", "sdg", "tdg", + "rx", "ry", "rz", "cx", "cz", "swap", "ccx", + "measure", +]; + +pub trait CanIntrospect { + fn name(&self) -> &str; + fn max_qubits(&self) -> usize; + fn supported_gates(&self) -> &[&str]; +} + +pub trait CanValidate { + fn validate(&self, circuit: &CircuitSource) -> Result; +} + +pub trait CanExecute { + fn run( + &self, + circuit: &CircuitSource, + shots: ShotCount, + return_statevector: bool, + ) -> Result; +} + +/// Marker trait combining all three capabilities. V1.5 IBM backend will impl this too. +pub trait Backend: CanIntrospect + CanValidate + CanExecute + Send + Sync {} + +#[cfg(test)] +mod tests { + use super::*; + + // Compile-time test: a mock struct can satisfy the trait bounds. + struct _MockBackend; + impl CanIntrospect for _MockBackend { + fn name(&self) -> &str { "mock" } + fn max_qubits(&self) -> usize { 4 } + fn supported_gates(&self) -> &[&str] { SUPPORTED_GATES } + } + impl CanValidate for _MockBackend { + fn validate(&self, _: &CircuitSource) -> Result { + Ok(ValidationResult { is_valid: true, diagnostics: vec![], num_qubits: None, num_gates: None }) + } + } + impl CanExecute for _MockBackend { + fn run(&self, _: &CircuitSource, _: ShotCount, _: bool) -> Result { + Ok(SimulationResult { counts: Default::default(), shots: 0, execution_time_ms: 0.0, statevector: None }) + } + } + impl Backend for _MockBackend {} +} +``` + +- [ ] **Step 2: Declare executor module and run tests** + +Add to `src/main.rs`: +```rust +mod executor; +``` + +```bash +cargo test +``` + +Expected: 0 tests run, 0 failures — the compile-time test above just confirms the trait bounds are satisfiable. + +- [ ] **Step 3: Commit** + +```bash +git add src/executor.rs src/main.rs +git commit -m "feat: define Backend trait hierarchy (CanIntrospect/CanValidate/CanExecute)" +``` + +--- + +## Task 4: CircuitValidator — oq3_semantics integration + +**Files:** +- Create: `src/validator.rs` + +### 4a — AST exploration (required before implementing) + +The `oq3_semantics` AST is lightly documented. This step maps the real types so the implementation is correct. + +- [ ] **Step 1: Write an AST exploration test in src/validator.rs** + +Create `src/validator.rs` with this content: + +```rust +use oq3_semantics::syntax_to_semantics::parse_source_string; + +#[cfg(test)] +mod exploration { + use super::*; + + #[test] + fn print_bell_circuit_ast() { + let qasm = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q;"#; + let result = parse_source_string(qasm, Some("bell.qasm"), None::<&[&str]>); + println!("any_errors: {}", result.any_errors()); + println!("program: {:#?}", result.program()); + println!("---"); + for err in result.semantic_errors().iter() { + println!("error: {:?}", err); + } + } + + #[test] + fn print_unsupported_gate_ast() { + let qasm = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let result = parse_source_string(qasm, Some("bad.qasm"), None::<&[&str]>); + println!("any_errors: {}", result.any_errors()); + println!("program: {:#?}", result.program()); + } +} +``` + +- [ ] **Step 2: Run exploration and read the output** + +```bash +cargo test exploration -- --nocapture 2>&1 | head -200 +``` + +**Read the output carefully.** You need to identify: +- How `Program` exposes statements (e.g. `program.stmts()`, `program.statements()`, or iterator impl) +- The name of the `Stmt` enum variant for gate calls (e.g. `Stmt::GateCall(...)`) +- The name of the variant for qubit declarations (e.g. `Stmt::QubitDecl(...)`) +- How to get the gate name string from a `GateCall` (check if `name()` returns `&str` or needs symbol table resolution) +- How to get qubit register size from a qubit declaration + +Write down the variant names — you will use them in the next steps. + +### 4b — Implementation + +- [ ] **Step 3: Write the failing validator tests** + +Replace `src/validator.rs` with: + +```rust +use oq3_semantics::syntax_to_semantics::parse_source_string; + +use crate::error::BridgeError; +use crate::executor::SUPPORTED_GATES; +use crate::types::{CircuitSource, DiagnosticSeverity, ValidationDiagnostic, ValidationResult}; + +pub struct CircuitValidator { + max_qubits: usize, +} + +impl CircuitValidator { + pub fn new(max_qubits: usize) -> Self { + Self { max_qubits } + } + + pub fn validate(&self, source: &CircuitSource) -> Result { + todo!("implement after AST exploration") + } +} + +/// Converts a byte offset in `source` to (1-based line, 1-based column). +fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { + let safe_offset = offset.min(source.len()); + let prefix = &source[..safe_offset]; + let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1; + let col = prefix.rfind('\n').map(|p| safe_offset - p - 1).unwrap_or(safe_offset) + 1; + (line, col) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::executor::MAX_LOCAL_QUBITS; + + fn validator() -> CircuitValidator { + CircuitValidator::new(MAX_LOCAL_QUBITS) + } + + 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;"#; + + #[test] + fn valid_bell_circuit_is_accepted() { + let result = validator() + .validate(&CircuitSource(BELL_CIRCUIT.to_string())) + .unwrap(); + assert!(result.is_valid, "diagnostics: {:?}", result.diagnostics); + assert!(result.diagnostics.is_empty()); + assert_eq!(result.num_qubits, Some(2)); + } + + #[test] + fn unsupported_gate_produces_error_diagnostic() { + // u3 is a valid QASM3 standard gate but not in our supported set + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + let msg = &result.diagnostics[0].message; + assert!( + msg.to_lowercase().contains("u3"), + "expected 'u3' in message, got: {msg}" + ); + } + + #[test] + fn too_many_qubits_produces_error_diagnostic() { + let n = MAX_LOCAL_QUBITS + 1; + let circuit = format!("OPENQASM 3.0;\nqubit[{n}] q;\n"); + let result = validator() + .validate(&CircuitSource(circuit)) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + let msg = &result.diagnostics[0].message; + assert!( + msg.contains(&n.to_string()), + "expected qubit count in message, got: {msg}" + ); + } + + #[test] + fn invalid_qasm_syntax_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\nthis is not valid qasm;\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } + + #[test] + fn undeclared_qubit_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nh q[5];\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } +} +``` + +- [ ] **Step 4: Run tests to confirm they all fail at the `todo!()`** + +```bash +cargo test validator::tests 2>&1 | tail -20 +``` + +Expected: all 5 tests panic at `todo!()`. + +- [ ] **Step 5: Implement CircuitValidator::validate** + +Replace the `todo!()` body with the real implementation. Use the variant names you identified in Step 2 above. + +```rust +pub fn validate(&self, source: &CircuitSource) -> Result { + let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); + let mut diagnostics: Vec = Vec::new(); + + // Collect errors from oq3_semantics (syntax + undeclared qubits, etc.) + for error in parse_result.semantic_errors().iter() { + let range = error.range(); + // TextRange::start() returns a rowan::TextSize; .into() gives u32, cast to usize + let offset: usize = u32::from(range.start()) as usize; + let (line, col) = byte_offset_to_line_col(&source.0, offset); + diagnostics.push(ValidationDiagnostic { + line, + column: col, + message: error.message(), + severity: DiagnosticSeverity::Error, + }); + } + + // Stop early on parse/semantic errors — AST may be incomplete + if parse_result.any_errors() { + return Ok(ValidationResult { + is_valid: false, + diagnostics, + num_qubits: None, + num_gates: None, + }); + } + + let program = parse_result.program(); + + // Walk statements to: + // (a) count total declared qubits + // (b) collect all gate calls and check against SUPPORTED_GATES + // + // NOTE: replace the Stmt variant names below with what you observed in the + // AST exploration step (Step 2). Common patterns: + // Stmt::GateCall(gc) — a gate application + // Stmt::QubitDeclaration(qd) — qubit register declaration + // Use `dbg!(stmt)` if unsure. + + let mut total_qubits: usize = 0; + let mut gate_count: usize = 0; + + for stmt in program.stmts() { + // --- Qubit declarations --- + // Adjust the pattern arm to match the real variant name from your exploration. + if let Some(num) = extract_qubit_count(stmt) { + total_qubits += num; + } + + // --- Gate calls --- + if let Some(gate_name) = extract_gate_name(stmt) { + if gate_name == "measure" { + continue; // measure is always allowed + } + gate_count += 1; + if !SUPPORTED_GATES.contains(&gate_name.as_str()) { + let line = extract_stmt_line(stmt, &source.0); + let supported = SUPPORTED_GATES.join(", "); + diagnostics.push(ValidationDiagnostic { + line, + column: 1, + message: format!( + "gate '{gate_name}' is not supported by this simulator (supported: {supported})" + ), + severity: DiagnosticSeverity::Error, + }); + } + } + } + + // Qubit limit check + if total_qubits > self.max_qubits { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: format!( + "circuit requires {total_qubits} qubits, exceeds local simulator limit of {}", + self.max_qubits + ), + severity: DiagnosticSeverity::Error, + }); + } + + Ok(ValidationResult { + is_valid: diagnostics.is_empty(), + diagnostics, + num_qubits: if total_qubits > 0 { Some(total_qubits) } else { None }, + num_gates: if gate_count > 0 { Some(gate_count) } else { None }, + }) +} +``` + +Then add the helper functions below `validate`. Adjust variant names to match your exploration output: + +```rust +use oq3_semantics::asg::Stmt; + +/// Returns the qubit register size if this statement is a qubit declaration. +/// Adjust the pattern to match the real Stmt variant name. +fn extract_qubit_count(stmt: &Stmt) -> Option { + // Example pattern — replace with actual variant from exploration: + // if let Stmt::QubitDeclaration(qd) = stmt { + // return Some(qd.width().unwrap_or(1)); + // } + // Fallback: if you can't find the variant, use the approach below + // and fill in after running the exploration test. + let _ = stmt; + None // Replace this with the real implementation +} + +/// Returns the lowercase gate name if this statement is a gate call. +fn extract_gate_name(stmt: &Stmt) -> Option { + // Example pattern — replace with actual variant from exploration: + // if let Stmt::GateCall(gc) = stmt { + // return Some(gc.name().to_string().to_lowercase()); + // } + let _ = stmt; + None // Replace this with the real implementation +} + +/// Returns the 1-based line number for a statement using its source span. +fn extract_stmt_line(stmt: &Stmt, source: &str) -> usize { + // Example: + // let offset: usize = u32::from(stmt.span().start()) as usize; + // byte_offset_to_line_col(source, offset).0 + let _ = (stmt, source); + 1 // Replace this with the real implementation +} +``` + +> **Important:** The three helper functions above contain placeholder `None`/`1` returns. After running Step 2's exploration test, replace each with the real match arms using the AST variant names you observed. This is intentional — AST introspection requires seeing live debug output first. The test suite (Step 6) will fail until these are real. + +- [ ] **Step 6: Run the tests and fix until all pass** + +```bash +cargo test validator::tests 2>&1 +``` + +Iterate on the helper functions until all 5 tests pass. If `program.stmts()` doesn't exist, check what methods `Program` exposes via `cargo doc --open` or `rust-analyzer` hover. + +- [ ] **Step 7: Declare the module and verify no regressions** + +Add to `src/main.rs`: +```rust +mod validator; +``` + +```bash +cargo test +``` + +Expected: all tests pass. + +- [ ] **Step 8: Commit** + +```bash +git add src/validator.rs src/main.rs +git commit -m "feat: implement CircuitValidator with oq3_semantics" +``` + +--- + +## Task 5: LocalSimulator — spinoza executor + +**Files:** +- Modify: `src/executor.rs` (add `LocalSimulator` below the traits) + +- [ ] **Step 1: Write failing executor tests** + +Append to `src/executor.rs` (inside the `#[cfg(test)]` block, replacing the mock test): + +```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} not in [0.45, 0.55]" + ); + } + + #[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} not in [0.45, 0.55]" + ); + } + + #[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 = 4 amplitudes for 2 qubits + // Bell state: amplitudes 0 and 3 are 1/√2, amplitudes 1 and 2 are 0 + 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: Run tests to confirm they all fail (LocalSimulator not defined yet)** + +```bash +cargo test executor::tests 2>&1 | tail -5 +``` + +Expected: compile error `cannot find struct LocalSimulator`. + +- [ ] **Step 3: Add LocalSimulator to src/executor.rs** + +Add after the trait definitions (before `#[cfg(test)]`): + +```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(); + + // Parse circuit — validation is assumed to have passed already. + let parse_result = parse_source_string(&circuit.0, Some("circuit.qasm"), None::<&[&str]>); + let program = parse_result.program(); + + // First pass: count qubits and identify measured qubits. + 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, + }); + } + + // Initialise statevector |0...0⟩ + let mut state = State::new(num_qubits); + + // Second pass: apply non-measurement gates in order. + for (gate_name, params, qubits) in extract_gate_ops(program) { + apply_gate(&gate_name, ¶ms, &qubits, &mut state)?; + } + + // Collect statevector before sampling (if requested). + let statevector = if return_statevector { + let sv: Vec<(f64, f64)> = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(&r, &i)| (r as f64, i as f64)) + .collect(); + Some(sv) + } else { + None + }; + + // Sample N shots from the final statevector. + 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(); + + // Convert usize indices to bitstrings. + let counts: HashMap = raw_counts + .into_iter() + .map(|(idx, cnt)| { + let bitstring = format!("{:0>width$b}", idx, width = num_qubits); + (bitstring, cnt as u64) + }) + .collect(); + + let execution_time_ms = start.elapsed().as_secs_f64() * 1000.0; + + Ok(SimulationResult { + counts, + shots: shots.0, + execution_time_ms, + statevector, + }) + } +} + +impl Backend for LocalSimulator {} + +/// Applies a single gate to the state. Returns an error for unsupported gates. +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", _) => {} // handled via statevector sampling — skip inline + _ => { + return Err(BridgeError::UnsupportedGate { + gate: gate_name.to_string(), + line: 0, + supported, + }) + } + } + Ok(()) +} + +/// Walks the AST and returns gate operations as (name, params, qubit_indices). +/// Fill in AST variant names after running the exploration test in Task 4. +fn extract_gate_ops(program: &oq3_semantics::asg::Program) -> Vec<(String, Vec, Vec)> { + let mut ops = Vec::new(); + for stmt in program.stmts() { + // TODO after exploration: match Stmt::GateCall(gc) and extract + // gate name, parameter float values, and qubit indices. + // Example skeleton: + // if let Stmt::GateCall(gc) = stmt { + // let name = resolve_gate_name(gc).to_lowercase(); + // let params = extract_float_params(gc); + // let qubits = extract_qubit_indices(gc); + // ops.push((name, params, qubits)); + // } + let _ = stmt; + } + ops +} + +/// Counts total declared qubits in the program. +/// Fill in AST variant names after running the exploration test in Task 4. +fn count_qubits(program: &oq3_semantics::asg::Program) -> usize { + let mut total = 0; + for stmt in program.stmts() { + // TODO after exploration: match Stmt::QubitDeclaration(qd) and sum sizes. + let _ = stmt; + } + total +} +``` + +> **Note:** `extract_gate_ops` and `count_qubits` have the same `TODO` pattern as Task 4's helper functions — fill them in using the AST variant names from the exploration step. The test suite below will guide you. + +- [ ] **Step 4: Run tests and fix until all pass** + +```bash +cargo test executor::tests 2>&1 +``` + +The tests will fail until `extract_gate_ops` and `count_qubits` correctly walk the AST. Use the exploration output from Task 4 Step 2 to fill in the match arms. Pay attention to: +- **Endianness**: if `bell_circuit_only_produces_00_and_11` sees `"01"` or `"10"`, spinoza's qubit ordering is reversed vs. QASM3. Fix by reversing the qubit index mapping: `let t = num_qubits - 1 - raw_qubit_idx`. +- **Norm**: if the statevector test fails, check that `state.reals` and `state.imags` are indexed correctly. + +- [ ] **Step 5: Run full test suite** + +```bash +cargo test 2>&1 +``` + +Expected: all tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/executor.rs +git commit -m "feat: implement LocalSimulator with spinoza statevector engine" +``` + +--- + +## Task 6: Tool — list_backends + +**Files:** +- Create: `src/tools/mod.rs` +- Create: `src/tools/list_backends.rs` + +- [ ] **Step 1: Write the failing test** + +Create `src/tools/list_backends.rs`: + +```rust +use serde_json::json; + +use crate::executor::{LocalSimulator, SUPPORTED_GATES, MAX_LOCAL_QUBITS}; + +pub fn list_backends_response() -> serde_json::Value { + todo!("implement list_backends_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_contains_local_simulator() { + let resp = list_backends_response(); + let backends = resp["backends"].as_array().unwrap(); + assert_eq!(backends.len(), 1); + assert_eq!(backends[0]["name"], "local_simulator"); + } + + #[test] + fn response_contains_max_qubits() { + let resp = list_backends_response(); + let backend = &resp["backends"][0]; + assert_eq!( + backend["max_qubits"].as_u64().unwrap(), + MAX_LOCAL_QUBITS as u64 + ); + } + + #[test] + fn response_lists_supported_gates() { + let resp = list_backends_response(); + let gates = resp["backends"][0]["supported_gates"] + .as_array() + .unwrap(); + assert!(!gates.is_empty()); + assert!(gates.iter().any(|g| g == "h")); + assert!(gates.iter().any(|g| g == "cx")); + } +} +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cargo test tools::list_backends 2>&1 | tail -5 +``` + +Expected: compile error (module not declared yet) or panic at `todo!()`. + +- [ ] **Step 3: Implement list_backends_response** + +Replace the `todo!()`: + +```rust +pub fn list_backends_response() -> serde_json::Value { + let sim = LocalSimulator::new(); + json!({ + "backends": [{ + "name": sim.name(), + "description": "Local statevector simulator (Spinoza engine). No network, no account required.", + "max_qubits": sim.max_qubits(), + "supported_gates": sim.supported_gates(), + "simulation_type": "statevector", + "notes": "Use run_circuit shots parameter (default 1024, max 100000). Statevector memory grows as 2^n × 16 bytes." + }] + }) +} +``` + +- [ ] **Step 4: Create src/tools/mod.rs** + +```rust +pub mod list_backends; +pub mod run_circuit; +pub mod validate_circuit; +``` + +- [ ] **Step 5: Declare the tools module in src/main.rs** + +```rust +mod tools; +``` + +- [ ] **Step 6: Run tests** + +```bash +cargo test tools::list_backends 2>&1 +``` + +Expected: all 3 tests pass. + +- [ ] **Step 7: Commit** + +```bash +git add src/tools/mod.rs src/tools/list_backends.rs src/main.rs +git commit -m "feat: implement list_backends tool handler" +``` + +--- + +## Task 7: Tool — validate_circuit + +**Files:** +- Create: `src/tools/validate_circuit.rs` + +- [ ] **Step 1: Write the failing test** + +Create `src/tools/validate_circuit.rs`: + +```rust +use serde_json::{json, Value}; + +use crate::executor::{LocalSimulator, MAX_LOCAL_QUBITS}; +use crate::types::CircuitSource; + +pub fn validate_circuit_response(circuit: &str) -> Value { + todo!("implement validate_circuit_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + 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;"#; + + #[test] + fn valid_circuit_returns_is_valid_true() { + let resp = validate_circuit_response(BELL_CIRCUIT); + assert_eq!(resp["is_valid"], true); + assert_eq!(resp["diagnostics"].as_array().unwrap().len(), 0); + } + + #[test] + fn valid_circuit_includes_qubit_count() { + let resp = validate_circuit_response(BELL_CIRCUIT); + assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); + } + + #[test] + fn invalid_circuit_returns_is_valid_false_with_message() { + let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;"); + assert_eq!(resp["is_valid"], false); + let diags = resp["diagnostics"].as_array().unwrap(); + assert!(!diags.is_empty()); + assert!(diags[0]["message"].as_str().unwrap().len() > 0); + } + + #[test] + fn unsupported_gate_returns_diagnostic_with_gate_name() { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let resp = validate_circuit_response(circuit); + assert_eq!(resp["is_valid"], false); + let diags = resp["diagnostics"].as_array().unwrap(); + let msg = diags[0]["message"].as_str().unwrap(); + assert!(msg.to_lowercase().contains("u3"), "msg: {msg}"); + } + + #[test] + fn diagnostic_includes_line_and_column() { + let circuit = "OPENQASM 3.0;\nnot_valid_qasm;\n"; + let resp = validate_circuit_response(circuit); + let diag = &resp["diagnostics"][0]; + assert!(diag["line"].as_u64().unwrap() >= 1); + assert!(diag["column"].as_u64().unwrap() >= 1); + } +} +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cargo test tools::validate_circuit 2>&1 | tail -5 +``` + +Expected: panic at `todo!()`. + +- [ ] **Step 3: Implement validate_circuit_response** + +Replace the `todo!()`: + +```rust +pub fn validate_circuit_response(circuit: &str) -> Value { + use crate::validator::CircuitValidator; + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => json!({ + "is_valid": false, + "diagnostics": [{"line": 1, "column": 1, "message": e.to_string(), "severity": "error"}], + "num_qubits": null, + "num_gates": null, + }), + Ok(result) => { + let diagnostics: Vec = result.diagnostics.iter().map(|d| { + json!({ + "line": d.line, + "column": d.column, + "message": d.message, + "severity": match d.severity { + crate::types::DiagnosticSeverity::Error => "error", + crate::types::DiagnosticSeverity::Warning => "warning", + } + }) + }).collect(); + json!({ + "is_valid": result.is_valid, + "diagnostics": diagnostics, + "num_qubits": result.num_qubits, + "num_gates": result.num_gates, + }) + } + } +} +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test tools::validate_circuit 2>&1 +``` + +Expected: all 5 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/tools/validate_circuit.rs +git commit -m "feat: implement validate_circuit tool handler" +``` + +--- + +## Task 8: Tool — run_circuit + +**Files:** +- Create: `src/tools/run_circuit.rs` + +- [ ] **Step 1: Write the failing test** + +Create `src/tools/run_circuit.rs`: + +```rust +use serde_json::{json, Value}; + +use crate::types::{CircuitSource, ShotCount}; + +pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { + todo!("implement run_circuit_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + 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;"#; + + #[test] + fn bell_circuit_returns_counts_with_total_matching_shots() { + let resp = run_circuit_response(BELL_CIRCUIT, 1_000, false); + let counts = resp["counts"].as_object().unwrap(); + let total: u64 = counts.values().map(|v| v.as_u64().unwrap()).sum(); + assert_eq!(total, 1_000); + } + + #[test] + fn bell_circuit_only_produces_00_and_11_outcomes() { + let resp = run_circuit_response(BELL_CIRCUIT, 1_000, false); + let counts = resp["counts"].as_object().unwrap(); + for key in counts.keys() { + assert!(key == "00" || key == "11", "unexpected key: {key}"); + } + } + + #[test] + fn x_gate_produces_only_one_outcome() { + let resp = run_circuit_response(X_CIRCUIT, 100, false); + let counts = resp["counts"].as_object().unwrap(); + assert_eq!(counts.get("1").and_then(|v| v.as_u64()), Some(100)); + } + + #[test] + fn response_includes_shots_and_execution_time() { + let resp = run_circuit_response(BELL_CIRCUIT, 512, false); + assert_eq!(resp["shots"].as_u64().unwrap(), 512); + assert!(resp["execution_time_ms"].as_f64().unwrap() >= 0.0); + } + + #[test] + fn statevector_is_absent_when_not_requested() { + let resp = run_circuit_response(BELL_CIRCUIT, 100, false); + assert!(resp["statevector"].is_null()); + } + + #[test] + fn statevector_has_correct_length_when_requested() { + let resp = run_circuit_response(BELL_CIRCUIT, 100, true); + let sv = resp["statevector"].as_array().unwrap(); + assert_eq!(sv.len(), 4); // 2^2 basis states + } + + #[test] + fn invalid_circuit_returns_error_field() { + let resp = run_circuit_response("OPENQASM 3.0;\nnot_valid;", 100, false); + assert!(resp.get("error").is_some(), "expected 'error' field, got: {resp}"); + } + + #[test] + fn shots_clamped_to_max() { + // ShotCount::MAX is 100_000 — requesting more should clamp or error + let resp = run_circuit_response(BELL_CIRCUIT, ShotCount::MAX.0 + 1, false); + // Expect either clamping (shots == MAX) or an error field + let has_error = resp.get("error").is_some(); + let shots = resp["shots"].as_u64().unwrap_or(0); + assert!(has_error || shots == ShotCount::MAX.0 as u64); + } +} +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cargo test tools::run_circuit 2>&1 | tail -5 +``` + +Expected: panic at `todo!()`. + +- [ ] **Step 3: Implement run_circuit_response** + +Replace the `todo!()`: + +```rust +pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { + use crate::executor::{CanExecute, CanValidate, LocalSimulator}; + use crate::validator::CircuitValidator; + use crate::executor::MAX_LOCAL_QUBITS; + + // Clamp shots to max + let shot_count = ShotCount(shots.min(ShotCount::MAX.0)); + + // Validate first — return structured error if invalid + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let validation = match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => return json!({"error": e.to_string()}), + Ok(v) => v, + }; + if !validation.is_valid { + let messages: Vec<&str> = validation.diagnostics.iter().map(|d| d.message.as_str()).collect(); + return json!({ + "error": format!("circuit validation failed: {}", messages.join("; ")) + }); + } + + let sim = LocalSimulator::new(); + match sim.run(&CircuitSource(circuit.to_string()), shot_count, return_statevector) { + Err(e) => json!({"error": e.to_string()}), + Ok(result) => { + let statevector: Value = match result.statevector { + None => Value::Null, + Some(sv) => sv.iter() + .map(|(r, i)| json!([r, i])) + .collect::>() + .into(), + }; + json!({ + "counts": result.counts, + "shots": result.shots, + "execution_time_ms": result.execution_time_ms, + "statevector": statevector, + "backend": "local_simulator", + }) + } + } +} +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test tools::run_circuit 2>&1 +``` + +Expected: all 8 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/tools/run_circuit.rs +git commit -m "feat: implement run_circuit tool handler" +``` + +--- + +## Task 9: MCP Server — main.rs with rmcp + +**Files:** +- Modify: `src/main.rs` (replace stub) +- Modify: `src/tools/mod.rs` (add QuantumBridgeServer) + +- [ ] **Step 1: Write the failing compilation test** + +First, write a compile-only test in `src/tools/mod.rs` to verify the struct can be created: + +```rust +pub mod list_backends; +pub mod run_circuit; +pub mod validate_circuit; + +use rmcp::{ + ErrorData as McpError, + ServerHandler, + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::*, + tool, tool_handler, tool_router, +}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::tools::list_backends::list_backends_response; +use crate::tools::run_circuit::run_circuit_response; +use crate::tools::validate_circuit::validate_circuit_response; + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ValidateCircuitParams { + /// OpenQASM 3.0 source string to validate. + pub circuit: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct RunCircuitParams { + /// OpenQASM 3.0 source string to execute. + pub circuit: String, + /// Number of measurement shots (default 1024, max 100 000). + #[serde(default = "default_shots")] + pub shots: u32, + /// Whether to include the full statevector in the response. + #[serde(default)] + pub return_statevector: bool, +} + +fn default_shots() -> u32 { + 1024 +} + +#[derive(Clone)] +pub struct QuantumBridgeServer { + tool_router: ToolRouter, +} + +#[tool_router] +impl QuantumBridgeServer { + pub fn new() -> Self { + Self { tool_router: Self::tool_router() } + } + + #[tool(description = "List available quantum simulation backends and their capabilities.")] + async fn list_backends(&self) -> Result { + let json = list_backends_response(); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Parse and validate an OpenQASM 3.0 circuit. Returns structured diagnostics with line/column for each error.")] + async fn validate_circuit( + &self, + Parameters(ValidateCircuitParams { circuit }): Parameters, + ) -> Result { + let json = validate_circuit_response(&circuit); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Execute an OpenQASM 3.0 circuit on the local statevector simulator. Returns measurement counts, execution time, and optionally the full statevector.")] + async fn run_circuit( + &self, + Parameters(RunCircuitParams { circuit, shots, return_statevector }): Parameters, + ) -> Result { + let json = run_circuit_response(&circuit, shots, return_statevector); + if json.get("error").is_some() { + return Err(McpError::invalid_params( + json["error"].as_str().unwrap_or("unknown error"), + None, + )); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } +} + +#[tool_handler] +impl ServerHandler for QuantumBridgeServer { + fn get_info(&self) -> ServerInfo { + ServerInfo::new( + ServerCapabilities::builder() + .enable_tools() + .build(), + ) + .with_server_info(Implementation::from_build_env()) + .with_protocol_version(ProtocolVersion::V_2024_11_05) + .with_instructions( + "Quantum circuit simulator MCP server. Accepts OpenQASM 3.0 circuits. \ + Use validate_circuit before run_circuit to get actionable error messages.".to_string(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quantum_bridge_server_can_be_constructed() { + let _server = QuantumBridgeServer::new(); + } +} +``` + +- [ ] **Step 2: Run compile test** + +```bash +cargo test tools::tests 2>&1 +``` + +Expected: compiles and the construction test passes. If `#[tool_router]` / `#[tool_handler]` macro names are wrong, check `cargo doc --open` for the rmcp crate or look at `~/.cargo/git/checkouts/rust-sdk-*/examples/`. + +- [ ] **Step 3: Write src/main.rs** + +```rust +mod error; +mod executor; +mod tools; +mod types; +mod validator; + +use anyhow::Result; +use rmcp::{ServiceExt, transport::stdio}; +use tools::QuantumBridgeServer; + +#[tokio::main] +async fn main() -> Result<()> { + // All logs go to stderr — stdout is the MCP JSON-RPC channel. + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("quantum-bridge-mcp starting"); + + let service = QuantumBridgeServer::new() + .serve(stdio()) + .await?; + + service.waiting().await?; + Ok(()) +} +``` + +- [ ] **Step 4: Verify binary compiles** + +```bash +cargo build 2>&1 +``` + +Expected: compiles without errors. Warnings about unused imports are acceptable at this stage. + +- [ ] **Step 5: Smoke test — send a JSON-RPC initialize message** + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}' | cargo run 2>/dev/null +``` + +Expected: a JSON response containing `"result"` with `"serverInfo"` including `"name":"quantum-bridge-mcp"`. The server will then wait for more input — press Ctrl+C. + +- [ ] **Step 6: Commit** + +```bash +git add src/main.rs src/tools/mod.rs +git commit -m "feat: wire rmcp stdio server with list_backends/validate_circuit/run_circuit tools" +``` + +--- + +## Task 10: Integration Tests — MCP protocol conformance + +**Files:** +- Create: `tests/integration/mcp_protocol.rs` +- Create: `tests/integration/mod.rs` + +- [ ] **Step 1: Create test infrastructure** + +```bash +mkdir -p tests/integration +``` + +Create `tests/integration/mod.rs`: +```rust +// Integration test module — empty re-export for cargo test discovery. +``` + +- [ ] **Step 2: Write the integration tests** + +Create `tests/integration/mcp_protocol.rs`: + +```rust +//! Roundtrip tests: spawn the binary, send JSON-RPC messages on stdin, assert stdout. + +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; + +struct McpProcess { + child: Child, +} + +impl McpProcess { + fn spawn() -> Self { + let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to spawn quantum-bridge-mcp binary"); + Self { child } + } + + fn send(&mut self, msg: &str) { + let stdin = self.child.stdin.as_mut().unwrap(); + writeln!(stdin, "{}", msg).unwrap(); + stdin.flush().unwrap(); + } + + fn recv_line(&mut self) -> String { + let stdout = self.child.stdout.as_mut().unwrap(); + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + line.trim().to_string() + } +} + +impl Drop for McpProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +fn initialize(proc: &mut McpProcess) -> serde_json::Value { + proc.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#); + let line = proc.recv_line(); + serde_json::from_str(&line).expect("initialize response is valid JSON") +} + +#[test] +fn initialize_returns_server_info() { + let mut proc = McpProcess::spawn(); + let resp = initialize(&mut proc); + assert!(resp["result"]["serverInfo"]["name"].as_str().is_some()); + assert_eq!(resp["result"]["protocolVersion"], "2024-11-05"); +} + +#[test] +fn tools_list_contains_three_tools() { + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let tools = resp["result"]["tools"].as_array().unwrap(); + assert_eq!(tools.len(), 3); + let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert!(names.contains(&"list_backends")); + assert!(names.contains(&"validate_circuit")); + assert!(names.contains(&"run_circuit")); +} + +#[test] +fn call_list_backends_returns_local_simulator() { + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["backends"][0]["name"], "local_simulator"); +} + +#[test] +fn call_validate_circuit_with_valid_bell_returns_is_valid_true() { + let bell = r#"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"#; + let call = format!( + r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{{"name":"validate_circuit","arguments":{{"circuit":"{}"}}}}}}"#, + bell + ); + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(&call); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["is_valid"], true); +} + +#[test] +fn call_run_circuit_with_x_gate_returns_count_of_1_for_all_shots() { + let x_circ = r#"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"#; + let call = format!( + r#"{{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{{"name":"run_circuit","arguments":{{"circuit":"{}","shots":100}}}}}}"#, + x_circ + ); + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(&call); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["counts"]["1"].as_u64().unwrap(), 100); +} +``` + +> **Note:** The integration tests use `env!("CARGO_BIN_EXE_quantum-bridge-mcp")` which cargo sets to the compiled binary path. Run with `cargo test --test integration` — not with the plain `cargo test` that only runs unit tests. + +- [ ] **Step 3: Run integration tests (requires binary build)** + +```bash +cargo test --test integration 2>&1 +``` + +Expected: all 5 tests pass. If the MCP framing uses length-prefixed lines rather than newlines, adjust `recv_line` accordingly (check rmcp's stdio transport output format). + +- [ ] **Step 4: Commit** + +```bash +git add tests/integration/ +git commit -m "test: add MCP protocol integration tests (initialize, tools/list, tools/call)" +``` + +--- + +## Task 11: Property-Based Tests + +**Files:** +- Create: `tests/proptest/invariants.rs` +- Create: `tests/proptest/mod.rs` + +- [ ] **Step 1: Create proptest directory** + +```bash +mkdir -p tests/proptest +``` + +Create `tests/proptest/mod.rs`: +```rust +// Property-based test module. +``` + +- [ ] **Step 2: Write the property tests** + +Create `tests/proptest/invariants.rs`: + +```rust +//! Quantum mechanics invariants: any valid circuit must preserve unitarity and normalisation. + +use proptest::prelude::*; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; + +// Make crate types visible — add `pub` to modules in lib.rs (see Step 3). + +proptest! { + /// Normalisation: shot counts always sum to exactly the requested number of shots. + #[test] + fn shot_counts_always_sum_to_requested_shots( + shots in 1u32..=1_000u32 + ) { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q;"#; + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit.to_string()), ShotCount(shots), false) + .unwrap(); + let total: u64 = result.counts.values().sum(); + prop_assert_eq!(total, shots as u64); + } + + /// Statevector norm is always 1.0 within floating-point tolerance. + #[test] + fn statevector_norm_is_one( + angle in -std::f64::consts::PI..=std::f64::consts::PI + ) { + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;" + ); + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit), ShotCount(1), true) + .unwrap(); + let sv = result.statevector.unwrap(); + let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum(); + prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}"); + } + + /// Bitstrings in counts have correct length (= number of qubits). + #[test] + fn count_bitstrings_have_correct_length( + n_qubits in 1usize..=5usize, + shots in 10u32..=100u32 + ) { + let qubit_decls: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect(); + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{qubit_decls}c = measure q;\n" + ); + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit), ShotCount(shots), false) + .unwrap(); + for key in result.counts.keys() { + prop_assert_eq!( + key.len(), n_qubits, + "bitstring '{key}' has wrong length for {n_qubits}-qubit circuit" + ); + } + } +} +``` + +- [ ] **Step 3: Expose crate internals for tests** + +Proptest files live in `tests/` and need `use quantum_bridge_mcp::...`. Add a `src/lib.rs` so the crate has a library target: + +Create `src/lib.rs`: +```rust +pub mod error; +pub mod executor; +pub mod types; +pub mod validator; +pub(crate) mod tools; +``` + +Update `src/main.rs` — remove the `mod` declarations that are now in `lib.rs`: +```rust +// Remove: mod error; mod executor; mod types; mod validator; mod tools; +// Keep only: +use quantum_bridge_mcp::tools::QuantumBridgeServer; // or use crate path + +use anyhow::Result; +use rmcp::{ServiceExt, transport::stdio}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("quantum-bridge-mcp starting"); + + let service = QuantumBridgeServer::new() + .serve(stdio()) + .await?; + + service.waiting().await?; + Ok(()) +} +``` + +Add to `Cargo.toml`: +```toml +[lib] +name = "quantum_bridge_mcp" +path = "src/lib.rs" +``` + +- [ ] **Step 4: Run proptest** + +```bash +cargo test --test proptest 2>&1 +``` + +Expected: all 3 property tests pass (each runs 256 cases by default). + +- [ ] **Step 5: Commit** + +```bash +git add tests/proptest/ src/lib.rs src/main.rs Cargo.toml +git commit -m "test: add property-based tests for shot normalisation, statevector norm, and bitstring length" +``` + +--- + +## Task 12: Benchmarks + +**Files:** +- Create: `benches/simulation.rs` + +- [ ] **Step 1: Create the bench file** + +```bash +mkdir -p benches +``` + +Create `benches/simulation.rs`: + +```rust +use criterion::{criterion_group, criterion_main, Criterion}; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::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;"#; + +fn make_h_circuit(n_qubits: usize, n_shots: u32) -> (CircuitSource, ShotCount) { + let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect(); + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n" + ); + (CircuitSource(circuit), ShotCount(n_shots)) +} + +fn bench_bell_1024_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + c.bench_function("bell_1024_shots", |b| { + b.iter(|| { + sim.run( + &CircuitSource(BELL_CIRCUIT.to_string()), + ShotCount(1024), + false, + ) + .unwrap() + }) + }); +} + +fn bench_10_qubits_10k_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circuit, shots) = make_h_circuit(10, 10_000); + c.bench_function("10_qubits_10k_shots", |b| { + b.iter(|| sim.run(&circuit, shots, false).unwrap()) + }); +} + +fn bench_20_qubits_10k_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circuit, shots) = make_h_circuit(20, 10_000); + c.bench_function("20_qubits_10k_shots", |b| { + b.iter(|| sim.run(&circuit, shots, false).unwrap()) + }); +} + +criterion_group!( + benches, + bench_bell_1024_shots, + bench_10_qubits_10k_shots, + bench_20_qubits_10k_shots, +); +criterion_main!(benches); +``` + +- [ ] **Step 2: Run benchmarks to establish baseline** + +```bash +cargo bench 2>&1 | tail -20 +``` + +Expected output includes: +``` +bell_1024_shots time: [X ms ...] +10_qubits_10k_shots time: [X ms ...] +20_qubits_10k_shots time: [X ms ...] +``` + +SLA targets (spec §6): +- `bell_1024_shots` < 5 ms +- `10_qubits_10k_shots` < 50 ms +- `20_qubits_10k_shots` < 500 ms + +If any target is missed, the bottleneck is almost certainly in `extract_gate_ops` (O(n) loop with AST allocation). Profile with `cargo bench -- --profile-time 5` and check if the AST is being cloned unnecessarily. + +- [ ] **Step 3: Commit** + +```bash +git add benches/simulation.rs +git commit -m "bench: add Criterion benchmarks for Bell, 10-qubit, and 20-qubit circuits" +``` + +--- + +## Task 13: Reference Golden Files + gen_reference.py + +**Files:** +- Create: `tests/reference/bell.qasm` +- Create: `tests/reference/bell_counts.json` +- Create: `tests/reference/ghz3.qasm` +- Create: `tests/reference/ghz3_counts.json` +- Create: `tests/reference/invalid/unsupported_gate.qasm` +- Create: `tests/reference/invalid/unsupported_gate.error.txt` +- Create: `tests/reference/invalid/undeclared_qubit.qasm` +- Create: `tests/reference/invalid/undeclared_qubit.error.txt` +- Create: `tests/reference/invalid/syntax_error.qasm` +- Create: `tests/reference/invalid/syntax_error.error.txt` +- Create: `tests/reference/invalid/qubit_limit.qasm` +- Create: `tests/reference/invalid/qubit_limit.error.txt` +- Create: `scripts/gen_reference.py` +- Create: `tests/reference/cross_validate.rs` (Rust test that reads the golden files) + +- [ ] **Step 1: Create reference circuit files** + +```bash +mkdir -p tests/reference/invalid scripts +``` + +`tests/reference/bell.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` + +`tests/reference/ghz3.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +bit[3] c; +h q[0]; +cx q[0], q[1]; +cx q[1], q[2]; +c = measure q; +``` + +`tests/reference/invalid/unsupported_gate.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0]; +``` + +`tests/reference/invalid/unsupported_gate.error.txt`: +``` +gate 'u3' is not supported +``` + +`tests/reference/invalid/undeclared_qubit.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +h q[5]; +``` + +`tests/reference/invalid/undeclared_qubit.error.txt`: +``` +out of range +``` + +`tests/reference/invalid/syntax_error.qasm`: +``` +OPENQASM 3.0; +this is not valid; +``` + +`tests/reference/invalid/syntax_error.error.txt`: +``` +error +``` + +`tests/reference/invalid/qubit_limit.qasm`: +``` +OPENQASM 3.0; +qubit[29] q; +``` + +`tests/reference/invalid/qubit_limit.error.txt`: +``` +exceeds +``` + +- [ ] **Step 2: Write gen_reference.py** + +Create `scripts/gen_reference.py`: + +```python +#!/usr/bin/env python3 +""" +Generate reference counts and statevectors via Qiskit Aer. +Run in CI to populate tests/reference/*.json. +Requires: pip install qiskit qiskit-aer +""" +import json +import sys +from pathlib import Path + +try: + from qiskit import QuantumCircuit + from qiskit_aer import AerSimulator +except ImportError: + print("ERROR: pip install qiskit qiskit-aer", file=sys.stderr) + sys.exit(1) + +SHOTS = 10_000 +SEED = 42 +REF_DIR = Path(__file__).parent.parent / "tests" / "reference" + +def run_circuit(qasm_path: Path) -> dict: + qc = QuantumCircuit.from_qasm_file(str(qasm_path)) + sim = AerSimulator(method="statevector") + job = sim.run(qc, shots=SHOTS, seed_simulator=SEED) + result = job.result() + counts = result.get_counts() + # Statevector from noiseless zero-shot run + qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path)) + qc_sv.save_statevector() + sv_job = sim.run(qc_sv, shots=0) + sv = list(sv_job.result().get_statevector()) + return { + "shots": SHOTS, + "seed": SEED, + "counts": {k: int(v) for k, v in counts.items()}, + "statevector": [[float(a.real), float(a.imag)] for a in sv], + } + +def validate_mode() -> bool: + """--validate: check that Spinoza counts match Qiskit within chi-squared tolerance.""" + import subprocess, scipy.stats as stats + all_pass = True + for qasm in REF_DIR.glob("*.qasm"): + ref_file = qasm.with_suffix(".json") + if not ref_file.exists(): + print(f"MISSING {ref_file}") + all_pass = False + continue + ref = json.loads(ref_file.read_text()) + # Run against the local binary + result = subprocess.run( + ["cargo", "run", "--", "--validate-only", str(qasm)], + capture_output=True, text=True + ) + # Parse Spinoza counts from stdout and chi-squared compare with ref["counts"] + # ... (implementation depends on CLI output format — add --validate-only flag in V1.5) + print(f"SKIP {qasm.name} (--validate-only CLI flag not implemented in V1)") + return all_pass + +if __name__ == "__main__": + if "--validate" in sys.argv: + sys.exit(0 if validate_mode() else 1) + + for qasm in REF_DIR.glob("*.qasm"): + out = REF_DIR / qasm.with_suffix(".json").name + data = run_circuit(qasm) + out.write_text(json.dumps(data, indent=2)) + print(f"wrote {out}") +``` + +- [ ] **Step 3: Write cross-validation Rust test** + +Create `tests/reference/cross_validate.rs`: + +```rust +//! Reads the golden .json files and verifies that LocalSimulator produces +//! statistically compatible counts (chi-squared, α=0.01, N=10k shots, seed=42). + +use std::collections::HashMap; +use std::path::Path; + +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; + +fn load_circuit(name: &str) -> CircuitSource { + let path = Path::new("tests/reference").join(name); + CircuitSource(std::fs::read_to_string(path).unwrap()) +} + +fn load_expected_counts(name: &str) -> HashMap { + let path = Path::new("tests/reference").join(name); + if !path.exists() { + // JSON not generated yet (Qiskit not installed) — skip + return HashMap::new(); + } + let text = std::fs::read_to_string(path).unwrap(); + let data: serde_json::Value = serde_json::from_str(&text).unwrap(); + data["counts"] + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.clone(), v.as_u64().unwrap())) + .collect() +} + +fn chi_squared_passes(observed: &HashMap, expected: &HashMap, total: u64) -> bool { + if expected.is_empty() { return true; } // skip if no golden file + let alpha = 0.01; + // Degrees of freedom = number of outcomes - 1 + let chi_sq: f64 = expected.iter().map(|(k, &exp_count)| { + let obs_count = *observed.get(k).unwrap_or(&0) as f64; + let exp_f = exp_count as f64; + if exp_f == 0.0 { 0.0 } else { (obs_count - exp_f).powi(2) / exp_f } + }).sum(); + let df = (expected.len() - 1) as f64; + // Rough critical value for chi-squared at α=0.01 — exact table lookup would need a stats crate + // For df=1 (Bell): critical ≈ 6.63; for df=7 (3-qubit GHZ): ≈ 18.5 + let critical = df * 3.0 + 6.63; // conservative upper bound + chi_sq < critical +} + +#[test] +fn bell_counts_match_qiskit_golden() { + let sim = LocalSimulator::new(); + let circuit = load_circuit("bell.qasm"); + let expected = load_expected_counts("bell_counts.json"); + if expected.is_empty() { return; } // Qiskit not available in this environment + + let result = sim.run(&circuit, ShotCount(10_000), false).unwrap(); + assert!( + chi_squared_passes(&result.counts, &expected, 10_000), + "Bell counts diverge from Qiskit golden file.\n\ + Spinoza: {:?}\nQiskit: {:?}", + result.counts, expected + ); +} + +#[test] +fn ghz3_counts_match_qiskit_golden() { + let sim = LocalSimulator::new(); + let circuit = load_circuit("ghz3.qasm"); + let expected = load_expected_counts("ghz3_counts.json"); + if expected.is_empty() { return; } + + let result = sim.run(&circuit, ShotCount(10_000), false).unwrap(); + assert!( + chi_squared_passes(&result.counts, &expected, 10_000), + "GHZ-3 counts diverge from Qiskit golden file.\n\ + Spinoza: {:?}\nQiskit: {:?}", + result.counts, expected + ); +} + +#[test] +fn invalid_circuits_produce_validation_errors() { + use quantum_bridge_mcp::validator::CircuitValidator; + use quantum_bridge_mcp::executor::MAX_LOCAL_QUBITS; + use std::fs; + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let invalid_dir = Path::new("tests/reference/invalid"); + + for entry in fs::read_dir(invalid_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().map(|e| e == "qasm").unwrap_or(false) { + let error_file = path.with_extension("error.txt"); + let expected_fragment = fs::read_to_string(&error_file) + .unwrap() + .trim() + .to_lowercase(); + + let source = fs::read_to_string(&path).unwrap(); + let result = validator + .validate(&quantum_bridge_mcp::types::CircuitSource(source)) + .unwrap(); + + assert!( + !result.is_valid, + "{} should be invalid", path.display() + ); + let all_messages: String = result.diagnostics + .iter() + .map(|d| d.message.to_lowercase()) + .collect::>() + .join(" "); + assert!( + all_messages.contains(&expected_fragment), + "expected '{}' in messages for {}\nGot: {}", + expected_fragment, path.display(), all_messages + ); + } + } +} +``` + +- [ ] **Step 4: Run the cross-validation tests** + +```bash +cargo test --test cross_validate 2>&1 +``` + +Expected: `bell_counts_match_qiskit_golden` and `ghz3_counts_match_qiskit_golden` are skipped (golden JSON not generated yet); `invalid_circuits_produce_validation_errors` passes. + +To generate the golden files (needs Python + Qiskit): +```bash +python3 scripts/gen_reference.py +cargo test --test cross_validate 2>&1 +``` + +- [ ] **Step 5: Commit** + +```bash +git add tests/reference/ scripts/gen_reference.py +git commit -m "test: add cross-validation golden files and gen_reference.py script" +``` + +--- + +## Task 14: CI — GitHub Actions + +**Files:** +- Create: `.github/workflows/ci.yml` +- Create: `.github/workflows/release.yml` + +- [ ] **Step 1: Write ci.yml** + +Create `.github/workflows/ci.yml`: + +```yaml +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + rust: + name: Rust checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy (deny warnings) + run: cargo clippy -- -D warnings + + - name: Run unit and integration tests + run: cargo test + + - name: Run benchmarks (smoke — compile + one iteration) + run: cargo bench -- --test + + cross-validate: + name: Cross-validate with Qiskit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-qiskit + restore-keys: ${{ runner.os }}-pip- + + - name: Install Qiskit Aer + run: pip install qiskit qiskit-aer + + - name: Generate reference golden files + run: python3 scripts/gen_reference.py + + - name: Run cross-validation tests + run: cargo test --test cross_validate +``` + +- [ ] **Step 2: Write release.yml** + +Create `.github/workflows/release.yml`: + +```yaml +name: Release + +on: + push: + tags: + - "v*" + +jobs: + dist: + name: Build release binaries + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + - os: macos-latest + target: x86_64-apple-darwin + - os: macos-latest + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross (Linux cross-compilation) + if: matrix.os == 'ubuntu-latest' && matrix.target == 'aarch64-unknown-linux-gnu' + run: cargo install cross + + - name: Build + run: | + if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then + cross build --release --target ${{ matrix.target }} + else + cargo build --release --target ${{ matrix.target }} + fi + shell: bash + + - name: Package (Unix) + if: matrix.os != 'windows-latest' + run: | + cd target/${{ matrix.target }}/release + tar czf quantum-bridge-mcp-${{ matrix.target }}.tar.gz quantum-bridge-mcp + mv quantum-bridge-mcp-${{ matrix.target }}.tar.gz ../../../ + + - name: Package (Windows) + if: matrix.os == 'windows-latest' + run: | + cd target/${{ matrix.target }}/release + Compress-Archive quantum-bridge-mcp.exe quantum-bridge-mcp-${{ matrix.target }}.zip + mv quantum-bridge-mcp-${{ matrix.target }}.zip ../../../ + shell: pwsh + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: quantum-bridge-mcp-${{ matrix.target }} + path: quantum-bridge-mcp-${{ matrix.target }}.* + + github-release: + name: Create GitHub Release + needs: dist + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: "quantum-bridge-mcp-*" + generate_release_notes: true +``` + +- [ ] **Step 3: Verify CI config is valid YAML** + +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo OK +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK +``` + +Expected: both print `OK`. + +- [ ] **Step 4: Run the full verification suite locally** + +```bash +cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -20 +``` + +Expected: all green. + +- [ ] **Step 5: Final commit** + +```bash +git add .github/ +git commit -m "ci: add GitHub Actions for Rust checks, cross-validation, and multi-platform release" +``` + +--- + +## Spec Coverage Checklist + +Self-review against `docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md`: + +| Spec requirement | Covered by task | +|---|---| +| `list_backends` tool | Task 6 | +| `validate_circuit` tool (line/col diagnostics) | Tasks 4, 7 | +| `run_circuit` tool (counts + optional statevector) | Tasks 5, 8 | +| Gates: H, X, Y, Z, S, T, Sdg, Tdg, RX, RY, RZ, CX, CZ, SWAP, CCX | Task 5 | +| MAX\_LOCAL\_QUBITS = 28 | Tasks 2, 4, 5 | +| Unsupported gate → explicit error with gate name + supported list | Tasks 4, 7 | +| rmcp stdio server | Task 9 | +| Trait `Backend` (OCP — IBM extension point) | Task 3 | +| DIP: tools depend on `&dyn Backend` | Task 3 (traits), Tasks 6–8 | +| Unit tests for gates | Task 5 | +| Statistical count tests (chi-squared / ratio) | Tasks 5, 11 | +| Cross-validation with Qiskit | Task 13 | +| Property-based tests (proptest) | Task 11 | +| Golden files for invalid circuits | Task 13 | +| MCP protocol roundtrip tests | Task 10 | +| Criterion benchmarks (Bell < 5 ms, 20q < 500 ms) | Task 12 | +| CI green (lint + tests + cross-val) | Task 14 | +| Multi-platform release binaries | Task 14 | +| No `unwrap`/`panic!` in production code | Enforced throughout | +| `--f32` mode flag | **Not covered** — add as `clap` CLI arg in a follow-up | +| Homebrew tap | **Not covered** — post-V1 per spec | + +**One gap identified:** the `--f32` mode (spec §6, §7) for 30% speedup is not implemented. This is a small addition: add `clap` as a dependency, parse a `--f32` flag in `main.rs`, and pass it as a `use_f32: bool` field on `LocalSimulator`. Add to a follow-up task or V1 polish. diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor.md b/docs/superpowers/plans/2026-04-29-quantum-tutor.md new file mode 100644 index 0000000..221e280 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor.md @@ -0,0 +1,65 @@ +# Quantum Tutor Implementation Plan (rev 2 — split index) + +> **For agentic workers:** this plan is split into 18 atomic sub-tasks. Each task has its own file under [`2026-04-29-quantum-tutor/`](2026-04-29-quantum-tutor/). When executing, **load only the matching sub-task file plus the design spec** — never load this index nor sibling tasks. Use `superpowers:executing-plans` or `superpowers:subagent-driven-development`. + +**Goal:** Add 4 MCP tools (`get_lesson`, `check_exercise`, `explain_result`, `get_progress`) to quantum-bridge-mcp, backed by a JSON curriculum (7 modules / 18 exercises) and a persistent progress file. + +**Spec:** [`docs/superpowers/specs/2026-04-29-quantum-tutor-design.md`](../specs/2026-04-29-quantum-tutor-design.md). + +--- + +## Sub-tasks (in order) + +| # | File | Goal | Depends on | +|---|------|------|------------| +| 0 | [`00-prerequisites.md`](2026-04-29-quantum-tutor/00-prerequisites.md) | `QuantumBridgeServer` holds `Arc` + `tempfile` dev-dep | — | +| 1 | [`01-curriculum-types.md`](2026-04-29-quantum-tutor/01-curriculum-types.md) | Curriculum types + module 1 JSON | 0 | +| 2 | [`02-curriculum-loader.md`](2026-04-29-quantum-tutor/02-curriculum-loader.md) | `OnceLock`-cached `CurriculumLoader`, no `expect()` | 1 | +| 3 | [`03-progress-store.md`](2026-04-29-quantum-tutor/03-progress-store.md) | Sandboxable `ProgressStore` (`QB_PROGRESS_PATH`) | 0 | +| 4 | [`04-circuit-analyzer.md`](2026-04-29-quantum-tutor/04-circuit-analyzer.md) | AST-based gate listing | 0 | +| 5 | [`05-exercise-checker.md`](2026-04-29-quantum-tutor/05-exercise-checker.md) | `ExerciseChecker` with `&dyn Backend`, 2σ, statevector | 2, 4 | +| 6 | [`06-get-lesson.md`](2026-04-29-quantum-tutor/06-get-lesson.md) | `get_lesson` response (first uncompleted lesson) | 2, 3 | +| 7 | [`07-check-exercise.md`](2026-04-29-quantum-tutor/07-check-exercise.md) | `check_exercise` response (structured diagnostics) | 5, 6 | +| 8 | [`08-explain-result.md`](2026-04-29-quantum-tutor/08-explain-result.md) | `explain_result` response (AST-based) | 4, 7 | +| 9 | [`09-get-progress.md`](2026-04-29-quantum-tutor/09-get-progress.md) | `get_progress` response (current_lesson + unlock) | 8 | +| 10 | [`10-wire-tools.md`](2026-04-29-quantum-tutor/10-wire-tools.md) | Register the 4 MCP tools on the server | 9 | +| 11 | [`11-curriculum-module-2.md`](2026-04-29-quantum-tutor/11-curriculum-module-2.md) | Module 2 — Superposition | 10 | +| 12 | [`12-curriculum-module-3.md`](2026-04-29-quantum-tutor/12-curriculum-module-3.md) | Module 3 — Interférence | 11 | +| 13 | [`13-curriculum-module-4.md`](2026-04-29-quantum-tutor/13-curriculum-module-4.md) | Module 4 — Intrication | 12 | +| 14 | [`14-curriculum-module-5.md`](2026-04-29-quantum-tutor/14-curriculum-module-5.md) | Module 5 — Multi-qubits | 13 | +| 15 | [`15-curriculum-module-6.md`](2026-04-29-quantum-tutor/15-curriculum-module-6.md) | Module 6 — Premiers algorithmes (phase kickback) | 14 | +| 16 | [`16-curriculum-module-7.md`](2026-04-29-quantum-tutor/16-curriculum-module-7.md) | Module 7 — Grover + 18-exercise assertion | 15 | +| 17 | [`17-integration-tests.md`](2026-04-29-quantum-tutor/17-integration-tests.md) | MCP integration tests + golden files | 16 | + +--- + +## Architecture invariants enforced by this plan + +(These are the only invariants you need to remember when walking sub-tasks; each task file repeats the relevant ones.) + +- `QuantumBridgeServer` owns `Arc`; tutor tools route through `self.backend`. +- `ExerciseChecker::check_circuit(backend: &dyn Backend, …)` — never instantiates `LocalSimulator` directly (CLAUDE.md §3 / spec §3). +- `CurriculumLoader` lazily parses an embedded JSON via `OnceLock`; returns `Result`, no `expect()` (CLAUDE.md §10). +- `ProgressStore` is sandboxed via `QB_PROGRESS_PATH`; missing `HOME` returns `Err`, never falls back to `.`. +- `CircuitAnalyzer` exposes AST-based gate listing — `explain_result` does not string-match the QASM source (spec §2.3). +- Statistical tolerance for exercise checks is **2σ** below `min_ratio` (spec §3). +- `check_exercise` distinguishes protocol errors (unknown exercise → `McpError`) from validation errors (circuit invalid → structured `diagnostics` payload, spec §2.2). +- Module 6 uses phase kickback rather than teleportation (the v1 executor walks past `Stmt::If`). + +--- + +## Changelog vs. rev 1 (legacy single-file plan) + +The previous monolithic plan was 1767 lines and contained: +1. DIP violations (`LocalSimulator::new()` inside `ExerciseChecker`). +2. String-matching in `explain_result`. +3. `expect()` in production (`CurriculumLoader::load`). +4. `get_lesson` always returning lesson 1. +5. 3σ tolerance instead of spec's 2σ. +6. `statevector_check` defined but unused. +7. Validation errors collapsed to `McpError`. +8. Test bugs (process-id collision in `ProgressStore` tests; integration tests writing to real `~/.config/`). +9. Module 6 teleportation that the v1 executor cannot run. +10. Task 5 (4 tools) and Task 7 (6 modules) bundled into single commits. + +Rev 2 fixes all of the above and splits the work into 18 atomic, single-responsibility tasks. diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/00-prerequisites.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/00-prerequisites.md new file mode 100644 index 0000000..91102de --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/00-prerequisites.md @@ -0,0 +1,73 @@ +# Task 0 — Backend injection seam + tempfile dev-dep + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Make `QuantumBridgeServer` hold `Arc` so the upcoming tutor tools depend on `&dyn Backend` (spec §3, CLAUDE.md §3). Existing v1 tools (`list_backends`, `validate_circuit`, `run_circuit`) are **not** refactored — they continue to construct their own `LocalSimulator`. Only the new path complies with DIP. Also add `tempfile` for sandboxed tests in later tasks. + +## Prerequisites + +None. This is the starting point. + +## Files + +- Modify: `Cargo.toml` +- Modify: `src/tools/mod.rs` +- Modify: `src/main.rs` + +## Steps + +- [ ] **Step 1: Add `tempfile` to `[dev-dependencies]` in `Cargo.toml`** + +```toml +tempfile = "3" +``` + +- [ ] **Step 2: Replace the unit struct in `src/tools/mod.rs`** + +```rust +use std::sync::Arc; +use crate::executor::{Backend, LocalSimulator}; + +#[derive(Clone)] +pub struct QuantumBridgeServer { + pub backend: Arc, +} + +impl QuantumBridgeServer { + pub fn new(backend: Arc) -> Self { + Self { backend } + } + + pub fn with_local_simulator() -> Self { + Self::new(Arc::new(LocalSimulator::new())) + } +} +``` + +The existing tool methods inside `#[tool_router(server_handler)] impl QuantumBridgeServer { ... }` are not modified. + +- [ ] **Step 3: Update the construction in `src/main.rs`** + +```rust +let service = QuantumBridgeServer::with_local_simulator() + .serve(stdio()) + .await + .inspect_err(|e| tracing::error!("serving error: {:?}", e))?; +``` + +- [ ] **Step 4: Verify the existing test suite still passes** + +```bash +cargo build && cargo test --lib 2>&1 | grep -E "test result|FAILED" +``` + +Expected: every existing suite green; the change is purely additive. + +- [ ] **Step 5: Commit** + +```bash +git add Cargo.toml src/tools/mod.rs src/main.rs +git commit -m "refactor: QuantumBridgeServer holds Arc; add tempfile dev-dep" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/01-curriculum-types.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/01-curriculum-types.md new file mode 100644 index 0000000..7b69763 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/01-curriculum-types.md @@ -0,0 +1,223 @@ +# Task 1 — Curriculum types + module 1 JSON (TDD) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Bootstrap the curriculum: define the Rust types, ship the embedded JSON for module 1 (2 exercises), and assert that every gate listed in `executor::SUPPORTED_GATES` has a description. TDD discipline: the test is written first and fails to compile until the types exist. + +## Prerequisites + +- Task 0 merged. + +## Files + +- Create: `src/tutor.rs` +- Create: `curriculum/curriculum.json` +- Modify: `src/lib.rs` + +## Steps + +- [ ] **Step 1: Create `src/tutor.rs` with the failing test only** + +```rust +//! Curriculum types and helpers — see docs/superpowers/specs/2026-04-29-quantum-tutor-design.md. + +use std::collections::HashMap; +use serde::Deserialize; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn curriculum_json_deserializes_with_module_1() { + let json = include_str!("../curriculum/curriculum.json"); + let curriculum: Curriculum = serde_json::from_str(json).expect("curriculum.json must parse"); + assert_eq!(curriculum.version, "1.0"); + let module_1 = curriculum.modules.iter().find(|m| m.id == 1).expect("module 1"); + let exercise_count: usize = module_1.lessons.iter().map(|l| l.exercises.len()).sum(); + assert_eq!(exercise_count, 2); + for gate in crate::executor::SUPPORTED_GATES { + if *gate == "measure" { continue; } + assert!( + curriculum.gate_descriptions.contains_key(*gate), + "missing gate_description for '{gate}'" + ); + } + } +} +``` + +- [ ] **Step 2: Confirm the test fails (Red)** + +```bash +cargo test tutor::tests 2>&1 | grep -E "cannot find|error\[" +``` + +Expected: a compile error on `Curriculum`. + +- [ ] **Step 3: Add the type definitions above the `#[cfg(test)]` block** + +```rust +#[derive(Debug, Deserialize, Clone)] +pub struct Curriculum { + pub version: String, + pub gate_descriptions: HashMap, + pub modules: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct GateDescription { + pub short: String, + #[serde(default)] + pub effect_on_zero: Option, + #[serde(default)] + pub effect_on_one: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Module { + pub id: u32, + pub title: String, + pub lessons: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Lesson { + pub id: u32, + pub title: String, + pub concept: String, + pub example_circuit: String, + pub what_to_observe: String, + pub exercises: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Exercise { + pub id: String, + pub prompt: String, + pub hint: String, + pub criteria: ExerciseCriteria, + pub feedback_pass: String, + pub feedback_fail: String, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ExerciseCriteria { + #[serde(default)] + pub required_outcomes: Vec, + #[serde(default)] + pub forbidden_outcomes: Vec, + #[serde(default)] + pub statevector_check: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RequiredOutcome { + pub bitstring: String, + #[serde(default)] + pub min_ratio: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct StatevectorCheck { + pub non_zero_amplitude_indices: Vec, + pub zero_amplitude_indices: Vec, + #[serde(default = "default_tolerance")] + pub tolerance: f64, +} + +fn default_tolerance() -> f64 { 1e-6 } +``` + +- [ ] **Step 4: Register the module in `src/lib.rs`** + +```rust +pub mod tutor; +``` + +- [ ] **Step 5: Create `curriculum/curriculum.json` with module 1 and ALL gate descriptions** + +```bash +mkdir -p curriculum +``` + +`curriculum/curriculum.json`: + +```json +{ + "version": "1.0", + "gate_descriptions": { + "h": {"short": "Hadamard — crée une superposition égale", "effect_on_zero": "Transforme |0⟩ en (|0⟩+|1⟩)/√2", "effect_on_one": "Transforme |1⟩ en (|0⟩-|1⟩)/√2"}, + "x": {"short": "Pauli-X — NOT quantique", "effect_on_zero": "Transforme |0⟩ en |1⟩", "effect_on_one": "Transforme |1⟩ en |0⟩"}, + "y": {"short": "Pauli-Y — rotation π autour de Y", "effect_on_zero": "Transforme |0⟩ en i|1⟩", "effect_on_one": "Transforme |1⟩ en -i|0⟩"}, + "z": {"short": "Pauli-Z — flip de phase", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en -|1⟩"}, + "s": {"short": "Phase S — rotation π/2 autour de Z", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en i|1⟩"}, + "sdg": {"short": "S† — rotation -π/2 autour de Z (inverse de S)", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en -i|1⟩"}, + "t": {"short": "Phase T — rotation π/4 autour de Z", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en e^(iπ/4)|1⟩"}, + "tdg": {"short": "T† — rotation -π/4 autour de Z (inverse de T)", "effect_on_zero": "Laisse |0⟩ inchangé", "effect_on_one": "Transforme |1⟩ en e^(-iπ/4)|1⟩"}, + "cx": {"short": "CNOT — flip la cible si le contrôle est |1⟩", "effect_on_zero": "Si contrôle=|0⟩, ne fait rien à la cible"}, + "cz": {"short": "CZ — flip de phase sur la cible si contrôle=|1⟩"}, + "swap":{"short": "SWAP — échange l'état de deux qubits"}, + "ccx": {"short": "Toffoli — flip la cible si les deux contrôles sont |1⟩"}, + "rx": {"short": "Rotation autour de l'axe X d'un angle θ"}, + "ry": {"short": "Rotation autour de l'axe Y d'un angle θ"}, + "rz": {"short": "Rotation autour de l'axe Z d'un angle θ"} + }, + "modules": [ + { + "id": 1, + "title": "Le qubit", + "lessons": [ + { + "id": 1, + "title": "États |0⟩ et |1⟩", + "concept": "Un qubit peut être dans l'état |0⟩ ou |1⟩, exactement comme un bit classique — mais aussi dans une superposition des deux. Sans rien faire, il part de |0⟩. La porte X (NOT quantique) bascule |0⟩ en |1⟩.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;", + "what_to_observe": "Lance run_circuit avec 100 shots. Tu devrais voir uniquement '1' dans les counts.", + "exercises": [ + { + "id": "1-1-a", + "prompt": "Écris un circuit OpenQASM 3.0 avec 1 qubit qui produit toujours '1' quand on le mesure. Utilise la porte X.", + "hint": "La porte X s'applique avec 'x q[0];' avant la mesure.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Parfait ! La porte X bascule |0⟩ en |1⟩.", + "feedback_fail": "Ton circuit ne produit pas toujours '1'." + }, + { + "id": "1-1-b", + "prompt": "Écris un circuit avec 1 qubit sans aucune porte, juste la mesure.", + "hint": "Un qubit non touché part de |0⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "0", "min_ratio": 0.99}], + "forbidden_outcomes": ["1"] + }, + "feedback_pass": "Sans aucune porte, le qubit reste dans |0⟩.", + "feedback_fail": "Ton circuit produit des '1' alors qu'il ne devrait pas." + } + ] + } + ] + } + ] +} +``` + +- [ ] **Step 6: Run the test (Green)** + +```bash +cargo test tutor::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 7: Commit** + +```bash +git add curriculum/curriculum.json src/tutor.rs src/lib.rs +git commit -m "feat: add curriculum types and module 1 content (tutor v1)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/02-curriculum-loader.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/02-curriculum-loader.md new file mode 100644 index 0000000..9bec9f4 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/02-curriculum-loader.md @@ -0,0 +1,144 @@ +# Task 2 — `CurriculumLoader` (`OnceLock`-cached, no `expect()`) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Provide a lazy, cached accessor over the embedded curriculum that returns `Result<&Curriculum, BridgeError>` instead of panicking. Required by every tutor tool. CLAUDE.md §10 forbids `expect()` in production. + +## Prerequisites + +- Task 1 merged. + +## Files + +- Modify: `src/tutor.rs` +- Modify: `src/error.rs` + +## Steps + +- [ ] **Step 1: Add a `Curriculum` variant to `BridgeError` in `src/error.rs`** + +```rust +#[error("curriculum data is malformed: {0}")] +Curriculum(String), +``` + +- [ ] **Step 2: Append failing tests to `src/tutor.rs` (inside the existing `#[cfg(test)]` block)** + +```rust + #[test] + fn loader_returns_curriculum_on_first_call() { + let loader = CurriculumLoader::default(); + let c = loader.curriculum().expect("embedded curriculum must parse"); + assert_eq!(c.version, "1.0"); + } + + #[test] + fn find_exercise_by_id_returns_correct_exercise() { + let loader = CurriculumLoader::default(); + let exercise = loader.find_exercise("1-1-a").unwrap(); + assert_eq!(exercise.id, "1-1-a"); + } + + #[test] + fn find_exercise_with_unknown_id_returns_none() { + let loader = CurriculumLoader::default(); + assert!(loader.find_exercise("99-99-z").is_none()); + } + + #[test] + fn get_lesson_unknown_module_returns_none() { + let loader = CurriculumLoader::default(); + assert!(loader.get_lesson(99, 1).is_none()); + } + + #[test] + fn loader_reports_curriculum_error_for_malformed_json() { + let result = CurriculumLoader::from_str("{ not valid json"); + assert!(result.is_err()); + } +``` + +- [ ] **Step 3: Implement `CurriculumLoader` (above the `#[cfg(test)]` block)** + +```rust +use std::sync::OnceLock; +use crate::error::BridgeError; + +const CURRICULUM_JSON: &str = include_str!("../curriculum/curriculum.json"); + +#[derive(Default)] +pub struct CurriculumLoader { + cell: OnceLock>, +} + +impl CurriculumLoader { + pub fn curriculum(&self) -> Result<&Curriculum, BridgeError> { + let result = self.cell.get_or_init(|| { + serde_json::from_str::(CURRICULUM_JSON).map_err(|e| e.to_string()) + }); + result + .as_ref() + .map_err(|msg| BridgeError::Curriculum(msg.clone())) + } + + pub fn from_str(json: &str) -> Result { + let curriculum: Curriculum = + serde_json::from_str(json).map_err(|e| BridgeError::Curriculum(e.to_string()))?; + let loader = Self::default(); + let _ = loader.cell.set(Ok(curriculum)); + Ok(loader) + } + + pub fn get_module(&self, module_id: u32) -> Option<&Module> { + self.curriculum().ok()?.modules.iter().find(|m| m.id == module_id) + } + + pub fn get_lesson(&self, module_id: u32, lesson_id: u32) -> Option<&Lesson> { + self.get_module(module_id)? + .lessons + .iter() + .find(|l| l.id == lesson_id) + } + + pub fn find_exercise(&self, exercise_id: &str) -> Option<&Exercise> { + for module in &self.curriculum().ok()?.modules { + for lesson in &module.lessons { + if let Some(ex) = lesson.exercises.iter().find(|e| e.id == exercise_id) { + return Some(ex); + } + } + } + None + } + + pub fn all_exercises(&self) -> Vec<&Exercise> { + self.curriculum() + .ok() + .map(|c| { + c.modules + .iter() + .flat_map(|m| m.lessons.iter()) + .flat_map(|l| l.exercises.iter()) + .collect() + }) + .unwrap_or_default() + } +} +``` + +- [ ] **Step 4: Run tests (Green)** + +```bash +cargo test tutor::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 6 passed`. + +- [ ] **Step 5: Commit** + +```bash +git add src/tutor.rs src/error.rs +git commit -m "feat: implement CurriculumLoader with OnceLock cache and structured error" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/03-progress-store.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/03-progress-store.md new file mode 100644 index 0000000..b47c745 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/03-progress-store.md @@ -0,0 +1,174 @@ +# Task 3 — `ProgressStore` (sandboxable, deterministic tests) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Persist solved exercises to disk. The path resolves via `QB_PROGRESS_PATH` first, then `$HOME/.config/quantum-bridge-mcp/progress.json`. If neither is available, return an explicit error (never silently fall back to `.`). Tests use `tempfile::TempDir` so they are deterministic under cargo's parallel test runner. + +## Prerequisites + +- Task 0 merged (for `tempfile` dev-dep). + +## Files + +- Create: `src/progress.rs` +- Modify: `src/error.rs` +- Modify: `src/lib.rs` + +## Steps + +- [ ] **Step 1: Add a `Configuration` variant to `BridgeError` in `src/error.rs`** + +```rust +#[error("configuration error: {0}")] +Configuration(String), +``` + +- [ ] **Step 2: Create `src/progress.rs`** + +```rust +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::error::BridgeError; + +pub const PROGRESS_PATH_ENV: &str = "QB_PROGRESS_PATH"; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct UserProgress { + pub solved_exercises: HashSet, +} + +impl UserProgress { + pub fn has_solved(&self, exercise_id: &str) -> bool { + self.solved_exercises.contains(exercise_id) + } + + pub fn mark_solved(&mut self, exercise_id: &str) { + self.solved_exercises.insert(exercise_id.to_string()); + } +} + +pub struct ProgressStore { + path: PathBuf, +} + +impl ProgressStore { + /// Env var override first, then `$HOME/.config/quantum-bridge-mcp/progress.json`. + /// Errors out if neither is available — never falls back to `.`. + pub fn default_path() -> Result { + if let Ok(override_path) = std::env::var(PROGRESS_PATH_ENV) { + return Ok(PathBuf::from(override_path)); + } + let home = std::env::var("HOME").map_err(|_| { + BridgeError::Configuration( + "neither QB_PROGRESS_PATH nor HOME is set; cannot resolve progress.json location" + .into(), + ) + })?; + Ok(PathBuf::from(home) + .join(".config") + .join("quantum-bridge-mcp") + .join("progress.json")) + } + + pub fn new(path: PathBuf) -> Self { Self { path } } + + pub fn load(&self) -> UserProgress { + match fs::read_to_string(&self.path) { + Ok(content) => serde_json::from_str(&content).unwrap_or_default(), + Err(_) => UserProgress::default(), + } + } + + pub fn save(&self, progress: &UserProgress) -> Result<(), BridgeError> { + if let Some(parent) = self.path.parent() { + fs::create_dir_all(parent) + .map_err(|e| BridgeError::Configuration(format!("create_dir_all failed: {e}")))?; + } + let json = serde_json::to_string_pretty(progress) + .map_err(|e| BridgeError::Configuration(format!("serialize failed: {e}")))?; + fs::write(&self.path, json) + .map_err(|e| BridgeError::Configuration(format!("write failed: {e}"))) + } + + pub fn mark_solved(&self, exercise_id: &str) -> Result<(), BridgeError> { + let mut progress = self.load(); + progress.mark_solved(exercise_id); + self.save(&progress) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn isolated_store() -> (ProgressStore, TempDir) { + let dir = TempDir::new().expect("create tempdir"); + let path = dir.path().join("progress.json"); + (ProgressStore::new(path), dir) + } + + #[test] + fn load_returns_empty_progress_when_no_file_exists() { + let (store, _dir) = isolated_store(); + assert!(store.load().solved_exercises.is_empty()); + } + + #[test] + fn save_and_reload_preserves_solved_exercises() { + let (store, _dir) = isolated_store(); + store.mark_solved("1-1-a").unwrap(); + assert!(store.load().has_solved("1-1-a")); + } + + #[test] + fn mark_solved_twice_does_not_duplicate() { + let (store, _dir) = isolated_store(); + store.mark_solved("1-1-a").unwrap(); + store.mark_solved("1-1-a").unwrap(); + assert_eq!(store.load().solved_exercises.len(), 1); + } + + #[test] + fn has_solved_returns_false_for_unknown_exercise() { + assert!(!UserProgress::default().has_solved("99-99-z")); + } + + #[test] + fn default_path_uses_env_override_when_set() { + let dir = TempDir::new().unwrap(); + let target = dir.path().join("override.json"); + std::env::set_var(PROGRESS_PATH_ENV, &target); + let resolved = ProgressStore::default_path().unwrap(); + std::env::remove_var(PROGRESS_PATH_ENV); + assert_eq!(resolved, target); + } +} +``` + +- [ ] **Step 3: Register `progress` in `src/lib.rs`** + +```rust +pub mod progress; +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test progress::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 5 passed`. + +- [ ] **Step 5: Commit** + +```bash +git add src/progress.rs src/error.rs src/lib.rs +git commit -m "feat: implement ProgressStore with QB_PROGRESS_PATH sandbox and tempfile tests" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/04-circuit-analyzer.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/04-circuit-analyzer.md new file mode 100644 index 0000000..6c73766 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/04-circuit-analyzer.md @@ -0,0 +1,119 @@ +# Task 4 — `CircuitAnalyzer` (AST-based gate listing) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Expose AST-based gate enumeration as a public utility so `explain_result` (Task 8) and any other consumer can iterate gates without re-parsing or string-matching the QASM source. Wraps the AST traversal already present in `executor::extract_gate_ops`. + +## Prerequisites + +- Task 0 merged. + +## Files + +- Create: `src/circuit_analyzer.rs` +- Modify: `src/executor.rs` (add public `list_gate_calls`) +- Modify: `src/lib.rs` + +## Steps + +- [ ] **Step 1: Create `src/circuit_analyzer.rs` with failing tests** + +```rust +//! Pure AST-based gate listing for OpenQASM 3.0 circuits. + +use crate::error::BridgeError; +use crate::types::CircuitSource; + +#[derive(Debug, Clone, PartialEq)] +pub struct GateCallInfo { + pub name: String, + pub params: Vec, + pub qubits: Vec, +} + +pub struct CircuitAnalyzer; + +impl CircuitAnalyzer { + pub fn list_gates(source: &CircuitSource) -> Result, BridgeError> { + crate::executor::list_gate_calls(source) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const BELL: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + + #[test] + fn list_gates_for_bell_returns_h_then_cx() { + let gates = CircuitAnalyzer::list_gates(&CircuitSource(BELL.into())).unwrap(); + assert_eq!(gates.len(), 2); + assert_eq!(gates[0].name, "h"); + assert_eq!(gates[0].qubits, vec![0]); + assert_eq!(gates[1].name, "cx"); + assert_eq!(gates[1].qubits, vec![0, 1]); + } + + #[test] + fn list_gates_ignores_measure() { + let gates = CircuitAnalyzer::list_gates(&CircuitSource(BELL.into())).unwrap(); + assert!(gates.iter().all(|g| g.name != "measure")); + } + + #[test] + fn list_gates_returns_empty_for_circuit_with_no_gates() { + let identity = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + let gates = CircuitAnalyzer::list_gates(&CircuitSource(identity.into())).unwrap(); + assert!(gates.is_empty()); + } +} +``` + +- [ ] **Step 2: Expose `list_gate_calls` in `src/executor.rs`** + +Add the public function near the existing `extract_gate_ops` helper (or below it). Reuses the same parser path as `LocalSimulator::run`. + +```rust +use crate::circuit_analyzer::GateCallInfo; + +/// Public AST-based gate enumeration. Reuses the parser path of `run`. +pub fn list_gate_calls(source: &CircuitSource) -> Result, BridgeError> { + let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); + if parse_result.any_syntax_errors() { + return Err(BridgeError::Simulation("circuit contains syntax errors".into())); + } + let context = parse_result.take_context(); + let symbol_table = context.symbol_table(); + let program = context.program(); + let (_, register_offsets) = build_register_map(program, symbol_table); + let ops = extract_gate_ops(program, symbol_table, ®ister_offsets)?; + Ok(ops + .into_iter() + .map(|(name, params, qubits)| GateCallInfo { name, params, qubits }) + .collect()) +} +``` + +- [ ] **Step 3: Register `circuit_analyzer` in `src/lib.rs`** + +```rust +pub mod circuit_analyzer; +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test circuit_analyzer::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 3 passed`. + +- [ ] **Step 5: Commit** + +```bash +git add src/circuit_analyzer.rs src/executor.rs src/lib.rs +git commit -m "feat: add CircuitAnalyzer for AST-based gate enumeration" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/05-exercise-checker.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/05-exercise-checker.md new file mode 100644 index 0000000..c3d169c --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/05-exercise-checker.md @@ -0,0 +1,222 @@ +# Task 5 — `ExerciseChecker` (`&dyn Backend`, 2σ tolerance, statevector check) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Verify whether a submitted circuit satisfies an `ExerciseCriteria`. Takes `&dyn Backend` so the checker is testable with mocks and remains compatible with V1.5 IBM (CLAUDE.md §3, spec §3). Runs 1024 fixed shots, applies 2σ tolerance, and validates `statevector_check` when present. + +## Prerequisites + +- Task 2 merged (CurriculumLoader available). +- Task 4 merged (CircuitAnalyzer not strictly required here, but typically present at this point). + +## Files + +- Modify: `src/tutor.rs` + +## Steps + +- [ ] **Step 1: Append failing tests to the existing `#[cfg(test)]` block in `src/tutor.rs`** + +```rust + use crate::executor::{Backend, LocalSimulator}; + + const X_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + const IDENTITY_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + const H_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;"; + + fn backend() -> LocalSimulator { LocalSimulator::new() } + + #[test] + fn x_circuit_passes_exercise_requiring_bitstring_1() { + let criteria = ExerciseCriteria { + required_outcomes: vec![RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.99) }], + forbidden_outcomes: vec!["0".into()], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, X_CIRCUIT, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } + + #[test] + fn identity_circuit_fails_exercise_requiring_bitstring_1() { + let criteria = ExerciseCriteria { + required_outcomes: vec![RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.99) }], + forbidden_outcomes: vec!["0".into()], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, IDENTITY_CIRCUIT, &criteria); + assert!(!result.passed); + } + + #[test] + fn h_circuit_passes_balanced_outcomes_with_2sigma_tolerance() { + let criteria = ExerciseCriteria { + required_outcomes: vec![ + RequiredOutcome { bitstring: "0".into(), min_ratio: Some(0.4) }, + RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.4) }, + ], + forbidden_outcomes: vec![], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, H_CIRCUIT, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } + + #[test] + fn invalid_circuit_returns_diagnostics_not_panic() { + let criteria = ExerciseCriteria::default(); + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, "not valid qasm", &criteria); + assert!(!result.passed); + assert!(result.error.is_some()); + } + + #[test] + fn statevector_check_validates_bell_state_amplitudes() { + const BELL: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let criteria = ExerciseCriteria { + required_outcomes: vec![], + forbidden_outcomes: vec![], + statevector_check: Some(StatevectorCheck { + non_zero_amplitude_indices: vec![0, 3], + zero_amplitude_indices: vec![1, 2], + tolerance: 1e-6, + }), + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, BELL, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } +``` + +- [ ] **Step 2: Implement `ExerciseChecker` (above the `#[cfg(test)]` block)** + +```rust +use crate::executor::{Backend, MAX_LOCAL_QUBITS}; +use crate::types::{CircuitSource, ShotCount, ValidationDiagnostic}; +use crate::validator::CircuitValidator; + +pub struct CheckResult { + pub passed: bool, + pub counts: HashMap, + pub diagnostics: Vec, + pub error: Option, +} + +pub struct ExerciseChecker; + +impl ExerciseChecker { + const CHECK_SHOTS: u32 = 1024; + /// Spec §3 — pass if `count ≥ (min_ratio - 2σ) × N`. + const SIGMA_MULTIPLIER: f64 = 2.0; + + pub fn check_circuit( + backend: &dyn Backend, + circuit_source: &str, + criteria: &ExerciseCriteria, + ) -> CheckResult { + let source = CircuitSource(circuit_source.to_string()); + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let validation = match validator.validate(&source) { + Err(e) => return CheckResult { + passed: false, counts: HashMap::new(), diagnostics: vec![], + error: Some(e.to_string()), + }, + Ok(v) => v, + }; + if !validation.is_valid { + let summary = validation + .diagnostics + .iter() + .map(|d| d.message.as_str()) + .collect::>() + .join("; "); + return CheckResult { + passed: false, counts: HashMap::new(), + diagnostics: validation.diagnostics, error: Some(summary), + }; + } + + let need_sv = criteria.statevector_check.is_some(); + let result = match backend.run(&source, ShotCount(Self::CHECK_SHOTS), need_sv) { + Err(e) => return CheckResult { + passed: false, counts: HashMap::new(), diagnostics: vec![], + error: Some(e.to_string()), + }, + Ok(r) => r, + }; + + let total = result.shots as f64; + let counts_pass = Self::counts_pass(&result.counts, criteria, total); + let sv_pass = match (&criteria.statevector_check, &result.statevector) { + (Some(check), Some(sv)) => Self::statevector_pass(sv, check), + (Some(_), None) => false, + (None, _) => true, + }; + + CheckResult { + passed: counts_pass && sv_pass, + counts: result.counts, + diagnostics: vec![], + error: None, + } + } + + fn counts_pass( + counts: &HashMap, + criteria: &ExerciseCriteria, + total: f64, + ) -> bool { + for req in &criteria.required_outcomes { + let count = counts.get(&req.bitstring).copied().unwrap_or(0) as f64; + match req.min_ratio { + Some(min_ratio) => { + let sigma = (min_ratio * (1.0 - min_ratio) / total).sqrt(); + let threshold = (min_ratio - Self::SIGMA_MULTIPLIER * sigma) * total; + if count < threshold { return false; } + } + None => if count == 0.0 { return false; }, + } + } + for forbidden in &criteria.forbidden_outcomes { + if counts.get(forbidden).copied().unwrap_or(0) > 0 { return false; } + } + true + } + + fn statevector_pass(sv: &[(f64, f64)], check: &StatevectorCheck) -> bool { + let magnitude = |idx: usize| -> Option { + sv.get(idx).map(|(r, i)| (r * r + i * i).sqrt()) + }; + for &idx in &check.non_zero_amplitude_indices { + match magnitude(idx) { + Some(m) if m > check.tolerance => {} + _ => return false, + } + } + for &idx in &check.zero_amplitude_indices { + match magnitude(idx) { + Some(m) if m <= check.tolerance => {} + _ => return false, + } + } + true + } +} +``` + +- [ ] **Step 3: Run tests (Green)** + +```bash +cargo test tutor::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 11 passed`. + +- [ ] **Step 4: Commit** + +```bash +git add src/tutor.rs +git commit -m "feat: ExerciseChecker with Backend injection, 2σ tolerance, statevector check" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/06-get-lesson.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/06-get-lesson.md new file mode 100644 index 0000000..9795a54 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/06-get-lesson.md @@ -0,0 +1,157 @@ +# Task 6 — `get_lesson` response (first uncompleted lesson default) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Pure response function for `get_lesson`. When `lesson_id` is `None`, returns the first lesson in the module that has at least one unsolved exercise (spec §2.1). Each call surfaces the next pending exercise rather than always the first one. The function is pure (no I/O) and parameterized by the caller's `UserProgress`. + +## Prerequisites + +- Task 2 merged (CurriculumLoader). +- Task 3 merged (UserProgress type). + +## Files + +- Create: `src/tools/tutor_tools.rs` +- Modify: `src/tools/mod.rs` (declare module) + +## Steps + +- [ ] **Step 1: Create `src/tools/tutor_tools.rs` with implementation + tests** + +```rust +use serde_json::{json, Value}; + +use crate::progress::UserProgress; +use crate::tutor::CurriculumLoader; + +pub fn get_lesson_response( + loader: &CurriculumLoader, + progress: &UserProgress, + module_id: u32, + lesson_id: Option, +) -> Value { + let module = match loader.get_module(module_id) { + None => return json!({ "error": format!("module {} not found", module_id) }), + Some(m) => m, + }; + + let resolved = lesson_id + .or_else(|| first_unsolved_lesson(module, progress)) + .or_else(|| module.lessons.first().map(|l| l.id)); + let resolved = match resolved { + Some(id) => id, + None => return json!({ "error": format!("module {} has no lessons", module_id) }), + }; + + let lesson = match loader.get_lesson(module_id, resolved) { + None => return json!({ "error": format!("module {} lesson {} not found", module_id, resolved) }), + Some(l) => l, + }; + + let pending_exercise = lesson + .exercises + .iter() + .find(|e| !progress.has_solved(&e.id)); + + if pending_exercise.is_none() && lesson.exercises.iter().all(|e| progress.has_solved(&e.id)) { + return json!({ + "module_id": module_id, + "lesson_id": resolved, + "title": lesson.title, + "concept": lesson.concept, + "module_completed": all_lessons_completed(module, progress), + "message": "Toutes les exercices de cette leçon sont résolus.", + }); + } + + json!({ + "module_id": module_id, + "lesson_id": resolved, + "title": lesson.title, + "concept": lesson.concept, + "example_circuit": lesson.example_circuit, + "what_to_observe": lesson.what_to_observe, + "exercise": pending_exercise.map(|e| json!({ + "id": e.id, + "prompt": e.prompt, + "hint": e.hint, + })), + }) +} + +fn first_unsolved_lesson(module: &crate::tutor::Module, progress: &UserProgress) -> Option { + module + .lessons + .iter() + .find(|l| l.exercises.iter().any(|e| !progress.has_solved(&e.id))) + .map(|l| l.id) +} + +fn all_lessons_completed(module: &crate::tutor::Module, progress: &UserProgress) -> bool { + module + .lessons + .iter() + .flat_map(|l| l.exercises.iter()) + .all(|e| progress.has_solved(&e.id)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn loader() -> CurriculumLoader { CurriculumLoader::default() } + + #[test] + fn get_lesson_returns_first_pending_exercise_for_known_module() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, Some(1)); + assert_eq!(resp["module_id"].as_u64().unwrap(), 1); + assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1); + assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-a"); + } + + #[test] + fn get_lesson_skips_solved_exercises_within_a_lesson() { + let mut progress = UserProgress::default(); + progress.mark_solved("1-1-a"); + let resp = get_lesson_response(&loader(), &progress, 1, Some(1)); + assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-b"); + } + + #[test] + fn get_lesson_default_resolves_to_first_lesson_with_pending_exercises() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, None); + assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1); + } + + #[test] + fn get_lesson_unknown_module_returns_error_payload() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 99, None); + assert!(resp.get("error").is_some()); + } +} +``` + +- [ ] **Step 2: Declare `tutor_tools` in `src/tools/mod.rs`** + +Add at the top: + +```rust +pub mod tutor_tools; +``` + +- [ ] **Step 3: Run tests** + +```bash +cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 4 passed`. + +- [ ] **Step 4: Commit** + +```bash +git add src/tools/tutor_tools.rs src/tools/mod.rs +git commit -m "feat: get_lesson response (first uncompleted lesson default)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/07-check-exercise.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/07-check-exercise.md new file mode 100644 index 0000000..19563a2 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/07-check-exercise.md @@ -0,0 +1,152 @@ +# Task 7 — `check_exercise` response (Backend injection, structured validation errors) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Pure response function for `check_exercise`. Distinguishes: +- **Protocol error** (unknown exercise id) → `{ "protocol_error": "..." }` so the wrapper can map it to `McpError`. +- **Validation error** (circuit invalid) → `{ "passed": false, "diagnostics": [...] }` per spec §2.2 — never escalates to JSON-RPC error. +- **Normal pass/fail** → `{ "passed": ..., "exercise_id", "feedback", "counts", ... }`. + +The caller (Task 10) is responsible for persisting `mark_solved` on `passed: true`. + +## Prerequisites + +- Task 5 merged (`ExerciseChecker`). +- Task 6 merged (`tutor_tools.rs` exists). + +## Files + +- Modify: `src/tools/tutor_tools.rs` + +## Steps + +- [ ] **Step 1: Append failing tests to the existing `#[cfg(test)]` block** + +```rust + use crate::executor::{Backend, LocalSimulator}; + + fn backend() -> LocalSimulator { LocalSimulator::new() } + + #[test] + fn check_exercise_x_gate_passes_1_1_a() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + let resp = check_exercise_response( + &loader(), &backend() as &dyn Backend, &UserProgress::default(), + "1-1-a", circuit, + ); + assert_eq!(resp["passed"], true); + assert_eq!(resp["exercise_id"], "1-1-a"); + } + + #[test] + fn check_exercise_identity_fails_1_1_a() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + let resp = check_exercise_response( + &loader(), &backend() as &dyn Backend, &UserProgress::default(), + "1-1-a", circuit, + ); + assert_eq!(resp["passed"], false); + assert!(resp["hint"].as_str().is_some()); + } + + #[test] + fn check_exercise_invalid_circuit_returns_diagnostics_not_protocol_error() { + let resp = check_exercise_response( + &loader(), &backend() as &dyn Backend, &UserProgress::default(), + "1-1-a", "not valid qasm", + ); + assert_eq!(resp["passed"], false); + assert!(resp["diagnostics"].as_array().is_some()); + assert!(resp.get("protocol_error").is_none()); + } + + #[test] + fn check_exercise_unknown_id_marks_protocol_error() { + let resp = check_exercise_response( + &loader(), &backend() as &dyn Backend, &UserProgress::default(), + "99-99-z", "x q[0];", + ); + assert!(resp["protocol_error"].as_str().is_some()); + } +``` + +- [ ] **Step 2: Implementation (add at the top-level of `tutor_tools.rs`)** + +```rust +use crate::executor::Backend; +use crate::tutor::ExerciseChecker; + +pub fn check_exercise_response( + loader: &CurriculumLoader, + backend: &dyn Backend, + progress: &UserProgress, + exercise_id: &str, + circuit: &str, +) -> Value { + let exercise = match loader.find_exercise(exercise_id) { + None => return json!({ + "protocol_error": format!("exercise '{}' not found in curriculum", exercise_id), + }), + Some(e) => e, + }; + + let check = ExerciseChecker::check_circuit(backend, circuit, &exercise.criteria); + + if let Some(err) = &check.error { + let diagnostics: Vec = check + .diagnostics + .iter() + .map(|d| json!({ + "line": d.line, + "column": d.column, + "message": d.message, + })) + .collect(); + return json!({ + "passed": false, + "exercise_id": exercise_id, + "feedback": exercise.feedback_fail, + "hint": exercise.hint, + "validation_error": err, + "diagnostics": diagnostics, + "counts": check.counts, + }); + } + + let already_solved = progress.has_solved(exercise_id); + if check.passed { + json!({ + "passed": true, + "exercise_id": exercise_id, + "feedback": exercise.feedback_pass, + "counts": check.counts, + "newly_solved": !already_solved, + }) + } else { + json!({ + "passed": false, + "exercise_id": exercise_id, + "feedback": exercise.feedback_fail, + "hint": exercise.hint, + "counts": check.counts, + }) + } +} +``` + +- [ ] **Step 3: Run tests** + +```bash +cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 8 passed`. + +- [ ] **Step 4: Commit** + +```bash +git add src/tools/tutor_tools.rs +git commit -m "feat: check_exercise response with backend injection and structured validation errors" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/08-explain-result.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/08-explain-result.md new file mode 100644 index 0000000..49ae162 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/08-explain-result.md @@ -0,0 +1,194 @@ +# Task 8 — `explain_result` response (AST-based gate breakdown) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Build a structured pedagogical breakdown of an executed circuit: gate-by-gate description, key concept (entanglement / superposition / interference / rotation / measurement), dominant and missing outcomes, optional statevector summary. Spec §2.3 mandates AST-based analysis — we use `CircuitAnalyzer::list_gates`, no string-matching. + +## Prerequisites + +- Task 4 merged (`CircuitAnalyzer`). +- Task 7 merged (`tutor_tools.rs` populated). + +## Files + +- Modify: `src/tools/tutor_tools.rs` + +## Steps + +- [ ] **Step 1: Append failing tests** + +```rust + #[test] + fn explain_result_bell_circuit_lists_h_then_cx_with_descriptions() { + let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let counts = json!({"00": 512, "11": 512}); + let resp = explain_result_response(&loader(), bell, &counts, None); + let breakdown = resp["gate_breakdown"].as_array().unwrap(); + assert_eq!(breakdown.len(), 2); + assert_eq!(breakdown[0]["name"].as_str().unwrap(), "h"); + assert_eq!(breakdown[1]["name"].as_str().unwrap(), "cx"); + assert!(breakdown[0]["description"].as_str().unwrap().contains("Hadamard")); + assert_eq!(resp["key_concept"].as_str().unwrap(), "entanglement"); + assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); + } + + #[test] + fn explain_result_dominant_outcomes_match_counts() { + let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let counts = json!({"00": 480, "11": 544}); + let resp = explain_result_response(&loader(), bell, &counts, None); + let dominant: Vec<&str> = resp["dominant_outcomes"] + .as_array().unwrap().iter().map(|v| v.as_str().unwrap()).collect(); + assert!(dominant.contains(&"00")); + assert!(dominant.contains(&"11")); + } + + #[test] + fn explain_result_for_invalid_circuit_returns_error_payload() { + let resp = explain_result_response(&loader(), "not valid qasm", &json!({}), None); + assert!(resp.get("error").is_some()); + } +``` + +- [ ] **Step 2: Implementation (top of `tutor_tools.rs`)** + +```rust +use crate::circuit_analyzer::{CircuitAnalyzer, GateCallInfo}; +use crate::types::CircuitSource; + +pub fn explain_result_response( + loader: &CurriculumLoader, + circuit: &str, + counts: &Value, + statevector: Option<&Value>, +) -> Value { + let curriculum = match loader.curriculum() { + Ok(c) => c, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let gates = match CircuitAnalyzer::list_gates(&CircuitSource(circuit.to_string())) { + Ok(g) => g, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let gate_breakdown: Vec = gates + .iter() + .map(|g| gate_entry(g, &curriculum.gate_descriptions)) + .collect(); + + let key_concept = classify_key_concept(&gates); + let num_qubits = max_qubit_index(&gates).map(|m| m + 1).unwrap_or(0); + let (dominant, missing) = outcomes_summary(counts, num_qubits); + let statevector_summary = statevector_summary(statevector); + + json!({ + "gate_breakdown": gate_breakdown, + "num_qubits": num_qubits, + "key_concept": key_concept, + "dominant_outcomes": dominant, + "missing_outcomes": missing, + "statevector_summary": statevector_summary, + }) +} + +fn gate_entry( + g: &GateCallInfo, + descs: &std::collections::HashMap, +) -> Value { + let mut entry = json!({ + "name": g.name, + "qubits": g.qubits, + "params": g.params, + }); + if let Some(d) = descs.get(&g.name) { + entry["description"] = json!(d.short); + if let Some(eff) = &d.effect_on_zero { + entry["effect_on_zero"] = json!(eff); + } + } + entry +} + +fn classify_key_concept(gates: &[GateCallInfo]) -> &'static str { + let names: std::collections::HashSet<&str> = gates.iter().map(|g| g.name.as_str()).collect(); + if names.contains("cx") || names.contains("cz") || names.contains("ccx") || names.contains("swap") { + "entanglement" + } else if names.contains("h") && (names.contains("rz") || names.contains("z") || names.contains("s") || names.contains("t")) { + "interference" + } else if names.contains("h") { + "superposition" + } else if names.contains("rx") || names.contains("ry") || names.contains("rz") { + "rotation" + } else { + "measurement" + } +} + +fn max_qubit_index(gates: &[GateCallInfo]) -> Option { + gates.iter().flat_map(|g| g.qubits.iter().copied()).max() +} + +fn outcomes_summary(counts: &Value, num_qubits: usize) -> (Vec, Vec) { + let map = match counts.as_object() { + Some(m) => m, + None => return (vec![], vec![]), + }; + let total: u64 = map.values().filter_map(|v| v.as_u64()).sum(); + if total == 0 { return (vec![], vec![]); } + let threshold = (total as f64 * 0.05) as u64; + + let mut dominant: Vec = map + .iter() + .filter(|(_, v)| v.as_u64().unwrap_or(0) > threshold) + .map(|(k, _)| k.clone()) + .collect(); + dominant.sort(); + + let missing: Vec = if num_qubits == 0 || num_qubits > 4 { + vec![] + } else { + let n = num_qubits; + let mut out: Vec = (0u64..(1u64 << n)) + .map(|i| format!("{i:0>n$b}")) + .filter(|bs| map.get(bs).and_then(|v| v.as_u64()).unwrap_or(0) == 0) + .collect(); + out.sort(); + out + }; + + (dominant, missing) +} + +fn statevector_summary(sv: Option<&Value>) -> Value { + let sv = match sv.and_then(|v| v.as_array()) { + Some(a) => a, + None => return Value::Null, + }; + let mut non_zero = Vec::new(); + let mut zero = Vec::new(); + for (i, amp) in sv.iter().enumerate() { + let r = amp.as_array().and_then(|a| a.first()).and_then(|v| v.as_f64()).unwrap_or(0.0); + let im = amp.as_array().and_then(|a| a.get(1)).and_then(|v| v.as_f64()).unwrap_or(0.0); + if r * r + im * im > 1e-10 { non_zero.push(i); } else { zero.push(i); } + } + json!({"non_zero_amplitudes": non_zero, "zero_amplitudes": zero}) +} +``` + +- [ ] **Step 3: Run tests** + +```bash +cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 11 passed`. + +- [ ] **Step 4: Commit** + +```bash +git add src/tools/tutor_tools.rs +git commit -m "feat: explain_result with AST-based gate breakdown (no string matching)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/09-get-progress.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/09-get-progress.md new file mode 100644 index 0000000..efee5a3 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/09-get-progress.md @@ -0,0 +1,138 @@ +# Task 9 — `get_progress` response (current_lesson + unlock rule) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Build a structured progress payload: per-module status (`locked | unlocked | in_progress | completed`), current module + current lesson, total exercises solved, percent complete. Spec §4 unlock rule: module `n+1` becomes `unlocked` once at least ⌈2/3⌉ of module `n`'s exercises are solved. + +## Prerequisites + +- Task 8 merged (`tutor_tools.rs` populated). + +## Files + +- Modify: `src/tools/tutor_tools.rs` + +## Steps + +- [ ] **Step 1: Append failing tests** + +```rust + #[test] + fn get_progress_with_no_progress_returns_module_1_unlocked_others_locked() { + let resp = get_progress_response(&loader(), &UserProgress::default()); + assert_eq!(resp["current_module"].as_u64().unwrap(), 1); + let modules = resp["modules"].as_array().unwrap(); + assert_eq!(modules[0]["status"].as_str().unwrap(), "unlocked"); + if modules.len() > 1 { + assert_eq!(modules[1]["status"].as_str().unwrap(), "locked"); + } + assert_eq!(resp["total_exercises_solved"].as_u64().unwrap(), 0); + } + + #[test] + fn get_progress_completed_module_marked_completed() { + let mut progress = UserProgress::default(); + progress.mark_solved("1-1-a"); + progress.mark_solved("1-1-b"); + let resp = get_progress_response(&loader(), &progress); + assert_eq!(resp["modules"].as_array().unwrap()[0]["status"].as_str().unwrap(), "completed"); + } + + #[test] + fn get_progress_returns_current_lesson_for_active_module() { + let resp = get_progress_response(&loader(), &UserProgress::default()); + assert_eq!(resp["current_lesson"].as_u64().unwrap(), 1); + } +``` + +- [ ] **Step 2: Implementation** + +```rust +pub fn get_progress_response(loader: &CurriculumLoader, progress: &UserProgress) -> Value { + let curriculum = match loader.curriculum() { + Ok(c) => c, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let total_exercises: usize = loader.all_exercises().len(); + let solved_count = progress.solved_exercises.len().min(total_exercises); + + let mut prev_unlocks_next = true; + let modules: Vec = curriculum + .modules + .iter() + .map(|m| { + let total_in_module: usize = m.lessons.iter().map(|l| l.exercises.len()).sum(); + let solved_in_module = m + .lessons + .iter() + .flat_map(|l| l.exercises.iter()) + .filter(|e| progress.has_solved(&e.id)) + .count(); + let status = if !prev_unlocks_next { + "locked" + } else if total_in_module > 0 && solved_in_module == total_in_module { + "completed" + } else if solved_in_module > 0 { + "in_progress" + } else { + "unlocked" + }; + // Spec §4: next module unlocks when ⌈2/3⌉ of current is solved. + let unlock_threshold = (total_in_module * 2 + 2) / 3; + prev_unlocks_next = solved_in_module >= unlock_threshold && total_in_module > 0; + json!({ + "id": m.id, + "title": m.title, + "status": status, + "exercises_solved": solved_in_module, + "exercises_total": total_in_module, + }) + }) + .collect(); + + let (current_module, current_lesson) = current_position(curriculum, progress); + let percent = if total_exercises == 0 { 0 } else { (solved_count * 100) / total_exercises }; + + json!({ + "current_module": current_module, + "current_lesson": current_lesson, + "modules": modules, + "total_exercises_solved": solved_count, + "total_exercises": total_exercises, + "percent_complete": percent, + }) +} + +fn current_position(curriculum: &crate::tutor::Curriculum, progress: &UserProgress) -> (u32, u32) { + for module in &curriculum.modules { + for lesson in &module.lessons { + if lesson.exercises.iter().any(|e| !progress.has_solved(&e.id)) { + return (module.id, lesson.id); + } + } + } + let last = curriculum.modules.last(); + ( + last.map(|m| m.id).unwrap_or(1), + last.and_then(|m| m.lessons.last()).map(|l| l.id).unwrap_or(1), + ) +} +``` + +- [ ] **Step 3: Run tests** + +```bash +cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: `test result: ok. 14 passed`. + +- [ ] **Step 4: Commit** + +```bash +git add src/tools/tutor_tools.rs +git commit -m "feat: get_progress with current_lesson and module unlock rule (spec §4)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/10-wire-tools.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/10-wire-tools.md new file mode 100644 index 0000000..ddc4804 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/10-wire-tools.md @@ -0,0 +1,157 @@ +# Task 10 — Wire 4 tutor tools into `QuantumBridgeServer` + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Register `get_lesson`, `check_exercise`, `explain_result`, `get_progress` as MCP tools. The wrapper: +1. Loads `UserProgress` via `ProgressStore` (sandboxed by env var). +2. Routes through `self.backend` (set up in Task 0). +3. Maps `protocol_error` to `McpError::invalid_params`; all other errors stay in the structured payload. +4. Persists `mark_solved` on `passed: true`. + +## Prerequisites + +- Task 9 merged (all 4 response functions exist in `tutor_tools.rs`). + +## Files + +- Modify: `src/tools/mod.rs` + +## Steps + +- [ ] **Step 1: Add imports + parameter structs at the top of `src/tools/mod.rs`** + +```rust +use crate::progress::ProgressStore; +use crate::tutor::CurriculumLoader; +use crate::tools::tutor_tools; + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetLessonParams { + /// Module number (1–7). + pub module_id: u32, + /// Lesson number within the module (optional, defaults to first lesson with pending exercises). + pub lesson_id: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct CheckExerciseParams { + /// Exercise identifier, e.g. "1-1-a". + pub exercise_id: String, + /// OpenQASM 3.0 source of the circuit to verify. + pub circuit: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ExplainResultParams { + /// OpenQASM 3.0 source of the circuit that was executed. + pub circuit: String, + /// Measurement counts from run_circuit (bitstring → count). + pub counts: serde_json::Value, + /// Optional statevector from run_circuit with return_statevector=true. + pub statevector: Option, +} +``` + +- [ ] **Step 2: Add helpers on `QuantumBridgeServer` (outside the `#[tool_router]` block)** + +```rust +impl QuantumBridgeServer { + fn loader(&self) -> CurriculumLoader { CurriculumLoader::default() } + + fn store(&self) -> Result { + let path = ProgressStore::default_path() + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(ProgressStore::new(path)) + } +} +``` + +- [ ] **Step 3: Add the 4 tool methods inside `#[tool_router(server_handler)] impl QuantumBridgeServer`** + +```rust + #[tool(description = "Get a quantum-computing lesson with concept, example circuit, and the next pending exercise.")] + async fn get_lesson( + &self, + Parameters(GetLessonParams { module_id, lesson_id }): Parameters, + ) -> Result { + let store = self.store()?; + let progress = store.load(); + let json = tutor_tools::get_lesson_response(&self.loader(), &progress, module_id, lesson_id); + if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Verify a submitted OpenQASM 3.0 circuit against a curriculum exercise. Returns pass/fail with feedback.")] + async fn check_exercise( + &self, + Parameters(CheckExerciseParams { exercise_id, circuit }): Parameters, + ) -> Result { + let store = self.store()?; + let progress = store.load(); + let mut json = tutor_tools::check_exercise_response( + &self.loader(), + self.backend.as_ref(), + &progress, + &exercise_id, + &circuit, + ); + if let Some(err) = json.get("protocol_error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + if json["passed"].as_bool().unwrap_or(false) { + store + .mark_solved(&exercise_id) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + json["progress_updated"] = serde_json::json!(true); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Analyze a circuit and its measurement results to produce structured pedagogical data (gate breakdown, key concept, outcome stats).")] + async fn explain_result( + &self, + Parameters(ExplainResultParams { circuit, counts, statevector }): Parameters, + ) -> Result { + let json = tutor_tools::explain_result_response(&self.loader(), &circuit, &counts, statevector.as_ref()); + if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Get the learner's progress through the curriculum (modules status, current lesson, percent complete).")] + async fn get_progress(&self) -> Result { + let store = self.store()?; + let progress = store.load(); + let json = tutor_tools::get_progress_response(&self.loader(), &progress); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } +``` + +- [ ] **Step 4: Build and run all unit tests** + +```bash +cargo build && cargo test --lib 2>&1 | grep -E "test result|FAILED" +``` + +Expected: every existing suite green; the 4 new methods compile and the existing tests are untouched. + +- [ ] **Step 5: Verify the MCP `tools/list` exposes 7 tools** + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}} +{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | timeout 3 cargo run 2>/dev/null | tail -1 | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d['result']['tools']), 'tools')" +``` + +Expected: `7 tools`. + +- [ ] **Step 6: Commit** + +```bash +git add src/tools/mod.rs +git commit -m "feat: register 4 tutor tools on QuantumBridgeServer" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/11-curriculum-module-2.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/11-curriculum-module-2.md new file mode 100644 index 0000000..640e134 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/11-curriculum-module-2.md @@ -0,0 +1,92 @@ +# Task 11 — Curriculum module 2 (Superposition) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 2 (3 exercises on the Hadamard gate and the |+⟩/|−⟩ states) to `curriculum/curriculum.json`. + +## Prerequisites + +- Task 10 merged (all infrastructure in place). + +## Files + +- Modify: `curriculum/curriculum.json` + +## Steps + +- [ ] **Step 1: Insert the module after module 1, inside the `"modules"` array** + +```json +, +{ + "id": 2, + "title": "Superposition", + "lessons": [ + { + "id": 1, + "title": "La porte Hadamard et l'état |+⟩", + "concept": "La porte H (Hadamard) transforme |0⟩ en une superposition égale de |0⟩ et |1⟩. Quand tu mesures, tu obtiens 0 ou 1 avec 50% de probabilité chacun — pas parce que c'est aléatoire au sens classique, mais parce que la mesure fait 'choisir' le qubit.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;", + "what_to_observe": "Lance run_circuit avec 1000 shots. Tu devrais voir ~500 '0' et ~500 '1'. Relance plusieurs fois : la distribution change légèrement à chaque fois.", + "exercises": [ + { + "id": "2-1-a", + "prompt": "Écris un circuit qui met un qubit en superposition parfaite (50/50) et le mesure.", + "hint": "La porte H est la seule dont tu as besoin.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.4}, + {"bitstring": "1", "min_ratio": 0.4} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Parfait ! La porte H crée bien une superposition équilibrée.", + "feedback_fail": "Tu dois voir à la fois des '0' et des '1' en quantités comparables." + }, + { + "id": "2-1-b", + "prompt": "Applique H deux fois de suite sur le même qubit. Quel est le résultat à la mesure ?", + "hint": "H est son propre inverse : H·H = Identité.", + "criteria": { + "required_outcomes": [{"bitstring": "0", "min_ratio": 0.99}], + "forbidden_outcomes": ["1"] + }, + "feedback_pass": "Excellent ! H·H = Identité : appliquer Hadamard deux fois ramène au point de départ.", + "feedback_fail": "Deux portes H d'affilée doivent ramener à |0⟩ avec certitude." + }, + { + "id": "2-1-c", + "prompt": "Pars de |1⟩ (utilise X d'abord), puis applique H. Que vois-tu ?", + "hint": "H|1⟩ = (|0⟩-|1⟩)/√2 = |−⟩. La mesure donne toujours 50/50.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.4}, + {"bitstring": "1", "min_ratio": 0.4} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Bien vu ! H|1⟩ donne aussi 50/50 — la différence (|−⟩ vs |+⟩) est dans la phase.", + "feedback_fail": "Tu devrais voir ~50/50. Utilise X puis H." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Verify JSON parses and module count updates** + +```bash +cargo test tutor::tests::curriculum_json_deserializes_with_module_1 2>&1 | grep "test result" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 3: Commit** + +```bash +git add curriculum/curriculum.json +git commit -m "feat: add curriculum module 2 (superposition)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/12-curriculum-module-3.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/12-curriculum-module-3.md new file mode 100644 index 0000000..48929f6 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/12-curriculum-module-3.md @@ -0,0 +1,89 @@ +# Task 12 — Curriculum module 3 (Interférence et phase) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 3 (3 exercises on phase, H·Z·H, Rz(π), H·S·H) to `curriculum/curriculum.json`. + +## Prerequisites + +- Task 11 merged. + +## Files + +- Modify: `curriculum/curriculum.json` + +## Steps + +- [ ] **Step 1: Insert after module 2** + +```json +, +{ + "id": 3, + "title": "Interférence et phase", + "lessons": [ + { + "id": 1, + "title": "La phase quantique et l'interférence", + "concept": "La phase d'un état quantique est invisible à la mesure directe, mais deux états en superposition peuvent interférer : leurs amplitudes s'additionnent ou s'annulent. C'est le moteur de l'avantage quantique.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nz q[0];\nh q[0];\nc = measure q;", + "what_to_observe": "Ce circuit (H·Z·H) donne toujours '1'. Compare avec un simple X.", + "exercises": [ + { + "id": "3-1-a", + "prompt": "Vérifie que H·Z·H = X. Écris le circuit H·Z·H et mesure.", + "hint": "Applique h, puis z, puis h, puis mesure.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Parfait ! H·Z·H = X — l'interférence transforme un flip de phase en flip de bit.", + "feedback_fail": "Assure-toi d'appliquer h, z, h dans cet ordre." + }, + { + "id": "3-1-b", + "prompt": "Applique H, puis Rz(π), puis H. Que vois-tu ?", + "hint": "rz(pi) q[0]; — la constante pi est disponible.", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Exact ! Rz(π) est équivalent à Z (à une phase globale près).", + "feedback_fail": "Vérifie ta syntaxe pour rz(pi) q[0];" + }, + { + "id": "3-1-c", + "prompt": "Crée un circuit H·S·H et mesure. Que devient la distribution ?", + "hint": "S introduit une phase de 90° qui modifie l'interférence.", + "criteria": { + "required_outcomes": [ + {"bitstring": "0", "min_ratio": 0.2}, + {"bitstring": "1", "min_ratio": 0.2} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Bien joué ! H·S·H produit une superposition non triviale.", + "feedback_fail": "Tu dois voir les deux outcomes. Applique h, s, h dans cet ordre." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Verify** + +```bash +cargo test tutor::tests::curriculum_json_deserializes_with_module_1 2>&1 | grep "test result" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 3: Commit** + +```bash +git add curriculum/curriculum.json +git commit -m "feat: add curriculum module 3 (interférence et phase)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/13-curriculum-module-4.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/13-curriculum-module-4.md new file mode 100644 index 0000000..df5326d --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/13-curriculum-module-4.md @@ -0,0 +1,89 @@ +# Task 13 — Curriculum module 4 (2 qubits et intrication) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 4 (3 exercises on Bell state and CNOT semantics) to `curriculum/curriculum.json`. + +## Prerequisites + +- Task 12 merged. + +## Files + +- Modify: `curriculum/curriculum.json` + +## Steps + +- [ ] **Step 1: Insert after module 3** + +```json +, +{ + "id": 4, + "title": "2 qubits et intrication", + "lessons": [ + { + "id": 1, + "title": "Le circuit de Bell et l'intrication", + "concept": "Deux qubits peuvent être 'intriqués' : mesurer l'un détermine instantanément le résultat de l'autre. L'état de Bell est le prototype de l'intrication.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;", + "what_to_observe": "Lance avec 1000 shots. Tu dois voir uniquement '00' et '11' en quantités égales — jamais '01' ou '10'.", + "exercises": [ + { + "id": "4-1-a", + "prompt": "Crée un état de Bell entre 2 qubits : la mesure doit donner uniquement '00' ou '11'.", + "hint": "H sur q[0], puis CX q[0]→q[1].", + "criteria": { + "required_outcomes": [ + {"bitstring": "00", "min_ratio": 0.4}, + {"bitstring": "11", "min_ratio": 0.4} + ], + "forbidden_outcomes": ["01", "10"] + }, + "feedback_pass": "Excellent ! Tu as créé l'état de Bell.", + "feedback_fail": "Tu dois voir uniquement '00' et '11'. Utilise H q[0] puis CX q[0],q[1]." + }, + { + "id": "4-1-b", + "prompt": "CNOT sans superposition : pars de |00⟩ et applique CX. Que vois-tu ?", + "hint": "Sans H, q[0] reste |0⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "00", "min_ratio": 0.99}], + "forbidden_outcomes": ["01", "10", "11"] + }, + "feedback_pass": "Correct ! CNOT avec contrôle=|0⟩ ne change rien.", + "feedback_fail": "Tu dois obtenir '00' systématiquement." + }, + { + "id": "4-1-c", + "prompt": "Prépare q[0]=|1⟩ avec X, puis applique CX. Que voit-on sur q[1] ?", + "hint": "CNOT flip la cible quand le contrôle est |1⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "11", "min_ratio": 0.99}], + "forbidden_outcomes": ["00", "01", "10"] + }, + "feedback_pass": "Parfait ! |10⟩ devient |11⟩ après CX.", + "feedback_fail": "Avec q[0]=|1⟩, le CNOT doit flipper q[1]." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Verify** + +```bash +cargo test tutor::tests::curriculum_json_deserializes_with_module_1 2>&1 | grep "test result" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 3: Commit** + +```bash +git add curriculum/curriculum.json +git commit -m "feat: add curriculum module 4 (2 qubits et intrication)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/14-curriculum-module-5.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/14-curriculum-module-5.md new file mode 100644 index 0000000..edc3837 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/14-curriculum-module-5.md @@ -0,0 +1,94 @@ +# Task 14 — Curriculum module 5 (Circuits multi-qubits) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 5 (3 exercises on GHZ, Toffoli, uniform superposition) to `curriculum/curriculum.json`. + +## Prerequisites + +- Task 13 merged. + +## Files + +- Modify: `curriculum/curriculum.json` + +## Steps + +- [ ] **Step 1: Insert after module 4** + +```json +, +{ + "id": 5, + "title": "Circuits multi-qubits", + "lessons": [ + { + "id": 1, + "title": "GHZ, Toffoli et superposition uniforme", + "concept": "On peut intriqué N qubits en même temps. L'état GHZ généralise Bell à 3+ qubits. La porte Toffoli (CCX) est un AND quantique réversible.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nc = measure q;", + "what_to_observe": "Avec 3 qubits GHZ, tu dois voir uniquement '000' et '111'.", + "exercises": [ + { + "id": "5-1-a", + "prompt": "Crée l'état GHZ à 3 qubits.", + "hint": "H sur q[0], puis CX q[0]→q[1], puis CX q[0]→q[2].", + "criteria": { + "required_outcomes": [ + {"bitstring": "000", "min_ratio": 0.4}, + {"bitstring": "111", "min_ratio": 0.4} + ], + "forbidden_outcomes": ["001", "010", "011", "100", "101", "110"] + }, + "feedback_pass": "Bravo ! L'état GHZ à 3 qubits est créé.", + "feedback_fail": "L'état GHZ doit produire uniquement '000' et '111'." + }, + { + "id": "5-1-b", + "prompt": "Toffoli : prépare q[0]=|1⟩ et q[1]=|1⟩, puis applique CCX.", + "hint": "CCX flip q[2] seulement si q[0] ET q[1] sont à |1⟩.", + "criteria": { + "required_outcomes": [{"bitstring": "111", "min_ratio": 0.99}], + "forbidden_outcomes": ["000", "001", "010", "011", "100", "101", "110"] + }, + "feedback_pass": "Toffoli est un AND quantique : il flip q[2] seulement si q[0]=q[1]=|1⟩.", + "feedback_fail": "Utilise x q[0]; x q[1]; ccx q[0],q[1],q[2];" + }, + { + "id": "5-1-c", + "prompt": "Crée une superposition uniforme de tous les états à 2 qubits (~25% chacun).", + "hint": "Applique H sur chaque qubit indépendamment.", + "criteria": { + "required_outcomes": [ + {"bitstring": "00", "min_ratio": 0.15}, + {"bitstring": "01", "min_ratio": 0.15}, + {"bitstring": "10", "min_ratio": 0.15}, + {"bitstring": "11", "min_ratio": 0.15} + ], + "forbidden_outcomes": [] + }, + "feedback_pass": "Avec H sur chaque qubit tu explores 4 états simultanément.", + "feedback_fail": "Tu dois voir les 4 états chacun avec ~25%." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Verify** + +```bash +cargo test tutor::tests::curriculum_json_deserializes_with_module_1 2>&1 | grep "test result" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 3: Commit** + +```bash +git add curriculum/curriculum.json +git commit -m "feat: add curriculum module 5 (multi-qubits)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/15-curriculum-module-6.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/15-curriculum-module-6.md new file mode 100644 index 0000000..6fe5d81 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/15-curriculum-module-6.md @@ -0,0 +1,77 @@ +# Task 15 — Curriculum module 6 (Premiers algorithmes — phase kickback) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 6 (2 exercises: Bernstein-Vazirani for s='1' and a phase-kickback demonstration) to `curriculum/curriculum.json`. + +> **Note vs. rev 1 of the master plan:** the original teleportation exercise required classical-controlled gates (`if (c[0]) ...`), which the v1 executor walks past silently — only `Stmt::GateCall` is handled in `executor::extract_gate_ops`. The phase-kickback exercise covers the same pedagogical territory and is executable on the v1 backend. + +## Prerequisites + +- Task 14 merged. + +## Files + +- Modify: `curriculum/curriculum.json` + +## Steps + +- [ ] **Step 1: Insert after module 5** + +```json +, +{ + "id": 6, + "title": "Premiers algorithmes quantiques", + "lessons": [ + { + "id": 1, + "title": "Bernstein-Vazirani et phase kickback", + "concept": "L'algorithme de Bernstein-Vazirani retrouve une chaîne secrète en un seul appel à l'oracle. Le phase kickback est le mécanisme central : faire 'remonter' une phase d'un qubit cible vers un qubit de contrôle en superposition.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[1] c;\nx q[1];\nh q[0];\nh q[1];\ncx q[0], q[1];\nh q[0];\nc[0] = measure q[0];", + "what_to_observe": "Bernstein-Vazirani pour s='1' : q[0] mesure toujours '1'.", + "exercises": [ + { + "id": "6-1-a", + "prompt": "Bernstein-Vazirani pour s='1' : prépare l'ancilla en |−⟩ (X+H), mets q[0] en superposition (H), applique CX q[0]→q[1] (oracle pour s=1), puis H sur q[0] et mesure q[0].", + "hint": "X q[1]; H q[1]; H q[0]; CX q[0],q[1]; H q[0]; puis mesure q[0].", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Excellent ! BV retrouve s='1' en un seul appel à l'oracle.", + "feedback_fail": "Tu dois obtenir '1' avec certitude. Vérifie l'ordre : X q[1]; H q[1]; H q[0]; cx q[0],q[1]; H q[0]; mesure q[0]." + }, + { + "id": "6-1-b", + "prompt": "Phase kickback : prépare q[1] en |1⟩ (X), mets q[0] en superposition (H), applique CZ q[0],q[1]. Puis H q[0] et mesure q[0].", + "hint": "Le CZ avec q[1]=|1⟩ ajoute une phase -1 quand q[0]=|1⟩ — c'est un Z 'remonté' sur q[0].", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.99}], + "forbidden_outcomes": ["0"] + }, + "feedback_pass": "Parfait ! La phase de q[1] s'est 'kickback' sur q[0]. C'est la clé des algorithmes quantiques.", + "feedback_fail": "Tu dois obtenir '1' sur q[0]. Vérifie : x q[1]; h q[0]; cz q[0],q[1]; h q[0]; mesure q[0]." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Verify** + +```bash +cargo test tutor::tests::curriculum_json_deserializes_with_module_1 2>&1 | grep "test result" +``` + +Expected: `test result: ok. 1 passed`. + +- [ ] **Step 3: Commit** + +```bash +git add curriculum/curriculum.json +git commit -m "feat: add curriculum module 6 (premiers algorithmes - phase kickback)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/16-curriculum-module-7.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/16-curriculum-module-7.md new file mode 100644 index 0000000..2dea367 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/16-curriculum-module-7.md @@ -0,0 +1,88 @@ +# Task 16 — Curriculum module 7 (Grover) + total-count assertion + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +Append module 7 (2 Grover exercises, 1 iteration on 2 qubits with markers `'11'` and `'00'`) and add a final assertion that the curriculum contains exactly 18 exercises. + +## Prerequisites + +- Task 15 merged. + +## Files + +- Modify: `curriculum/curriculum.json` +- Modify: `src/tutor.rs` (add the count assertion test) + +## Steps + +- [ ] **Step 1: Insert after module 6** + +```json +, +{ + "id": 7, + "title": "Algorithme de Grover", + "lessons": [ + { + "id": 1, + "title": "Recherche quantique : amplification d'amplitude", + "concept": "L'algorithme de Grover retrouve un élément marqué dans N éléments en O(√N) appels — quadratiquement plus rapide que la recherche classique. Il utilise un oracle de phase + un opérateur de diffusion.", + "example_circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[0];\nh q[1];\nx q[0];\nx q[1];\ncz q[0], q[1];\nx q[0];\nx q[1];\nh q[0];\nh q[1];\nc = measure q;", + "what_to_observe": "Grover sur 2 qubits avec marqueur '11' produit '11' avec ~100% après 1 itération.", + "exercises": [ + { + "id": "7-1-a", + "prompt": "Implémente Grover sur 2 qubits pour trouver l'état marqué '11'. 1 itération suffit.", + "hint": "H H | CZ (oracle) | H H X X CZ X X H H (diffuseur).", + "criteria": { + "required_outcomes": [{"bitstring": "11", "min_ratio": 0.85}], + "forbidden_outcomes": [] + }, + "feedback_pass": "Magnifique ! Grover trouve '11' en une itération avec haute probabilité.", + "feedback_fail": "L'état '11' doit dominer (>85%). Vérifie oracle et diffuseur." + }, + { + "id": "7-1-b", + "prompt": "Grover pour l'état '00'. Oracle = X X CZ X X.", + "hint": "X q[0]; X q[1]; CZ q[0],q[1]; X q[0]; X q[1]; puis diffuseur.", + "criteria": { + "required_outcomes": [{"bitstring": "00", "min_ratio": 0.85}], + "forbidden_outcomes": [] + }, + "feedback_pass": "Excellent ! En changeant l'oracle, Grover trouve n'importe quel état marqué.", + "feedback_fail": "L'état '00' doit dominer (>85%). Ton oracle doit marquer '00'." + } + ] + } + ] +} +``` + +- [ ] **Step 2: Append a total-count assertion in `src/tutor.rs`** + +Inside the existing `#[cfg(test)] mod tests`: + +```rust + #[test] + fn curriculum_has_18_exercises_total() { + let loader = CurriculumLoader::default(); + assert_eq!(loader.all_exercises().len(), 18); + } +``` + +- [ ] **Step 3: Verify** + +```bash +cargo test tutor::tests 2>&1 | grep -E "test result|FAILED" +``` + +Expected: every tutor test passes; the new assertion confirms 18 exercises across 7 modules. + +- [ ] **Step 4: Commit** + +```bash +git add curriculum/curriculum.json src/tutor.rs +git commit -m "feat: add curriculum module 7 (Grover) and 18-exercise total assertion" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/17-integration-tests.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/17-integration-tests.md new file mode 100644 index 0000000..7f13d1b --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/17-integration-tests.md @@ -0,0 +1,222 @@ +# Task 17 — Integration tests + golden files (sandboxed) + +> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). + +## Goal + +End-to-end MCP roundtrip for the 4 tutor tools, using `QB_PROGRESS_PATH` to point each test process at a private temp file. Add golden files in `tests/reference/tutor/` (spec §6) that exercise pass/fail per module. + +## Prerequisites + +- Task 16 merged (curriculum complete). + +## Files + +- Create: `tests/tutor_integration.rs` +- Create: `tests/reference/tutor/exercises_pass.jsonl` +- Create: `tests/reference/tutor/exercises_fail.jsonl` + +## Steps + +- [ ] **Step 1: Create `tests/tutor_integration.rs`** + +```rust +//! MCP JSON-RPC roundtrip tests for the tutor tools. Sandboxed via QB_PROGRESS_PATH. + +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; + +use tempfile::TempDir; + +struct McpProcess { + child: Child, + _tmp: TempDir, +} + +impl McpProcess { + fn spawn() -> Self { + let tmp = TempDir::new().expect("tempdir"); + let progress_path = tmp.path().join("progress.json"); + let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) + .env("QB_PROGRESS_PATH", &progress_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn"); + Self { child, _tmp: tmp } + } + + fn send(&mut self, msg: &str) { + let stdin = self.child.stdin.as_mut().unwrap(); + writeln!(stdin, "{msg}").unwrap(); + stdin.flush().unwrap(); + } + + fn recv(&mut self) -> serde_json::Value { + let stdout = self.child.stdout.as_mut().unwrap(); + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + serde_json::from_str(line.trim()).expect("response is JSON") + } +} + +impl Drop for McpProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +fn initialize(p: &mut McpProcess) { + p.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#); + p.recv(); +} + +fn extract(resp: &serde_json::Value) -> serde_json::Value { + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + serde_json::from_str(text).unwrap() +} + +#[test] +fn tools_list_exposes_seven_tools() { + let mut p = McpProcess::spawn(); + initialize(&mut p); + p.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); + let resp = p.recv(); + let tools = resp["result"]["tools"].as_array().unwrap(); + assert_eq!(tools.len(), 7); + let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); + for required in ["get_lesson", "check_exercise", "explain_result", "get_progress"] { + assert!(names.contains(&required), "missing {required}"); + } +} + +#[test] +fn check_exercise_persists_progress_in_sandbox() { + let mut p = McpProcess::spawn(); + initialize(&mut p); + let circuit = r#"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"#; + let call = format!( + r#"{{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{{"name":"check_exercise","arguments":{{"exercise_id":"1-1-a","circuit":"{circuit}"}}}}}}"# + ); + p.send(&call); + let payload = extract(&p.recv()); + assert_eq!(payload["passed"], true); + assert_eq!(payload["progress_updated"], true); + + p.send(r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_progress","arguments":{}}}"#); + let progress = extract(&p.recv()); + assert!(progress["total_exercises_solved"].as_u64().unwrap() >= 1); +} + +#[test] +fn check_exercise_invalid_circuit_returns_diagnostics_in_payload_not_protocol_error() { + let mut p = McpProcess::spawn(); + initialize(&mut p); + p.send(r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"check_exercise","arguments":{"exercise_id":"1-1-a","circuit":"not valid qasm"}}}"#); + let resp = p.recv(); + assert!(resp.get("error").is_none(), "expected payload not error: {resp}"); + let payload = extract(&resp); + assert_eq!(payload["passed"], false); + assert!(payload["diagnostics"].as_array().is_some()); +} + +#[test] +fn explain_result_returns_ast_based_breakdown_with_descriptions() { + let mut p = McpProcess::spawn(); + initialize(&mut p); + let call = r#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"explain_result","arguments":{"circuit":"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;","counts":{"00":512,"11":512}}}}"#; + p.send(call); + let payload = extract(&p.recv()); + assert_eq!(payload["key_concept"], "entanglement"); + assert_eq!(payload["num_qubits"], 2); + let breakdown = payload["gate_breakdown"].as_array().unwrap(); + assert_eq!(breakdown.len(), 2); + assert_eq!(breakdown[0]["name"], "h"); + assert_eq!(breakdown[1]["name"], "cx"); +} + +#[test] +fn golden_pass_cases_all_pass() { + let golden = std::fs::read_to_string("tests/reference/tutor/exercises_pass.jsonl").unwrap(); + let mut p = McpProcess::spawn(); + initialize(&mut p); + for line in golden.lines().filter(|l| !l.trim().is_empty()) { + let case: serde_json::Value = serde_json::from_str(line).unwrap(); + let id = case["exercise_id"].as_str().unwrap(); + let circuit = case["circuit"].as_str().unwrap(); + let call = format!( + r#"{{"jsonrpc":"2.0","id":99,"method":"tools/call","params":{{"name":"check_exercise","arguments":{{"exercise_id":"{id}","circuit":{circuit:?}}}}}}}"# + ); + p.send(&call); + let payload = extract(&p.recv()); + assert!(payload["passed"].as_bool().unwrap_or(false), "id={id} payload={payload}"); + } +} + +#[test] +fn golden_fail_cases_all_fail() { + let golden = std::fs::read_to_string("tests/reference/tutor/exercises_fail.jsonl").unwrap(); + let mut p = McpProcess::spawn(); + initialize(&mut p); + for line in golden.lines().filter(|l| !l.trim().is_empty()) { + let case: serde_json::Value = serde_json::from_str(line).unwrap(); + let id = case["exercise_id"].as_str().unwrap(); + let circuit = case["circuit"].as_str().unwrap(); + let call = format!( + r#"{{"jsonrpc":"2.0","id":98,"method":"tools/call","params":{{"name":"check_exercise","arguments":{{"exercise_id":"{id}","circuit":{circuit:?}}}}}}}"# + ); + p.send(&call); + let payload = extract(&p.recv()); + assert_eq!(payload["passed"].as_bool().unwrap_or(true), false, "id={id} should fail"); + } +} +``` + +- [ ] **Step 2: Create `tests/reference/tutor/exercises_pass.jsonl`** + +One line per test case (≥ 1 per module). Format: `{"exercise_id": "...", "circuit": "..."}`. + +```jsonl +{"exercise_id": "1-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"} +{"exercise_id": "1-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"} +{"exercise_id": "2-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;"} +{"exercise_id": "2-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nh q[0];\nc = measure q;"} +{"exercise_id": "3-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nz q[0];\nh q[0];\nc = measure q;"} +{"exercise_id": "4-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"} +{"exercise_id": "5-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nc = measure q;"} +{"exercise_id": "6-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[1] c;\nx q[1];\nh q[0];\ncz q[0], q[1];\nh q[0];\nc[0] = measure q[0];"} +{"exercise_id": "7-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[0];\nh q[1];\nx q[0];\nx q[1];\ncz q[0], q[1];\nx q[0];\nx q[1];\nh q[0];\nh q[1];\nc = measure q;"} +``` + +- [ ] **Step 3: Create `tests/reference/tutor/exercises_fail.jsonl`** + +```jsonl +{"exercise_id": "1-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"} +{"exercise_id": "2-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"} +{"exercise_id": "4-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\nc = measure q;"} +``` + +- [ ] **Step 4: Run the integration suite** + +```bash +cargo test --test tutor_integration 2>&1 | grep -E "test result|FAILED" +``` + +Expected: every test green. + +- [ ] **Step 5: Run the full test suite (regression check)** + +```bash +cargo test 2>&1 | grep -E "test result|FAILED" +``` + +Expected: no regression in v1 suites; every tutor suite green. + +- [ ] **Step 6: Commit** + +```bash +git add tests/tutor_integration.rs tests/reference/tutor/ +git commit -m "test: add MCP tutor integration tests + golden files (sandboxed)" +``` diff --git a/docs/superpowers/plans/2026-04-29-quantum-tutor/README.md b/docs/superpowers/plans/2026-04-29-quantum-tutor/README.md new file mode 100644 index 0000000..ec4f682 --- /dev/null +++ b/docs/superpowers/plans/2026-04-29-quantum-tutor/README.md @@ -0,0 +1,52 @@ +# Quantum Tutor — sub-task index + +**Goal:** Add 4 MCP tools (`get_lesson`, `check_exercise`, `explain_result`, `get_progress`) to quantum-bridge-mcp, backed by a JSON curriculum (7 modules / 18 exercises) and a persistent progress file. + +**Spec:** [`docs/superpowers/specs/2026-04-29-quantum-tutor-design.md`](../../specs/2026-04-29-quantum-tutor-design.md). + +This plan is split into 18 atomic sub-tasks. Each file is self-contained — when executing a task, load only the matching file plus the spec. Do not load the master plan or sibling tasks. + +## Execution order + +| # | File | Goal | Depends on | +|---|------|------|------------| +| 0 | [`00-prerequisites.md`](00-prerequisites.md) | Backend injection seam + tempfile dev-dep | — | +| 1 | [`01-curriculum-types.md`](01-curriculum-types.md) | Curriculum types + module 1 JSON | 0 | +| 2 | [`02-curriculum-loader.md`](02-curriculum-loader.md) | `OnceLock`-cached `CurriculumLoader` | 1 | +| 3 | [`03-progress-store.md`](03-progress-store.md) | Sandboxable `ProgressStore` | 0 | +| 4 | [`04-circuit-analyzer.md`](04-circuit-analyzer.md) | AST-based gate listing | 0 | +| 5 | [`05-exercise-checker.md`](05-exercise-checker.md) | `ExerciseChecker` with `&dyn Backend` | 2, 4 | +| 6 | [`06-get-lesson.md`](06-get-lesson.md) | `get_lesson` response | 2, 3 | +| 7 | [`07-check-exercise.md`](07-check-exercise.md) | `check_exercise` response | 5, 6 | +| 8 | [`08-explain-result.md`](08-explain-result.md) | `explain_result` response | 4, 7 | +| 9 | [`09-get-progress.md`](09-get-progress.md) | `get_progress` response | 8 | +| 10 | [`10-wire-tools.md`](10-wire-tools.md) | Register the 4 tools on `QuantumBridgeServer` | 9 | +| 11 | [`11-curriculum-module-2.md`](11-curriculum-module-2.md) | Module 2 — Superposition | 10 | +| 12 | [`12-curriculum-module-3.md`](12-curriculum-module-3.md) | Module 3 — Interférence | 11 | +| 13 | [`13-curriculum-module-4.md`](13-curriculum-module-4.md) | Module 4 — Intrication | 12 | +| 14 | [`14-curriculum-module-5.md`](14-curriculum-module-5.md) | Module 5 — Multi-qubits | 13 | +| 15 | [`15-curriculum-module-6.md`](15-curriculum-module-6.md) | Module 6 — Premiers algorithmes | 14 | +| 16 | [`16-curriculum-module-7.md`](16-curriculum-module-7.md) | Module 7 — Grover | 15 | +| 17 | [`17-integration-tests.md`](17-integration-tests.md) | Integration tests + golden files | 16 | + +## Architecture summary (the only context you need from the master plan) + +- `QuantumBridgeServer` owns `Arc`; tutor tools route through `self.backend`. +- `ExerciseChecker::check_circuit(backend: &dyn Backend, ...)` — never instantiates `LocalSimulator` directly. +- `CurriculumLoader` lazily parses an embedded JSON via `OnceLock`; returns `Result`, no `expect()`. +- `ProgressStore` is sandboxed via `QB_PROGRESS_PATH`; missing `HOME` returns `Err`. +- `CircuitAnalyzer` (new module) exposes AST-based gate listing — `explain_result` does not string-match the QASM source. +- Statistical tolerance for exercise checks is **2σ** below `min_ratio` (spec §3). +- `check_exercise` distinguishes protocol errors (unknown exercise → `McpError`) from validation errors (circuit invalid → structured `diagnostics` payload). +- Module 6 teleportation exercise was replaced by phase kickback (the v1 executor ignores classical-controlled gates). + +## Convention per task file + +Each task file contains: +1. **Goal** — single paragraph. +2. **Prerequisites** — which tasks must already be merged. +3. **Files** — what is created or modified. +4. **Steps** — `- [ ]` checkboxes with code blocks and verification commands. +5. **Commit** — exact `git commit` message. + +Use `superpowers:executing-plans` or `superpowers:subagent-driven-development` to walk one task at a time. diff --git a/docs/superpowers/plans/sub1-foundation.md b/docs/superpowers/plans/sub1-foundation.md new file mode 100644 index 0000000..a1d2ca8 --- /dev/null +++ b/docs/superpowers/plans/sub1-foundation.md @@ -0,0 +1,308 @@ +# Sub-plan 1 — Foundation (Tasks 1–3) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. + +**Goal:** Cargo bootstrap + error types + domain newtypes + Backend trait hierarchy. Produces a compiling skeleton with all dependencies resolved. + +**Starting state:** Greenfield — only `CLAUDE.md`, `.gitignore`, and `docs/` exist. No `src/`, no `Cargo.toml`. + +**Deliverable:** `cargo test` exits 0. Three commits in git history. + +**Tech Stack:** Rust 2021, `rmcp` (git), `oq3_semantics 0.7`, `spinoza 0.5`, `tokio`, `serde`, `thiserror 2`, `tracing`. + +**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 1–3. + +--- + +## Task 1: Bootstrap + +**Files:** +- Create: `Cargo.toml` +- Create: `src/main.rs` (via `cargo new`) + +- [ ] **Step 1: Initialise project** + +```bash +cd /home/vincent/src/misc/quantum-bridge-mcp +cargo new --name quantum-bridge-mcp . +``` + +Expected: `src/main.rs` and `Cargo.toml` created. + +- [ ] **Step 2: Write Cargo.toml** + +> Run `cargo search spinoza` and `cargo search oq3_semantics` to confirm latest patch versions before pinning. + +```toml +[package] +name = "quantum-bridge-mcp" +version = "0.1.0" +edition = "2021" +description = "Zero-dependency MCP server for local OpenQASM 3.0 quantum circuit simulation" +license = "MIT" + +[dependencies] +rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", features = ["server", "transport-io", "macros"] } +oq3_semantics = "0.7" +spinoza = "0.5" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +schemars = "0.8" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" + +[dev-dependencies] +proptest = "1" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "simulation" +harness = false +``` + +- [ ] **Step 3: Write minimal src/main.rs** + +```rust +fn main() { + println!("quantum-bridge-mcp"); +} +``` + +- [ ] **Step 4: Verify dependencies compile** + +```bash +cargo build +``` + +Expected: success (first run ~200 MB). If a version constraint fails, run `cargo search ` and adjust. + +- [ ] **Step 5: Commit** + +```bash +git add Cargo.toml Cargo.lock src/main.rs +git commit -m "chore: bootstrap project with all V1 dependencies" +``` + +--- + +## Task 2: Foundation — Error types and domain newtypes + +**Files:** +- Create: `src/error.rs` +- Create: `src/types.rs` +- Modify: `src/main.rs` + +- [ ] **Step 1: Write src/error.rs** + +```rust +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BridgeError { + #[error("parse error at line {line}, col {col}: {message}")] + Parse { line: usize, col: usize, message: String }, + + #[error("gate '{gate}' is not supported at line {line} — supported: {supported}")] + UnsupportedGate { gate: String, line: usize, supported: String }, + + #[error("qubit index {index} is out of range (circuit declares {declared} qubits)")] + QubitOutOfRange { index: usize, declared: usize }, + + #[error("circuit requires {requested} qubits, exceeds the local simulator limit of {limit}")] + QubitLimitExceeded { requested: usize, limit: usize }, + + #[error("simulation failed: {0}")] + Simulation(String), + + #[error("measure at line {line} maps to an undeclared classical bit")] + MeasurementMapping { line: usize }, +} +``` + +- [ ] **Step 2: Write src/types.rs** + +```rust +use std::collections::HashMap; + +/// Newtype for qubit indices — prevents mixing with plain usizes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct QubitIndex(pub usize); + +/// Newtype for shot count — enforces range and intent at type level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ShotCount(pub u32); + +impl ShotCount { + pub const DEFAULT: ShotCount = ShotCount(1024); + pub const MAX: ShotCount = ShotCount(100_000); +} + +/// Wraps an OpenQASM 3.0 source string. +#[derive(Debug, Clone)] +pub struct CircuitSource(pub String); + +#[derive(Debug, Clone)] +pub enum DiagnosticSeverity { + Error, + Warning, +} + +#[derive(Debug, Clone)] +pub struct ValidationDiagnostic { + pub line: usize, + pub column: usize, + pub message: String, + pub severity: DiagnosticSeverity, +} + +#[derive(Debug, Clone)] +pub struct ValidationResult { + pub is_valid: bool, + pub diagnostics: Vec, + pub num_qubits: Option, + pub num_gates: Option, +} + +#[derive(Debug, Clone)] +pub struct SimulationResult { + /// Bitstring → count, e.g. `{"00": 512, "11": 512}`. + pub counts: HashMap, + pub shots: u32, + pub execution_time_ms: f64, + /// Optional full statevector as (real, imag) pairs per basis state. + pub statevector: Option>, +} +``` + +- [ ] **Step 3: Update src/main.rs** + +```rust +mod error; +mod types; + +fn main() { + println!("quantum-bridge-mcp"); +} +``` + +- [ ] **Step 4: Verify** + +```bash +cargo build +``` + +Expected: no warnings. + +- [ ] **Step 5: Commit** + +```bash +git add src/error.rs src/types.rs src/main.rs +git commit -m "feat: add error types and domain newtypes" +``` + +--- + +## Task 3: Backend Traits + +**Files:** +- Create: `src/executor.rs` (traits + constants only — `LocalSimulator` comes in sub-plan 3) + +- [ ] **Step 1: Write src/executor.rs** + +```rust +use crate::error::BridgeError; +use crate::types::{CircuitSource, ShotCount, SimulationResult, ValidationResult}; + +pub const MAX_LOCAL_QUBITS: usize = 28; + +pub const SUPPORTED_GATES: &[&str] = &[ + "h", "x", "y", "z", "s", "t", "sdg", "tdg", + "rx", "ry", "rz", "cx", "cz", "swap", "ccx", + "measure", +]; + +pub trait CanIntrospect { + fn name(&self) -> &str; + fn max_qubits(&self) -> usize; + fn supported_gates(&self) -> &[&str]; +} + +pub trait CanValidate { + fn validate(&self, circuit: &CircuitSource) -> Result; +} + +pub trait CanExecute { + fn run( + &self, + circuit: &CircuitSource, + shots: ShotCount, + return_statevector: bool, + ) -> Result; +} + +/// Marker trait combining all three capabilities. V1.5 IBM backend will impl this too. +pub trait Backend: CanIntrospect + CanValidate + CanExecute + Send + Sync {} + +#[cfg(test)] +mod tests { + use super::*; + + struct MockBackend; + impl CanIntrospect for MockBackend { + fn name(&self) -> &str { "mock" } + fn max_qubits(&self) -> usize { 4 } + fn supported_gates(&self) -> &[&str] { SUPPORTED_GATES } + } + impl CanValidate for MockBackend { + fn validate(&self, _: &CircuitSource) -> Result { + Ok(ValidationResult { is_valid: true, diagnostics: vec![], num_qubits: None, num_gates: None }) + } + } + impl CanExecute for MockBackend { + fn run(&self, _: &CircuitSource, _: ShotCount, _: bool) -> Result { + Ok(SimulationResult { counts: Default::default(), shots: 0, execution_time_ms: 0.0, statevector: None }) + } + } + impl Backend for MockBackend {} + + #[test] + fn mock_backend_satisfies_trait_bounds() { + let b = MockBackend; + assert_eq!(b.name(), "mock"); + assert_eq!(b.max_qubits(), 4); + } +} +``` + +- [ ] **Step 2: Declare module and run tests** + +Add to `src/main.rs`: +```rust +mod executor; +``` + +```bash +cargo test +``` + +Expected: 1 test passes (`mock_backend_satisfies_trait_bounds`). + +- [ ] **Step 3: Commit** + +```bash +git add src/executor.rs src/main.rs +git commit -m "feat: define Backend trait hierarchy (CanIntrospect/CanValidate/CanExecute)" +``` + +--- + +## Final verification + +```bash +cargo fmt --check && cargo clippy -- -D warnings && cargo test +``` + +Expected: all green. Sub-plan 1 complete — hand off to sub-plan 2. diff --git a/docs/superpowers/plans/sub2-validator.md b/docs/superpowers/plans/sub2-validator.md new file mode 100644 index 0000000..35ea967 --- /dev/null +++ b/docs/superpowers/plans/sub2-validator.md @@ -0,0 +1,358 @@ +# Sub-plan 2 — CircuitValidator (Task 4) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. + +**Goal:** Implement `CircuitValidator` wrapping `oq3_semantics`. Validates OpenQASM 3.0 syntax, qubit bounds, and our supported gate subset. Returns structured `ValidationResult` with line/column diagnostics. + +**Starting state (after sub-plan 1):** +- `Cargo.toml` with all deps +- `src/main.rs` declares `mod error; mod executor; mod types;` +- `src/error.rs` — `BridgeError` +- `src/types.rs` — `CircuitSource`, `ValidationResult`, `ValidationDiagnostic`, `DiagnosticSeverity` +- `src/executor.rs` — `SUPPORTED_GATES`, `MAX_LOCAL_QUBITS`, trait defs only +- `cargo test` passes + +**Deliverable:** `cargo test` passes with 5 validator unit tests green. One commit added. + +**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Task 4. + +--- + +## Task 4: CircuitValidator — oq3_semantics integration + +**Files:** +- Create: `src/validator.rs` +- Modify: `src/main.rs` (add `mod validator;`) + +### 4a — AST exploration + +The `oq3_semantics` AST is lightly documented (6.9% rustdoc coverage). You MUST run the exploration test first to discover the real type names before implementing. + +- [ ] **Step 1: Create src/validator.rs with exploration tests** + +```rust +use oq3_semantics::syntax_to_semantics::parse_source_string; + +use crate::error::BridgeError; +use crate::executor::SUPPORTED_GATES; +use crate::types::{CircuitSource, DiagnosticSeverity, ValidationDiagnostic, ValidationResult}; + +pub struct CircuitValidator { + max_qubits: usize, +} + +impl CircuitValidator { + pub fn new(max_qubits: usize) -> Self { + Self { max_qubits } + } + + pub fn validate(&self, source: &CircuitSource) -> Result { + todo!("implement after AST exploration") + } +} + +fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { + let safe_offset = offset.min(source.len()); + let prefix = &source[..safe_offset]; + let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1; + let col = prefix.rfind('\n').map(|p| safe_offset - p - 1).unwrap_or(safe_offset) + 1; + (line, col) +} + +#[cfg(test)] +mod exploration { + use super::*; + + #[test] + fn print_bell_circuit_ast() { + let qasm = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q;"#; + let result = parse_source_string(qasm, Some("bell.qasm"), None::<&[&str]>); + println!("any_errors: {}", result.any_errors()); + println!("program: {:#?}", result.program()); + for err in result.semantic_errors().iter() { + println!("semantic_error: {:?}", err); + } + } + + #[test] + fn print_unsupported_gate_ast() { + let qasm = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let result = parse_source_string(qasm, Some("bad.qasm"), None::<&[&str]>); + println!("any_errors: {}", result.any_errors()); + println!("program: {:#?}", result.program()); + } + + #[test] + fn print_qubit_declaration_ast() { + let qasm = "OPENQASM 3.0;\nqubit[3] myq;\n"; + let result = parse_source_string(qasm, Some("q.qasm"), None::<&[&str]>); + println!("program: {:#?}", result.program()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::executor::MAX_LOCAL_QUBITS; + + fn validator() -> CircuitValidator { + CircuitValidator::new(MAX_LOCAL_QUBITS) + } + + 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;"#; + + #[test] + fn valid_bell_circuit_is_accepted() { + let result = validator() + .validate(&CircuitSource(BELL_CIRCUIT.to_string())) + .unwrap(); + assert!(result.is_valid, "diagnostics: {:?}", result.diagnostics); + assert!(result.diagnostics.is_empty()); + assert_eq!(result.num_qubits, Some(2)); + } + + #[test] + fn unsupported_gate_produces_error_diagnostic() { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + let msg = &result.diagnostics[0].message; + assert!(msg.to_lowercase().contains("u3"), "expected 'u3' in: {msg}"); + } + + #[test] + fn too_many_qubits_produces_error_diagnostic() { + let n = MAX_LOCAL_QUBITS + 1; + let circuit = format!("OPENQASM 3.0;\nqubit[{n}] q;\n"); + let result = validator().validate(&CircuitSource(circuit)).unwrap(); + assert!(!result.is_valid); + let msg = &result.diagnostics[0].message; + assert!(msg.contains(&n.to_string()), "expected {n} in: {msg}"); + } + + #[test] + fn invalid_qasm_syntax_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\nthis is not valid qasm;\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } + + #[test] + fn undeclared_qubit_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nh q[5];\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } +} +``` + +- [ ] **Step 2: Declare the module** + +Add to `src/main.rs`: +```rust +mod validator; +``` + +- [ ] **Step 3: Run exploration tests and read AST output** + +```bash +cargo test exploration -- --nocapture 2>&1 | head -300 +``` + +**Read the output carefully. Identify:** +1. How `Program` exposes statements — look for `stmts()`, `statements()`, or `iter()` +2. The `Stmt` enum variant name for gate calls (e.g. `GateCall`, `Gate`) +3. The `Stmt` variant name for qubit register declarations (e.g. `QubitDeclaration`, `QubitDecl`) +4. How to extract the gate name string from a gate call node +5. How to extract the register size (width) from a qubit declaration node +6. What `semantic_errors().iter()` yields — specifically the `.range()` method's return type + +Write down the exact variant and method names before continuing. + +- [ ] **Step 4: Confirm tests fail at todo!()** + +```bash +cargo test validator::tests 2>&1 | tail -10 +``` + +Expected: all 5 panic at `todo!()`. + +### 4b — Implementation + +- [ ] **Step 5: Implement CircuitValidator::validate** + +Replace the `todo!()` with the full implementation. The skeleton below uses placeholder helper names — replace variant patterns with what you observed in Step 3: + +```rust +pub fn validate(&self, source: &CircuitSource) -> Result { + let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); + let mut diagnostics: Vec = Vec::new(); + + // Collect oq3_semantics errors (syntax + undeclared symbols) + for error in parse_result.semantic_errors().iter() { + let range = error.range(); + // TextRange::start() → rowan::TextSize → u32 → usize + let offset: usize = u32::from(range.start()) as usize; + let (line, col) = byte_offset_to_line_col(&source.0, offset); + diagnostics.push(ValidationDiagnostic { + line, + column: col, + message: error.message(), + severity: DiagnosticSeverity::Error, + }); + } + + if parse_result.any_errors() { + return Ok(ValidationResult { + is_valid: false, + diagnostics, + num_qubits: None, + num_gates: None, + }); + } + + let program = parse_result.program(); + let mut total_qubits: usize = 0; + let mut gate_count: usize = 0; + + for stmt in program.stmts() { // ← adjust method name from exploration + // Qubit declarations — adjust variant name from exploration output + if let Some(n) = extract_qubit_count(stmt) { + total_qubits += n; + } + + // Gate calls — adjust variant name from exploration output + if let Some(gate_name) = extract_gate_name(stmt) { + if gate_name == "measure" { + continue; + } + gate_count += 1; + if !SUPPORTED_GATES.contains(&gate_name.as_str()) { + let line = extract_stmt_line(stmt, &source.0); + let supported = SUPPORTED_GATES.join(", "); + diagnostics.push(ValidationDiagnostic { + line, + column: 1, + message: format!( + "gate '{gate_name}' is not supported (supported: {supported})" + ), + severity: DiagnosticSeverity::Error, + }); + } + } + } + + if total_qubits > self.max_qubits { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: format!( + "circuit requires {total_qubits} qubits, exceeds local simulator limit of {}", + self.max_qubits + ), + severity: DiagnosticSeverity::Error, + }); + } + + Ok(ValidationResult { + is_valid: diagnostics.is_empty(), + diagnostics, + num_qubits: if total_qubits > 0 { Some(total_qubits) } else { None }, + num_gates: if gate_count > 0 { Some(gate_count) } else { None }, + }) +} +``` + +- [ ] **Step 6: Implement the three AST helper functions** + +Add below `validate`. Fill in match arms using the variant names from Step 3: + +```rust +use oq3_semantics::asg::Stmt; + +fn extract_qubit_count(stmt: &Stmt) -> Option { + // Fill in from exploration output, e.g.: + // if let Stmt::QubitDeclaration(qd) = stmt { + // return Some(qd.width().unwrap_or(1)); + // } + todo!("fill in from AST exploration") +} + +fn extract_gate_name(stmt: &Stmt) -> Option { + // Fill in from exploration output, e.g.: + // if let Stmt::GateCall(gc) = stmt { + // return Some(gc.name().to_string().to_lowercase()); + // } + todo!("fill in from AST exploration") +} + +fn extract_stmt_line(stmt: &Stmt, source: &str) -> usize { + // Fill in from exploration output, e.g.: + // let offset = u32::from(stmt.span().start()) as usize; + // byte_offset_to_line_col(source, offset).0 + todo!("fill in from AST exploration") +} +``` + +- [ ] **Step 7: Run tests and iterate until all 5 pass** + +```bash +cargo test validator::tests 2>&1 +``` + +Common issues: +- If `program.stmts()` doesn't exist: use `cargo doc --open` or hover in rust-analyzer to find the correct method on `Program` +- If gate name extraction returns wrong case: add `.to_lowercase()` +- If `too_many_qubits` test fails: `extract_qubit_count` may be returning 0 — verify the qubit declaration variant name + +- [ ] **Step 8: Run full test suite** + +```bash +cargo test 2>&1 +``` + +Expected: all tests pass (exploration + validator::tests + executor::tests from sub-plan 1). + +- [ ] **Step 9: Commit** + +```bash +git add src/validator.rs src/main.rs +git commit -m "feat: implement CircuitValidator with oq3_semantics" +``` + +--- + +## Final verification + +```bash +cargo fmt --check && cargo clippy -- -D warnings && cargo test +``` + +Expected: all green. Sub-plan 2 complete — hand off to sub-plan 3. diff --git a/docs/superpowers/plans/sub3-executor.md b/docs/superpowers/plans/sub3-executor.md new file mode 100644 index 0000000..d7060d4 --- /dev/null +++ b/docs/superpowers/plans/sub3-executor.md @@ -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<` +- 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. diff --git a/docs/superpowers/plans/sub4-tools-server.md b/docs/superpowers/plans/sub4-tools-server.md new file mode 100644 index 0000000..4d7a1a1 --- /dev/null +++ b/docs/superpowers/plans/sub4-tools-server.md @@ -0,0 +1,608 @@ +# Sub-plan 4 — Tools + MCP Server (Tasks 6–9) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. + +**Goal:** Implement the three tool handlers (`list_backends`, `validate_circuit`, `run_circuit`) and wire everything into a working rmcp stdio MCP server. Smoke-test with a live JSON-RPC call. + +**Starting state (after sub-plan 3):** +- All of sub-plans 1–3 files present +- `src/executor.rs` — `LocalSimulator` fully implemented, all 7 tests pass +- `src/validator.rs` — `CircuitValidator` fully implemented, all 5 tests pass +- `cargo test` passes + +**Deliverable:** `cargo build` produces a working MCP binary. `cargo test` passes with 16+ unit tests. Smoke test shows a valid JSON-RPC `initialize` response. Four commits added. + +**Key rmcp API:** +- Macros: `#[tool_router]` on `impl`, `#[tool_handler]` on `impl ServerHandler`, `#[tool(description="...")]` on each method +- Parameters: `Parameters(MyParams { field }): Parameters` where `MyParams` derives `Deserialize + JsonSchema` +- Return: `Result` — success via `CallToolResult::success(vec![Content::text("...")])`, error via `McpError::invalid_params("msg", None)` +- Server field: `tool_router: ToolRouter` initialised with `Self::tool_router()` +- stdio: `QuantumBridgeServer::new().serve(stdio()).await?.waiting().await?` +- Logs go to **stderr** — stdout is the MCP wire + +**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 6–9. + +--- + +## Task 6: Tool — list_backends + +**Files:** +- Create: `src/tools/list_backends.rs` +- Create: `src/tools/mod.rs` (stub, extended in Task 9) + +- [ ] **Step 1: Create src/tools/list_backends.rs with failing test** + +```rust +use serde_json::json; + +use crate::executor::{CanIntrospect, LocalSimulator, MAX_LOCAL_QUBITS}; + +pub fn list_backends_response() -> serde_json::Value { + todo!("implement list_backends_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_contains_local_simulator() { + let resp = list_backends_response(); + let backends = resp["backends"].as_array().unwrap(); + assert_eq!(backends.len(), 1); + assert_eq!(backends[0]["name"], "local_simulator"); + } + + #[test] + fn response_contains_max_qubits() { + let resp = list_backends_response(); + assert_eq!( + resp["backends"][0]["max_qubits"].as_u64().unwrap(), + MAX_LOCAL_QUBITS as u64 + ); + } + + #[test] + fn response_lists_supported_gates() { + let resp = list_backends_response(); + let gates = resp["backends"][0]["supported_gates"].as_array().unwrap(); + assert!(!gates.is_empty()); + assert!(gates.iter().any(|g| g == "h")); + assert!(gates.iter().any(|g| g == "cx")); + } +} +``` + +- [ ] **Step 2: Create src/tools/mod.rs (stub)** + +```rust +pub mod list_backends; +pub mod run_circuit; +pub mod validate_circuit; +``` + +- [ ] **Step 3: Declare tools module in src/main.rs** + +Add `mod tools;` to `src/main.rs`. + +- [ ] **Step 4: Run tests to confirm todo!() panics** + +```bash +cargo test tools::list_backends 2>&1 | tail -5 +``` + +- [ ] **Step 5: Implement list_backends_response** + +Replace `todo!()`: + +```rust +pub fn list_backends_response() -> serde_json::Value { + let sim = LocalSimulator::new(); + json!({ + "backends": [{ + "name": sim.name(), + "description": "Local statevector simulator (Spinoza engine). No network, no account required.", + "max_qubits": sim.max_qubits(), + "supported_gates": sim.supported_gates(), + "simulation_type": "statevector", + "notes": "Statevector memory: 2^n × 16 bytes. Max 28 qubits." + }] + }) +} +``` + +- [ ] **Step 6: Run tests** + +```bash +cargo test tools::list_backends 2>&1 +``` + +Expected: 3 tests pass. + +- [ ] **Step 7: Commit** + +```bash +git add src/tools/mod.rs src/tools/list_backends.rs src/main.rs +git commit -m "feat: implement list_backends tool handler" +``` + +--- + +## Task 7: Tool — validate_circuit + +**Files:** +- Create: `src/tools/validate_circuit.rs` + +- [ ] **Step 1: Create file with failing test** + +```rust +use serde_json::{json, Value}; + +use crate::executor::MAX_LOCAL_QUBITS; +use crate::types::CircuitSource; + +pub fn validate_circuit_response(circuit: &str) -> Value { + todo!("implement validate_circuit_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + const BELL: &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;"#; + + #[test] + fn valid_circuit_returns_is_valid_true() { + let resp = validate_circuit_response(BELL); + assert_eq!(resp["is_valid"], true); + assert_eq!(resp["diagnostics"].as_array().unwrap().len(), 0); + } + + #[test] + fn valid_circuit_includes_qubit_count() { + let resp = validate_circuit_response(BELL); + assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); + } + + #[test] + fn invalid_circuit_returns_is_valid_false_with_message() { + let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;"); + assert_eq!(resp["is_valid"], false); + assert!(!resp["diagnostics"].as_array().unwrap().is_empty()); + } + + #[test] + fn unsupported_gate_returns_diagnostic_with_gate_name() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nu3(0.1,0.2,0.3) q[0];"; + let resp = validate_circuit_response(circuit); + assert_eq!(resp["is_valid"], false); + let msg = resp["diagnostics"][0]["message"].as_str().unwrap(); + assert!(msg.to_lowercase().contains("u3"), "got: {msg}"); + } + + #[test] + fn diagnostic_includes_line_and_column() { + let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;\n"); + assert!(resp["diagnostics"][0]["line"].as_u64().unwrap() >= 1); + assert!(resp["diagnostics"][0]["column"].as_u64().unwrap() >= 1); + } +} +``` + +- [ ] **Step 2: Run to confirm todo!() failure** + +```bash +cargo test tools::validate_circuit 2>&1 | tail -5 +``` + +- [ ] **Step 3: Implement validate_circuit_response** + +```rust +pub fn validate_circuit_response(circuit: &str) -> Value { + use crate::types::DiagnosticSeverity; + use crate::validator::CircuitValidator; + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => json!({ + "is_valid": false, + "diagnostics": [{"line": 1, "column": 1, "message": e.to_string(), "severity": "error"}], + "num_qubits": null, + "num_gates": null, + }), + Ok(result) => { + let diagnostics: Vec = result.diagnostics.iter().map(|d| { + json!({ + "line": d.line, + "column": d.column, + "message": d.message, + "severity": match d.severity { + DiagnosticSeverity::Error => "error", + DiagnosticSeverity::Warning => "warning", + } + }) + }).collect(); + json!({ + "is_valid": result.is_valid, + "diagnostics": diagnostics, + "num_qubits": result.num_qubits, + "num_gates": result.num_gates, + }) + } + } +} +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test tools::validate_circuit 2>&1 +``` + +Expected: 5 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/tools/validate_circuit.rs +git commit -m "feat: implement validate_circuit tool handler" +``` + +--- + +## Task 8: Tool — run_circuit + +**Files:** +- Create: `src/tools/run_circuit.rs` + +- [ ] **Step 1: Create file with failing test** + +```rust +use serde_json::{json, Value}; + +use crate::types::{CircuitSource, ShotCount}; + +pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { + todo!("implement run_circuit_response") +} + +#[cfg(test)] +mod tests { + use super::*; + + const BELL: &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: &str = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +bit[1] c; +x q[0]; +c = measure q;"#; + + #[test] + fn bell_counts_total_matches_shots() { + let resp = run_circuit_response(BELL, 1_000, false); + let total: u64 = resp["counts"].as_object().unwrap().values() + .map(|v| v.as_u64().unwrap()).sum(); + assert_eq!(total, 1_000); + } + + #[test] + fn bell_only_produces_00_and_11_outcomes() { + let resp = run_circuit_response(BELL, 1_000, false); + for key in resp["counts"].as_object().unwrap().keys() { + assert!(key == "00" || key == "11", "unexpected: {key}"); + } + } + + #[test] + fn x_gate_produces_only_one_outcome() { + let resp = run_circuit_response(X, 100, false); + assert_eq!(resp["counts"]["1"].as_u64().unwrap(), 100); + } + + #[test] + fn response_includes_shots_and_execution_time() { + let resp = run_circuit_response(BELL, 512, false); + assert_eq!(resp["shots"].as_u64().unwrap(), 512); + assert!(resp["execution_time_ms"].as_f64().unwrap() >= 0.0); + } + + #[test] + fn statevector_absent_when_not_requested() { + assert!(run_circuit_response(BELL, 100, false)["statevector"].is_null()); + } + + #[test] + fn statevector_has_correct_length_when_requested() { + let sv = run_circuit_response(BELL, 100, true)["statevector"].as_array().unwrap().len(); + assert_eq!(sv, 4); + } + + #[test] + fn invalid_circuit_returns_error_field() { + let resp = run_circuit_response("OPENQASM 3.0;\nnot_valid;", 100, false); + assert!(resp.get("error").is_some(), "got: {resp}"); + } + + #[test] + fn shots_clamped_to_max() { + let resp = run_circuit_response(BELL, ShotCount::MAX.0 + 1, false); + let has_error = resp.get("error").is_some(); + let shots = resp["shots"].as_u64().unwrap_or(0); + assert!(has_error || shots == ShotCount::MAX.0 as u64); + } +} +``` + +- [ ] **Step 2: Run to confirm todo!() failure** + +```bash +cargo test tools::run_circuit 2>&1 | tail -5 +``` + +- [ ] **Step 3: Implement run_circuit_response** + +```rust +pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { + use crate::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS}; + use crate::validator::CircuitValidator; + + let shot_count = ShotCount(shots.min(ShotCount::MAX.0)); + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let validation = match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => return json!({"error": e.to_string()}), + Ok(v) => v, + }; + if !validation.is_valid { + let msgs: Vec<&str> = validation.diagnostics.iter().map(|d| d.message.as_str()).collect(); + return json!({"error": format!("validation failed: {}", msgs.join("; "))}); + } + + let sim = LocalSimulator::new(); + match sim.run(&CircuitSource(circuit.to_string()), shot_count, return_statevector) { + Err(e) => json!({"error": e.to_string()}), + Ok(result) => { + let statevector: Value = match result.statevector { + None => Value::Null, + Some(sv) => sv.iter().map(|(r, i)| json!([r, i])).collect::>().into(), + }; + json!({ + "counts": result.counts, + "shots": result.shots, + "execution_time_ms": result.execution_time_ms, + "statevector": statevector, + "backend": "local_simulator", + }) + } + } +} +``` + +- [ ] **Step 4: Run tests** + +```bash +cargo test tools::run_circuit 2>&1 +``` + +Expected: 8 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add src/tools/run_circuit.rs +git commit -m "feat: implement run_circuit tool handler" +``` + +--- + +## Task 9: MCP Server — main.rs + rmcp wiring + +**Files:** +- Modify: `src/tools/mod.rs` (replace stub with full QuantumBridgeServer) +- Modify: `src/main.rs` (replace stub with async tokio main) + +- [ ] **Step 1: Replace src/tools/mod.rs with full server implementation** + +```rust +pub mod list_backends; +pub mod run_circuit; +pub mod validate_circuit; + +use rmcp::{ + ErrorData as McpError, + ServerHandler, + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::*, + tool, tool_handler, tool_router, +}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::tools::list_backends::list_backends_response; +use crate::tools::run_circuit::run_circuit_response; +use crate::tools::validate_circuit::validate_circuit_response; + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ValidateCircuitParams { + /// OpenQASM 3.0 source string to validate. + pub circuit: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct RunCircuitParams { + /// OpenQASM 3.0 source string to execute. + pub circuit: String, + /// Number of measurement shots (default 1024, max 100 000). + #[serde(default = "default_shots")] + pub shots: u32, + /// Return the full statevector in the response. + #[serde(default)] + pub return_statevector: bool, +} + +fn default_shots() -> u32 { 1024 } + +#[derive(Clone)] +pub struct QuantumBridgeServer { + tool_router: ToolRouter, +} + +#[tool_router] +impl QuantumBridgeServer { + pub fn new() -> Self { + Self { tool_router: Self::tool_router() } + } + + #[tool(description = "List available quantum simulation backends and their capabilities.")] + async fn list_backends(&self) -> Result { + let json = list_backends_response(); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Validate an OpenQASM 3.0 circuit. Returns structured diagnostics with line/column.")] + async fn validate_circuit( + &self, + Parameters(ValidateCircuitParams { circuit }): Parameters, + ) -> Result { + let json = validate_circuit_response(&circuit); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Execute an OpenQASM 3.0 circuit on the local statevector simulator. Returns counts, execution time, and optionally the full statevector.")] + async fn run_circuit( + &self, + Parameters(RunCircuitParams { circuit, shots, return_statevector }): Parameters, + ) -> Result { + let json = run_circuit_response(&circuit, shots, return_statevector); + if json.get("error").is_some() { + return Err(McpError::invalid_params( + json["error"].as_str().unwrap_or("unknown error"), + None, + )); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } +} + +#[tool_handler] +impl ServerHandler for QuantumBridgeServer { + fn get_info(&self) -> ServerInfo { + ServerInfo::new( + ServerCapabilities::builder().enable_tools().build(), + ) + .with_server_info(Implementation::from_build_env()) + .with_protocol_version(ProtocolVersion::V_2024_11_05) + .with_instructions( + "Quantum circuit simulator. Accepts OpenQASM 3.0. \ + Run validate_circuit first to get actionable errors.".to_string(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quantum_bridge_server_can_be_constructed() { + let _server = QuantumBridgeServer::new(); + } +} +``` + +- [ ] **Step 2: Run compile test** + +```bash +cargo test tools::tests 2>&1 +``` + +Expected: passes. If macro names are wrong (`#[tool_router]` / `#[tool_handler]`), check `~/.cargo/git/checkouts/rust-sdk-*/examples/servers/src/common/` for the canonical pattern. + +- [ ] **Step 3: Replace src/main.rs with async server entry point** + +```rust +mod error; +mod executor; +mod tools; +mod types; +mod validator; + +use anyhow::Result; +use rmcp::{ServiceExt, transport::stdio}; +use tools::QuantumBridgeServer; + +#[tokio::main] +async fn main() -> Result<()> { + // Logs go to stderr — stdout is the MCP JSON-RPC channel. + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("quantum-bridge-mcp starting"); + + let service = QuantumBridgeServer::new().serve(stdio()).await?; + service.waiting().await?; + Ok(()) +} +``` + +- [ ] **Step 4: Build the binary** + +```bash +cargo build 2>&1 +``` + +Expected: compiles without errors. + +- [ ] **Step 5: Smoke test** + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}' \ + | cargo run 2>/dev/null +``` + +Expected: JSON response with `"result"."serverInfo"."name":"quantum-bridge-mcp"`. Press Ctrl+C to stop. + +- [ ] **Step 6: Run full test suite** + +```bash +cargo test 2>&1 +``` + +Expected: all tests pass (3 list_backends + 5 validate_circuit + 8 run_circuit + 1 server construction + 7 executor + 5 validator). + +- [ ] **Step 7: Commit** + +```bash +git add src/main.rs src/tools/mod.rs +git commit -m "feat: wire rmcp stdio server with list_backends/validate_circuit/run_circuit tools" +``` + +--- + +## Final verification + +```bash +cargo fmt --check && cargo clippy -- -D warnings && cargo test +``` + +Expected: all green. Sub-plan 4 complete — hand off to sub-plan 5. diff --git a/docs/superpowers/plans/sub5-quality-ci.md b/docs/superpowers/plans/sub5-quality-ci.md new file mode 100644 index 0000000..44e0463 --- /dev/null +++ b/docs/superpowers/plans/sub5-quality-ci.md @@ -0,0 +1,802 @@ +# Sub-plan 5 — Quality & CI (Tasks 10–14) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. + +**Goal:** Integration tests (JSON-RPC roundtrip), property-based tests, Criterion benchmarks, Qiskit cross-validation golden files, and GitHub Actions CI + release workflows. + +**Starting state (after sub-plan 4):** +- Full working MCP server binary +- All tool handlers implemented and unit-tested +- `cargo test` passes, `cargo build` succeeds + +**Deliverable:** `cargo test` passes all test suites. `cargo bench` compiles. CI YAML is valid. Five commits added. + +**Important — lib target:** Tasks 11–12 need `use quantum_bridge_mcp::...` from `tests/`. This requires adding `src/lib.rs` and a `[lib]` entry in `Cargo.toml`. Task 11 includes these steps. + +**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 10–14. + +--- + +## Task 10: Integration Tests — MCP protocol roundtrip + +**Files:** +- Create: `tests/integration/mod.rs` +- Create: `tests/integration/mcp_protocol.rs` + +- [ ] **Step 1: Create test directory** + +```bash +mkdir -p tests/integration +``` + +- [ ] **Step 2: Create tests/integration/mod.rs** + +```rust +// Integration test module — empty file for cargo test discovery. +``` + +- [ ] **Step 3: Create tests/integration/mcp_protocol.rs** + +```rust +//! Roundtrip: spawn binary, send JSON-RPC on stdin, assert stdout. + +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; + +struct McpProcess { child: Child } + +impl McpProcess { + fn spawn() -> Self { + let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to spawn binary"); + Self { child } + } + + fn send(&mut self, msg: &str) { + let stdin = self.child.stdin.as_mut().unwrap(); + writeln!(stdin, "{}", msg).unwrap(); + stdin.flush().unwrap(); + } + + fn recv_line(&mut self) -> String { + let stdout = self.child.stdout.as_mut().unwrap(); + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + line.trim().to_string() + } +} + +impl Drop for McpProcess { + fn drop(&mut self) { let _ = self.child.kill(); } +} + +fn initialize(proc: &mut McpProcess) -> serde_json::Value { + proc.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#); + serde_json::from_str(&proc.recv_line()).expect("valid JSON") +} + +#[test] +fn initialize_returns_server_info() { + let mut proc = McpProcess::spawn(); + let resp = initialize(&mut proc); + assert!(resp["result"]["serverInfo"]["name"].as_str().is_some()); + assert_eq!(resp["result"]["protocolVersion"], "2024-11-05"); +} + +#[test] +fn tools_list_contains_three_tools() { + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let tools = resp["result"]["tools"].as_array().unwrap(); + assert_eq!(tools.len(), 3); + let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert!(names.contains(&"list_backends")); + assert!(names.contains(&"validate_circuit")); + assert!(names.contains(&"run_circuit")); +} + +#[test] +fn call_list_backends_returns_local_simulator() { + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["backends"][0]["name"], "local_simulator"); +} + +#[test] +fn call_validate_circuit_with_valid_bell_returns_is_valid_true() { + let bell = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;"; + let call = format!( + r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{{"name":"validate_circuit","arguments":{{"circuit":"{}"}}}}}}"#, + bell + ); + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(&call); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["is_valid"], true); +} + +#[test] +fn call_run_circuit_x_gate_returns_100_ones() { + let x_circ = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[1] q;\\nbit[1] c;\\nx q[0];\\nc = measure q;"; + let call = format!( + r#"{{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{{"name":"run_circuit","arguments":{{"circuit":"{}","shots":100}}}}}}"#, + x_circ + ); + let mut proc = McpProcess::spawn(); + initialize(&mut proc); + proc.send(&call); + let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap(); + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + let payload: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(payload["counts"]["1"].as_u64().unwrap(), 100); +} +``` + +> **Note:** Uses `env!("CARGO_BIN_EXE_quantum-bridge-mcp")` — cargo sets this automatically. Run with `cargo test --test integration`. If rmcp uses length-prefixed framing instead of newlines, adjust `recv_line` to skip the header bytes. + +- [ ] **Step 4: Run integration tests** + +```bash +cargo test --test integration 2>&1 +``` + +Expected: all 5 pass. + +- [ ] **Step 5: Commit** + +```bash +git add tests/integration/ +git commit -m "test: add MCP protocol integration tests (initialize, tools/list, tools/call)" +``` + +--- + +## Task 11: Property-Based Tests + lib target + +**Files:** +- Create: `src/lib.rs` +- Modify: `Cargo.toml` (add `[lib]` section) +- Modify: `src/main.rs` (remove mod declarations now in lib.rs) +- Create: `tests/proptest/mod.rs` +- Create: `tests/proptest/invariants.rs` + +- [ ] **Step 1: Create src/lib.rs** + +```rust +pub mod error; +pub mod executor; +pub mod types; +pub mod validator; +pub(crate) mod tools; +``` + +- [ ] **Step 2: Add [lib] to Cargo.toml** + +```toml +[lib] +name = "quantum_bridge_mcp" +path = "src/lib.rs" +``` + +- [ ] **Step 3: Update src/main.rs — remove mod declarations, use crate name** + +```rust +use anyhow::Result; +use quantum_bridge_mcp::tools::QuantumBridgeServer; +use rmcp::{ServiceExt, transport::stdio}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("quantum-bridge-mcp starting"); + + let service = QuantumBridgeServer::new().serve(stdio()).await?; + service.waiting().await?; + Ok(()) +} +``` + +- [ ] **Step 4: Verify cargo test still passes** + +```bash +cargo test 2>&1 +``` + +Expected: all existing tests still pass. + +- [ ] **Step 5: Create proptest files** + +```bash +mkdir -p tests/proptest +``` + +`tests/proptest/mod.rs`: +```rust +// Property-based test module. +``` + +`tests/proptest/invariants.rs`: + +```rust +//! Quantum mechanics invariants that must hold for any valid circuit. + +use proptest::prelude::*; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; + +proptest! { + /// Shot counts always sum to exactly the requested number of shots. + #[test] + fn shot_counts_always_sum_to_requested_shots(shots in 1u32..=1_000u32) { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q;"#; + let sim = LocalSimulator::new(); + let result = sim.run(&CircuitSource(circuit.to_string()), ShotCount(shots), false).unwrap(); + let total: u64 = result.counts.values().sum(); + prop_assert_eq!(total, shots as u64); + } + + /// Statevector norm is always 1.0 (±1e-9). + #[test] + fn statevector_norm_is_one(angle in -std::f64::consts::PI..=std::f64::consts::PI) { + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;" + ); + let sim = LocalSimulator::new(); + let result = sim.run(&CircuitSource(circuit), ShotCount(1), true).unwrap(); + let sv = result.statevector.unwrap(); + let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum(); + prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}"); + } + + /// Bitstrings in counts have length equal to the number of qubits. + #[test] + fn count_bitstrings_have_correct_length( + n_qubits in 1usize..=5usize, + shots in 10u32..=100u32, + ) { + let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect(); + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n" + ); + let sim = LocalSimulator::new(); + let result = sim.run(&CircuitSource(circuit), ShotCount(shots), false).unwrap(); + for key in result.counts.keys() { + prop_assert_eq!(key.len(), n_qubits, "wrong length for key '{key}'"); + } + } +} +``` + +- [ ] **Step 6: Run proptest** + +```bash +cargo test --test proptest 2>&1 +``` + +Expected: 3 property tests pass (each runs 256 cases). + +- [ ] **Step 7: Commit** + +```bash +git add src/lib.rs src/main.rs Cargo.toml tests/proptest/ +git commit -m "test: add property-based tests for shot normalisation, statevector norm, and bitstring length" +``` + +--- + +## Task 12: Benchmarks + +**Files:** +- Create: `benches/simulation.rs` + +- [ ] **Step 1: Create bench file** + +```bash +mkdir -p benches +``` + +`benches/simulation.rs`: + +```rust +use criterion::{criterion_group, criterion_main, Criterion}; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; + +const BELL: &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;"#; + +fn make_h_circuit(n: usize, shots: u32) -> (CircuitSource, ShotCount) { + let gates: String = (0..n).map(|i| format!("h q[{i}];\n")).collect(); + ( + CircuitSource(format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n{gates}c = measure q;\n" + )), + ShotCount(shots), + ) +} + +fn bench_bell_1024_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + c.bench_function("bell_1024_shots", |b| { + b.iter(|| sim.run(&CircuitSource(BELL.to_string()), ShotCount(1024), false).unwrap()) + }); +} + +fn bench_10_qubits_10k_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circ, shots) = make_h_circuit(10, 10_000); + c.bench_function("10_qubits_10k_shots", |b| { + b.iter(|| sim.run(&circ, shots, false).unwrap()) + }); +} + +fn bench_20_qubits_10k_shots(c: &mut Criterion) { + let sim = LocalSimulator::new(); + let (circ, shots) = make_h_circuit(20, 10_000); + c.bench_function("20_qubits_10k_shots", |b| { + b.iter(|| sim.run(&circ, shots, false).unwrap()) + }); +} + +criterion_group!(benches, bench_bell_1024_shots, bench_10_qubits_10k_shots, bench_20_qubits_10k_shots); +criterion_main!(benches); +``` + +- [ ] **Step 2: Run benchmarks (smoke pass — verifies they compile and run)** + +```bash +cargo bench -- --test 2>&1 +``` + +Expected: all three benchmark functions report a timing without panicking. + +- [ ] **Step 3: Run full benchmarks to check SLAs** + +```bash +cargo bench 2>&1 | grep -E "bell|qubits" +``` + +SLA targets (spec §6): +- `bell_1024_shots` < **5 ms** +- `10_qubits_10k_shots` < **50 ms** +- `20_qubits_10k_shots` < **500 ms** + +If any target is missed, the bottleneck is likely `extract_gate_ops` allocating on every call. Check if the AST is being re-parsed unnecessarily — consider caching or pre-converting on first call. + +- [ ] **Step 4: Commit** + +```bash +git add benches/simulation.rs +git commit -m "bench: add Criterion benchmarks for Bell, 10-qubit, and 20-qubit circuits" +``` + +--- + +## Task 13: Reference Golden Files + Cross-Validation + +**Files:** +- Create: `tests/reference/bell.qasm` +- Create: `tests/reference/ghz3.qasm` +- Create: `tests/reference/invalid/` (4 × .qasm + .error.txt pairs) +- Create: `scripts/gen_reference.py` +- Create: `tests/reference/cross_validate.rs` + +- [ ] **Step 1: Create directory structure and circuit files** + +```bash +mkdir -p tests/reference/invalid scripts +``` + +`tests/reference/bell.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +``` + +`tests/reference/ghz3.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +bit[3] c; +h q[0]; +cx q[0], q[1]; +cx q[1], q[2]; +c = measure q; +``` + +`tests/reference/invalid/unsupported_gate.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0]; +``` + +`tests/reference/invalid/unsupported_gate.error.txt`: +``` +gate 'u3' is not supported +``` + +`tests/reference/invalid/undeclared_qubit.qasm`: +``` +OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +h q[5]; +``` + +`tests/reference/invalid/undeclared_qubit.error.txt`: +``` +out of range +``` + +`tests/reference/invalid/syntax_error.qasm`: +``` +OPENQASM 3.0; +this is not valid; +``` + +`tests/reference/invalid/syntax_error.error.txt`: +``` +error +``` + +`tests/reference/invalid/qubit_limit.qasm`: +``` +OPENQASM 3.0; +qubit[29] q; +``` + +`tests/reference/invalid/qubit_limit.error.txt`: +``` +exceeds +``` + +- [ ] **Step 2: Write scripts/gen_reference.py** + +```python +#!/usr/bin/env python3 +"""Generate reference counts/statevectors via Qiskit Aer. Requires: pip install qiskit qiskit-aer""" +import json, sys +from pathlib import Path + +try: + from qiskit import QuantumCircuit + from qiskit_aer import AerSimulator +except ImportError: + print("ERROR: pip install qiskit qiskit-aer", file=sys.stderr); sys.exit(1) + +SHOTS = 10_000 +SEED = 42 +REF_DIR = Path(__file__).parent.parent / "tests" / "reference" + +def run_circuit(qasm_path: Path) -> dict: + qc = QuantumCircuit.from_qasm_file(str(qasm_path)) + sim = AerSimulator(method="statevector") + counts = sim.run(qc, shots=SHOTS, seed_simulator=SEED).result().get_counts() + qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path)) + qc_sv.save_statevector() + sv = list(sim.run(qc_sv, shots=0).result().get_statevector()) + return { + "shots": SHOTS, "seed": SEED, + "counts": {k: int(v) for k, v in counts.items()}, + "statevector": [[float(a.real), float(a.imag)] for a in sv], + } + +if __name__ == "__main__": + for qasm in REF_DIR.glob("*.qasm"): + out = REF_DIR / qasm.with_suffix(".json").name + data = run_circuit(qasm) + out.write_text(json.dumps(data, indent=2)) + print(f"wrote {out}") +``` + +- [ ] **Step 3: Write tests/reference/cross_validate.rs** + +```rust +//! Cross-validates LocalSimulator counts against Qiskit golden files (chi-squared, α=0.01). + +use std::collections::HashMap; +use std::path::Path; + +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; +use quantum_bridge_mcp::validator::CircuitValidator; + +fn load_circuit(name: &str) -> CircuitSource { + CircuitSource(std::fs::read_to_string(Path::new("tests/reference").join(name)).unwrap()) +} + +fn load_expected_counts(name: &str) -> HashMap { + let path = Path::new("tests/reference").join(name); + if !path.exists() { return HashMap::new(); } + let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + data["counts"].as_object().unwrap().iter() + .map(|(k, v)| (k.clone(), v.as_u64().unwrap())) + .collect() +} + +fn chi_squared_passes(observed: &HashMap, expected: &HashMap) -> bool { + if expected.is_empty() { return true; } + let chi_sq: f64 = expected.iter().map(|(k, &exp)| { + let obs = *observed.get(k).unwrap_or(&0) as f64; + let e = exp as f64; + if e == 0.0 { 0.0 } else { (obs - e).powi(2) / e } + }).sum(); + let critical = (expected.len() - 1) as f64 * 3.0 + 6.63; // conservative + chi_sq < critical +} + +#[test] +fn bell_counts_match_qiskit_golden() { + let expected = load_expected_counts("bell_counts.json"); + if expected.is_empty() { return; } + let result = LocalSimulator::new().run(&load_circuit("bell.qasm"), ShotCount(10_000), false).unwrap(); + assert!(chi_squared_passes(&result.counts, &expected), + "Bell diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected); +} + +#[test] +fn ghz3_counts_match_qiskit_golden() { + let expected = load_expected_counts("ghz3_counts.json"); + if expected.is_empty() { return; } + let result = LocalSimulator::new().run(&load_circuit("ghz3.qasm"), ShotCount(10_000), false).unwrap(); + assert!(chi_squared_passes(&result.counts, &expected), + "GHZ-3 diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected); +} + +#[test] +fn invalid_circuits_produce_validation_errors() { + use std::fs; + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + for entry in fs::read_dir("tests/reference/invalid").unwrap() { + let path = entry.unwrap().path(); + if path.extension().map(|e| e == "qasm").unwrap_or(false) { + let expected_frag = fs::read_to_string(path.with_extension("error.txt")) + .unwrap().trim().to_lowercase(); + let source = fs::read_to_string(&path).unwrap(); + let result = validator.validate(&quantum_bridge_mcp::types::CircuitSource(source)).unwrap(); + assert!(!result.is_valid, "{} should be invalid", path.display()); + let msgs: String = result.diagnostics.iter().map(|d| d.message.to_lowercase()).collect::>().join(" "); + assert!(msgs.contains(&expected_frag), + "expected '{}' in messages for {}\nGot: {}", expected_frag, path.display(), msgs); + } + } +} +``` + +- [ ] **Step 4: Run cross-validation tests** + +```bash +cargo test --test cross_validate 2>&1 +``` + +Expected: `bell_counts_match_qiskit_golden` and `ghz3_counts_match_qiskit_golden` are **skipped** (no JSON yet); `invalid_circuits_produce_validation_errors` **passes**. + +Optional — generate golden files (needs Python + Qiskit): +```bash +python3 scripts/gen_reference.py && cargo test --test cross_validate +``` + +- [ ] **Step 5: Commit** + +```bash +git add tests/reference/ scripts/gen_reference.py +git commit -m "test: add cross-validation golden files and gen_reference.py script" +``` + +--- + +## Task 14: GitHub Actions CI + Release + +**Files:** +- Create: `.github/workflows/ci.yml` +- Create: `.github/workflows/release.yml` + +- [ ] **Step 1: Create .github/workflows/ci.yml** + +```bash +mkdir -p .github/workflows +``` + +```yaml +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + rust: + name: Rust checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - run: cargo fmt --check + - run: cargo clippy -- -D warnings + - run: cargo test + - run: cargo bench -- --test + + cross-validate: + name: Cross-validate with Qiskit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-qiskit + - run: pip install qiskit qiskit-aer + - run: python3 scripts/gen_reference.py + - run: cargo test --test cross_validate +``` + +- [ ] **Step 2: Create .github/workflows/release.yml** + +```yaml +name: Release + +on: + push: + tags: + - "v*" + +jobs: + dist: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } + - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu } + - { os: macos-latest, target: x86_64-apple-darwin } + - { os: macos-latest, target: aarch64-apple-darwin } + - { os: windows-latest, target: x86_64-pc-windows-msvc } + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - name: Install cross (aarch64 Linux) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: cargo install cross + - name: Build + shell: bash + run: | + if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then + cross build --release --target ${{ matrix.target }} + else + cargo build --release --target ${{ matrix.target }} + fi + - name: Package (Unix) + if: matrix.os != 'windows-latest' + run: | + cd target/${{ matrix.target }}/release + tar czf quantum-bridge-mcp-${{ matrix.target }}.tar.gz quantum-bridge-mcp + mv quantum-bridge-mcp-${{ matrix.target }}.tar.gz ../../../ + - name: Package (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + cd target/${{ matrix.target }}/release + Compress-Archive quantum-bridge-mcp.exe quantum-bridge-mcp-${{ matrix.target }}.zip + mv quantum-bridge-mcp-${{ matrix.target }}.zip ../../../ + - uses: actions/upload-artifact@v4 + with: + name: quantum-bridge-mcp-${{ matrix.target }} + path: quantum-bridge-mcp-${{ matrix.target }}.* + + github-release: + needs: dist + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: true + - uses: softprops/action-gh-release@v2 + with: + files: "quantum-bridge-mcp-*" + generate_release_notes: true +``` + +- [ ] **Step 3: Validate YAML** + +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo "ci.yml OK" +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo "release.yml OK" +``` + +- [ ] **Step 4: Full local verification pass** + +```bash +cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10 +``` + +Expected: all green. + +- [ ] **Step 5: Commit** + +```bash +git add .github/ +git commit -m "ci: add GitHub Actions for Rust checks, Qiskit cross-validation, and multi-platform release" +``` + +--- + +## Final verification — all sub-plans complete + +```bash +cargo fmt --check \ + && cargo clippy -- -D warnings \ + && cargo test \ + && cargo test --test integration \ + && cargo test --test proptest \ + && cargo test --test cross_validate \ + && cargo bench -- --test \ + && echo "ALL CHECKS PASSED" +``` + +Expected: `ALL CHECKS PASSED`. V1 implementation complete. diff --git a/docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md b/docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md new file mode 100644 index 0000000..f6d34f0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-28-quantum-bridge-mcp-design.md @@ -0,0 +1,236 @@ +# quantum-bridge-mcp — Design Spec + +**Date:** 2026-04-28 +**Status:** Approved (revised after competitive analysis) +**Author:** Vincent Bourdon + +--- + +## 1. Problème et positionnement + +Les développeurs sans expertise quantique ne peuvent pas intégrer du calcul quantique dans leurs projets sans quitter leur workflow et apprendre Qiskit. L'écosystème MCP quantique existant (Qiskit/mcp-servers officiel IBM, plusieurs forks communautaires) est **100% Python**, dépend de l'installation lourde de Qiskit (~200 Mo + venv + résolution pip), et requiert souvent un compte IBM même pour de la simulation locale. + +**Cible :** développeurs qui veulent intégrer ou prototyper du calcul quantique simulé sans installer l'écosystème Python/Qiskit. + +**Positionnement :** *« Le simulateur quantique le plus léger et le plus performant pour Claude — un binaire, zéro setup, zéro réseau, zéro compte »*. + +Trois angles défendables face au MCP officiel IBM : +- **Léger** : binaire unique ~10 Mo vs ~200 Mo de Qiskit + dépendances Python +- **Rapide à démarrer** : pas de cold start Python, pas de pip resolve +- **Performant** : moteur Spinoza (pure Rust), top tier single-thread sur 5–25 qubits +- **Privacy / air-gapped** : aucune télémétrie, aucun appel réseau, fonctionne hors-ligne et en CI minimal + +--- + +## 2. Scope V1 vs V1.5 + +| Phase | Périmètre | Cible | +|-------|-----------|-------| +| **V1** | Simulateur local uniquement, mode synchrone, OpenQASM 3.0 | livrable rapide, valeur immédiate | +| **V1.5** | Ajout backend IBM Quantum (token utilisateur), mode asynchrone avec persistence des job_ids pour reprise de session | après validation V1 | + +V1 ne couvre **pas** : IBM Quantum, autres providers (AWS Braket, Azure, IonQ), simulation avec bruit, monétisation/tiers, gestion de licence. + +--- + +## 3. Outils MCP exposés (V1) + +| Outil | Description | +|-------|-------------| +| `list_backends` | Liste les backends disponibles (en V1 : seulement le simulateur local + ses capacités : qubits max, gates supportées) | +| `validate_circuit` | Parse un programme OpenQASM 3.0, vérifie sémantique (qubits déclarés, gates supportées, mesures bien formées). Retourne diagnostics structurés. | +| `run_circuit` | Exécute un circuit OpenQASM 3.0 sur le simulateur local (synchrone). Retourne counts + statevector optionnel + métadonnées. | + +Trois outils en V1, contre six initialement prévus. La compression vient du mode synchrone (pas de cycle submit/status/results pour le local) et du retrait du `select_backend` (un seul backend en V1) et de `estimate_cost` (gratuit en local). + +V1.5 réintroduira : `submit_circuit`, `get_job_status`, `get_results` (cycle async pour IBM), `select_backend`, `estimate_cost`. + +Claude se charge de générer le circuit OpenQASM **et** d'interpréter les counts en langage clair. Le MCP fait exclusivement parsing, validation et exécution. + +--- + +## 4. Architecture (V1) + +``` +Claude (génère le circuit OpenQASM 3.0) + ↓ + MCP Server (JSON-RPC sur stdio via rmcp) + ↓ + ┌─────────────────────┐ + │ validate_circuit │ → oq3_semantics + │ │ (AST + analyse sémantique) + └─────────────────────┘ + ↓ + ┌─────────────────────┐ + │ run_circuit │ → AST → mapping vers gates Spinoza + │ │ → spinoza::simulate + │ │ → counts (sampling N shots) + └─────────────────────┘ + ↓ + Retour à Claude + (counts + métadonnées brutes — Claude interprète) +``` + +### Composants internes + +1. **MCP layer** — utilise le SDK officiel `rmcp` (modelcontextprotocol/rust-sdk) plutôt qu'une implémentation JSON-RPC manuelle. Réduit dette de maintenance et risque de divergence avec le protocole. + +2. **CircuitValidator** — wrappe `oq3_semantics` (parser officiel Qiskit en Rust). Produit des diagnostics structurés (ligne, colonne, message) que Claude peut afficher tel quel à l'utilisateur. Vérifie : + - syntaxe OpenQASM 3.0 valide + - tous les qubits référencés sont déclarés + - toutes les gates utilisées sont supportées par le moteur + - nombre de qubits ≤ limite configurable (défaut 28, ajustable selon RAM) + - mesures correctement mappées sur des classical bits + +3. **CircuitExecutor** — convertit l'AST OpenQASM 3 en séquence de gates Spinoza, lance la simulation, échantillonne N shots à partir des probabilités du statevector final. + +4. **Pas de JobTracker, pas de persistence** en V1 — l'exécution est synchrone, retour direct. Ces composants reviennent en V1.5 pour les jobs IBM. + +5. **Pas de ResultInterpreter en Rust** — l'interprétation langage naturel des counts (« état de Bell », « superposition », etc.) est laissée à Claude qui le fait nativement bien mieux qu'une table de patterns figée. + +### Gates supportées (V1) + +Pauli (X, Y, Z), Hadamard (H), phase (S, T, Sdg, Tdg), rotations (RX, RY, RZ), CNOT, CZ, SWAP, Toffoli (CCX), mesures. + +Toute gate non listée doit produire une erreur explicite via `validate_circuit` avec la liste des gates supportées. + +--- + +## 5. Stack technique + +| Composant | Choix | Justification | +|-----------|-------|---------------| +| Langage | Rust (édition 2021) | Cohérence positionnement, perf, binaire unique | +| MCP SDK | `rmcp` (officiel) | Évite réimplémenter JSON-RPC stdio à la main | +| Parser OpenQASM 3 | `oq3_semantics` (Qiskit/openqasm3_parser) | Officiel, AST + analyse sémantique, bons diagnostics | +| Moteur simulation | `spinoza` (QuState/Wells Fargo) | Pure Rust, top tier 5–25 qubits, mode f32 optionnel | +| Async runtime | `tokio` | Nécessaire pour rmcp | +| Sérialisation | `serde` + `serde_json` | Standard | +| Logs | `tracing` | Standard observabilité Rust | + +Aucune dépendance Python en runtime. Distribution : binaires précompilés via `cargo-dist` (Linux/macOS/Windows × x86_64/arm64) + `cargo install` pour les utilisateurs Rust. + +--- + +## 6. Performances cibles + +Cibles basées sur Spinoza single-thread sur laptop moderne (M-series ou Ryzen équivalent) : + +| Circuit | Cible | +|---------|-------| +| Bell (2 qubits, 2 gates, 1024 shots) | **< 5 ms** | +| 10 qubits / 50 gates / 10k shots | **< 50 ms** | +| 20 qubits / 100 gates / 10k shots | **< 500 ms** | +| 25 qubits / 100 gates / 1k shots | **< 5 s** | +| Limite pratique RAM 16 Go | **28 qubits** | + +Mode `--f32` optionnel pour +30% de vitesse et +1 qubit à RAM égale (perte de précision négligeable pour les counts en pratique). + +--- + +## 7. Structure de fichiers + +``` +quantum-bridge-mcp/ +├── src/ +│ ├── main.rs # Point d'entrée, MCP server stdio (rmcp) +│ ├── tools/ +│ │ ├── mod.rs +│ │ ├── list_backends.rs +│ │ ├── validate_circuit.rs +│ │ └── run_circuit.rs +│ ├── validator.rs # Wrapper oq3_semantics +│ ├── executor.rs # AST → Spinoza +│ └── error.rs # Diagnostics structurés +├── tests/ +│ ├── integration/ # Tests intégration MCP +│ ├── reference/ # Circuits QASM3 + résultats Qiskit attendus +│ └── proptest/ # Property-based tests +├── benches/ # Benchmarks Criterion +├── scripts/ +│ └── gen_reference.py # Génère counts/statevectors via Qiskit Aer +├── Cargo.toml +├── .github/workflows/ +│ ├── ci.yml # Tests Rust + cross-validation Qiskit +│ └── release.yml # cargo-dist +└── README.md +``` + +--- + +## 8. Stratégie de tests + +Sept couches, du plus déterministe au plus statistique : + +1. **Unit tests gates** — chaque gate appliquée à un état initial connu produit le statevector exact attendu (tolérance 1e-10). + +2. **Tests statistiques counts** — circuits canoniques (Bell, GHZ, superposition uniforme) exécutés en N=10k shots, comparaison aux distributions attendues via test du chi² (α=0.01). + +3. **Cross-validation Qiskit** *(critique)* — un dossier `tests/reference/` contient ~30 circuits QASM3 avec leurs statevectors et counts pré-calculés via Qiskit Aer (script Python séparé, exécuté en CI). Comparaison bit-à-bit du statevector (tolérance 1e-10) et statistique des counts. **Indispensable** pour rattraper les bugs d'endianness / ordre de qubits. + +4. **Property-based tests** (`proptest`) — invariants : `U·U† = I`, `Σ|amplitudes|² = 1`, idempotence des mesures pures. + +5. **Tests de validation** — golden tests : dossier de fichiers QASM3 invalides + messages d'erreur exacts attendus. Couvre : qubit hors range, gate non supportée, mesure mal mappée, syntaxe invalide. + +6. **Tests protocole MCP** — roundtrip JSON-RPC sur mock stdio, conformité schéma tools, format erreurs MCP. + +7. **Smoke perf** (Criterion) — Bell < 5 ms, 20 qubits / 100 gates < 500 ms, alarmes si régression > 20%. + +CI : Rust toolchain + Python 3.11 + Qiskit Aer pour la couche 3 (Python utilisé **uniquement** en CI, jamais runtime). + +--- + +## 9. Distribution + +| Cible | Méthode | Audience | +|-------|---------|----------| +| `cargo install quantum-bridge-mcp` | crates.io | Utilisateurs Rust | +| Binaires précompilés | GitHub Releases via `cargo-dist` | Cible principale (devs sans Rust) | +| Installer one-liner | `curl ... \| sh` | Setup rapide depuis README Claude | +| Homebrew tap | `brew install vbourdon/tap/quantum-bridge-mcp` | macOS power users (post-V1) | + +Targets de build : `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`. + +--- + +## 10. Critères de succès V1 + +- [ ] Binaire installable sans Rust toolchain sur Linux/macOS/Windows +- [ ] Bell state < 5 ms sur laptop moderne +- [ ] 20 qubits / 100 gates / 10k shots < 500 ms +- [ ] Les 3 outils MCP sont opérationnels et conformes au protocole +- [ ] Validation produit des messages d'erreur actionnables (ligne, colonne, suggestion) +- [ ] Cross-validation Qiskit passe sur les 30 circuits de référence +- [ ] README avec exemple d'usage Claude Code end-to-end +- [ ] CI verte (lint, tests Rust, cross-validation Qiskit) + +--- + +## 11. Hors scope V1 (pour mémoire) + +Repoussé en V1.5 : +- Backend IBM Quantum (token utilisateur, REST API ou Qiskit Runtime) +- Mode asynchrone (`submit_circuit` / `get_job_status` / `get_results`) +- Persistence des job_ids (sled ou JSON file dans `~/.local/share/quantum-bridge-mcp/`) +- `select_backend` / `estimate_cost` +- Gestion d'erreurs réseau, retry, backoff + +Repoussé en V2+ : +- Simulation avec bruit (decoherence, gate errors, readout errors) +- Simulation par densité matricielle +- Autres providers quantiques (AWS Braket, Azure Quantum, IonQ) +- Monétisation / système de licences +- Dashboard web d'usage +- Support OpenQASM 2.0 (le standard a basculé sur 3.0) + +--- + +## 12. Risques identifiés + +| Risque | Mitigation | +|--------|------------| +| `oq3_semantics` mal documenté ou incomplet sur certaines features QASM3 | POC parsing dès la 1ère semaine, fallback sur `jlapeyre/openqasm-rust` si bloquant | +| Spinoza API instable ou licence ambiguë | Vérifier licence (Apache 2.0 attendu) avant intégration ; alternative roqoqo identifiée | +| MCP officiel IBM cannibalise le projet | Le positionnement « binaire léger » reste défendable ; surveiller leur roadmap | +| Bugs d'endianness silencieux | Cross-validation Qiskit obligatoire en CI dès le début | +| RAM saturée à 28+ qubits chez user | Limite configurable, message d'erreur clair suggérant V1.5/IBM | diff --git a/docs/superpowers/specs/2026-04-29-quantum-tutor-design.md b/docs/superpowers/specs/2026-04-29-quantum-tutor-design.md new file mode 100644 index 0000000..20c76fb --- /dev/null +++ b/docs/superpowers/specs/2026-04-29-quantum-tutor-design.md @@ -0,0 +1,264 @@ +# Quantum Tutor — Design Spec + +**Date:** 2026-04-29 +**Status:** Approved +**Scope:** Extension pédagogique de quantum-bridge-mcp — 4 nouveaux outils MCP + curriculum JSON + suivi de progression + +--- + +## 1. Contexte et objectif + +quantum-bridge-mcp est un serveur MCP Rust qui expose 3 outils de simulation de circuits OpenQASM 3.0. Ce module ajoute une **couche pédagogique** au-dessus des outils existants : un tuteur quantique qui guide l'utilisateur à travers un curriculum structuré, vérifie ses exercices, et suit sa progression. + +**Utilisateur cible :** débutant en informatique quantique avec des notions vagues de superposition/intrication. Objectif : aller des bases jusqu'aux algorithmes (Grover). + +**Style d'apprentissage :** explication courte → exercice guidé → exploration libre. Maths introduites progressivement quand elles deviennent nécessaires. + +--- + +## 2. Nouveaux outils MCP + +### 2.1 `get_lesson` + +``` +get_lesson(module_id: u32, lesson_id?: u32) → JSON +``` + +Retourne le contenu d'une leçon : explication du concept, circuit(s) d'illustration à exécuter, et exercice(s) à résoudre. + +**Paramètres :** +- `module_id` : 1–7 (voir curriculum §4) +- `lesson_id` : optionnel ; si absent, retourne la première leçon non complétée du module + +**Réponse :** +```json +{ + "module_id": 2, + "lesson_id": 1, + "title": "La porte H et la superposition", + "concept": "...", + "example_circuit": "OPENQASM 3.0;\n...", + "what_to_observe": "Lance run_circuit avec 1000 shots. Tu devrais voir ~500 '0' et ~500 '1'.", + "exercise": { + "id": "2-1-a", + "prompt": "Écris un circuit qui met un qubit en superposition parfaite et le mesure.", + "hint": "Une seule porte suffit avant la mesure." + } +} +``` + +**Comportement :** si `lesson_id` est absent et que `get_progress()` indique que le module est déjà complété, retourne un résumé du module avec suggestion d'avancer. + +--- + +### 2.2 `check_exercise` + +``` +check_exercise(exercise_id: str, circuit: str) → JSON +``` + +Vérifie si le circuit soumis résout l'exercice identifié par `exercise_id`. Exécute le circuit en interne (1024 shots), compare les résultats aux critères définis dans `curriculum.json`, met à jour la progression. + +**Paramètres :** +- `exercise_id` : identifiant de l'exercice (ex: `"2-1-a"`) +- `circuit` : source OpenQASM 3.0 + +**Réponse (succès) :** +```json +{ + "passed": true, + "exercise_id": "2-1-a", + "feedback": "Parfait ! Ta porte H crée bien une superposition équilibrée. Tu observes ~50% de '0' et ~50% de '1'.", + "counts": {"0": 512, "1": 512}, + "progress_updated": true +} +``` + +**Réponse (échec) :** +```json +{ + "passed": false, + "exercise_id": "2-1-a", + "feedback": "Ton circuit produit toujours '0'. Le qubit n'est pas en superposition. Essaie d'ajouter une porte avant la mesure.", + "counts": {"0": 1024}, + "hint": "La porte H transforme |0⟩ en (|0⟩ + |1⟩)/√2." +} +``` + +**Critères de vérification (définis par exercice dans curriculum.json) :** +- `required_outcomes` : liste de bitstrings qui doivent apparaître (avec proportion min/max optionnelle) +- `forbidden_outcomes` : bitstrings qui ne doivent pas apparaître +- `statevector_check` : optionnel — amplitudes attendues avec tolérance ε=1e-6 + +**Erreurs :** si le circuit est invalide, retourne l'erreur de validation avec position (réutilise `CircuitValidator`). + +--- + +### 2.3 `explain_result` + +``` +explain_result(circuit: str, counts: object, statevector?: array) → JSON +``` + +Génère une explication pédagogique de ce qui s'est passé dans le circuit : quelles portes ont fait quoi, pourquoi on observe cette distribution, ce que ça signifie quantiquement. + +**Paramètres :** +- `circuit` : source OpenQASM 3.0 (le circuit qui vient d'être exécuté) +- `counts` : résultat de `run_circuit` (bitstring → count) +- `statevector` : optionnel — amplitudes complexes de `run_circuit` avec `return_statevector: true` + +**Réponse :** +```json +{ + "gate_breakdown": [ + {"gate": "h q[0]", "qubit": 0, "description": "Hadamard — crée une superposition égale", "effect_on_zero": "Transforme |0⟩ en (|0⟩+|1⟩)/√2"}, + {"gate": "cx q[0], q[1]", "control": 0, "target": 1, "description": "CNOT — flip la cible si le contrôle est |1⟩"} + ], + "num_qubits": 2, + "key_concept": "intrication", + "dominant_outcomes": ["00", "11"], + "missing_outcomes": ["01", "10"], + "statevector_summary": {"non_zero_amplitudes": [0, 3], "zero_amplitudes": [1, 2]} +} +``` + +**Implémentation :** le serveur Rust analyse le circuit via `CircuitValidator` et retourne des **données structurées** (liste de gates avec descriptions depuis `curriculum.json`, statistiques des counts, concept clé). C'est Claude (le client MCP) qui synthétise l'explication en langage naturel à partir de ces données — cette séparation exploite les capacités respectives du simulateur (analyse structurelle) et du LLM (narration pédagogique). + +--- + +### 2.4 `get_progress` + +``` +get_progress() → JSON +``` + +Retourne l'état de progression de l'utilisateur dans le curriculum. + +**Réponse :** +```json +{ + "current_module": 2, + "current_lesson": 1, + "modules": [ + {"id": 1, "title": "Le qubit", "status": "completed", "exercises_solved": 3}, + {"id": 2, "title": "Superposition", "status": "in_progress", "exercises_solved": 0}, + {"id": 3, "title": "Interférence", "status": "locked", "exercises_solved": 0} + ], + "total_exercises_solved": 3, + "total_exercises": 18, + "percent_complete": 17 +} +``` + +**Persistence :** fichier JSON à `~/.config/quantum-bridge-mcp/progress.json`. Créé automatiquement au premier appel avec progression à zéro. + +--- + +## 3. Architecture interne + +### Nouveaux fichiers + +``` +curriculum/ + curriculum.json — leçons, exercices, critères de vérification, descriptions de gates +src/ + tutor.rs — CurriculumLoader + ExerciseChecker (logique de vérification) + progress.rs — ProgressStore (lecture/écriture progress.json) + tools/ + tutor_tools.rs — handlers MCP des 4 nouveaux outils +``` + +### Réutilisation des composants existants + +| Composant existant | Réutilisé par | +|---|---| +| `CircuitValidator` (`src/validator.rs`) | `check_exercise` (validation avant exécution), `explain_result` (analyse des gates) | +| `LocalSimulator` via `CanExecute` | `check_exercise` (exécution interne des circuits) | +| `CircuitSource`, `ShotCount`, `SimulationResult` (`src/types.rs`) | `check_exercise`, `explain_result` | +| Trait `Backend` | `check_exercise` — dépend de `&dyn Backend`, compatible IBM V1.5 | + +### Contraintes d'architecture + +- Les outils tutor dépendent de `&dyn Backend`, jamais de `LocalSimulator` directement (même règle que les outils existants). +- `check_exercise` utilise `ShotCount(1024)` fixe pour la vérification (reproductibilité statistique). +- `ExerciseChecker` compare avec tolérance : un résultat passe si chaque outcome requis représente au moins `min_ratio - 2σ` des shots (σ calculé pour N=1024 shots, distribution binomiale). + +--- + +## 4. Curriculum — structure des 7 modules + +| Module | Titre | Concepts clés | Nb exercices | +|--------|-------|--------------|--------------| +| 1 | Le qubit | États \|0⟩/\|1⟩, gate X, mesure | 2 | +| 2 | Superposition | Gate H, état \|+⟩, distribution 50/50 | 3 | +| 3 | Interférence et phase | H·H=I, phase relative, gates Z/S/T/RZ | 3 | +| 4 | 2 qubits et intrication | Bell state, CNOT, mesure corrélée | 3 | +| 5 | Circuits multi-qubits | GHZ, Toffoli, statevecteur complet | 3 | +| 6 | Premiers algorithmes | Deutsch-Jozsa, Bernstein-Vazirani, téléportation | 2 | +| 7 | Algorithme de Grover | Oracle de phase, opérateur de diffusion | 2 | + +**Total : 18 exercices** sur 7 modules. + +Les modules 2–7 sont verrouillés jusqu'à complétion du module précédent. Exception : le module suivant se déverrouille dès qu'au moins 2/3 des exercices du module courant sont résolus. + +--- + +## 5. Format curriculum.json (schéma) + +```json +{ + "version": "1.0", + "gate_descriptions": { + "h": { + "short": "Hadamard — crée une superposition égale", + "effect_on_zero": "Transforme |0⟩ en (|0⟩+|1⟩)/√2", + "effect_on_one": "Transforme |1⟩ en (|0⟩-|1⟩)/√2" + } + }, + "modules": [ + { + "id": 1, + "title": "Le qubit", + "lessons": [ + { + "id": 1, + "title": "États |0⟩ et |1⟩", + "concept": "...", + "example_circuit": "OPENQASM 3.0;\n...", + "what_to_observe": "...", + "exercises": [ + { + "id": "1-1-a", + "prompt": "...", + "hint": "...", + "criteria": { + "required_outcomes": [{"bitstring": "1", "min_ratio": 0.95}], + "forbidden_outcomes": ["0"] + } + } + ] + } + ] + } + ] +} +``` + +--- + +## 6. Tests + +- **Unitaires** : `ExerciseChecker` avec critères simples (gate X → bitstring "1" avec ratio 1.0) +- **Intégration** : `check_exercise` roundtrip via MCP JSON-RPC pour 3 exercices représentatifs (un par module difficile) +- **Golden files** : `tests/tutor/` — circuits d'exercices + résultats attendus (pass/fail) +- **Progress** : `ProgressStore` — création, mise à jour, lecture, reset + +--- + +## 7. Hors scope (V1) + +- Visualisation sphère de Bloch (V2) +- Dessin de circuits en ASCII/SVG (V2) +- Explications générées par LLM (V2) +- Support multi-utilisateurs (V2) +- Gamification / badges (V2) diff --git a/src/circuit_analyzer.rs b/src/circuit_analyzer.rs new file mode 100644 index 0000000..c17fccf --- /dev/null +++ b/src/circuit_analyzer.rs @@ -0,0 +1,47 @@ +use crate::error::BridgeError; +use crate::types::CircuitSource; + +#[derive(Debug, Clone, PartialEq)] +pub struct GateCallInfo { + pub name: String, + pub params: Vec, + pub qubits: Vec, +} + +pub struct CircuitAnalyzer; + +impl CircuitAnalyzer { + pub fn list_gates(source: &CircuitSource) -> Result, BridgeError> { + crate::executor::list_gate_calls(source) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const BELL: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + + #[test] + fn list_gates_for_bell_returns_h_then_cx() { + let gates = CircuitAnalyzer::list_gates(&CircuitSource(BELL.into())).unwrap(); + assert_eq!(gates.len(), 2); + assert_eq!(gates[0].name, "h"); + assert_eq!(gates[0].qubits, vec![0]); + assert_eq!(gates[1].name, "cx"); + assert_eq!(gates[1].qubits, vec![0, 1]); + } + + #[test] + fn list_gates_ignores_measure() { + let gates = CircuitAnalyzer::list_gates(&CircuitSource(BELL.into())).unwrap(); + assert!(gates.iter().all(|g| g.name != "measure")); + } + + #[test] + fn list_gates_returns_empty_for_circuit_with_no_gates() { + let identity = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + let gates = CircuitAnalyzer::list_gates(&CircuitSource(identity.into())).unwrap(); + assert!(gates.is_empty()); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d36b4f6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,79 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BridgeError { + #[error("parse error at line {line}, col {col}: {message}")] + Parse { + line: usize, + col: usize, + message: String, + }, + + #[error("gate '{gate}' is not supported at line {line} — supported: {supported}")] + UnsupportedGate { + gate: String, + line: usize, + supported: String, + }, + + #[error("qubit index {index} is out of range (circuit declares {declared} qubits)")] + QubitOutOfRange { index: usize, declared: usize }, + + #[error("circuit requires {requested} qubits, exceeds the local simulator limit of {limit}")] + QubitLimitExceeded { requested: usize, limit: usize }, + + #[error("simulation failed: {0}")] + Simulation(String), + + #[error("curriculum data is malformed: {0}")] + Curriculum(String), + + #[error("configuration error: {0}")] + Configuration(String), + + #[error("measure at line {line} maps to an undeclared classical bit")] + MeasurementMapping { line: usize }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_error_display_includes_line_and_col() { + let err = BridgeError::Parse { + line: 3, + col: 7, + message: "unexpected token".to_string(), + }; + let msg = err.to_string(); + assert!(msg.contains("3"), "expected line 3 in: {msg}"); + assert!(msg.contains("7"), "expected col 7 in: {msg}"); + assert!( + msg.contains("unexpected token"), + "expected message in: {msg}" + ); + } + + #[test] + fn qubit_limit_exceeded_display_includes_counts() { + let err = BridgeError::QubitLimitExceeded { + requested: 30, + limit: 28, + }; + let msg = err.to_string(); + assert!(msg.contains("30"), "expected requested in: {msg}"); + assert!(msg.contains("28"), "expected limit in: {msg}"); + } + + #[test] + fn unsupported_gate_display_includes_gate_name() { + let err = BridgeError::UnsupportedGate { + gate: "u3".to_string(), + line: 5, + supported: "h, cx".to_string(), + }; + let msg = err.to_string(); + assert!(msg.contains("u3"), "expected gate name in: {msg}"); + } +} diff --git a/src/executor.rs b/src/executor.rs new file mode 100644 index 0000000..461648c --- /dev/null +++ b/src/executor.rs @@ -0,0 +1,581 @@ +use std::collections::HashMap; +use std::time::Instant; + +use oq3_semantics::asg::{Expr, GateOperand, IndexOperator, Literal, Program, Stmt, TExpr}; +use oq3_semantics::symbols::{SymbolId, SymbolTable, SymbolType}; +use oq3_semantics::syntax_to_semantics::parse_source_string; +use oq3_semantics::types::{ArrayDims, Type}; +use rand::distributions::{Distribution, WeightedIndex}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use spinoza::core::State; +use spinoza::gates::{apply, c_apply, cc_apply, Gate}; +use spinoza::math::{Float, PI}; + +use crate::error::BridgeError; +use crate::types::{CircuitSource, ShotCount, SimulationResult, ValidationResult}; + +pub const MAX_LOCAL_QUBITS: usize = 28; + +pub const SUPPORTED_GATES: &[&str] = &[ + "h", "x", "y", "z", "s", "t", "sdg", "tdg", "rx", "ry", "rz", "cx", "cz", "swap", "ccx", + "measure", +]; + +pub trait CanIntrospect { + fn name(&self) -> &str; + fn max_qubits(&self) -> usize; + fn supported_gates(&self) -> &[&str]; +} + +pub trait CanValidate { + fn validate(&self, circuit: &CircuitSource) -> Result; +} + +pub trait CanExecute { + fn run( + &self, + circuit: &CircuitSource, + shots: ShotCount, + return_statevector: bool, + ) -> Result; +} + +/// Marker trait combining all three capabilities. V1.5 IBM backend will impl this too. +pub trait Backend: CanIntrospect + CanValidate + CanExecute + Send + Sync {} + +// ── LocalSimulator ───────────────────────────────────────────────────────── + +/// A resolved gate operation: (gate_name, params, flat_qubit_indices). +type GateOp = (String, Vec, Vec); + +pub struct LocalSimulator { + seed: Option, +} + +impl LocalSimulator { + pub fn new() -> Self { + Self { seed: None } + } + + pub fn with_seed(seed: u64) -> Self { + Self { seed: Some(seed) } + } +} + +impl Default for LocalSimulator { + fn default() -> Self { + Self::new() + } +} + +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 context = parse_result.take_context(); + let symbol_table = context.symbol_table(); + let program = context.program(); + + let (num_qubits, register_offsets) = build_register_map(program, symbol_table); + + 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, symbol_table, ®ister_offsets)? + { + 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, i)) + .collect(), + ) + } else { + None + }; + + // Compute per-basis-state probabilities from the statevector, then draw + // `shots` samples from the categorical distribution. O(2^n + shots·log(2^n)) + // vs the previous reservoir_sampling which was O(shots² · 2^n). + let num_basis_states = 1usize << num_qubits; + let probs: Vec = (0..num_basis_states) + .map(|i| { + let r = state.reals[i] as f64; + let im = state.imags[i] as f64; + r * r + im * im + }) + .collect(); + + let dist = WeightedIndex::new(&probs) + .map_err(|e| BridgeError::Simulation(format!("sampling error: {e}")))?; + + let mut rng: StdRng = match self.seed { + Some(s) => StdRng::seed_from_u64(s), + None => StdRng::from_entropy(), + }; + + let mut counts: HashMap = HashMap::new(); + for _ in 0..shots.0 { + let outcome = dist.sample(&mut rng); + let bitstring = format!("{outcome:0>width$b}", width = num_qubits); + *counts.entry(bitstring).or_insert(0) += 1; + } + + let execution_time_ms = start.elapsed().as_secs_f64() * 1000.0; + + Ok(SimulationResult { + counts, + shots: shots.0, + execution_time_ms, + statevector, + }) + } +} + +impl Backend for LocalSimulator {} + +/// Build a map of register SymbolId → starting qubit offset, and return total qubit count. +fn build_register_map( + program: &Program, + symbol_table: &SymbolTable, +) -> (usize, HashMap) { + let mut offsets: HashMap = HashMap::new(); + let mut offset = 0usize; + + for stmt in program.stmts() { + if let Stmt::DeclareQuantum(decl) = stmt { + if let Ok(symbol_id) = decl.name() { + offsets.insert(symbol_id.clone(), offset); + let width = qubit_width(symbol_table[symbol_id].symbol_type()); + offset += width; + } + } + } + + (offset, offsets) +} + +/// Walk AST gate calls and return `(gate_name, params, flat_qubit_indices)`. +/// Returns an error if any qubit operand cannot be resolved, preventing silent wrong operations. +fn extract_gate_ops( + program: &Program, + symbol_table: &SymbolTable, + register_offsets: &HashMap, +) -> Result, BridgeError> { + let mut ops = Vec::new(); + + for stmt in program.stmts() { + let Stmt::GateCall(gate_call) = stmt else { + continue; + }; + + let Ok(symbol_id) = gate_call.name() else { + continue; + }; + let gate_name = symbol_table[symbol_id].name().to_lowercase(); + + if gate_name == "measure" { + continue; + } + + let params: Vec = gate_call + .params() + .unwrap_or(&[]) + .iter() + .filter_map(extract_float_param) + .collect(); + + let mut qubit_resolution_failed = false; + let qubits: Vec = gate_call + .qubits() + .iter() + .filter_map(|q| { + let result = extract_qubit_index(q, symbol_table, register_offsets); + if result.is_none() { + qubit_resolution_failed = true; + } + result + }) + .collect(); + + if qubit_resolution_failed { + return Err(BridgeError::Simulation(format!( + "could not resolve qubit operand for gate '{gate_name}'" + ))); + } + + ops.push((gate_name, params, qubits)); + } + + Ok(ops) +} + +/// Extract a float value from a parameter `TExpr`. +/// `FloatLiteral::value()` returns a `&str` that must be parsed. +fn extract_float_param(texpr: &TExpr) -> Option { + match texpr.expression() { + Expr::Literal(Literal::Float(f)) => f.value().parse::().ok(), + Expr::Literal(Literal::Int(i)) => Some(*i.value() as f64), + _ => None, + } +} + +/// Resolve a qubit `TExpr` to its flat index in the statevector. +fn extract_qubit_index( + texpr: &TExpr, + _symbol_table: &SymbolTable, + register_offsets: &HashMap, +) -> Option { + let Expr::GateOperand(operand) = texpr.expression() else { + return None; + }; + let GateOperand::IndexedIdentifier(indexed) = operand else { + return None; + }; + let Ok(reg_id) = indexed.identifier() else { + return None; + }; + + let base_offset = *register_offsets.get(reg_id)?; + + let mut within_reg = 0usize; + for index_op in indexed.indexes() { + let IndexOperator::ExpressionList(elist) = index_op else { + continue; + }; + for idx_texpr in &elist.expressions { + if let Expr::Literal(Literal::Int(int_lit)) = idx_texpr.expression() { + within_reg = *int_lit.value() as usize; + } + } + } + + Some(base_offset + within_reg) +} + +fn qubit_width(typ: &Type) -> usize { + match typ { + Type::Qubit => 1, + Type::QubitArray(ArrayDims::D1(n)) => *n, + _ => 0, + } +} + +fn apply_gate( + gate_name: &str, + params: &[f64], + qubits: &[usize], + state: &mut State, +) -> Result<(), BridgeError> { + 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] as Float), state, t), + ("ry", &[t]) if params.len() == 1 => apply(Gate::RY(params[0] as Float), state, t), + ("rz", &[t]) if params.len() == 1 => apply(Gate::RZ(params[0] as Float), state, t), + ("cx", &[ctrl, tgt]) => c_apply(Gate::X, state, ctrl, tgt), + ("cz", &[ctrl, tgt]) => c_apply(Gate::P(PI), 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), + _ => { + return Err(BridgeError::UnsupportedGate { + gate: gate_name.to_string(), + line: 0, + supported: SUPPORTED_GATES.join(", "), + }) + } + } + Ok(()) +} + +/// Public AST-based gate enumeration. Reuses the parser path of `run`. +pub fn list_gate_calls(source: &CircuitSource) -> Result, BridgeError> { + let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); + if parse_result.any_syntax_errors() { + return Err(BridgeError::Simulation("circuit contains syntax errors".into())); + } + let context = parse_result.take_context(); + let symbol_table = context.symbol_table(); + let program = context.program(); + let (_, register_offsets) = build_register_map(program, symbol_table); + let ops = extract_gate_ops(program, symbol_table, ®ister_offsets)?; + Ok(ops + .into_iter() + .map(|(name, params, qubits)| crate::circuit_analyzer::GateCallInfo { name, params, qubits }) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{CircuitSource, ShotCount}; + use std::collections::HashMap; + + 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::with_seed(42) + } + + #[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} not in [0.45, 0.55]" + ); + } + + #[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} not in [0.45, 0.55]" + ); + } + + #[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 = 4 amplitudes + 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); + } + + // Compile-time test: a mock struct can satisfy the trait bounds. + struct MockBackend; + + impl CanIntrospect for MockBackend { + fn name(&self) -> &str { + "mock" + } + fn max_qubits(&self) -> usize { + 4 + } + fn supported_gates(&self) -> &[&str] { + SUPPORTED_GATES + } + } + + impl CanValidate for MockBackend { + fn validate(&self, _: &CircuitSource) -> Result { + Ok(ValidationResult { + is_valid: true, + diagnostics: vec![], + num_qubits: None, + num_gates: None, + }) + } + } + + impl CanExecute for MockBackend { + fn run( + &self, + _: &CircuitSource, + _: ShotCount, + _: bool, + ) -> Result { + Ok(SimulationResult { + counts: HashMap::new(), + shots: 0, + execution_time_ms: 0.0, + statevector: None, + }) + } + } + + impl Backend for MockBackend {} + + #[test] + fn mock_backend_satisfies_trait_bounds() { + let b: &dyn Backend = &MockBackend; + assert_eq!(b.name(), "mock"); + assert_eq!(b.max_qubits(), 4); + assert!(b.validate(&CircuitSource("".to_string())).unwrap().is_valid); + let result = b + .run(&CircuitSource("".to_string()), ShotCount(1), false) + .unwrap(); + assert_eq!(result.shots, 0); + } + + #[test] + fn supported_gates_includes_hadamard() { + assert!(SUPPORTED_GATES.contains(&"h")); + } + + #[test] + fn supported_gates_includes_cnot() { + assert!(SUPPORTED_GATES.contains(&"cx")); + } + + #[test] + fn supported_gates_includes_measure() { + assert!(SUPPORTED_GATES.contains(&"measure")); + } + + #[test] + fn supported_gates_has_16_entries() { + assert_eq!(SUPPORTED_GATES.len(), 16); + } + + #[test] + fn max_local_qubits_is_28() { + assert_eq!(MAX_LOCAL_QUBITS, 28); + } + + #[test] + fn cz_does_not_panic_and_phase_kickback_produces_eleven() { + const PHASE_KICKBACK: &str = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +x q[1]; +h q[0]; +cz q[0], q[1]; +h q[0]; +c = measure q;"#; + let result = sim() + .run(&CircuitSource(PHASE_KICKBACK.to_string()), ShotCount(1_000), false) + .unwrap(); + assert_eq!( + result.counts.get("11").copied().unwrap_or(0), + 1_000, + "phase kickback should deterministically produce '11', got counts={:?}", + result.counts + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..de770b3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod circuit_analyzer; +pub mod error; +pub mod executor; +pub mod progress; +pub mod tutor; +pub mod types; +pub mod validator; +pub(crate) mod tools; + +pub use tools::QuantumBridgeServer; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..96f234c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use quantum_bridge_mcp::QuantumBridgeServer; +use rmcp::{ServiceExt, transport::stdio}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_ansi(false) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("quantum-bridge-mcp starting"); + + let service = QuantumBridgeServer::with_local_simulator() + .serve(stdio()) + .await + .inspect_err(|e| tracing::error!("serving error: {:?}", e))?; + + service.waiting().await?; + Ok(()) +} diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 0000000..e3c7a5a --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,124 @@ +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::error::BridgeError; + +pub const PROGRESS_PATH_ENV: &str = "QB_PROGRESS_PATH"; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct UserProgress { + pub solved_exercises: HashSet, +} + +impl UserProgress { + pub fn has_solved(&self, exercise_id: &str) -> bool { + self.solved_exercises.contains(exercise_id) + } + + pub fn mark_solved(&mut self, exercise_id: &str) { + self.solved_exercises.insert(exercise_id.to_string()); + } +} + +pub struct ProgressStore { + path: PathBuf, +} + +impl ProgressStore { + /// Env var override first, then `$HOME/.config/quantum-bridge-mcp/progress.json`. + /// Returns an error if neither is available — never falls back to `.`. + pub fn default_path() -> Result { + if let Ok(override_path) = std::env::var(PROGRESS_PATH_ENV) { + return Ok(PathBuf::from(override_path)); + } + let home = std::env::var("HOME").map_err(|_| { + BridgeError::Configuration( + "neither QB_PROGRESS_PATH nor HOME is set; cannot resolve progress.json location" + .into(), + ) + })?; + Ok(PathBuf::from(home) + .join(".config") + .join("quantum-bridge-mcp") + .join("progress.json")) + } + + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + pub fn load(&self) -> UserProgress { + match fs::read_to_string(&self.path) { + Ok(content) => serde_json::from_str(&content).unwrap_or_default(), + Err(_) => UserProgress::default(), + } + } + + pub fn save(&self, progress: &UserProgress) -> Result<(), BridgeError> { + if let Some(parent) = self.path.parent() { + fs::create_dir_all(parent) + .map_err(|e| BridgeError::Configuration(format!("create_dir_all failed: {e}")))?; + } + let json = serde_json::to_string_pretty(progress) + .map_err(|e| BridgeError::Configuration(format!("serialize failed: {e}")))?; + fs::write(&self.path, json) + .map_err(|e| BridgeError::Configuration(format!("write failed: {e}"))) + } + + pub fn mark_solved(&self, exercise_id: &str) -> Result<(), BridgeError> { + let mut progress = self.load(); + progress.mark_solved(exercise_id); + self.save(&progress) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn isolated_store() -> (ProgressStore, TempDir) { + let dir = TempDir::new().expect("create tempdir"); + let path = dir.path().join("progress.json"); + (ProgressStore::new(path), dir) + } + + #[test] + fn load_returns_empty_progress_when_no_file_exists() { + let (store, _dir) = isolated_store(); + assert!(store.load().solved_exercises.is_empty()); + } + + #[test] + fn save_and_reload_preserves_solved_exercises() { + let (store, _dir) = isolated_store(); + store.mark_solved("1-1-a").unwrap(); + assert!(store.load().has_solved("1-1-a")); + } + + #[test] + fn mark_solved_twice_does_not_duplicate() { + let (store, _dir) = isolated_store(); + store.mark_solved("1-1-a").unwrap(); + store.mark_solved("1-1-a").unwrap(); + assert_eq!(store.load().solved_exercises.len(), 1); + } + + #[test] + fn has_solved_returns_false_for_unknown_exercise() { + assert!(!UserProgress::default().has_solved("99-99-z")); + } + + #[test] + fn default_path_uses_env_override_when_set() { + let dir = TempDir::new().unwrap(); + let target = dir.path().join("override.json"); + std::env::set_var(PROGRESS_PATH_ENV, &target); + let resolved = ProgressStore::default_path().unwrap(); + std::env::remove_var(PROGRESS_PATH_ENV); + assert_eq!(resolved, target); + } +} diff --git a/src/tools/list_backends.rs b/src/tools/list_backends.rs new file mode 100644 index 0000000..171d118 --- /dev/null +++ b/src/tools/list_backends.rs @@ -0,0 +1,49 @@ +use serde_json::{json, Value}; + +use crate::executor::{CanIntrospect, LocalSimulator, MAX_LOCAL_QUBITS, SUPPORTED_GATES}; + +pub fn list_backends_response() -> Value { + let sim = LocalSimulator::new(); + json!({ + "backends": [{ + "name": sim.name(), + "description": "Local statevector simulator (Spinoza engine). No network, no account required.", + "max_qubits": sim.max_qubits(), + "supported_gates": sim.supported_gates(), + "simulation_type": "statevector", + "notes": "Use run_circuit shots parameter (default 1024, max 100000). Statevector memory grows as 2^n × 16 bytes." + }] + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_contains_local_simulator() { + let resp = list_backends_response(); + let backends = resp["backends"].as_array().unwrap(); + assert_eq!(backends.len(), 1); + assert_eq!(backends[0]["name"], "local_simulator"); + } + + #[test] + fn response_contains_max_qubits() { + let resp = list_backends_response(); + let backend = &resp["backends"][0]; + assert_eq!( + backend["max_qubits"].as_u64().unwrap(), + MAX_LOCAL_QUBITS as u64 + ); + } + + #[test] + fn response_lists_supported_gates() { + let resp = list_backends_response(); + let gates = resp["backends"][0]["supported_gates"].as_array().unwrap(); + assert!(!gates.is_empty()); + assert!(gates.iter().any(|g| g == "h")); + assert!(gates.iter().any(|g| g == "cx")); + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs new file mode 100644 index 0000000..237b6d7 --- /dev/null +++ b/src/tools/mod.rs @@ -0,0 +1,200 @@ +pub mod list_backends; +pub mod run_circuit; +pub mod tutor_tools; +pub mod validate_circuit; + +use std::sync::Arc; + +use rmcp::{ + ErrorData as McpError, + handler::server::wrapper::Parameters, + model::*, + schemars, tool, tool_router, +}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::executor::{Backend, LocalSimulator}; +use crate::progress::ProgressStore; +use crate::tools::list_backends::list_backends_response; +use crate::tools::run_circuit::run_circuit_response; +use crate::tools::validate_circuit::validate_circuit_response; +use crate::tutor::CurriculumLoader; + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ValidateCircuitParams { + /// OpenQASM 3.0 source string to validate. + pub circuit: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct RunCircuitParams { + /// OpenQASM 3.0 source string to execute. + pub circuit: String, + /// Number of measurement shots (default 1024, max 100 000). + #[serde(default = "default_shots")] + pub shots: u32, + /// Whether to include the full statevector in the response. + #[serde(default)] + pub return_statevector: bool, +} + +fn default_shots() -> u32 { + 1024 +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct GetLessonParams { + /// Module number (1–7). + pub module_id: u32, + /// Lesson number within the module (optional, defaults to first lesson with pending exercises). + pub lesson_id: Option, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct CheckExerciseParams { + /// Exercise identifier, e.g. "1-1-a". + pub exercise_id: String, + /// OpenQASM 3.0 source of the circuit to verify. + pub circuit: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ExplainResultParams { + /// OpenQASM 3.0 source of the circuit that was executed. + pub circuit: String, + /// Measurement counts from run_circuit (bitstring → count). + pub counts: serde_json::Value, + /// Optional statevector from run_circuit with return_statevector=true. + pub statevector: Option, +} + +#[derive(Clone)] +pub struct QuantumBridgeServer { + pub backend: Arc, +} + +impl QuantumBridgeServer { + pub fn new(backend: Arc) -> Self { + Self { backend } + } + + pub fn with_local_simulator() -> Self { + Self::new(Arc::new(LocalSimulator::new())) + } + + fn loader(&self) -> CurriculumLoader { + CurriculumLoader::default() + } + + fn store(&self) -> Result { + let path = ProgressStore::default_path() + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + Ok(ProgressStore::new(path)) + } +} + +#[tool_router(server_handler)] +impl QuantumBridgeServer { + #[tool(description = "List available quantum simulation backends and their capabilities.")] + async fn list_backends(&self) -> Result { + let json = list_backends_response(); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Parse and validate an OpenQASM 3.0 circuit. Returns structured diagnostics with line/column for each error.")] + async fn validate_circuit( + &self, + Parameters(ValidateCircuitParams { circuit }): Parameters, + ) -> Result { + let json = validate_circuit_response(&circuit); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Execute an OpenQASM 3.0 circuit on the local statevector simulator. Returns measurement counts, execution time, and optionally the full statevector.")] + async fn run_circuit( + &self, + Parameters(RunCircuitParams { circuit, shots, return_statevector }): Parameters, + ) -> Result { + let json = run_circuit_response(&circuit, shots, return_statevector); + if let Some(err_msg) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err_msg, None)); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Get a quantum-computing lesson with concept, example circuit, and the next pending exercise.")] + async fn get_lesson( + &self, + Parameters(GetLessonParams { module_id, lesson_id }): Parameters, + ) -> Result { + let store = self.store()?; + let progress = store.load(); + let json = tutor_tools::get_lesson_response(&self.loader(), &progress, module_id, lesson_id); + if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Verify a submitted OpenQASM 3.0 circuit against a curriculum exercise. Returns pass/fail with feedback.")] + async fn check_exercise( + &self, + Parameters(CheckExerciseParams { exercise_id, circuit }): Parameters, + ) -> Result { + let store = self.store()?; + let progress = store.load(); + let mut json = tutor_tools::check_exercise_response( + &self.loader(), + self.backend.as_ref(), + &progress, + &exercise_id, + &circuit, + ); + if let Some(err) = json.get("protocol_error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + if json["passed"].as_bool().unwrap_or(false) { + store + .mark_solved(&exercise_id) + .map_err(|e| McpError::internal_error(e.to_string(), None))?; + json["progress_updated"] = serde_json::json!(true); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Analyze a circuit and its measurement results to produce structured pedagogical data (gate breakdown, key concept, outcome stats).")] + async fn explain_result( + &self, + Parameters(ExplainResultParams { circuit, counts, statevector }): Parameters, + ) -> Result { + let json = tutor_tools::explain_result_response( + &self.loader(), + &circuit, + &counts, + statevector.as_ref(), + ); + if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) { + return Err(McpError::invalid_params(err, None)); + } + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } + + #[tool(description = "Get the learner's progress through the curriculum (modules status, current lesson, percent complete).")] + async fn get_progress(&self) -> Result { + let store = self.store()?; + let progress = store.load(); + let json = tutor_tools::get_progress_response(&self.loader(), &progress); + Ok(CallToolResult::success(vec![Content::text(json.to_string())])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quantum_bridge_server_can_be_constructed() { + let _server = QuantumBridgeServer::with_local_simulator(); + } +} diff --git a/src/tools/run_circuit.rs b/src/tools/run_circuit.rs new file mode 100644 index 0000000..d3da095 --- /dev/null +++ b/src/tools/run_circuit.rs @@ -0,0 +1,112 @@ +use serde_json::{json, Value}; + +use crate::executor::{CanExecute, CanValidate, LocalSimulator, MAX_LOCAL_QUBITS}; +use crate::types::{CircuitSource, ShotCount}; +use crate::validator::CircuitValidator; + +pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { + let shot_count = ShotCount(shots.min(ShotCount::MAX.0)); + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let validation = match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => return json!({"error": e.to_string()}), + Ok(v) => v, + }; + if !validation.is_valid { + let messages: Vec<&str> = validation + .diagnostics + .iter() + .map(|d| d.message.as_str()) + .collect(); + return json!({ + "error": format!("circuit validation failed: {}", messages.join("; ")) + }); + } + + let sim = LocalSimulator::new(); + match sim.run(&CircuitSource(circuit.to_string()), shot_count, return_statevector) { + Err(e) => json!({"error": e.to_string()}), + Ok(result) => { + let statevector: Value = match result.statevector { + None => Value::Null, + Some(sv) => sv.iter().map(|(r, i)| json!([r, i])).collect::>().into(), + }; + json!({ + "counts": result.counts, + "shots": result.shots, + "execution_time_ms": result.execution_time_ms, + "statevector": statevector, + "backend": "local_simulator", + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + 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;"#; + + #[test] + fn bell_circuit_returns_counts_with_total_matching_shots() { + let resp = run_circuit_response(BELL_CIRCUIT, 1_000, false); + let counts = resp["counts"].as_object().unwrap(); + let total: u64 = counts.values().map(|v| v.as_u64().unwrap()).sum(); + assert_eq!(total, 1_000); + } + + #[test] + fn x_gate_produces_only_one_outcome() { + let resp = run_circuit_response(X_CIRCUIT, 100, false); + let counts = resp["counts"].as_object().unwrap(); + assert_eq!(counts.get("1").and_then(|v| v.as_u64()), Some(100)); + } + + #[test] + fn response_includes_shots_and_execution_time() { + let resp = run_circuit_response(BELL_CIRCUIT, 512, false); + assert_eq!(resp["shots"].as_u64().unwrap(), 512); + assert!(resp["execution_time_ms"].as_f64().unwrap() >= 0.0); + } + + #[test] + fn statevector_is_null_when_not_requested() { + let resp = run_circuit_response(BELL_CIRCUIT, 100, false); + assert!(resp["statevector"].is_null()); + } + + #[test] + fn statevector_has_correct_length_when_requested() { + let resp = run_circuit_response(BELL_CIRCUIT, 100, true); + let sv = resp["statevector"].as_array().unwrap(); + assert_eq!(sv.len(), 4); // 2^2 basis states + } + + #[test] + fn invalid_circuit_returns_error_field() { + let resp = run_circuit_response("OPENQASM 3.0;\nnot_valid;", 100, false); + assert!(resp.get("error").is_some(), "expected 'error' field, got: {resp}"); + } + + #[test] + fn shots_clamped_to_max() { + let resp = run_circuit_response(X_CIRCUIT, ShotCount::MAX.0 + 1, false); + let has_error = resp.get("error").is_some(); + let shots = resp["shots"].as_u64().unwrap_or(0); + assert!(has_error || shots == ShotCount::MAX.0 as u64); + } +} diff --git a/src/tools/tutor_tools.rs b/src/tools/tutor_tools.rs new file mode 100644 index 0000000..45b067a --- /dev/null +++ b/src/tools/tutor_tools.rs @@ -0,0 +1,495 @@ +use serde_json::{json, Value}; + +use crate::circuit_analyzer::{CircuitAnalyzer, GateCallInfo}; +use crate::executor::Backend; +use crate::progress::UserProgress; +use crate::tutor::{CurriculumLoader, ExerciseChecker}; +use crate::types::CircuitSource; + +pub fn get_lesson_response( + loader: &CurriculumLoader, + progress: &UserProgress, + module_id: u32, + lesson_id: Option, +) -> Value { + let module = match loader.get_module(module_id) { + None => return json!({ "error": format!("module {} not found", module_id) }), + Some(m) => m, + }; + + let resolved = lesson_id + .or_else(|| first_unsolved_lesson(module, progress)) + .or_else(|| module.lessons.first().map(|l| l.id)); + let resolved = match resolved { + Some(id) => id, + None => return json!({ "error": format!("module {} has no lessons", module_id) }), + }; + + let lesson = match loader.get_lesson(module_id, resolved) { + None => { + return json!({ "error": format!("module {} lesson {} not found", module_id, resolved) }) + } + Some(l) => l, + }; + + let pending_exercise = lesson.exercises.iter().find(|e| !progress.has_solved(&e.id)); + + if pending_exercise.is_none() && lesson.exercises.iter().all(|e| progress.has_solved(&e.id)) { + return json!({ + "module_id": module_id, + "lesson_id": resolved, + "title": lesson.title, + "concept": lesson.concept, + "module_completed": all_lessons_completed(module, progress), + "message": "Toutes les exercices de cette leçon sont résolus.", + }); + } + + json!({ + "module_id": module_id, + "lesson_id": resolved, + "title": lesson.title, + "concept": lesson.concept, + "example_circuit": lesson.example_circuit, + "what_to_observe": lesson.what_to_observe, + "exercise": pending_exercise.map(|e| json!({ + "id": e.id, + "prompt": e.prompt, + "hint": e.hint, + })), + }) +} + +fn first_unsolved_lesson(module: &crate::tutor::Module, progress: &UserProgress) -> Option { + module + .lessons + .iter() + .find(|l| l.exercises.iter().any(|e| !progress.has_solved(&e.id))) + .map(|l| l.id) +} + +fn all_lessons_completed(module: &crate::tutor::Module, progress: &UserProgress) -> bool { + module + .lessons + .iter() + .flat_map(|l| l.exercises.iter()) + .all(|e| progress.has_solved(&e.id)) +} + +pub fn get_progress_response(loader: &CurriculumLoader, progress: &UserProgress) -> Value { + let curriculum = match loader.curriculum() { + Ok(c) => c, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let total_exercises: usize = loader.all_exercises().len(); + let solved_count = progress.solved_exercises.len().min(total_exercises); + + let mut prev_unlocks_next = true; + let modules: Vec = curriculum + .modules + .iter() + .map(|m| { + let total_in_module: usize = m.lessons.iter().map(|l| l.exercises.len()).sum(); + let solved_in_module = m + .lessons + .iter() + .flat_map(|l| l.exercises.iter()) + .filter(|e| progress.has_solved(&e.id)) + .count(); + let status = if !prev_unlocks_next { + "locked" + } else if total_in_module > 0 && solved_in_module == total_in_module { + "completed" + } else if solved_in_module > 0 { + "in_progress" + } else { + "unlocked" + }; + // Spec §4: next module unlocks when ⌈2/3⌉ of current exercises are solved. + let unlock_threshold = (total_in_module * 2 + 2) / 3; + prev_unlocks_next = total_in_module > 0 && solved_in_module >= unlock_threshold; + json!({ + "id": m.id, + "title": m.title, + "status": status, + "exercises_solved": solved_in_module, + "exercises_total": total_in_module, + }) + }) + .collect(); + + let (current_module, current_lesson) = current_position(curriculum, progress); + let percent = if total_exercises == 0 { 0 } else { (solved_count * 100) / total_exercises }; + + json!({ + "current_module": current_module, + "current_lesson": current_lesson, + "modules": modules, + "total_exercises_solved": solved_count, + "total_exercises": total_exercises, + "percent_complete": percent, + }) +} + +fn current_position(curriculum: &crate::tutor::Curriculum, progress: &UserProgress) -> (u32, u32) { + for module in &curriculum.modules { + for lesson in &module.lessons { + if lesson.exercises.iter().any(|e| !progress.has_solved(&e.id)) { + return (module.id, lesson.id); + } + } + } + let last = curriculum.modules.last(); + ( + last.map(|m| m.id).unwrap_or(1), + last.and_then(|m| m.lessons.last()).map(|l| l.id).unwrap_or(1), + ) +} + +pub fn explain_result_response( + loader: &CurriculumLoader, + circuit: &str, + counts: &Value, + statevector: Option<&Value>, +) -> Value { + let curriculum = match loader.curriculum() { + Ok(c) => c, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let gates = match CircuitAnalyzer::list_gates(&CircuitSource(circuit.to_string())) { + Ok(g) => g, + Err(e) => return json!({ "error": e.to_string() }), + }; + + let gate_breakdown: Vec = gates + .iter() + .map(|g| gate_entry(g, &curriculum.gate_descriptions)) + .collect(); + + let key_concept = classify_key_concept(&gates); + let num_qubits = max_qubit_index(&gates).map(|m| m + 1).unwrap_or(0); + let (dominant, missing) = outcomes_summary(counts, num_qubits); + let sv_summary = statevector_summary(statevector); + + json!({ + "gate_breakdown": gate_breakdown, + "num_qubits": num_qubits, + "key_concept": key_concept, + "dominant_outcomes": dominant, + "missing_outcomes": missing, + "statevector_summary": sv_summary, + }) +} + +fn gate_entry( + g: &GateCallInfo, + descs: &std::collections::HashMap, +) -> Value { + let mut entry = json!({ "name": g.name, "qubits": g.qubits, "params": g.params }); + if let Some(d) = descs.get(&g.name) { + entry["description"] = json!(d.short); + if let Some(eff) = &d.effect_on_zero { + entry["effect_on_zero"] = json!(eff); + } + } + entry +} + +fn classify_key_concept(gates: &[GateCallInfo]) -> &'static str { + let names: std::collections::HashSet<&str> = + gates.iter().map(|g| g.name.as_str()).collect(); + if names.contains("cx") || names.contains("cz") || names.contains("ccx") || names.contains("swap") { + "entanglement" + } else if names.contains("h") + && (names.contains("rz") || names.contains("z") || names.contains("s") || names.contains("t")) + { + "interference" + } else if names.contains("h") { + "superposition" + } else if names.contains("rx") || names.contains("ry") || names.contains("rz") { + "rotation" + } else { + "measurement" + } +} + +fn max_qubit_index(gates: &[GateCallInfo]) -> Option { + gates.iter().flat_map(|g| g.qubits.iter().copied()).max() +} + +fn outcomes_summary(counts: &Value, num_qubits: usize) -> (Vec, Vec) { + let map = match counts.as_object() { + Some(m) => m, + None => return (vec![], vec![]), + }; + let total: u64 = map.values().filter_map(|v| v.as_u64()).sum(); + if total == 0 { + return (vec![], vec![]); + } + let threshold = (total as f64 * 0.05) as u64; + let mut dominant: Vec = map + .iter() + .filter(|(_, v)| v.as_u64().unwrap_or(0) > threshold) + .map(|(k, _)| k.clone()) + .collect(); + dominant.sort(); + + let missing: Vec = if num_qubits == 0 || num_qubits > 4 { + vec![] + } else { + let n = num_qubits; + let mut out: Vec = (0u64..(1u64 << n)) + .map(|i| format!("{i:0>n$b}")) + .filter(|bs| map.get(bs).and_then(|v| v.as_u64()).unwrap_or(0) == 0) + .collect(); + out.sort(); + out + }; + (dominant, missing) +} + +fn statevector_summary(sv: Option<&Value>) -> Value { + let sv = match sv.and_then(|v| v.as_array()) { + Some(a) => a, + None => return Value::Null, + }; + let mut non_zero = Vec::new(); + let mut zero = Vec::new(); + for (i, amp) in sv.iter().enumerate() { + let r = amp.as_array().and_then(|a| a.first()).and_then(|v| v.as_f64()).unwrap_or(0.0); + let im = amp.as_array().and_then(|a| a.get(1)).and_then(|v| v.as_f64()).unwrap_or(0.0); + if r * r + im * im > 1e-10 { + non_zero.push(i); + } else { + zero.push(i); + } + } + json!({ "non_zero_amplitudes": non_zero, "zero_amplitudes": zero }) +} + +pub fn check_exercise_response( + loader: &CurriculumLoader, + backend: &dyn Backend, + progress: &UserProgress, + exercise_id: &str, + circuit: &str, +) -> Value { + let exercise = match loader.find_exercise(exercise_id) { + None => { + return json!({ + "protocol_error": format!("exercise '{}' not found in curriculum", exercise_id), + }) + } + Some(e) => e, + }; + + let check = ExerciseChecker::check_circuit(backend, circuit, &exercise.criteria); + + if let Some(err) = &check.error { + let diagnostics: Vec = check + .diagnostics + .iter() + .map(|d| { + json!({ + "line": d.line, + "column": d.column, + "message": d.message, + }) + }) + .collect(); + return json!({ + "passed": false, + "exercise_id": exercise_id, + "feedback": exercise.feedback_fail, + "hint": exercise.hint, + "validation_error": err, + "diagnostics": diagnostics, + "counts": check.counts, + }); + } + + let already_solved = progress.has_solved(exercise_id); + if check.passed { + json!({ + "passed": true, + "exercise_id": exercise_id, + "feedback": exercise.feedback_pass, + "counts": check.counts, + "newly_solved": !already_solved, + }) + } else { + json!({ + "passed": false, + "exercise_id": exercise_id, + "feedback": exercise.feedback_fail, + "hint": exercise.hint, + "counts": check.counts, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn loader() -> CurriculumLoader { + CurriculumLoader::default() + } + + #[test] + fn get_lesson_returns_first_pending_exercise_for_known_module() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, Some(1)); + assert_eq!(resp["module_id"].as_u64().unwrap(), 1); + assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1); + assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-a"); + } + + #[test] + fn get_lesson_skips_solved_exercises_within_a_lesson() { + let mut progress = UserProgress::default(); + progress.mark_solved("1-1-a"); + let resp = get_lesson_response(&loader(), &progress, 1, Some(1)); + assert_eq!(resp["exercise"]["id"].as_str().unwrap(), "1-1-b"); + } + + #[test] + fn get_lesson_default_resolves_to_first_lesson_with_pending_exercises() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 1, None); + assert_eq!(resp["lesson_id"].as_u64().unwrap(), 1); + } + + #[test] + fn get_lesson_unknown_module_returns_error_payload() { + let resp = get_lesson_response(&loader(), &UserProgress::default(), 99, None); + assert!(resp.get("error").is_some()); + } + + use crate::executor::{Backend, LocalSimulator}; + + fn backend() -> LocalSimulator { + LocalSimulator::new() + } + + #[test] + fn check_exercise_x_gate_passes_1_1_a() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + let resp = check_exercise_response( + &loader(), + &backend() as &dyn Backend, + &UserProgress::default(), + "1-1-a", + circuit, + ); + assert_eq!(resp["passed"], true); + assert_eq!(resp["exercise_id"], "1-1-a"); + } + + #[test] + fn check_exercise_identity_fails_1_1_a() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + let resp = check_exercise_response( + &loader(), + &backend() as &dyn Backend, + &UserProgress::default(), + "1-1-a", + circuit, + ); + assert_eq!(resp["passed"], false); + assert!(resp["hint"].as_str().is_some()); + } + + #[test] + fn check_exercise_invalid_circuit_returns_diagnostics_not_protocol_error() { + let resp = check_exercise_response( + &loader(), + &backend() as &dyn Backend, + &UserProgress::default(), + "1-1-a", + "not valid qasm", + ); + assert_eq!(resp["passed"], false); + assert!(resp["diagnostics"].as_array().is_some()); + assert!(resp.get("protocol_error").is_none()); + } + + #[test] + fn check_exercise_unknown_id_marks_protocol_error() { + let resp = check_exercise_response( + &loader(), + &backend() as &dyn Backend, + &UserProgress::default(), + "99-99-z", + "x q[0];", + ); + assert!(resp["protocol_error"].as_str().is_some()); + } + + #[test] + fn explain_result_bell_circuit_lists_h_then_cx_with_descriptions() { + let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let counts = json!({"00": 512, "11": 512}); + let resp = explain_result_response(&loader(), bell, &counts, None); + let breakdown = resp["gate_breakdown"].as_array().unwrap(); + assert_eq!(breakdown.len(), 2); + assert_eq!(breakdown[0]["name"].as_str().unwrap(), "h"); + assert_eq!(breakdown[1]["name"].as_str().unwrap(), "cx"); + assert!(breakdown[0]["description"].as_str().unwrap().contains("Hadamard")); + assert_eq!(resp["key_concept"].as_str().unwrap(), "entanglement"); + assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); + } + + #[test] + fn explain_result_dominant_outcomes_match_counts() { + let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let counts = json!({"00": 480, "11": 544}); + let resp = explain_result_response(&loader(), bell, &counts, None); + let dominant: Vec<&str> = resp["dominant_outcomes"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert!(dominant.contains(&"00")); + assert!(dominant.contains(&"11")); + } + + #[test] + fn explain_result_for_invalid_circuit_returns_error_payload() { + let resp = explain_result_response(&loader(), "not valid qasm", &json!({}), None); + assert!(resp.get("error").is_some()); + } + + #[test] + fn get_progress_with_no_progress_returns_module_1_unlocked_others_locked() { + let resp = get_progress_response(&loader(), &UserProgress::default()); + assert_eq!(resp["current_module"].as_u64().unwrap(), 1); + let modules = resp["modules"].as_array().unwrap(); + assert_eq!(modules[0]["status"].as_str().unwrap(), "unlocked"); + if modules.len() > 1 { + assert_eq!(modules[1]["status"].as_str().unwrap(), "locked"); + } + assert_eq!(resp["total_exercises_solved"].as_u64().unwrap(), 0); + } + + #[test] + fn get_progress_completed_module_marked_completed() { + let mut progress = UserProgress::default(); + progress.mark_solved("1-1-a"); + progress.mark_solved("1-1-b"); + let resp = get_progress_response(&loader(), &progress); + assert_eq!( + resp["modules"].as_array().unwrap()[0]["status"] + .as_str() + .unwrap(), + "completed" + ); + } + + #[test] + fn get_progress_returns_current_lesson_for_active_module() { + let resp = get_progress_response(&loader(), &UserProgress::default()); + assert_eq!(resp["current_lesson"].as_u64().unwrap(), 1); + } +} diff --git a/src/tools/validate_circuit.rs b/src/tools/validate_circuit.rs new file mode 100644 index 0000000..86f5360 --- /dev/null +++ b/src/tools/validate_circuit.rs @@ -0,0 +1,97 @@ +use serde_json::{json, Value}; + +use crate::executor::MAX_LOCAL_QUBITS; +use crate::types::CircuitSource; +use crate::validator::CircuitValidator; + +pub fn validate_circuit_response(circuit: &str) -> Value { + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + match validator.validate(&CircuitSource(circuit.to_string())) { + Err(e) => json!({ + "is_valid": false, + "diagnostics": [{"line": 1, "column": 1, "message": e.to_string(), "severity": "error"}], + "num_qubits": null, + "num_gates": null, + }), + Ok(result) => { + let diagnostics: Vec = result + .diagnostics + .iter() + .map(|d| { + json!({ + "line": d.line, + "column": d.column, + "message": d.message, + "severity": match d.severity { + crate::types::DiagnosticSeverity::Error => "error", + crate::types::DiagnosticSeverity::Warning => "warning", + } + }) + }) + .collect(); + json!({ + "is_valid": result.is_valid, + "diagnostics": diagnostics, + "num_qubits": result.num_qubits, + "num_gates": result.num_gates, + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + 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;"#; + + #[test] + fn valid_circuit_returns_is_valid_true() { + let resp = validate_circuit_response(BELL_CIRCUIT); + assert_eq!(resp["is_valid"], true); + assert_eq!(resp["diagnostics"].as_array().unwrap().len(), 0); + } + + #[test] + fn valid_circuit_includes_qubit_count() { + let resp = validate_circuit_response(BELL_CIRCUIT); + assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); + } + + #[test] + fn invalid_circuit_returns_is_valid_false_with_message() { + let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;"); + assert_eq!(resp["is_valid"], false); + let diags = resp["diagnostics"].as_array().unwrap(); + assert!(!diags.is_empty()); + assert!(diags[0]["message"].as_str().unwrap().len() > 0); + } + + #[test] + fn unsupported_gate_returns_diagnostic_with_gate_name() { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let resp = validate_circuit_response(circuit); + assert_eq!(resp["is_valid"], false); + let diags = resp["diagnostics"].as_array().unwrap(); + let msg = diags[0]["message"].as_str().unwrap(); + assert!(msg.to_lowercase().contains("u3"), "msg: {msg}"); + } + + #[test] + fn diagnostic_includes_line_and_column() { + let circuit = "OPENQASM 3.0;\nnot_valid_qasm;\n"; + let resp = validate_circuit_response(circuit); + let diag = &resp["diagnostics"][0]; + assert!(diag["line"].as_u64().unwrap() >= 1); + assert!(diag["column"].as_u64().unwrap() >= 1); + } +} diff --git a/src/tutor.rs b/src/tutor.rs new file mode 100644 index 0000000..d6e40f3 --- /dev/null +++ b/src/tutor.rs @@ -0,0 +1,386 @@ +use std::collections::HashMap; +use std::sync::OnceLock; +use serde::Deserialize; +use crate::error::BridgeError; + +const CURRICULUM_JSON: &str = include_str!("../curriculum/curriculum.json"); + +#[derive(Default)] +pub struct CurriculumLoader { + cell: OnceLock>, +} + +impl CurriculumLoader { + pub fn curriculum(&self) -> Result<&Curriculum, BridgeError> { + let result = self.cell.get_or_init(|| { + serde_json::from_str::(CURRICULUM_JSON).map_err(|e| e.to_string()) + }); + result.as_ref().map_err(|msg| BridgeError::Curriculum(msg.clone())) + } + + pub fn from_str(json: &str) -> Result { + let curriculum: Curriculum = + serde_json::from_str(json).map_err(|e| BridgeError::Curriculum(e.to_string()))?; + let loader = Self::default(); + let _ = loader.cell.set(Ok(curriculum)); + Ok(loader) + } + + pub fn get_module(&self, module_id: u32) -> Option<&Module> { + self.curriculum().ok()?.modules.iter().find(|m| m.id == module_id) + } + + pub fn get_lesson(&self, module_id: u32, lesson_id: u32) -> Option<&Lesson> { + self.get_module(module_id)?.lessons.iter().find(|l| l.id == lesson_id) + } + + pub fn find_exercise(&self, exercise_id: &str) -> Option<&Exercise> { + for module in &self.curriculum().ok()?.modules { + for lesson in &module.lessons { + if let Some(ex) = lesson.exercises.iter().find(|e| e.id == exercise_id) { + return Some(ex); + } + } + } + None + } + + pub fn all_exercises(&self) -> Vec<&Exercise> { + self.curriculum() + .ok() + .map(|c| { + c.modules + .iter() + .flat_map(|m| m.lessons.iter()) + .flat_map(|l| l.exercises.iter()) + .collect() + }) + .unwrap_or_default() + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Curriculum { + pub version: String, + pub gate_descriptions: HashMap, + pub modules: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct GateDescription { + pub short: String, + #[serde(default)] + pub effect_on_zero: Option, + #[serde(default)] + pub effect_on_one: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Module { + pub id: u32, + pub title: String, + pub lessons: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Lesson { + pub id: u32, + pub title: String, + pub concept: String, + pub example_circuit: String, + pub what_to_observe: String, + pub exercises: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Exercise { + pub id: String, + pub prompt: String, + pub hint: String, + pub criteria: ExerciseCriteria, + pub feedback_pass: String, + pub feedback_fail: String, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ExerciseCriteria { + #[serde(default)] + pub required_outcomes: Vec, + #[serde(default)] + pub forbidden_outcomes: Vec, + #[serde(default)] + pub statevector_check: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RequiredOutcome { + pub bitstring: String, + #[serde(default)] + pub min_ratio: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct StatevectorCheck { + pub non_zero_amplitude_indices: Vec, + pub zero_amplitude_indices: Vec, + #[serde(default = "default_tolerance")] + pub tolerance: f64, +} + +fn default_tolerance() -> f64 { + 1e-6 +} + +use crate::executor::{Backend, MAX_LOCAL_QUBITS}; +use crate::types::{CircuitSource, ShotCount, ValidationDiagnostic}; +use crate::validator::CircuitValidator; + +pub struct CheckResult { + pub passed: bool, + pub counts: HashMap, + pub diagnostics: Vec, + pub error: Option, +} + +pub struct ExerciseChecker; + +impl ExerciseChecker { + const CHECK_SHOTS: u32 = 1024; + const SIGMA_MULTIPLIER: f64 = 2.0; + + pub fn check_circuit( + backend: &dyn Backend, + circuit_source: &str, + criteria: &ExerciseCriteria, + ) -> CheckResult { + let source = CircuitSource(circuit_source.to_string()); + + let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); + let validation = match validator.validate(&source) { + Err(e) => { + return CheckResult { + passed: false, + counts: HashMap::new(), + diagnostics: vec![], + error: Some(e.to_string()), + } + } + Ok(v) => v, + }; + if !validation.is_valid { + let summary = validation + .diagnostics + .iter() + .map(|d| d.message.as_str()) + .collect::>() + .join("; "); + return CheckResult { + passed: false, + counts: HashMap::new(), + diagnostics: validation.diagnostics, + error: Some(summary), + }; + } + + let need_sv = criteria.statevector_check.is_some(); + let result = match backend.run(&source, ShotCount(Self::CHECK_SHOTS), need_sv) { + Err(e) => { + return CheckResult { + passed: false, + counts: HashMap::new(), + diagnostics: vec![], + error: Some(e.to_string()), + } + } + Ok(r) => r, + }; + + let total = result.shots as f64; + let counts_pass = Self::counts_pass(&result.counts, criteria, total); + let sv_pass = match (&criteria.statevector_check, &result.statevector) { + (Some(check), Some(sv)) => Self::statevector_pass(sv, check), + (Some(_), None) => false, + (None, _) => true, + }; + + CheckResult { + passed: counts_pass && sv_pass, + counts: result.counts, + diagnostics: vec![], + error: None, + } + } + + fn counts_pass(counts: &HashMap, criteria: &ExerciseCriteria, total: f64) -> bool { + for req in &criteria.required_outcomes { + let count = counts.get(&req.bitstring).copied().unwrap_or(0) as f64; + match req.min_ratio { + Some(min_ratio) => { + let sigma = (min_ratio * (1.0 - min_ratio) / total).sqrt(); + let threshold = (min_ratio - Self::SIGMA_MULTIPLIER * sigma) * total; + if count < threshold { + return false; + } + } + None => { + if count == 0.0 { + return false; + } + } + } + } + for forbidden in &criteria.forbidden_outcomes { + if counts.get(forbidden).copied().unwrap_or(0) > 0 { + return false; + } + } + true + } + + fn statevector_pass(sv: &[(f64, f64)], check: &StatevectorCheck) -> bool { + let magnitude = |idx: usize| -> Option { + sv.get(idx).map(|(r, i)| (r * r + i * i).sqrt()) + }; + for &idx in &check.non_zero_amplitude_indices { + match magnitude(idx) { + Some(m) if m > check.tolerance => {} + _ => return false, + } + } + for &idx in &check.zero_amplitude_indices { + match magnitude(idx) { + Some(m) if m <= check.tolerance => {} + _ => return false, + } + } + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn loader_returns_curriculum_on_first_call() { + let loader = CurriculumLoader::default(); + let c = loader.curriculum().expect("embedded curriculum must parse"); + assert_eq!(c.version, "1.0"); + } + + #[test] + fn find_exercise_by_id_returns_correct_exercise() { + let loader = CurriculumLoader::default(); + let exercise = loader.find_exercise("1-1-a").unwrap(); + assert_eq!(exercise.id, "1-1-a"); + } + + #[test] + fn find_exercise_with_unknown_id_returns_none() { + let loader = CurriculumLoader::default(); + assert!(loader.find_exercise("99-99-z").is_none()); + } + + #[test] + fn get_lesson_unknown_module_returns_none() { + let loader = CurriculumLoader::default(); + assert!(loader.get_lesson(99, 1).is_none()); + } + + #[test] + fn loader_reports_curriculum_error_for_malformed_json() { + let result = CurriculumLoader::from_str("{ not valid json"); + assert!(result.is_err()); + } + + #[test] + fn curriculum_json_deserializes_with_module_1() { + let json = include_str!("../curriculum/curriculum.json"); + let curriculum: Curriculum = serde_json::from_str(json).expect("curriculum.json must parse"); + assert_eq!(curriculum.version, "1.0"); + let module_1 = curriculum.modules.iter().find(|m| m.id == 1).expect("module 1"); + let exercise_count: usize = module_1.lessons.iter().map(|l| l.exercises.len()).sum(); + assert_eq!(exercise_count, 2); + for gate in crate::executor::SUPPORTED_GATES { + if *gate == "measure" { continue; } + assert!( + curriculum.gate_descriptions.contains_key(*gate), + "missing gate_description for '{gate}'" + ); + } + } + + use crate::executor::{Backend, LocalSimulator}; + + const X_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + const IDENTITY_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"; + const H_CIRCUIT: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;"; + + fn backend() -> LocalSimulator { LocalSimulator::new() } + + #[test] + fn x_circuit_passes_exercise_requiring_bitstring_1() { + let criteria = ExerciseCriteria { + required_outcomes: vec![RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.99) }], + forbidden_outcomes: vec!["0".into()], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, X_CIRCUIT, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } + + #[test] + fn identity_circuit_fails_exercise_requiring_bitstring_1() { + let criteria = ExerciseCriteria { + required_outcomes: vec![RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.99) }], + forbidden_outcomes: vec!["0".into()], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, IDENTITY_CIRCUIT, &criteria); + assert!(!result.passed); + } + + #[test] + fn h_circuit_passes_balanced_outcomes_with_2sigma_tolerance() { + let criteria = ExerciseCriteria { + required_outcomes: vec![ + RequiredOutcome { bitstring: "0".into(), min_ratio: Some(0.4) }, + RequiredOutcome { bitstring: "1".into(), min_ratio: Some(0.4) }, + ], + forbidden_outcomes: vec![], + statevector_check: None, + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, H_CIRCUIT, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } + + #[test] + fn invalid_circuit_returns_diagnostics_not_panic() { + let criteria = ExerciseCriteria::default(); + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, "not valid qasm", &criteria); + assert!(!result.passed); + assert!(result.error.is_some()); + } + + #[test] + fn statevector_check_validates_bell_state_amplitudes() { + const BELL: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let criteria = ExerciseCriteria { + required_outcomes: vec![], + forbidden_outcomes: vec![], + statevector_check: Some(StatevectorCheck { + non_zero_amplitude_indices: vec![0, 3], + zero_amplitude_indices: vec![1, 2], + tolerance: 1e-6, + }), + }; + let result = ExerciseChecker::check_circuit(&backend() as &dyn Backend, BELL, &criteria); + assert!(result.passed, "counts: {:?}", result.counts); + } + + #[test] + fn curriculum_has_18_exercises_total() { + let loader = CurriculumLoader::default(); + assert_eq!(loader.all_exercises().len(), 18); + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..1c38ac3 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +/// Newtype for qubit indices — prevents mixing with plain usizes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct QubitIndex(pub usize); + +/// Newtype for shot count — enforces range and intent at type level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ShotCount(pub u32); + +impl ShotCount { + pub const DEFAULT: ShotCount = ShotCount(1024); + pub const MAX: ShotCount = ShotCount(100_000); +} + +/// Wraps an OpenQASM 3.0 source string. +#[derive(Debug, Clone)] +pub struct CircuitSource(pub String); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiagnosticSeverity { + Error, + Warning, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ValidationDiagnostic { + pub line: usize, + pub column: usize, + pub message: String, + pub severity: DiagnosticSeverity, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ValidationResult { + pub is_valid: bool, + pub diagnostics: Vec, + pub num_qubits: Option, + pub num_gates: Option, +} + +#[derive(Debug, Clone)] +pub struct SimulationResult { + /// Bitstring → count, e.g. `{"00": 512, "11": 512}`. + pub counts: HashMap, + pub shots: u32, + pub execution_time_ms: f64, + /// Optional full statevector as (real, imag) pairs per basis state. + pub statevector: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shot_count_default_is_1024() { + assert_eq!(ShotCount::DEFAULT.0, 1024); + } + + #[test] + fn shot_count_max_is_100_000() { + assert_eq!(ShotCount::MAX.0, 100_000); + } + + #[test] + fn diagnostic_severity_eq() { + assert_eq!(DiagnosticSeverity::Error, DiagnosticSeverity::Error); + assert_ne!(DiagnosticSeverity::Error, DiagnosticSeverity::Warning); + } +} diff --git a/src/validator.rs b/src/validator.rs new file mode 100644 index 0000000..26b593f --- /dev/null +++ b/src/validator.rs @@ -0,0 +1,302 @@ +use oq3_semantics::asg::{Expr, GateOperand, IndexOperator, Literal, Program, Stmt, TExpr}; +use oq3_semantics::symbols::{SymbolTable, SymbolType}; +use oq3_semantics::syntax_to_semantics::parse_source_string; +use oq3_semantics::types::{ArrayDims, Type}; + +use crate::error::BridgeError; +use crate::executor::SUPPORTED_GATES; +use crate::types::{CircuitSource, DiagnosticSeverity, ValidationDiagnostic, ValidationResult}; + +pub struct CircuitValidator { + max_qubits: usize, +} + +impl CircuitValidator { + pub fn new(max_qubits: usize) -> Self { + Self { max_qubits } + } + + pub fn validate(&self, source: &CircuitSource) -> Result { + let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); + + let has_syntax_errors = parse_result.any_syntax_errors(); + + // take_context() consumes parse_result, giving us program, symbol table, and errors. + let context = parse_result.take_context(); + let symbol_table = context.symbol_table(); + let program = context.program(); + let errors = context.errors(); + + let mut diagnostics: Vec = Vec::new(); + + // Surface syntax errors as a single generic diagnostic (we have no span for them). + if has_syntax_errors { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: "circuit contains syntax errors".to_string(), + severity: DiagnosticSeverity::Error, + }); + return Ok(ValidationResult { + is_valid: false, + diagnostics, + num_qubits: None, + num_gates: None, + }); + } + + // Collect semantic errors (undefined variables, bad gate calls, etc.). + for error in errors.iter() { + let range = error.range(); + let offset: usize = u32::from(range.start()) as usize; + let (line, col) = byte_offset_to_line_col(&source.0, offset); + diagnostics.push(ValidationDiagnostic { + line, + column: col, + message: error.message(), + severity: DiagnosticSeverity::Error, + }); + } + + if !diagnostics.is_empty() { + return Ok(ValidationResult { + is_valid: false, + diagnostics, + num_qubits: None, + num_gates: None, + }); + } + + let (total_qubits, gate_count) = + collect_circuit_stats(program, symbol_table, &mut diagnostics); + + if total_qubits > self.max_qubits { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: format!( + "circuit requires {total_qubits} qubits, exceeds local simulator limit of {}", + self.max_qubits + ), + severity: DiagnosticSeverity::Error, + }); + } + + Ok(ValidationResult { + is_valid: diagnostics.is_empty(), + diagnostics, + num_qubits: if total_qubits > 0 { + Some(total_qubits) + } else { + None + }, + num_gates: if gate_count > 0 { + Some(gate_count) + } else { + None + }, + }) + } +} + +/// Walk program statements to count total qubits declared and gate calls made. +/// Pushes error diagnostics for unsupported gates and out-of-bounds qubit indices. +fn collect_circuit_stats( + program: &Program, + symbol_table: &SymbolTable, + diagnostics: &mut Vec, +) -> (usize, usize) { + let mut total_qubits: usize = 0; + let mut gate_count: usize = 0; + + for stmt in program.stmts() { + match stmt { + Stmt::DeclareQuantum(decl) => { + if let Ok(symbol_id) = decl.name() { + let typ = symbol_table[symbol_id].symbol_type(); + total_qubits += qubit_width(typ); + } + } + Stmt::GateCall(gate_call) => { + if let Ok(symbol_id) = gate_call.name() { + let gate_name = symbol_table[symbol_id].name(); + gate_count += 1; + if !SUPPORTED_GATES.contains(&gate_name) { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: format!( + "gate '{gate_name}' is not supported; supported gates: {}", + SUPPORTED_GATES.join(", ") + ), + severity: DiagnosticSeverity::Error, + }); + } + } + // oq3_semantics does not check qubit index bounds — we do it here. + for qubit_texpr in gate_call.qubits() { + check_qubit_index_bounds(qubit_texpr, symbol_table, diagnostics); + } + } + _ => {} + } + } + + (total_qubits, gate_count) +} + +/// Check that an indexed qubit operand (e.g. `q[5]`) is within the declared register size. +/// oq3_semantics resolves the symbol but does not verify the index is in range. +fn check_qubit_index_bounds( + texpr: &TExpr, + symbol_table: &SymbolTable, + diagnostics: &mut Vec, +) { + let Expr::GateOperand(operand) = texpr.expression() else { + return; + }; + let GateOperand::IndexedIdentifier(indexed) = operand else { + return; + }; + let Ok(symbol_id) = indexed.identifier() else { + return; + }; + let declared_width = qubit_width(symbol_table[symbol_id].symbol_type()); + let register_name = symbol_table[symbol_id].name(); + + for index_op in indexed.indexes() { + let IndexOperator::ExpressionList(elist) = index_op else { + continue; + }; + for idx_texpr in &elist.expressions { + let Expr::Literal(Literal::Int(int_lit)) = idx_texpr.expression() else { + continue; + }; + let index = *int_lit.value() as usize; + if index >= declared_width { + diagnostics.push(ValidationDiagnostic { + line: 1, + column: 1, + message: format!( + "qubit index {index} is out of range for register '{register_name}' \ + (declared size {declared_width})" + ), + severity: DiagnosticSeverity::Error, + }); + } + } + } +} + +/// Extract the number of qubits from a `Type::Qubit` or `Type::QubitArray`. +fn qubit_width(typ: &Type) -> usize { + match typ { + Type::Qubit => 1, + Type::QubitArray(ArrayDims::D1(n)) => *n, + // Multi-dimensional qubit arrays are not valid QASM 3 in practice, + // but handle gracefully by taking the first dimension. + Type::QubitArray(ArrayDims::D2(n, _)) => *n, + Type::QubitArray(ArrayDims::D3(n, _, _)) => *n, + _ => 0, + } +} + +fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { + let safe_offset = offset.min(source.len()); + let prefix = &source[..safe_offset]; + let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1; + let col = prefix + .rfind('\n') + .map(|p| safe_offset - p - 1) + .unwrap_or(safe_offset) + + 1; + (line, col) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::executor::MAX_LOCAL_QUBITS; + + fn validator() -> CircuitValidator { + CircuitValidator::new(MAX_LOCAL_QUBITS) + } + + 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;"#; + + #[test] + fn valid_bell_circuit_is_accepted() { + let result = validator() + .validate(&CircuitSource(BELL_CIRCUIT.to_string())) + .unwrap(); + assert!(result.is_valid, "diagnostics: {:?}", result.diagnostics); + assert!(result.diagnostics.is_empty()); + assert_eq!(result.num_qubits, Some(2)); + } + + #[test] + fn unsupported_gate_produces_error_diagnostic() { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] q; +u3(0.1, 0.2, 0.3) q[0];"#; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + let msg = &result.diagnostics[0].message; + assert!( + msg.to_lowercase().contains("u3"), + "expected 'u3' in message, got: {msg}" + ); + } + + #[test] + fn too_many_qubits_produces_error_diagnostic() { + let n = MAX_LOCAL_QUBITS + 1; + let circuit = format!("OPENQASM 3.0;\nqubit[{n}] q;\n"); + let result = validator().validate(&CircuitSource(circuit)).unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + let msg = &result.diagnostics[0].message; + assert!( + msg.contains(&n.to_string()), + "expected qubit count in message, got: {msg}" + ); + } + + #[test] + fn invalid_qasm_syntax_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\nthis is not valid qasm;\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } + + #[test] + fn valid_circuit_num_gates_is_counted() { + let result = validator() + .validate(&CircuitSource(BELL_CIRCUIT.to_string())) + .unwrap(); + assert_eq!(result.num_gates, Some(2)); + } + + #[test] + fn out_of_bounds_qubit_index_produces_error_diagnostic() { + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nh q[5];\n"; + let result = validator() + .validate(&CircuitSource(circuit.to_string())) + .unwrap(); + assert!(!result.is_valid); + assert!(!result.diagnostics.is_empty()); + } +} diff --git a/tests/mcp_protocol.rs b/tests/mcp_protocol.rs new file mode 100644 index 0000000..e66f3c9 --- /dev/null +++ b/tests/mcp_protocol.rs @@ -0,0 +1,170 @@ +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; + +use serde_json::Value; + +struct McpProcess { + child: Child, + reader: BufReader, +} + +impl McpProcess { + fn spawn() -> Self { + let mut child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("failed to spawn quantum-bridge-mcp binary"); + let stdout = child.stdout.take().expect("failed to take stdout"); + let reader = BufReader::new(stdout); + Self { child, reader } + } + + fn send(&mut self, msg: &str) { + let stdin = self.child.stdin.as_mut().expect("failed to get stdin"); + writeln!(stdin, "{}", msg).expect("failed to write to stdin"); + stdin.flush().expect("failed to flush stdin"); + } + + fn recv(&mut self) -> Value { + let mut line = String::new(); + self.reader + .read_line(&mut line) + .expect("failed to read line from stdout"); + serde_json::from_str(line.trim()).expect("failed to parse JSON response") + } + + /// Send initialize and consume the response, returning self for chaining. + fn initialize(&mut self) -> &mut Self { + self.send( + r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#, + ); + let _ = self.recv(); + self + } + + /// Extract the text payload from a tools/call response content array. + fn tool_text(response: &Value) -> Value { + let text = response["result"]["content"][0]["text"] + .as_str() + .expect("expected text field in content[0]"); + serde_json::from_str(text).expect("failed to parse tool response text as JSON") + } +} + +impl Drop for McpProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +#[test] +fn initialize_returns_server_info() { + let mut proc = McpProcess::spawn(); + proc.send( + r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#, + ); + let resp = proc.recv(); + + assert_eq!(resp["result"]["protocolVersion"], "2024-11-05"); + assert!( + resp["result"]["serverInfo"]["name"].is_string(), + "serverInfo.name should be a string" + ); +} + +#[test] +fn tools_list_contains_core_tools() { + let mut proc = McpProcess::spawn(); + proc.initialize(); + proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); + let resp = proc.recv(); + + let tools = resp["result"]["tools"] + .as_array() + .expect("tools should be an array"); + let names: Vec<&str> = tools + .iter() + .map(|t| t["name"].as_str().expect("tool name should be a string")) + .collect(); + assert!(names.contains(&"list_backends")); + assert!(names.contains(&"validate_circuit")); + assert!(names.contains(&"run_circuit")); +} + +#[test] +fn call_list_backends_returns_local_simulator() { + let mut proc = McpProcess::spawn(); + proc.initialize(); + proc.send( + r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#, + ); + let resp = proc.recv(); + + let payload = McpProcess::tool_text(&resp); + let backends = payload["backends"] + .as_array() + .expect("backends should be an array"); + assert!(!backends.is_empty(), "expected at least one backend"); + assert_eq!( + backends[0]["name"], "local_simulator", + "first backend name should be local_simulator" + ); +} + +#[test] +fn call_validate_circuit_with_valid_bell_returns_is_valid_true() { + let bell_circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; + let params = serde_json::json!({ + "name": "validate_circuit", + "arguments": { "circuit": bell_circuit } + }); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": params + }); + + let mut proc = McpProcess::spawn(); + proc.initialize(); + proc.send(&request.to_string()); + let resp = proc.recv(); + + let payload = McpProcess::tool_text(&resp); + assert_eq!( + payload["is_valid"], true, + "Bell circuit should be valid, got: {payload}" + ); +} + +#[test] +fn call_run_circuit_with_x_gate_returns_count_of_1_for_all_shots() { + let x_circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + let params = serde_json::json!({ + "name": "run_circuit", + "arguments": { "circuit": x_circuit, "shots": 100 } + }); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": params + }); + + let mut proc = McpProcess::spawn(); + proc.initialize(); + proc.send(&request.to_string()); + let resp = proc.recv(); + + let payload = McpProcess::tool_text(&resp); + let counts = payload["counts"] + .as_object() + .expect("counts should be an object"); + assert_eq!( + counts.get("1").and_then(|v| v.as_u64()), + Some(100), + "all 100 shots should produce outcome '1', got counts: {counts:?}" + ); +} diff --git a/tests/proptest_invariants.rs b/tests/proptest_invariants.rs new file mode 100644 index 0000000..3124039 --- /dev/null +++ b/tests/proptest_invariants.rs @@ -0,0 +1,67 @@ +//! Quantum mechanics invariants: any valid circuit must preserve unitarity and normalisation. + +use proptest::prelude::*; +use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator}; +use quantum_bridge_mcp::types::{CircuitSource, ShotCount}; + +proptest! { + /// Normalisation: shot counts always sum to exactly the requested number of shots. + #[test] + fn shot_counts_always_sum_to_requested_shots( + shots in 1u32..=1_000u32 + ) { + let circuit = r#"OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q;"#; + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit.to_string()), ShotCount(shots), false) + .unwrap(); + let total: u64 = result.counts.values().sum(); + prop_assert_eq!(total, shots as u64); + } + + /// Statevector norm is always 1.0 within floating-point tolerance. + #[test] + fn statevector_norm_is_one( + angle in -std::f64::consts::PI..=std::f64::consts::PI + ) { + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;" + ); + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit), ShotCount(1), true) + .unwrap(); + let sv = result.statevector.unwrap(); + let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum(); + prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}"); + } + + /// Bitstrings in counts have correct length (= number of qubits). + #[test] + fn count_bitstrings_have_correct_length( + n_qubits in 1usize..=5usize, + shots in 10u32..=100u32 + ) { + let qubit_decls: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect(); + let circuit = format!( + "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{qubit_decls}c = measure q;\n" + ); + let sim = LocalSimulator::new(); + let result = sim + .run(&CircuitSource(circuit), ShotCount(shots), false) + .unwrap(); + for key in result.counts.keys() { + prop_assert_eq!( + key.len(), n_qubits, + "bitstring '{}' has wrong length for {}-qubit circuit", + key, n_qubits + ); + } + } +} diff --git a/tests/reference/tutor/exercises_fail.jsonl b/tests/reference/tutor/exercises_fail.jsonl new file mode 100644 index 0000000..7829eab --- /dev/null +++ b/tests/reference/tutor/exercises_fail.jsonl @@ -0,0 +1,3 @@ +{"exercise_id": "1-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"} +{"exercise_id": "2-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"} +{"exercise_id": "4-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\nc = measure q;"} diff --git a/tests/reference/tutor/exercises_pass.jsonl b/tests/reference/tutor/exercises_pass.jsonl new file mode 100644 index 0000000..7f240b2 --- /dev/null +++ b/tests/reference/tutor/exercises_pass.jsonl @@ -0,0 +1,9 @@ +{"exercise_id": "1-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"} +{"exercise_id": "1-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;"} +{"exercise_id": "2-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;"} +{"exercise_id": "2-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nh q[0];\nc = measure q;"} +{"exercise_id": "3-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nz q[0];\nh q[0];\nc = measure q;"} +{"exercise_id": "4-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"} +{"exercise_id": "5-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nc = measure q;"} +{"exercise_id": "6-1-b", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nx q[1];\nh q[0];\ncz q[0], q[1];\nh q[0];\nc = measure q;"} +{"exercise_id": "7-1-a", "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[0];\nh q[1];\nx q[0];\nx q[1];\ncz q[0], q[1];\nx q[0];\nx q[1];\nh q[0];\nh q[1];\nc = measure q;"} diff --git a/tests/tutor_integration.rs b/tests/tutor_integration.rs new file mode 100644 index 0000000..3efc206 --- /dev/null +++ b/tests/tutor_integration.rs @@ -0,0 +1,159 @@ +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; + +use tempfile::TempDir; + +struct McpProcess { + child: Child, + reader: BufReader, + _tmp: TempDir, +} + +impl McpProcess { + fn spawn() -> Self { + let tmp = TempDir::new().expect("tempdir"); + let progress_path = tmp.path().join("progress.json"); + let mut child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) + .env("QB_PROGRESS_PATH", &progress_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn"); + let stdout = child.stdout.take().expect("take stdout"); + let reader = BufReader::new(stdout); + Self { child, reader, _tmp: tmp } + } + + fn send(&mut self, msg: &str) { + let stdin = self.child.stdin.as_mut().unwrap(); + writeln!(stdin, "{msg}").unwrap(); + stdin.flush().unwrap(); + } + + fn recv(&mut self) -> serde_json::Value { + let mut line = String::new(); + self.reader.read_line(&mut line).unwrap(); + serde_json::from_str(line.trim()).expect("response is JSON") + } + + fn initialize(&mut self) { + self.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#); + let _ = self.recv(); + } + + fn extract(resp: &serde_json::Value) -> serde_json::Value { + let text = resp["result"]["content"][0]["text"].as_str().unwrap(); + serde_json::from_str(text).unwrap() + } +} + +impl Drop for McpProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +#[test] +fn tools_list_exposes_seven_tools() { + let mut p = McpProcess::spawn(); + p.initialize(); + p.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); + let resp = p.recv(); + let tools = resp["result"]["tools"].as_array().unwrap(); + assert_eq!(tools.len(), 7); + let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); + for required in ["get_lesson", "check_exercise", "explain_result", "get_progress"] { + assert!(names.contains(&required), "missing {required}"); + } +} + +#[test] +fn check_exercise_persists_progress_in_sandbox() { + let mut p = McpProcess::spawn(); + p.initialize(); + let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; + let call = serde_json::json!({ + "jsonrpc": "2.0", "id": 3, "method": "tools/call", + "params": {"name": "check_exercise", "arguments": {"exercise_id": "1-1-a", "circuit": circuit}} + }); + p.send(&call.to_string()); + let payload = McpProcess::extract(&p.recv()); + assert_eq!(payload["passed"], true); + assert_eq!(payload["progress_updated"], true); + + p.send(r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_progress","arguments":{}}}"#); + let progress = McpProcess::extract(&p.recv()); + assert!(progress["total_exercises_solved"].as_u64().unwrap() >= 1); +} + +#[test] +fn check_exercise_invalid_circuit_returns_diagnostics_in_payload_not_protocol_error() { + let mut p = McpProcess::spawn(); + p.initialize(); + p.send(r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"check_exercise","arguments":{"exercise_id":"1-1-a","circuit":"not valid qasm"}}}"#); + let resp = p.recv(); + assert!(resp.get("error").is_none(), "expected payload not error: {resp}"); + let payload = McpProcess::extract(&resp); + assert_eq!(payload["passed"], false); + assert!(payload["diagnostics"].as_array().is_some()); +} + +#[test] +fn explain_result_returns_ast_based_breakdown_with_descriptions() { + let mut p = McpProcess::spawn(); + p.initialize(); + let call = serde_json::json!({ + "jsonrpc": "2.0", "id": 6, "method": "tools/call", + "params": {"name": "explain_result", "arguments": { + "circuit": "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;", + "counts": {"00": 512, "11": 512} + }} + }); + p.send(&call.to_string()); + let payload = McpProcess::extract(&p.recv()); + assert_eq!(payload["key_concept"], "entanglement"); + assert_eq!(payload["num_qubits"], 2); + let breakdown = payload["gate_breakdown"].as_array().unwrap(); + assert_eq!(breakdown.len(), 2); + assert_eq!(breakdown[0]["name"], "h"); + assert_eq!(breakdown[1]["name"], "cx"); +} + +#[test] +fn golden_pass_cases_all_pass() { + let golden = std::fs::read_to_string("tests/reference/tutor/exercises_pass.jsonl").unwrap(); + let mut p = McpProcess::spawn(); + p.initialize(); + for line in golden.lines().filter(|l| !l.trim().is_empty()) { + let case: serde_json::Value = serde_json::from_str(line).unwrap(); + let id = case["exercise_id"].as_str().unwrap(); + let circuit = case["circuit"].as_str().unwrap(); + let call = serde_json::json!({ + "jsonrpc": "2.0", "id": 99, "method": "tools/call", + "params": {"name": "check_exercise", "arguments": {"exercise_id": id, "circuit": circuit}} + }); + p.send(&call.to_string()); + let payload = McpProcess::extract(&p.recv()); + assert!(payload["passed"].as_bool().unwrap_or(false), "id={id} payload={payload}"); + } +} + +#[test] +fn golden_fail_cases_all_fail() { + let golden = std::fs::read_to_string("tests/reference/tutor/exercises_fail.jsonl").unwrap(); + let mut p = McpProcess::spawn(); + p.initialize(); + for line in golden.lines().filter(|l| !l.trim().is_empty()) { + let case: serde_json::Value = serde_json::from_str(line).unwrap(); + let id = case["exercise_id"].as_str().unwrap(); + let circuit = case["circuit"].as_str().unwrap(); + let call = serde_json::json!({ + "jsonrpc": "2.0", "id": 98, "method": "tools/call", + "params": {"name": "check_exercise", "arguments": {"exercise_id": id, "circuit": circuit}} + }); + p.send(&call.to_string()); + let payload = McpProcess::extract(&p.recv()); + assert_eq!(payload["passed"].as_bool().unwrap_or(true), false, "id={id} should fail"); + } +} diff --git a/vendor/spinoza/Cargo.toml b/vendor/spinoza/Cargo.toml new file mode 100644 index 0000000..eeec2c8 --- /dev/null +++ b/vendor/spinoza/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "spinoza" +version = "0.5.1" +edition = "2021" + +authors = ["Saveliy Yusufov", "Charlee Stefanski", "Constantin Gonciulea"] +license = "Apache-2.0" +description = "A High Performance Quantum State Simulator" +documentation = "https://qustate.github.io/spinoza/" +repository = "A High Performance Quantum State Simulator implemented in Rust " +keywords = ["quantum-computing", "simulation", "simulator", "quantum"] +categories = ["science", "simulation"] + +[features] +default = ["double"] +single = [] +double = [] + +[dependencies] +arrayvec = "0.7.4" +clap = { version = "4.4.13", features = ["derive"] } +comfy-table = "7.1.0" +env_logger = "0.10.1" +evalexpr = "11.3.0" +multiversion = "0.7.4" +qasm = "1.0.0" +rand = "0.8.5" +rand_distr = "0.4.3" +rayon = "1.8.0" + +[dev-dependencies] +criterion = "0.5.1" + +[[bench]] +name = "benchmark" +harness = false diff --git a/vendor/spinoza/benches/benchmark.rs b/vendor/spinoza/benches/benchmark.rs new file mode 100644 index 0000000..12a9a5b --- /dev/null +++ b/vendor/spinoza/benches/benchmark.rs @@ -0,0 +1,188 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::prelude::*; +use spinoza::{ + circuit::{QuantumCircuit, QuantumRegister}, + core::{iqft, State}, + gates::{apply, c_apply, Gate}, + math::{pow2f, Float, PI}, + measurement::measure_qubit, + utils::{gen_random_state, pretty_print_int}, +}; + +fn first_rotation(circuit: &mut QuantumCircuit, nqubits: usize, angles: &mut Vec) { + for k in 0..nqubits { + circuit.rx(angles.pop().unwrap(), k); + circuit.rz(angles.pop().unwrap(), k); + } +} + +fn mid_rotation(circuit: &mut QuantumCircuit, nqubits: usize, angles: &mut Vec) { + for k in 0..nqubits { + circuit.rz(angles.pop().unwrap(), k); + circuit.rx(angles.pop().unwrap(), k); + circuit.rz(angles.pop().unwrap(), k); + } +} + +fn last_rotation(circuit: &mut QuantumCircuit, nqubits: usize, angles: &mut Vec) { + for k in 0..nqubits { + circuit.rz(angles.pop().unwrap(), k); + circuit.rx(angles.pop().unwrap(), k); + } +} + +fn entangler(circuit: &mut QuantumCircuit, pairs: &[(usize, usize)]) { + for (a, b) in pairs.iter() { + circuit.cx(*a, *b); + } +} + +fn build_circuit(nqubits: usize, depth: usize, pairs: &[(usize, usize)]) -> QuantumCircuit { + let mut rng = StdRng::seed_from_u64(42); + let mut angles: Vec<_> = (0..(nqubits * 2) + (depth * nqubits * 3) + (nqubits * 2)) + .map(|_| rng.gen()) + .collect(); + + let mut q = QuantumRegister::new(nqubits); + let mut circuit = QuantumCircuit::new(&mut [&mut q]); + first_rotation(&mut circuit, nqubits, &mut angles); + entangler(&mut circuit, pairs); + for _ in 0..depth { + mid_rotation(&mut circuit, nqubits, &mut angles); + entangler(&mut circuit, pairs); + } + + last_rotation(&mut circuit, nqubits, &mut angles); + circuit +} + +pub fn qcbm(circuit: &mut QuantumCircuit) { + circuit.execute(); +} + +fn value_encoding(n: usize, v: Float) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } + for i in 0..n { + apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); + } + + let targets: Vec = (0..n).rev().collect(); + iqft(&mut state, &targets); +} + +fn h_gate(n: usize) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } +} + +fn rx_gate(n: usize) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::RX(1.0), &mut state, i); + } +} + +fn rz_gate(state: &mut State, n: usize) { + for i in 0..n { + apply(Gate::RZ(1.0), state, i); + } +} + +fn x_gate(n: usize) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::X, &mut state, i); + } +} + +fn p_gate(n: usize) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::P(1.0), &mut state, i); + } +} + +fn z_gate(n: usize) { + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Z, &mut state, i); + } +} + +fn cx_gate(state: &mut State, n: usize, pairs: &[(usize, usize)]) { + for i in 0..n { + let (p0, p1) = pairs[i]; + c_apply(Gate::X, state, p0, p1); + } +} + +fn u_gate(n: usize) { + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::U(1.0, 2.0, 3.0), &mut state, t); + } +} + +fn pprint_int(i: u128) { + let _res = pretty_print_int(i); +} + +fn measure(n: usize) { + let mut state = gen_random_state(n); + measure_qubit(&mut state, 0, true, None); +} + +fn criterion_benchmark(c: &mut Criterion) { + let n = 25; + + c.bench_function("h", |b| b.iter(|| h_gate(black_box(n)))); + + c.bench_function("x", |b| b.iter(|| x_gate(black_box(n)))); + + let mut state = State::new(n); + let pairs: Vec<_> = (0..n).into_iter().map(|i| (i, (i + 1) % n)).collect(); + c.bench_function("cx", |b| { + b.iter(|| cx_gate(black_box(&mut state), black_box(n), black_box(&pairs))) + }); + + let mut state = State::new(n); + c.bench_function("rz", |b| { + b.iter(|| rz_gate(black_box(&mut state), black_box(n))) + }); + + c.bench_function("rx", |b| b.iter(|| rx_gate(black_box(n)))); + + let mut circuit = build_circuit(n, 9, &pairs); + c.bench_function("qcbm", |b| b.iter(|| qcbm(&mut circuit))); + + c.bench_function("p", |b| b.iter(|| p_gate(black_box(n)))); + + c.bench_function("z", |b| b.iter(|| z_gate(black_box(n)))); + + c.bench_function("u", |b| b.iter(|| u_gate(black_box(n)))); + + c.bench_function("value_encoding", |b| { + b.iter(|| value_encoding(black_box(n), black_box(2.4))) + }); + + c.bench_function("pprint_int", |b| { + b.iter(|| pprint_int(black_box(u128::MAX))) + }); + + c.bench_function("measure", |b| b.iter(|| measure(black_box(n)))); +} + +criterion_group! {name = benches; config = Criterion::default().sample_size(100); targets = criterion_benchmark} +criterion_main!(benches); diff --git a/vendor/spinoza/examples/ccx.rs b/vendor/spinoza/examples/ccx.rs new file mode 100644 index 0000000..49f7c13 --- /dev/null +++ b/vendor/spinoza/examples/ccx.rs @@ -0,0 +1,37 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, cc_apply, Gate}, + math::PI, + utils::{pretty_print_int, to_table}, +}; + +fn x(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + apply(Gate::RZ(PI), &mut state, i); + } + + if show_results { + to_table(&state); + } + + cc_apply(Gate::X, &mut state, 0, 2, 1); + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + x(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/circuits.rs b/vendor/spinoza/examples/circuits.rs new file mode 100644 index 0000000..0fe0bd2 --- /dev/null +++ b/vendor/spinoza/examples/circuits.rs @@ -0,0 +1,40 @@ +use clap::Parser; +use spinoza::{ + circuit::{QuantumCircuit, QuantumRegister}, + config::{Config, QSArgs}, + core::CONFIG, + math::{pow2f, PI}, + utils::{pretty_print_int, to_table}, +}; + +fn benchmark_circuit_value_encoding(n: usize, show_results: bool) { + let v = 2.4; + let now = std::time::Instant::now(); + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + for i in 0..n { + qc.p(2.0 * PI / pow2f(i + 1) * v, i) + } + + let targets: Vec = (0..n).rev().collect(); + qc.iqft(&targets); + qc.execute(); + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + + if show_results { + println!("{}", to_table(qc.get_statevector())); + } +} + +pub fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + benchmark_circuit_value_encoding(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/expectation.rs b/vendor/spinoza/examples/expectation.rs new file mode 100644 index 0000000..b2a4160 --- /dev/null +++ b/vendor/spinoza/examples/expectation.rs @@ -0,0 +1,30 @@ +use clap::Parser; +use spinoza::core::qubit_expectation_value; +use spinoza::{ + config::{Config, QSArgs}, + core::{xyz_expectation_value, State, CONFIG}, + gates::{apply, Gate}, +}; + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + + let n = config.qubits.into(); + let mut state = State::new(n); + let target = 0; + + apply(Gate::RX(0.54), &mut state, target); + apply(Gate::RY(0.12), &mut state, target); + + let targets = (0..n).collect::>(); + let exp_vals = xyz_expectation_value('z', &state, &targets); + println!("expectation values: {:?}", exp_vals); + + let exp_vals: Vec<_> = (0..n).map(|t| qubit_expectation_value(&state, t)).collect(); + println!( + "expectation values using `qubit_expectation_value`: {:?}", + exp_vals + ); +} diff --git a/vendor/spinoza/examples/h.rs b/vendor/spinoza/examples/h.rs new file mode 100644 index 0000000..675a3f6 --- /dev/null +++ b/vendor/spinoza/examples/h.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn h(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::H, &mut state, t); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + h(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/measurement.rs b/vendor/spinoza/examples/measurement.rs new file mode 100644 index 0000000..52131f0 --- /dev/null +++ b/vendor/spinoza/examples/measurement.rs @@ -0,0 +1,25 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::CONFIG, + measurement::measure_qubit, + utils::{gen_random_state, pretty_print_int}, +}; + +fn measure_qubits(n: usize) { + let mut state = gen_random_state(n); + + let now = std::time::Instant::now(); + for t in 0..n { + measure_qubit(&mut state, t, true, None); + } + let elapsed = now.elapsed().as_micros(); + println!("measured all qubits in {} us", pretty_print_int(elapsed)); +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + measure_qubits(config.qubits.into()); +} diff --git a/vendor/spinoza/examples/multicontrol.rs b/vendor/spinoza/examples/multicontrol.rs new file mode 100644 index 0000000..36f98af --- /dev/null +++ b/vendor/spinoza/examples/multicontrol.rs @@ -0,0 +1,24 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{mc_apply, Gate}, + utils::to_table, +}; + +fn mcx(_n: usize, show_results: bool) { + let mut state = State::new(4); + + mc_apply(Gate::X, &mut state, &[1, 2], None, 0); + + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + mcx(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/p.rs b/vendor/spinoza/examples/p.rs new file mode 100644 index 0000000..d3c95f8 --- /dev/null +++ b/vendor/spinoza/examples/p.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn p(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::P(1.0), &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + p(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/pairs.rs b/vendor/spinoza/examples/pairs.rs new file mode 100644 index 0000000..a23041d --- /dev/null +++ b/vendor/spinoza/examples/pairs.rs @@ -0,0 +1,92 @@ +use spinoza::utils::padded_bin; + +fn print_pairs() { + let timer = std::time::Instant::now(); + let n = 6; + let p2n = 1 << n; + + let t = 0; + let p2t = 1 << t; + + let c = 3; + let p2c = 1 << (c + 1); + + let p2m = 1 << (c - t - 1); + + for i in 0..p2m { + for j in (2 * i + 2 * p2m + 1) * p2t..(2 * i + 2 * p2m + 2) * p2t { + // println!("{} = {}", j, padded_bin(j, n)); + for k in (j..j + p2n).step_by(p2c) { + println!("{} = {}", k, padded_bin(k, n)); + } + } + } + + println!("time elapsed: {} us ... ", timer.elapsed().as_micros()); +} + +fn print_target_pairs() { + let n = 3; + + let target = 1; + let dist = 1 << target; + + // 2 for loops + println!("for loops"); + for i in 0..1 << (n - 1 - target) { + for j in 2 * i * dist..(2 * i + 1) * dist { + println!( + "{} = {} -> {} = {}", + j, + padded_bin(j, n), + dist + j, + padded_bin(dist + j, n) + ) + } + } + + // divmod + println!("\ndivmod"); + for i in 0..1 << (n - 1) { + // let (p, s) = (i / dist, i % dist); + // i = dist*p + s + let j = i + ((i >> target) << target); //i + (p << target); // i + dist*p; // 2 * dist * p + s; + println!( + "{} = {} -> {} = {}", + j, + padded_bin(j, n), + dist + j, // i + ((1 + (i >> target)) << target), + padded_bin(dist + j, n) + ) + } + + // bit manipulation + println!("\nbit manipulation"); + let neg_dist = !0 << target; + for i in 0..1 << (n - 1) { + let j = i + (i & neg_dist); + println!( + "{} = {} -> {} = {}", + j, + padded_bin(j, n), + dist + j, + padded_bin(dist + j, n) + ) + } + + for target in 0..n { + for i in 0..1 << (n - 1) { + assert_eq!( + (i >> target) << target, + i & !0 << target, + "different for {}", + i + ); + } + } +} + +fn main() { + print_target_pairs(); + print_pairs(); +} diff --git a/vendor/spinoza/examples/pauli.rs b/vendor/spinoza/examples/pauli.rs new file mode 100644 index 0000000..a957aa8 --- /dev/null +++ b/vendor/spinoza/examples/pauli.rs @@ -0,0 +1,34 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn pauli_functional(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + apply(Gate::X, &mut state, 0); + + for _ in 1..(1 << 30) { + apply(Gate::H, &mut state, 0); + apply(Gate::X, &mut state, 0); + apply(Gate::Z, &mut state, 0); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +pub fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + + pauli_functional(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/qcbm.rs b/vendor/spinoza/examples/qcbm.rs new file mode 100644 index 0000000..b16ce4b --- /dev/null +++ b/vendor/spinoza/examples/qcbm.rs @@ -0,0 +1,55 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, c_apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +pub fn qcbm_functional(n: usize, show_results: bool) { + let pairs: Vec<_> = (0..n).into_iter().map(|i| (i, (i + 1) % n)).collect(); + + let now = std::time::Instant::now(); + let mut state: State = State::new(n); + + for i in 0..n { + apply(Gate::RX(1.0), &mut state, i); + apply(Gate::RZ(1.0), &mut state, i); + } + + for i in 0..n { + let (p0, p1) = pairs[i]; + c_apply(Gate::X, &mut state, p0, p1); + } + + for _ in 0..9 { + for i in 0..n { + apply(Gate::RZ(1.0), &mut state, i); + apply(Gate::RX(1.0), &mut state, i); + apply(Gate::RZ(1.0), &mut state, i); + } + + for i in 0..n { + let (p0, p1) = pairs[i]; + c_apply(Gate::X, &mut state, p0, p1); + } + } + + for i in 0..n { + apply(Gate::RZ(1.0), &mut state, i); + apply(Gate::RX(1.0), &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +pub fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + qcbm_functional(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/rx.rs b/vendor/spinoza/examples/rx.rs new file mode 100644 index 0000000..4ec6a5c --- /dev/null +++ b/vendor/spinoza/examples/rx.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn rx(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::RX(1.0), &mut state, t); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + rx(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/ry.rs b/vendor/spinoza/examples/ry.rs new file mode 100644 index 0000000..422e234 --- /dev/null +++ b/vendor/spinoza/examples/ry.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn ry(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::RY(1.0), &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + ry(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/rz.rs b/vendor/spinoza/examples/rz.rs new file mode 100644 index 0000000..86578dd --- /dev/null +++ b/vendor/spinoza/examples/rz.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn rz(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::RZ(1.0), &mut state, t); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + rz(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/sampling.rs b/vendor/spinoza/examples/sampling.rs new file mode 100644 index 0000000..3f119c7 --- /dev/null +++ b/vendor/spinoza/examples/sampling.rs @@ -0,0 +1,42 @@ +use clap::Parser; +use spinoza::core::reservoir_sampling; +use spinoza::{ + config::{Config, QSArgs}, + core::{iqft, State, CONFIG}, + gates::{apply, Gate}, + math::{pow2f, PI}, + utils::{pretty_print_int, to_table}, +}; + +fn run(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + let v = 2.4; + for i in 0..n { + apply(Gate::H, &mut state, i); + } + for i in 0..n { + apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); + } + let targets: Vec = (0..n).rev().collect(); + + iqft(&mut state, &targets); + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } + + let reservoir = reservoir_sampling(&state, state.len(), state.len() * 100_000); + let histogram = reservoir.get_outcome_count(); + println!("{:?}", histogram); +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + run(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/u.rs b/vendor/spinoza/examples/u.rs new file mode 100644 index 0000000..7c2b77b --- /dev/null +++ b/vendor/spinoza/examples/u.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn u(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::U(1.0, 2.0, 3.0), &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + u(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/unitaries.rs b/vendor/spinoza/examples/unitaries.rs new file mode 100644 index 0000000..e7f8ec2 --- /dev/null +++ b/vendor/spinoza/examples/unitaries.rs @@ -0,0 +1,16 @@ +use spinoza::{ + core::State, + gates::Gate, + unitaries::{apply_unitary, Unitary}, +}; + +fn main() { + let n = 16; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::U(1.0, 2.0, 3.0), 0); + + let now = std::time::Instant::now(); + let _s = apply_unitary(&state, &u); + let elapsed = now.elapsed().as_micros(); + println!("{elapsed} us elapsed"); +} diff --git a/vendor/spinoza/examples/value_encoding.rs b/vendor/spinoza/examples/value_encoding.rs new file mode 100644 index 0000000..9e3be95 --- /dev/null +++ b/vendor/spinoza/examples/value_encoding.rs @@ -0,0 +1,37 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{iqft, State, CONFIG}, + gates::{apply, Gate}, + math::{pow2f, PI}, + utils::{pretty_print_int, to_table}, +}; + +fn value_encoding(n: usize, show_results: bool) { + let v = 2.4; + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } + for i in 0..n { + apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); + } + let targets: Vec = (0..n).rev().collect(); + + iqft(&mut state, &targets); + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +pub fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + value_encoding(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/x.rs b/vendor/spinoza/examples/x.rs new file mode 100644 index 0000000..8422616 --- /dev/null +++ b/vendor/spinoza/examples/x.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn x(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::X, &mut state, t); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + x(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/y.rs b/vendor/spinoza/examples/y.rs new file mode 100644 index 0000000..a52befd --- /dev/null +++ b/vendor/spinoza/examples/y.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn y(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Y, &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + y(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/examples/z.rs b/vendor/spinoza/examples/z.rs new file mode 100644 index 0000000..2694724 --- /dev/null +++ b/vendor/spinoza/examples/z.rs @@ -0,0 +1,29 @@ +use clap::Parser; +use spinoza::{ + config::{Config, QSArgs}, + core::{State, CONFIG}, + gates::{apply, Gate}, + utils::{pretty_print_int, to_table}, +}; + +fn z(n: usize, show_results: bool) { + let now = std::time::Instant::now(); + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Z, &mut state, i); + } + + let elapsed = now.elapsed().as_micros(); + println!("{}", pretty_print_int(elapsed)); + if show_results { + println!("{}", to_table(&state)); + } +} + +fn main() { + let args = QSArgs::parse(); + let config = Config::from_cli(args); + CONFIG.set(config).unwrap(); + z(config.qubits.into(), config.print); +} diff --git a/vendor/spinoza/src/circuit.rs b/vendor/spinoza/src/circuit.rs new file mode 100644 index 0000000..ebf7191 --- /dev/null +++ b/vendor/spinoza/src/circuit.rs @@ -0,0 +1,1251 @@ +//! Abstractions for a quantum circuit +use std::{collections::HashSet, ops::Index}; + +use crate::gates::mc_apply; +use crate::unitaries::Unitary; +use crate::{ + core::State, + gates::{apply, c_apply, c_transform_u, cc_apply, transform_u, Gate}, + math::{pow2f, Float, PI}, + measurement::measure_qubit, +}; + +/// See +#[derive(Clone)] +pub struct QuantumRegister(pub Vec); + +impl Index for QuantumRegister { + type Output = usize; + + #[inline] + fn index(&self, i: usize) -> &Self::Output { + &self.0[i] + } +} + +impl QuantumRegister { + /// Create a new QuantumRegister + pub fn new(size: usize) -> Self { + assert!(size > 0); + QuantumRegister((0..size).collect()) + } + + /// The length of the quantum register + #[allow(clippy::len_without_is_empty)] + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Update quantum register by shift + #[inline] + pub fn update_shift(&mut self, shift: usize) { + self.0.iter_mut().for_each(|x| *x += shift); + } + + /// Get the shift size for this register + #[inline] + pub fn get_shift(&self) -> usize { + self[0] + } +} + +/// Control qubits +#[derive(Clone)] +pub enum Controls { + /// No controls + None, + /// Single Control + Single(usize), + /// Multiple Controls + Ones(Vec), + + /// Mixed Controls + Mixed { + /// Control qubits + controls: Vec, + /// Zeroes + zeros: HashSet, + }, +} + +impl Controls { + fn from(&self, controls: &[usize], zeros: Option>) -> Self { + if let Some(zs) = zeros { + Self::Mixed { + controls: controls.to_vec(), + zeros: zs, + } + } else if controls.is_empty() { + Self::None + } else if controls.len() == 1 { + Self::Single(controls[0]) + } else { + Self::Ones(controls.to_vec()) + } + } + + fn unpack(&self) -> (Vec, HashSet) { + match self { + Self::None => (vec![], HashSet::new()), + Self::Single(c0) => (vec![*c0], HashSet::new()), + Self::Ones(controls) => (controls.clone(), HashSet::new()), + Self::Mixed { controls, zeros } => (controls.clone(), zeros.clone()), + } + } + + fn new_with_control(&self, control: usize, shift: usize) -> Self { + let (mut controls, mut zeros) = self.unpack(); + controls.iter_mut().for_each(|c| *c += shift); + controls.push(control); + + if zeros.is_empty() { + self.from(&controls, None) + } else { + zeros = zeros.iter().map(|z| z + shift).collect(); + self.from(&controls, Some(zeros)) + } + } +} + +/// QuantumTransformation to be applied to the State +#[derive(Clone)] +pub struct QuantumTransformation { + /// The quantum logic gate + pub gate: Gate, + /// The target qubits + pub target: usize, + /// The control qubits + pub controls: Controls, +} + +struct QubitTracker { + /// Used to keep track of the qubits that were already measured We can use a u64, since the + /// number of qubits that one can simulate is very small. Hence, 64 bits suffices to keep + /// track of all qubits. + measured_qubits: u64, + /// Used to keep track of the *values* of qubits that were already measured We can use a u64, + /// since the number of qubits that one can simulate is very small, and the measured values are + /// either 0 or 1. Hence, 64 bits suffices to keep track of all measurement values. + measured_qubits_vals: u64, +} + +impl QubitTracker { + pub fn new() -> Self { + Self { + measured_qubits: 0, + measured_qubits_vals: 0, + } + } + + fn is_qubit_measured(&self, target_qubit: usize) -> bool { + ((self.measured_qubits >> target_qubit) & 1) == 1 + } + + fn get_qubit_measured_val(&mut self, target_qubit: usize) -> Option { + if self.is_qubit_measured(target_qubit) { + ((self.measured_qubits_vals & (1 << target_qubit)) >> target_qubit) + .try_into() + .ok() + } else { + None + } + } + + /// Set the given target qubit as measured + fn set_measured_qubit(&mut self, target_qubit: usize) { + self.measured_qubits |= 1 << target_qubit; + } + + fn set_val_for_measured_qubit(&mut self, target_qubit: usize, value: u8) { + self.measured_qubits_vals &= !(1 << target_qubit); + self.measured_qubits_vals |= (value as u64) << target_qubit; + } +} + +/// A model of a Quantum circuit +/// See +pub struct QuantumCircuit { + /// The list of operations to be applied to the State + pub transformations: Vec, + /// The Quantum State to which transformations are applied + pub state: State, + /// Tracks measured qubits for dynamic circuits + qubit_tracker: QubitTracker, + /// The sizes of the provided quantum registers + pub quantum_registers_info: Vec, +} + +impl QuantumCircuit { + /// Create a new QuantumCircuit from multiple QuantumRegisters + pub fn new(registers: &mut [&mut QuantumRegister]) -> Self { + let mut bits = 0; + + let mut qr_sizes = Vec::with_capacity(registers.len()); + + for r in registers.iter_mut() { + r.update_shift(bits); + qr_sizes.push(r.len()); + bits += r.len(); + } + Self { + transformations: Vec::new(), + state: State::new(bits), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: qr_sizes, + } + } + + /// Get a reference to the Quantum State + pub fn get_statevector(&self) -> &State { + &self.state + } + + // TODO(saveliy): look into fusing the two loops + /// Invert this circuit + pub fn inverse(&mut self) { + self.transformations.reverse(); + self.transformations.iter_mut().for_each(|qt| { + qt.gate = qt.gate.clone().inverse(); + }); + } + + /// Measure a single qubit + #[inline] + pub fn measure(&mut self, target: usize) { + self.add(QuantumTransformation { + gate: Gate::M, + target, + controls: Controls::None, + }); + } + + /// Add a SWAP gate that swaps two qubits + #[inline] + pub fn swap(&mut self, t0: usize, t1: usize) { + self.add(QuantumTransformation { + gate: Gate::SWAP(t0, t1), + target: 0, + controls: Controls::None, + }); + } + + /// Add the X gate for a given target to the list of QuantumTransformations + #[inline] + pub fn x(&mut self, target: usize) { + self.add(QuantumTransformation { + gate: Gate::X, + target, + controls: Controls::None, + }); + } + + /// Add the Y gate for a given target to the list of QuantumTransformations + #[inline] + pub fn y(&mut self, target: usize) { + self.add(QuantumTransformation { + gate: Gate::Y, + target, + controls: Controls::None, + }); + } + + /// Add the Rx gate for a given target to the list of QuantumTransformations + #[inline] + pub fn rx(&mut self, angle: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RX(angle), + target, + controls: Controls::None, + }); + } + + /// Add the CX gate for a given target qubit and control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn cx(&mut self, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::X, + target, + controls: Controls::Single(control), + }); + } + + /// Add the CCX gate for a given target qubit and two control qubits to the list of + /// QuantumTransformations + #[inline] + pub fn ccx(&mut self, control1: usize, control2: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::X, + target, + controls: Controls::Ones(vec![control1, control2]), + }); + } + + /// Add the Hadamard (H) gate for a given target qubit to the list of QuantumTransformations + #[inline] + pub fn h(&mut self, target: usize) { + self.add(QuantumTransformation { + gate: Gate::H, + target, + controls: Controls::None, + }); + } + + /// Add a controlled Hadamard gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn ch(&mut self, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::H, + target, + controls: Controls::Single(control), + }); + } + + /// Add Ry gate for a given target qubit to the list of QuantumTransformations + #[inline] + pub fn ry(&mut self, angle: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RY(angle), + target, + controls: Controls::None, + }); + } + + /// Add CRy gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn cry(&mut self, angle: Float, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RY(angle), + target, + controls: Controls::Single(control), + }); + } + + /// Add Phase (P) gate for a given target qubit to the list of QuantumTransformations + #[inline] + pub fn p(&mut self, angle: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::P(angle), + target, + controls: Controls::None, + }); + } + + /// Add the Controlled Phase (CP) gate for a given target qubit and a given control qubit to + /// the list of QuantumTransformations + #[inline] + pub fn cp(&mut self, angle: Float, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::P(angle), + target, + controls: Controls::Single(control), + }); + } + + /// Add the Controlled U gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn cu(&mut self, theta: Float, phi: Float, lambda: Float, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::U(theta, phi, lambda), + target, + controls: Controls::Single(control), + }); + } + + /// Add the Controlled Y gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn cy(&mut self, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::Y, + target, + controls: Controls::Single(control), + }); + } + + /// Add the Controlled Rx gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn crx(&mut self, angle: Float, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RX(angle), + target, + controls: Controls::Single(control), + }); + } + + /// Add Z gate for a given target qubit to the list of QuantumTransformations + #[inline] + pub fn z(&mut self, target: usize) { + self.add(QuantumTransformation { + gate: Gate::Z, + target, + controls: Controls::None, + }); + } + + /// Add Rz gate for a given target qubit to the list of QuantumTransformations + #[inline] + pub fn rz(&mut self, angle: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RZ(angle), + target, + controls: Controls::None, + }); + } + + /// Add the Controlled Rz gate for a given target qubit and a given control qubit to the list of + /// QuantumTransformations + #[inline] + pub fn crz(&mut self, angle: Float, control: usize, target: usize) { + self.add(QuantumTransformation { + gate: Gate::RZ(angle), + target, + controls: Controls::Single(control), + }); + } + + /// Add the U gate for a given target to the list of QuantumTransformations + #[inline] + pub fn u(&mut self, theta: Float, phi: Float, lambda: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::U(theta, phi, lambda), + target, + controls: Controls::None, + }); + } + + /// Add the bit flip noise gate for a given target qubit, given a probability + // TODO(saveliy) this can potentially be optimized by checking the probability here, + // and then add the X gate, if it evaluates the true. The advantage is for the case + // where the overall number of `QuantumTransformation`s is large, and/or the + // probability of bitflip bit is trivial. + #[inline] + pub fn bit_flip_noise(&mut self, prob: Float, target: usize) { + self.add(QuantumTransformation { + gate: Gate::BitFlipNoise(prob), + target, + controls: Controls::None, + }); + } + + /// Add all transformations for an inverse Quantum Fourier Transform to the list of QuantumTransformations + #[inline] + pub fn iqft(&mut self, targets: &[usize]) { + for j in (0..targets.len()).rev() { + self.h(targets[j]); + for k in (0..j).rev() { + self.cp(-PI / pow2f(j - k), targets[j], targets[k]); + } + } + } + + /// Append a QuantumCircuit to the given register of *this* QuantumCircuit + pub fn append(&mut self, circuit: &QuantumCircuit, reg: &QuantumRegister) { + assert_eq!( + reg.len(), + circuit.quantum_registers_info.iter().sum::() + ); + for tr in circuit.transformations.iter() { + self.add(QuantumTransformation { + gate: tr.gate.clone(), + target: reg.get_shift() + tr.target, + controls: tr.controls.clone(), + }); + } + } + + /// Append a QuantumCircuit to the given register of *this* QuantumCircuit + pub fn c_append(&mut self, circuit: &QuantumCircuit, c: usize, reg: &QuantumRegister) { + assert!(!std::ops::Range { + start: reg.get_shift(), + end: reg.get_shift() + reg.len() + } + .contains(&c)); + for tr in circuit.transformations.iter() { + self.add(QuantumTransformation { + gate: tr.gate.clone(), + target: reg.get_shift() + tr.target, + controls: tr.controls.new_with_control(c, reg.get_shift()), + }); + } + } + + /// Append a QuantumCircuit to the given register of *this* QuantumCircuit + pub fn mc_append( + &mut self, + circuit: &QuantumCircuit, + controls: &[usize], + reg: &QuantumRegister, + ) { + let hashed_controls: HashSet<_> = controls.iter().copied().collect(); + + assert_eq!(controls.len(), hashed_controls.len()); + let range = std::ops::Range { + start: reg.get_shift(), + end: reg.get_shift() + reg.len(), + }; + + controls.iter().for_each(|c| { + if range.contains(c) { + panic!( + "control {} should not be in: Range(start: {} end: {})", + c, range.start, range.end + ); + } + }); + + for control in controls.iter() { + for tr in circuit.transformations.iter() { + self.add(QuantumTransformation { + gate: tr.gate.clone(), + target: reg.get_shift() + tr.target, + controls: tr.controls.new_with_control(*control, reg.get_shift()), + }); + } + } + } + + /// Add a Unitary for a given target qubit to the list of QuantumTransformations + pub fn unitary(&mut self, u: Unitary, target: usize) { + self.add(QuantumTransformation { + gate: Gate::Unitary(u), + target, + controls: Controls::None, + }); + } + + /// Append a `Unitary` to a `QuantumRegister` + pub fn append_u(&mut self, u: Unitary, qr: &QuantumRegister) { + assert_eq!(u.height, u.width); + assert_eq!(u.height, 1 << qr.len()); + self.unitary(u, qr.get_shift()); + } + + /// Add a controlled Unitary for a given target qubit to the list of QuantumTransformations + pub fn c_unitary(&mut self, u: Unitary, c: usize, t: usize) { + self.add(QuantumTransformation { + gate: Gate::Unitary(u), + target: t, + controls: Controls::Single(c), + }); + } + + /// Append a controlled `Unitary` to a `QuantumRegister` + pub fn c_append_u(&mut self, u: Unitary, c: usize, qr: &QuantumRegister) { + assert_eq!(u.height, u.width); + assert_eq!(u.height, 1 << qr.len()); + self.c_unitary(u, c, qr.get_shift()); + } + + /// Add a given `QuantumTransformation` to the list of transformations + #[inline] + pub fn add(&mut self, transformation: QuantumTransformation) { + self.transformations.push(transformation); + } + + /// Run the list of transformations against the State + pub fn execute(&mut self) { + for tr in self.transformations.drain(..) { + match (&tr.controls, tr.gate) { + (Controls::None, Gate::Unitary(u)) => transform_u(&mut self.state, &u, tr.target), + (Controls::Single(c), Gate::Unitary(u)) => { + c_transform_u(&mut self.state, &u, *c, tr.target) + } + (Controls::None, Gate::M) => { + if !self.qubit_tracker.is_qubit_measured(tr.target) { + let value = measure_qubit(&mut self.state, tr.target, true, None); + self.qubit_tracker.set_measured_qubit(tr.target); + self.qubit_tracker + .set_val_for_measured_qubit(tr.target, value); + } + } + (Controls::None, gate) => { + apply(gate, &mut self.state, tr.target); + } + (Controls::Single(control), gate) => { + if let Some(c_bit) = self.qubit_tracker.get_qubit_measured_val(*control) { + if c_bit == 1 { + apply(gate, &mut self.state, tr.target); + } + } else { + c_apply(gate, &mut self.state, *control, tr.target); + } + } + (Controls::Ones(controls), Gate::X) => { + cc_apply( + Gate::X, + &mut self.state, + controls[0], + controls[1], + tr.target, + ); + } + (Controls::Mixed { controls, zeros }, gate) => { + mc_apply( + gate, + &mut self.state, + controls, + Some(zeros.to_owned()), + tr.target, + ); + } + _ => todo!(), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::to_table; + use crate::{ + math::modulus, + utils::{assert_float_closeness, gen_random_state, swap}, + }; + + #[test] + fn register_shift() { + const N: usize = 4; + let mut qr = QuantumRegister::new(N); + assert_eq!(qr.get_shift(), 0); + + qr.update_shift(4); + assert_eq!(qr.get_shift(), 4); + } + + #[test] + fn value_encoding() { + let v = 2.4; + let n = 3; + + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + + for i in 0..n { + qc.p((2 as Float) * PI / pow2f(i + 1) * v, i) + } + + let targets: Vec = (0..n).rev().collect(); + qc.iqft(&targets); + qc.execute(); + } + + #[test] + fn z_gate() { + let n = 2; + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + + qc.z(0); + + qc.execute(); + } + + #[test] + fn crx() { + let n = 3; + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + + let mut t = 0; + while t < n - 1 { + qc.crx(3.043, t, t + 1); + t += 2; + } + + qc.execute(); + } + + #[test] + fn ch() { + let n = 3; + let mut state = State::new(n); + + for target in 0..n { + apply(Gate::H, &mut state, target); + } + c_apply(Gate::H, &mut state, 0, 1); + + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + for target in 0..n { + qc.h(target); + } + qc.ch(0, 1); + qc.execute(); + + assert_eq!(qc.state.reals, state.reals); + assert_eq!(qc.state.imags, state.imags); + } + + #[test] + fn crz() { + let n = 3; + let mut state = State::new(n); + + for target in 0..n { + apply(Gate::H, &mut state, target); + } + c_apply(Gate::RZ(PI / 2.0), &mut state, 0, 1); + + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + for target in 0..n { + qc.h(target); + } + qc.crz(PI / 2.0, 0, 1); + qc.execute(); + + assert_eq!(qc.state.reals, state.reals); + assert_eq!(qc.state.imags, state.imags); + } + + #[test] + fn cy() { + let n = 3; + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + + let mut t = 0; + while t < n - 1 { + qc.cy(t, t + 1); + t += 2; + } + + qc.execute(); + } + + #[test] + fn ccx() { + let n = 3; + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n - 1 { + qc.h(i) + } + + qc.ccx(0, 1, 2); + + qc.execute(); + let _state = qc.get_statevector(); + } + + #[test] + fn x_gate() { + let n = 2; + let mut q = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for i in 0..n { + qc.h(i) + } + + qc.rx(PI / 2.0, 0); + qc.execute(); + } + + #[test] + fn all_gates_as_transformations() { + const N: usize = 17; + let mut q = QuantumRegister::new(N); + let mut qc = QuantumCircuit::new(&mut [&mut q]); + + for t in 0..N { + qc.h(t) + } + + qc.x(0); + qc.y(1); + qc.z(2); + qc.p(PI, 3); + qc.cp(PI, 3, 4); + qc.rx(PI, 5); + qc.ry(PI, 6); + qc.rz(PI, 7); + qc.u(PI, PI, PI, 8); + qc.cy(9, 10); + qc.crx(PI, 11, 12); + qc.cry(PI, 13, 14); + qc.execute(); + + let mut state = State::new(N); + for t in 0..N { + apply(Gate::H, &mut state, t); + } + + apply(Gate::X, &mut state, 0); + apply(Gate::Y, &mut state, 1); + apply(Gate::Z, &mut state, 2); + apply(Gate::P(PI), &mut state, 3); + c_apply(Gate::P(PI), &mut state, 3, 4); + apply(Gate::RX(PI), &mut state, 5); + apply(Gate::RY(PI), &mut state, 6); + apply(Gate::RZ(PI), &mut state, 7); + apply(Gate::U(PI, PI, PI), &mut state, 8); + c_apply(Gate::Y, &mut state, 9, 10); + c_apply(Gate::RX(PI), &mut state, 11, 12); + c_apply(Gate::RY(PI), &mut state, 13, 14); + + state + .reals + .iter() + .zip(state.imags.iter()) + .zip(qc.state.reals.iter()) + .zip(qc.state.imags.iter()) + .for_each(|(((s_re, s_im), qc_re), qc_im)| { + assert_float_closeness(*qc_re, *s_re, 0.001); + assert_float_closeness(*qc_im, *s_im, 0.001); + }); + } + + #[test] + fn measure() { + const N: usize = 21; + let state = gen_random_state(N); + + let sum = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(re, im)| modulus(*re, *im).powi(2)) + .sum(); + + // Make sure the generated random state is sound + assert_float_closeness(sum, 1.0, 0.001); + + let mut qc = QuantumCircuit { + state, + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + // Add Measure gates for all the qubits + for target in 0..N { + qc.measure(target); + } + + // Execute the circuit + qc.execute(); + + // Allocate stack storage of measured qubit values + let mut measured_vals = [0; N]; + + // Now collect the measured values + for (target, measured_val) in measured_vals.iter_mut().enumerate() { + let val = qc + .qubit_tracker + .get_qubit_measured_val(target) + .expect("qubit: {target} should be measured"); + *measured_val = val; + } + + // Now we need to measure again... + + // Add Measure gates for all the qubits + for target in 0..N { + qc.measure(target); + } + + // Execute the circuit + qc.execute(); + + // Now check that the new measured values are the same as what we got before + for (target, measured_val) in measured_vals.iter().enumerate() { + assert!( + qc.qubit_tracker.is_qubit_measured(target), + "qubit {target} was already measured, but it wasn't marked as measured" + ); + assert_eq!( + *measured_val, + qc.qubit_tracker + .get_qubit_measured_val(target) + .expect("qubit: {target} should be measured") + ); + } + } + + #[test] + fn swap_all_qubits() { + const N: usize = 9; + let mut qc = QuantumCircuit { + state: gen_random_state(N), + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + let sum = qc + .state + .reals + .iter() + .zip(qc.state.imags.iter()) + .map(|(re, im)| modulus(*re, *im).powi(2)) + .sum(); + + // Make sure the generated random state is sound + assert_float_closeness(sum, 1.0, 0.001); + + let mut state = qc.state.clone(); + + // Add swap gates, for pairs of qubits, to the circuit. Simultaneously, directly apply swap + // to the lone state (using the 3 CX gate implementation) + for i in 0..(N >> 1) { + qc.swap(i, N - 1 - i); + swap(&mut state, i, N - 1 - i); + } + + qc.execute(); + + assert_eq!(qc.state.n, state.n); + assert_eq!(qc.state.reals, state.reals); + assert_eq!(qc.state.imags, state.imags); + } + + #[test] + fn inverse_iqft() { + const N: usize = 2; + let original_state = gen_random_state(N); + let mut qc1 = QuantumCircuit { + state: original_state.clone(), + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + // First we apply iqft to all qubits + let targets: Vec<_> = (0..N).rev().collect(); + qc1.iqft(&targets); + qc1.execute(); + + // Next, we apply inverse of iqft to all qubits + qc1.iqft(&targets); + qc1.inverse(); + qc1.execute(); + + // Check that after applying iqft and iqft inverse, we get the original state back + original_state + .reals + .iter() + .zip(original_state.imags.iter()) + .zip(qc1.state.reals.iter()) + .zip(qc1.state.imags.iter()) + .for_each(|(((os_re, os_im), qc1_re), qc1_im)| { + assert_float_closeness(*qc1_re, *os_re, 0.001); + assert_float_closeness(*qc1_im, *os_im, 0.001); + }); + } + + #[test] + fn inverse() { + const N: usize = 2; + let mut qc1 = QuantumCircuit { + state: State::new(N), + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + qc1.h(0); + qc1.p(PI / 4.0, 1); + qc1.inverse(); + qc1.execute(); + + let mut qc2 = QuantumCircuit { + state: State::new(N), + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + qc2.p(-(PI / 4.0), 1); + qc2.h(0); + qc2.execute(); + + assert_eq!(qc1.state.reals, qc2.state.reals); + assert_eq!(qc1.state.imags, qc2.state.imags); + } + + // Helper function for testing QuantumCircuit *append methods + fn gate_to_single_qubit_circuit(gate: Gate) -> QuantumCircuit { + let mut qr = QuantumRegister::new(1); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + qc.add(QuantumTransformation { + gate, + target: 0, + controls: Controls::None, + }); + qc + } + + // Helper function for testing `QuantumCircuit`'s `append` method + fn gate_to_circuit(gate: Gate, n: usize, target: usize) -> QuantumCircuit { + let mut qr = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + qc.add(QuantumTransformation { + gate, + target, + controls: Controls::None, + }); + qc + } + + // Helper function for creating an IQFT circuit for testing `QuantumCicuit`'s + // `append` method + fn iqft_circuit(n: usize) -> QuantumCircuit { + let mut iqft_qr = QuantumRegister::new(n); + let mut iqft_qc = QuantumCircuit::new(&mut [&mut iqft_qr]); + let targets: Vec = (0..n).rev().collect(); + iqft_qc.iqft(&targets); + iqft_qc + } + + // Helper function for creating an IQFT circuit for testing `QuantumCircuit`'s + // `c_append` or `mc_append` method + fn iqft_circuit_from_controlled_append(n: usize, multi_control: bool) -> QuantumCircuit { + let mut iqft_qr = QuantumRegister::new(n); + let mut iqft_qc = QuantumCircuit::new(&mut [&mut iqft_qr]); + let targets: Vec = (0..n).rev().collect(); + + for j in (0..targets.len()).rev() { + iqft_qc.append(&gate_to_circuit(Gate::H, n, targets[j]), &iqft_qr); + for k in (0..j).rev() { + let mut qr = QuantumRegister::new(1); + qr.update_shift(targets[k]); + if multi_control { + iqft_qc.mc_append( + &gate_to_single_qubit_circuit(Gate::P(-PI / pow2f(j - k))), + &[targets[j]], + &qr, + ); + } else { + iqft_qc.c_append( + &gate_to_single_qubit_circuit(Gate::P(-PI / pow2f(j - k))), + targets[j], + &qr, + ); + } + } + } + iqft_qc + } + + #[test] + fn append() { + let mut qr0 = QuantumRegister::new(1); + let mut qr1 = QuantumRegister::new(1); + let mut qc = QuantumCircuit::new(&mut [&mut qr0, &mut qr1]); + qc.append(&gate_to_single_qubit_circuit(Gate::H), &qr0); + qc.append(&gate_to_single_qubit_circuit(Gate::H), &qr1); + qc.execute(); + + qc.state + .reals + .iter() + .zip(qc.state.imags.iter()) + .for_each(|(z_re, z_im)| { + assert_float_closeness(*z_re, 0.5, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + }); + println!("{}", to_table(&qc.state)); + } + + #[test] + fn append_value_encoding() { + let n = 3; + let v = 4.0; + let mut qr = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + + for t in 0..n { + qc.append(&gate_to_circuit(Gate::H, n, t), &qr); + } + for t in 0..n { + qc.append( + &gate_to_circuit(Gate::P(2.0 * PI / (pow2f(t + 1)) * v), n, t), + &qr, + ); + } + + let iqft_qc = iqft_circuit(n); + qc.append(&iqft_qc, &qr); + qc.execute(); + + let encoded_integer = v as usize; + + qc.state + .reals + .iter() + .zip(qc.state.imags.iter()) + .enumerate() + .for_each(|(i, (z_re, z_im))| { + if i == encoded_integer { + assert_float_closeness(*z_re, 1.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } else { + assert_float_closeness(*z_re, 0.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } + }); + println!("{}", to_table(&qc.state)); + } + + #[test] + fn c_append_value_encoding() { + let n = 3; + let v = 4.0; + let mut qr = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + + for t in 0..n { + qc.append(&gate_to_circuit(Gate::H, n, t), &qr); + } + for t in 0..n { + qc.append( + &gate_to_circuit(Gate::P(2.0 * PI / (pow2f(t + 1)) * v), n, t), + &qr, + ); + } + + let iqft_qc = iqft_circuit_from_controlled_append(n, false); + qc.append(&iqft_qc, &qr); + qc.execute(); + let encoded_integer = v as usize; + + qc.state + .reals + .iter() + .zip(qc.state.imags.iter()) + .enumerate() + .for_each(|(i, (z_re, z_im))| { + if i == encoded_integer { + assert_float_closeness(*z_re, 1.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } else { + assert_float_closeness(*z_re, 0.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } + }); + println!("{}", to_table(&qc.state)); + } + + #[test] + fn mc_append_value_encoding() { + let n = 3; + let v = 4.0; + let mut qr = QuantumRegister::new(n); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + + for t in 0..n { + qc.append(&gate_to_circuit(Gate::H, n, t), &qr); + } + for t in 0..n { + qc.append( + &gate_to_circuit(Gate::P(2.0 * PI / (pow2f(t + 1)) * v), n, t), + &qr, + ); + } + + let iqft_qc = iqft_circuit_from_controlled_append(n, true); + qc.append(&iqft_qc, &qr); + qc.execute(); + + let encoded_integer = v as usize; + + qc.state + .reals + .iter() + .zip(qc.state.imags.iter()) + .enumerate() + .for_each(|(i, (z_re, z_im))| { + if i == encoded_integer { + assert_float_closeness(*z_re, 1.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } else { + assert_float_closeness(*z_re, 0.0, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + } + }); + println!("{}", to_table(&qc.state)); + } + + #[test] + fn bit_flip_noise() { + const N: usize = 1; + let state = gen_random_state(N); + + let mut qc1 = QuantumCircuit { + state: state.clone(), + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + let mut qc2 = QuantumCircuit { + state, + transformations: Vec::new(), + qubit_tracker: QubitTracker::new(), + quantum_registers_info: Vec::new(), + }; + + // Sanity checks + assert_eq!(qc1.state.reals, qc2.state.reals); + assert_eq!(qc1.state.imags, qc2.state.imags); + + for target in 0..N { + qc2.bit_flip_noise(0.0, target); + } + + // BitFlipNoise with prob==0.0 should not change anything + assert_eq!(qc1.state.reals, qc2.state.reals); + assert_eq!(qc1.state.imags, qc2.state.imags); + + // BitFlipNoise with prob==1.0 should be equivalent to applying the X gate + for target in 0..N { + qc1.x(target); + qc2.bit_flip_noise(1.0, target); + } + assert_eq!(qc1.state.reals, qc2.state.reals); + assert_eq!(qc1.state.imags, qc2.state.imags); + } + + #[test] + fn controlled_u() { + const N: usize = 3; + let mut qr = QuantumRegister::new(N); + let mut qc = QuantumCircuit::new(&mut [&mut qr]); + + let (control, target) = (0, 1); + qc.cu(1.0, 2.0, 3.0, control, target); + qc.execute(); + + let mut state = State::new(N); + c_apply(Gate::U(1.0, 2.0, 3.0), &mut state, control, target); + + assert_eq!(qc.state.n, state.n); + assert_eq!(qc.state.reals, state.reals); + assert_eq!(qc.state.imags, state.imags); + } +} diff --git a/vendor/spinoza/src/config.rs b/vendor/spinoza/src/config.rs new file mode 100644 index 0000000..9a73e91 --- /dev/null +++ b/vendor/spinoza/src/config.rs @@ -0,0 +1,81 @@ +//! Configuration options for running spinoza +use clap::Parser; + +use crate::core::CONFIG; + +/// Config for simulations that are run using the CLI +#[derive(Clone, Copy, Debug)] +pub struct Config { + /// The number of threads to distribute the workload. + /// `u32` is used to represent number of threads since 4,294,967,295 is a + /// reasonable upperbound. If you have access to a matrioshka brain, and you + /// need a larger data type, please reach out. + pub threads: u32, + /// Whether or not to print the State represented as a table. + pub print: bool, + /// The number of qubits that will make up the State. State vector size is 2^{n}, where n is + /// the # of qubits. Assuming single precision complex numbers, the upper bound with u8 is + /// 2^255 * 64 bit ≈ 4.632 * 10^{65} TB (terabytes). Thus, using u8 suffices. + pub qubits: u8, +} + +impl Config { + /// Get or init the global Config. The default + pub fn global() -> &'static Config { + CONFIG.get_or_init(Config::test) + } + + /// Convert the provided CLI args and turn it into a Config + pub const fn from_cli(args: QSArgs) -> Config { + assert!(args.threads > 0 && args.qubits > 0); + Config { + threads: args.threads, + qubits: args.qubits, + print: args.print, + } + } + + fn test() -> Config { + Config { + threads: std::thread::available_parallelism() + .unwrap() + .get() + .try_into() + .expect("Too much power"), + // no input for tests, so this quantity should not matter + qubits: 0, + print: false, + } + } +} + +/// Representation of the CLI args +#[derive(Parser)] +#[command(author, version, about)] +pub struct QSArgs { + /// Number of threads to use + #[clap(short, long)] + threads: u32, + /// Whether or not to print the state in tabular format + #[clap(short, long)] + print: bool, + /// The number of qubits to use in the system + #[clap(short, long)] + qubits: u8, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_args() { + let config = Config::global(); + assert_eq!( + config.threads, + u32::try_from(std::thread::available_parallelism().unwrap().get()).unwrap() + ); + assert_eq!(config.qubits, 0); + assert!(!config.print); + } +} diff --git a/vendor/spinoza/src/consts.rs b/vendor/spinoza/src/consts.rs new file mode 100644 index 0000000..4be2e24 --- /dev/null +++ b/vendor/spinoza/src/consts.rs @@ -0,0 +1,46 @@ +//! Consts for Quantum State Simulation, such as the Pauli Gates and the Hadamard gate +use crate::math::{Amplitude, SQRT_ONE_HALF}; + +/// The 2 x 2 matrix representation of the Hadamard gate +pub const H: [Amplitude; 4] = [ + Amplitude { + re: SQRT_ONE_HALF, + im: 0.0, + }, + Amplitude { + re: SQRT_ONE_HALF, + im: 0.0, + }, + Amplitude { + re: SQRT_ONE_HALF, + im: 0.0, + }, + Amplitude { + re: -SQRT_ONE_HALF, + im: 0.0, + }, +]; + +/// The 2 x 2 matrix representation of the X gate +pub const X: [Amplitude; 4] = [ + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: 1.0, im: 0.0 }, + Amplitude { re: 1.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, +]; + +/// The 2 x 2 matrix representation of the Y gate +pub const Y: [Amplitude; 4] = [ + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: 0.0, im: -1.0 }, + Amplitude { re: 0.0, im: 1.0 }, + Amplitude { re: 0.0, im: 0.0 }, +]; + +/// The 2 x 2 matrix representation of the Z gate +pub const Z: [Amplitude; 4] = [ + Amplitude { re: 1.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: -1.0, im: 0.0 }, +]; diff --git a/vendor/spinoza/src/core.rs b/vendor/spinoza/src/core.rs new file mode 100644 index 0000000..f83d7be --- /dev/null +++ b/vendor/spinoza/src/core.rs @@ -0,0 +1,340 @@ +//! Abstractions for representing a Quantum State +use std::{collections::HashMap, fmt}; + +use rand::distributions::Uniform; +use rand::prelude::*; +use rayon::prelude::*; +use std::sync::OnceLock; + +use crate::{ + config::Config, + gates::{apply, c_apply, x_apply, y_apply, z_apply, Gate}, + math::{modulus, pow2f, Float, PI}, +}; + +/// Reference to the Config for user passed config args +pub static CONFIG: OnceLock = OnceLock::new(); + +#[derive(Clone)] +/// Representation of a Quantum State. Amplitudes are split between two vectors. +pub struct State { + /// The real components of the state. + pub reals: Vec, + /// The imaginary components of the state. + pub imags: Vec, + /// The number of qubits represented by the state. + pub n: u8, +} + +impl State { + /// Create a new State. The state will always be of size 2^{n}, + /// where n is the number of qubits. Note that n cannot be 0. + pub fn new(n: usize) -> Self { + assert!(n > 0); + let mut reals = vec![0.0; 1 << n]; + let imags = vec![0.0; 1 << n]; + reals[0] = 1.0; + Self { + n: n as u8, + reals, + imags, + } + } + + /// Get the size of the state vector. Size of the state should always be + /// 2^{n}, where n is the number of qubits. + #[allow(clippy::len_without_is_empty)] + #[inline] + pub fn len(&self) -> usize { + self.imags.len() + } +} + +impl fmt::Display for State { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.reals + .iter() + .zip(self.imags.iter()) + .for_each(|(re, im)| { + writeln!(f, "{re} + i{im}").unwrap(); + }); + Ok(()) + } +} + +/// Reservoir for sampling +/// See +pub struct Reservoir { + entries: Vec, + w_s: Float, +} + +impl Reservoir { + /// Create a new reservoir for sampling + pub fn new(k: usize) -> Self { + Self { + entries: vec![0; k], + w_s: 0.0, + } + } + + fn update(&mut self, e_i: usize, w_i: Float) { + self.w_s += w_i; + let delta = w_i / self.w_s; + + self.entries + .par_iter_mut() + .with_min_len(1 << 16) + .for_each_init(thread_rng, |rng, e| { + let epsilon_k: Float = rng.gen(); + if epsilon_k < delta { + *e = e_i; + } + }); + } + + fn weight(reals: &[Float], imags: &[Float], index: usize) -> Float { + let (z_re, z_im) = (reals[index], imags[index]); + modulus(z_re, z_im).powi(2) + } + + /// Run the sampling based on the given State + pub fn sampling(&mut self, reals: &[Float], imags: &[Float], num_tests: usize) { + debug_assert_eq!(reals.len(), imags.len()); + let uniform_dist = Uniform::from(0..reals.len()); + let mut rng = thread_rng(); + + self.w_s = 0.0; + for _ in 0..num_tests { + let outcome = uniform_dist.sample(&mut rng); // aka the index + self.update(outcome, Self::weight(reals, imags, outcome)); + } + } + + /// Create a histogram of the counts for each outcome + pub fn get_outcome_count(&self) -> HashMap { + let mut samples = HashMap::new(); + for entry in self.entries.iter() { + *samples.entry(*entry).or_insert(0) += 1; + } + samples + } +} + +/// Convenience function for running reservoir sampling +pub fn reservoir_sampling(state: &State, reservoir_size: usize, num_tests: usize) -> Reservoir { + let mut reservoir = Reservoir::new(reservoir_size); + reservoir.sampling(&state.reals, &state.imags, num_tests); + reservoir +} + +// pub fn g_proc( +// state: &mut State, +// range: std::ops::Range, +// gate: &Gate, +// marks: &[usize], +// zeros: &std::collections::HashSet, +// dist: usize, +// ) { +// let n = marks.len(); +// +// let mut offset = 0; +// for j in 0..n { +// if !zeros.contains(&j) { +// offset = offset + (1 << marks[j]); +// } +// } +// +// for i in range { +// let mut l1 = i; +// +// for j in (0..n).rev() { +// let k = marks[j] - j; +// l1 = l1 + ((l1 >> k) << k); +// } +// l1 = l1 + offset; +// apply(gate, state, l1 - dist, l1); +// } +// } +// +// /// general transformation +// pub fn g_transform( +// state: &mut [Amplitude], +// controls: &[usize], +// zeros: &HashSet, +// target: usize, +// gate: &impl Gate, +// ) { +// let mut marks = [controls, &[target]].concat(); +// let num_pairs = state.len() >> marks.len(); +// marks.sort_unstable(); +// +// let data = SendPtr(state.as_mut_ptr()); +// let dist = 1 << target; +// +// let range = Range { +// start: 0, +// end: num_pairs, +// }; +// +// g_proc(data, range, gate, &marks, zeros, dist); +// } + +/// Inverse Quantum Fourier transform +pub fn iqft(state: &mut State, targets: &[usize]) { + for j in (0..targets.len()).rev() { + apply(Gate::H, state, targets[j]); + for k in (0..j).rev() { + c_apply(Gate::P(-PI / pow2f(j - k)), state, targets[j], targets[k]); + } + } +} + +// fn apply_bit_flip(prob: Float, target: usize) { +// todo!() +// } + +/// Compute the expectation value of a qubit measurement. +pub fn qubit_expectation_value(state: &State, target: usize) -> Float { + let chunk_size = 1 << (target + 1); + let dist = 1 << target; + + let prob0 = state + .reals + .par_chunks_exact(chunk_size) + .zip_eq(state.imags.par_chunks_exact(chunk_size)) + .map(|(reals_chunk, imags_chunk)| { + reals_chunk + .par_iter() + .take(dist) + .zip_eq(imags_chunk.par_iter().take(dist)) + .with_min_len(1 << 16) + .map(|(re_s0, im_s0)| re_s0.powi(2) + im_s0.powi(2)) + .sum::() + }) + .sum::(); + + // p0 - p1 == p0 - (1 - p0) == p0 - 1 + p0 == 2p0 - 1 + 2.0 * prob0 - 1.0 +} + +/// Compute the expectation value of certain observables (either X, Y, or Z) in the given state. +pub fn xyz_expectation_value(observable: char, state: &State, targets: &[usize]) -> Vec { + if !"xyz".contains(observable) { + panic!("observable {observable} not supported"); + } + + let mut working_state = state.clone(); + let mut values = Vec::with_capacity(targets.len()); + + for target in targets.iter() { + if observable == 'z' { + z_apply(&mut working_state, *target); + } else if observable == 'y' { + y_apply(&mut working_state, *target); + } else { + x_apply(&mut working_state, *target); + } + + // v = O * psi + // + + // + // (a + ib) * (c + id) = a * c + ibc + iad - bd = ac - bd + i(bc + ad) + let k_re = ( + &state.reals, + &state.imags, + &working_state.reals, + &working_state.imags, + ) + .into_par_iter() + .map(|(s_re, s_im, v_re, v_im)| { + let a = *s_re; + let b = *s_im; + let c = v_re; + let d = v_im; + a * c + b * d + }) + .sum(); + + values.push(k_re); + working_state = state.clone(); + } + values +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::assert_float_closeness; + + #[test] + fn encoded_integers() { + const N: usize = 3; + + let state = State::new(N); + let reservoir = reservoir_sampling(&state, state.len(), state.len() * 10_000); + let histogram = reservoir.get_outcome_count(); + let count = *histogram.get(&0).unwrap(); + assert_eq!(count, state.len()); + + for i in 1..(1 << N) { + let mut state = State::new(N); + state.reals[0] = 0.0; + state.reals[i] = 1.0; + + let reservoir = reservoir_sampling(&state, state.len(), state.len() * 10_000); + let histogram = reservoir.get_outcome_count(); + let count = *histogram.get(&i).unwrap(); + assert_eq!(count, state.len()); + } + } + + #[test] + fn xyz_exp_val() { + let mut state = State::new(1); + + apply(Gate::RX(0.54), &mut state, 0); + apply(Gate::RY(0.12), &mut state, 0); + let exp_vals = xyz_expectation_value('z', &state, &[0]); + assert_float_closeness(exp_vals[0], 0.8515405859048367, 0.0001); + } + + #[test] + #[should_panic] + fn xyz_exp_val_bad_observable() { + let mut state = State::new(1); + apply(Gate::RX(0.54), &mut state, 0); + apply(Gate::RY(0.12), &mut state, 0); + xyz_expectation_value('a', &state, &[0]); + } + + #[test] + fn xyz_exp_val_x_as_observable() { + let mut state = State::new(1); + apply(Gate::RX(0.54), &mut state, 0); + apply(Gate::RY(0.12), &mut state, 0); + xyz_expectation_value('x', &state, &[0]); + } + + #[test] + fn xyz_exp_val_y_as_observable() { + let mut state = State::new(1); + apply(Gate::RX(0.54), &mut state, 0); + apply(Gate::RY(0.12), &mut state, 0); + xyz_expectation_value('y', &state, &[0]); + } + + #[test] + fn qubit_exp_val() { + let mut state = State::new(1); + let target = 0; + + apply(Gate::RX(0.54), &mut state, target); + apply(Gate::RY(0.12), &mut state, target); + let exp_vals = xyz_expectation_value('z', &state, &[target]); + let qubit_exp_val = qubit_expectation_value(&state, target); + + assert_float_closeness(qubit_exp_val, exp_vals[0], 0.0001); + } +} diff --git a/vendor/spinoza/src/gates.rs b/vendor/spinoza/src/gates.rs new file mode 100644 index 0000000..a88c227 --- /dev/null +++ b/vendor/spinoza/src/gates.rs @@ -0,0 +1,2237 @@ +//! Abstractions for quantum logic gates +use rand::Rng; +use rayon::prelude::*; +use std::collections::HashSet; + +use crate::unitaries::Unitary; +use crate::{ + config::Config, + consts::{H, X, Y, Z}, + core::State, + math::{Amplitude, Float, SQRT_ONE_HALF}, +}; + +const LOW_QUBIT_THRESHOLD: u8 = 15; + +// https://github.com/rayon-rs/rayon/blob/master/src/lib.rs +struct SendPtr(*mut T); + +// SAFETY: !Send for raw pointers is not for safety, just as a lint +unsafe impl Send for SendPtr {} + +// SAFETY: !Sync for raw pointers is not for safety, just as a lint +unsafe impl Sync for SendPtr {} + +impl SendPtr { + // Helper to avoid disjoint captures of `send_ptr.0` + fn get(self) -> *mut T { + self.0 + } +} + +// Implement Clone without the T: Clone bound from the derive +impl Clone for SendPtr { + fn clone(&self) -> Self { + *self + } +} + +// Implement Copy without the T: Copy bound from the derive +impl Copy for SendPtr {} + +/// Quantum Logic Gates +/// See for more info +#[derive(Clone)] +pub enum Gate { + /// Hadamard gate. See + H, + /// Measurement 'gate' + M, + /// The Pauli-X gate is the quantum equivalent of the NOT gate for classical computers with + /// respect to the standard basis |0>, |1>. See + /// + X, + /// See + Y, + /// See + Z, + /// Phase shift gate. See + P(Float), + /// Rx gate for rotation about the x-axis. See + RX(Float), + /// Ry gate for rotation about the y-axis. See + RY(Float), + /// Rz gate for rotation about the z-axis. See + RZ(Float), + /// Swap gate swaps two qubits. See + SWAP(usize, usize), + /// General single qubit rotation. See + U(Float, Float, Float), + /// A Unitary matrix. See + Unitary(Unitary), + /// A gate to simulate a bit flip based on the provided probability. + BitFlipNoise(Float), +} + +impl Gate { + /// Return the inverted gate + pub fn inverse(self) -> Self { + match self { + Self::H | Self::X | Self::Y | Self::Z | Self::SWAP(_, _) => self, + Self::P(theta) => Self::P(-theta), + Self::RX(theta) => Self::RX(-theta), + Self::RY(theta) => Self::RY(-theta), + Self::RZ(theta) => Self::RZ(-theta), + Self::U(theta, phi, lambda) => Self::U(-theta, -lambda, -phi), + Self::M | Self::BitFlipNoise(_) => unimplemented!(), + Self::Unitary(mut unitary) => { + unitary.conj_t(); + Self::Unitary(unitary) + } + } + } + + /// Return the 2 x 2 matrix representation of the gate + pub fn to_matrix(&self) -> [Amplitude; 4] { + match self { + Self::H => H, + Self::X => X, + Self::Y => Y, + Self::Z => Z, + Self::P(theta) => [ + Amplitude { re: 1.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { + re: theta.cos(), + im: theta.sin(), + }, + ], + Self::RX(theta) => { + let theta = theta / 2.0; + [ + Amplitude { + re: theta.cos(), + im: 0.0, + }, + Amplitude { + re: 0.0, + im: -theta.sin(), + }, + Amplitude { + re: 0.0, + im: -theta.sin(), + }, + Amplitude { + re: theta.cos(), + im: 0.0, + }, + ] + } + Self::RY(theta) => { + let theta = theta / 2.0; + [ + Amplitude { + re: theta.cos(), + im: 0.0, + }, + Amplitude { + re: -theta.sin(), + im: 0.0, + }, + Amplitude { + re: theta.sin(), + im: 0.0, + }, + Amplitude { + re: theta.cos(), + im: 0.0, + }, + ] + } + Self::RZ(theta) => { + let theta = theta / 2.0; + [ + Amplitude { + re: theta.cos(), + im: -theta.sin(), + }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { re: 0.0, im: 0.0 }, + Amplitude { + re: theta.cos(), + im: theta.sin(), + }, + ] + } + Self::U(theta, phi, lambda) => { + let theta = theta / 2.0; + [ + Amplitude { + re: theta.cos(), + im: 0.0, + }, + Amplitude { + re: -lambda.cos() * theta.sin(), + im: -lambda.sin() * theta.sin(), + }, + Amplitude { + re: phi.cos() * theta.sin(), + im: phi.sin() * theta.sin(), + }, + Amplitude { + re: (phi + lambda).cos() * theta.cos(), + im: (phi + lambda).sin() * theta.cos(), + }, + ] + } + _ => unimplemented!(), + } + } +} + +/// Apply a transformation to a single target qubit, with no control(s). +/// +/// # Examples +/// ``` +/// use spinoza::{gates::{apply, Gate}, core::State}; +/// +/// let n = 3; +/// let mut state = State::new(n); +/// +/// for t in 0..n { +/// apply(Gate::H, &mut state, t); +/// } +/// ``` +#[multiversion::multiversion( + targets("x86_64+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", // x86_64-v4 + "x86_64+avx2+fma", // x86_64-v3 + "x86_64+sse4.2", // x86_64-v2 + "x86+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", + "x86+avx2+fma", + "x86+sse4.2", + "x86+sse2", + ))] +pub fn apply(gate: Gate, state: &mut State, target: usize) { + match gate { + Gate::H => h_apply(state, target), + Gate::X => x_apply(state, target), + Gate::Y => y_apply(state, target), + Gate::Z => z_apply(state, target), + Gate::P(theta) => p_apply(state, target, theta), + Gate::RX(theta) => rx_apply(state, target, theta), + Gate::RY(theta) => ry_apply(state, target, theta), + Gate::RZ(theta) => rz_apply(state, target, theta), + Gate::SWAP(t0, t1) => swap_apply(state, t0, t1), + Gate::U(theta, phi, lambda) => u_apply(state, target, theta, phi, lambda), + Gate::BitFlipNoise(prob) => { + bit_flip_noise_apply(state, prob, target); + } + _ => unimplemented!(), + } +} + +/// Apply a transformation to a single target qubit, with a single control. +/// +/// # Examples +/// ``` +/// use spinoza::{gates::{c_apply, Gate}, core::State}; +/// +/// let n = 3; +/// let mut state = State::new(n); +/// +/// let pairs: Vec<(usize, usize)> = (0..n).map(|i| (i, (i +1) % n)).collect(); +/// for (control, target) in pairs.iter() { +/// c_apply(Gate::H, &mut state, *control, *target); +/// } +/// ``` +#[multiversion::multiversion( + targets("x86_64+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", // x86_64-v4 + "x86_64+avx2+fma", // x86_64-v3 + "x86_64+sse4.2", // x86_64-v2 + "x86+avx512f+avx512bw+avx512cd+avx512dq+avx512vl", + "x86+avx2+fma", + "x86+sse4.2", + "x86+sse2", + ))] +pub fn c_apply(gate: Gate, state: &mut State, control: usize, target: usize) { + match gate { + Gate::H => h_c_apply(state, control, target), + Gate::X => x_c_apply(state, control, target), + Gate::Y => y_c_apply(state, control, target), + Gate::P(theta) => p_c_apply(state, control, target, theta), + Gate::RX(theta) => rx_c_apply(state, control, target, theta), + Gate::RY(theta) => ry_c_apply(state, control, target, theta), + Gate::RZ(theta) => rz_c_apply(state, control, target, theta), + Gate::U(theta, phi, lambda) => u_c_apply(state, theta, phi, lambda, control, target), + _ => todo!(), + } +} + +/// Two Controls, Single Target +pub fn cc_apply(gate: Gate, state: &mut State, control0: usize, control1: usize, target: usize) { + match gate { + Gate::X => x_cc_apply(state, control0, control1, target), + _ => todo!(), + } +} + +/// Apply a transformation to a single target qubit, with multiple controls. +/// +/// # Examples +/// ``` +/// use spinoza::{gates::{mc_apply, Gate}, core::State}; +/// +/// let n = 3; +/// let mut state = State::new(n); +/// +/// mc_apply(Gate::P(3.14), &mut state, &[0, 1], None, 2); +/// ``` +pub fn mc_apply( + gate: Gate, + state: &mut State, + controls: &[usize], + zeros: Option>, + target: usize, +) { + debug_assert!(usize::from(state.n) > controls.len()); + let mut mask: u64 = 0; + + if let Some(z) = zeros { + for control in controls.iter() { + if z.contains(control) { + continue; + } + mask |= 1 << control + } + } else { + for control in controls.iter() { + mask |= 1 << control + } + } + + match gate { + Gate::X => x_mc_apply(state, mask, target), + Gate::P(theta) => p_mc_apply(state, mask, target, theta), + Gate::RX(theta) => rx_mc_apply(state, mask, target, theta), + Gate::RY(theta) => ry_mc_apply(state, mask, target, theta), + _ => todo!(), + } +} + +fn x_apply_target_0(state_re: SendPtr, state_im: SendPtr, s0: usize) { + unsafe { + std::ptr::swap(state_re.get().add(s0), state_re.get().add(s0 + 1)); + std::ptr::swap(state_im.get().add(s0), state_im.get().add(s0 + 1)); + } +} + +fn x_apply_target(state_re: SendPtr, state_im: SendPtr, s0: usize, s1: usize) { + unsafe { + std::ptr::swap(state_re.get().add(s0), state_re.get().add(s1)); + std::ptr::swap(state_im.get().add(s0), state_im.get().add(s1)); + } +} + +fn x_proc_chunk(state_re: SendPtr, state_im: SendPtr, chunk: usize, target: usize) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + x_apply_target(state_re, state_im, s0, s1) + } +} + +pub(crate) fn x_apply(state: &mut State, target: usize) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if target == 0 { + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..state.len()).step_by(2).for_each(|s0| { + x_apply_target_0(state_re, state_im, s0); + }); + } else { + (0..state.len()).into_par_iter().step_by(2).for_each(|s0| { + x_apply_target_0(state_re, state_im, s0); + }); + } + } else { + let end = state.len() >> 1; + let chunks = end >> target; + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + x_proc_chunk(state_re, state_im, chunk, target); + }); + } else { + (0..chunks).into_par_iter().for_each(|chunk| { + x_proc_chunk(state_re, state_im, chunk, target); + }); + } + } +} + +fn x_c_apply(state: &mut State, control: usize, target: usize) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let end = state.len() >> 2; + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..end).for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + x_apply_target(state_re, state_im, s0, s1); + }); + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + x_apply_target(state_re, state_im, s0, s1); + }); + } +} + +fn x_cc_apply(state: &mut State, control0: usize, control1: usize, target: usize) { + let mut i = 0; + let dist = 1 << target; + + while i < state.len() { + let c0c1_set = (((i >> control0) & 1) != 0) && (((i >> control1) & 1) != 0); + if c0c1_set { + let s0 = i; + let s1 = i + dist; + unsafe { + let temp0 = *state.reals.get_unchecked(s0); + *state.reals.get_unchecked_mut(s0) = *state.reals.get_unchecked(s1); + *state.reals.get_unchecked_mut(s1) = temp0; + + let temp1 = *state.imags.get_unchecked(s0); + *state.imags.get_unchecked_mut(s0) = *state.imags.get_unchecked(s1); + *state.imags.get_unchecked_mut(s1) = temp1; + }; + i += dist; + } + i += 1; + } +} + +fn x_mc_apply(state: &mut State, mask: u64, target: usize) { + let mut i = 0; + let dist = 1 << target; + + while i < state.len() { + if (i as u64 & mask) == mask { + let s0 = i; + let s1 = i + dist; + unsafe { + let temp0 = *state.reals.get_unchecked(s0); + *state.reals.get_unchecked_mut(s0) = *state.reals.get_unchecked(s1); + *state.reals.get_unchecked_mut(s1) = temp0; + + let temp1 = *state.imags.get_unchecked(s0); + *state.imags.get_unchecked_mut(s0) = *state.imags.get_unchecked(s1); + *state.imags.get_unchecked_mut(s1) = temp1; + }; + i += dist; + } + i += 1; + } +} + +fn y_proc_chunk(state_re: SendPtr, state_im: SendPtr, chunk: usize, target: usize) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + + unsafe { + std::ptr::swap(state_re.get().add(s0), state_re.get().add(s1)); + std::ptr::swap(state_im.get().add(s0), state_im.get().add(s1)); + + std::ptr::swap(state_re.get().add(s0), state_im.get().add(s0)); + std::ptr::swap(state_re.get().add(s1), state_im.get().add(s1)); + + *state_im.get().add(s0) = -(*state_im.get().add(s0)); + *state_re.get().add(s1) = -(*state_re.get().add(s1)); + } + } +} + +pub(crate) fn y_apply(state: &mut State, target: usize) { + let end = state.len() >> 1; + let chunks = end >> target; + + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + y_proc_chunk(state_re, state_im, chunk, target); + }); + } else { + (0..chunks).into_par_iter().for_each(|chunk| { + y_proc_chunk(state_re, state_im, chunk, target); + }); + } +} + +fn y_c_apply_to_range(state: &mut State, control: usize, target: usize) { + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + let end = state.len() >> 2; + + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + for i in 0..end { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + + unsafe { + std::ptr::swap(state_re.get().add(s0), state_re.get().add(s1)); + std::ptr::swap(state_im.get().add(s0), state_im.get().add(s1)); + + std::ptr::swap(state_re.get().add(s0), state_im.get().add(s0)); + std::ptr::swap(state_re.get().add(s1), state_im.get().add(s1)); + + *state_im.get().add(s0) = -(*state_im.get().add(s0)); + *state_re.get().add(s1) = -(*state_re.get().add(s1)); + } + } + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + + unsafe { + std::ptr::swap(state_re.get().add(s0), state_re.get().add(s1)); + std::ptr::swap(state_im.get().add(s0), state_im.get().add(s1)); + + std::ptr::swap(state_re.get().add(s0), state_im.get().add(s0)); + std::ptr::swap(state_re.get().add(s1), state_im.get().add(s1)); + + *state_im.get().add(s0) = -(*state_im.get().add(s0)); + *state_re.get().add(s1) = -(*state_re.get().add(s1)); + } + }); + } +} + +fn y_c_apply(state: &mut State, control: usize, target: usize) { + y_c_apply_to_range(state, control, target); +} + +/// Apply H gate via strategy 2 +fn h_apply_strat2(state: &mut State, chunk: usize, target: usize) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + + let (a, b, c, d) = unsafe { + let a = *state.reals.get_unchecked(s0); + let b = *state.imags.get_unchecked(s0); + let c = *state.reals.get_unchecked(s1); + let d = *state.imags.get_unchecked(s1); + (a, b, c, d) + }; + + let a1 = SQRT_ONE_HALF * a; + let b1 = SQRT_ONE_HALF * b; + let c1 = SQRT_ONE_HALF * c; + let d1 = SQRT_ONE_HALF * d; + + unsafe { + *state.reals.get_unchecked_mut(s0) = a1 + c1; + *state.imags.get_unchecked_mut(s0) = b1 + d1; + *state.reals.get_unchecked_mut(s1) = a1 - c1; + *state.imags.get_unchecked_mut(s1) = b1 - d1; + } + } +} + +fn h_apply_strat2_par( + state_re: SendPtr, + state_im: SendPtr, + chunk: usize, + target: usize, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + + let (a, b, c, d) = unsafe { + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + (a, b, c, d) + }; + + let a1 = SQRT_ONE_HALF * a; + let b1 = SQRT_ONE_HALF * b; + let c1 = SQRT_ONE_HALF * c; + let d1 = SQRT_ONE_HALF * d; + + unsafe { + *state_re.get().add(s0) = a1 + c1; + *state_im.get().add(s0) = b1 + c1; + *state_re.get().add(s1) = a1 - c1; + *state_im.get().add(s1) = b1 - d1; + } + } +} + +fn h_apply(state: &mut State, target: usize) { + let end = state.len() >> 1; + let chunks = end >> target; + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|c| { + h_apply_strat2(state, c, target); + }); + } else { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + (0..chunks).into_par_iter().for_each(|c| { + h_apply_strat2_par(state_re, state_im, c, target); + }); + } +} + +fn h_c_apply(state: &mut State, control: usize, target: usize) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let end = state.len() >> 2; + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..end).for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + let (a, b, c, d) = unsafe { + let a = *state.reals.get_unchecked(s0); + let b = *state.imags.get_unchecked(s0); + let c = *state.reals.get_unchecked(s1); + let d = *state.imags.get_unchecked(s1); + (a, b, c, d) + }; + + let a1 = SQRT_ONE_HALF * a; + let b1 = SQRT_ONE_HALF * b; + let c1 = SQRT_ONE_HALF * c; + let d1 = SQRT_ONE_HALF * d; + + unsafe { + *state.reals.get_unchecked_mut(s0) = a1 + c1; + *state.imags.get_unchecked_mut(s0) = b1 + d1; + *state.reals.get_unchecked_mut(s1) = a1 - c1; + *state.imags.get_unchecked_mut(s1) = b1 - d1; + } + }); + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + let (a, b, c, d) = unsafe { + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + (a, b, c, d) + }; + + let a1 = SQRT_ONE_HALF * a; + let b1 = SQRT_ONE_HALF * b; + let c1 = SQRT_ONE_HALF * c; + let d1 = SQRT_ONE_HALF * d; + + unsafe { + *state_re.get().add(s0) = a1 + c1; + *state_im.get().add(s0) = b1 + d1; + *state_re.get().add(s1) = a1 - c1; + *state_im.get().add(s1) = b1 - d1; + } + }); + } +} + +fn rx_apply_target_0( + state_re: SendPtr, + state_im: SendPtr, + l: usize, + cos: Float, + neg_sin: Float, +) { + unsafe { + // a + ib + let a = *state_re.get().add(l); + let b = *state_im.get().add(l); + + // c + id + let c = *state_re.get().add(l + 1); + let d = *state_im.get().add(l + 1); + + *state_re.get().add(l) = a * cos - d * neg_sin; + *state_im.get().add(l) = b * cos + c * neg_sin; + + *state_re.get().add(l + 1) = b * -neg_sin + c * cos; + *state_im.get().add(l + 1) = d * cos + a * neg_sin; + } +} + +fn rx_apply_target( + state_re: SendPtr, + state_im: SendPtr, + s0: usize, + s1: usize, + cos: Float, + neg_sin: Float, +) { + unsafe { + // a + ib + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + + // c + id + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + + *state_re.get().add(s0) = a * cos - d * neg_sin; + *state_im.get().add(s0) = b * cos + c * neg_sin; + + *state_re.get().add(s1) = b * -neg_sin + c * cos; + *state_im.get().add(s1) = d * cos + a * neg_sin; + } +} + +fn rx_proc_chunk( + state_re: SendPtr, + state_im: SendPtr, + chunk: usize, + target: usize, + cos: Float, + neg_sin: Float, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + rx_apply_target(state_re, state_im, s0, s1, cos, neg_sin); + } +} + +fn rx_apply(state: &mut State, target: usize, angle: Float) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let theta = angle * 0.5; + let ct = Float::cos(theta); + let nst = -Float::sin(theta); + + if target == 0 { + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..state.len()).step_by(2).for_each(|l| { + rx_apply_target_0(state_re, state_im, l, ct, nst); + }) + } else { + (0..state.len()).into_par_iter().step_by(2).for_each(|l| { + rx_apply_target_0(state_re, state_im, l, ct, nst); + }) + } + } else { + let end = state.len() >> 1; + let chunks = end >> target; + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + rx_proc_chunk(state_re, state_im, chunk, target, ct, nst); + }); + } else { + (0..chunks).into_par_iter().for_each(|chunk| { + rx_proc_chunk(state_re, state_im, chunk, target, ct, nst); + }); + } + } +} + +fn rx_c_apply(state: &mut State, control: usize, target: usize, angle: Float) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let theta = angle * 0.5; + let ct = Float::cos(theta); + let nst = -Float::sin(theta); + let end = state.len() >> 2; + + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..end).for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + rx_apply_target(state_re, state_im, s0, s1, ct, nst); + }); + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + rx_apply_target(state_re, state_im, s0, s1, ct, nst); + }); + } +} + +fn rx_mc_apply(state: &mut State, mask: u64, target: usize, angle: Float) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let theta = angle * 0.5; + let ct = Float::cos(theta); + let nst = -Float::sin(theta); + + let mut i = 0; + let dist = 1 << target; + + while i < state.len() { + if (i as u64 & mask) == mask { + let s0 = i; + let s1 = i + dist; + rx_apply_target(state_re, state_im, s0, s1, ct, nst); + i += dist; + } + i += 1; + } +} + +fn p_proc_chunk(state: &mut State, chunk: usize, target: usize, cos: Float, sin: Float) { + let dist = 1 << target; + let base = (2 * chunk) << target; + + // s1 = base + i + dist, for i \in \{0, 1, 2, \ldots, dist-1\} + for s1 in (base + dist)..(base + 2 * dist) { + let z_re = state.reals[s1]; + let z_im = state.imags[s1]; + state.reals[s1] = z_re.mul_add(cos, -z_im * sin); + state.imags[s1] = z_im.mul_add(cos, z_re * sin); + } +} + +fn p_proc_chunk_par( + state_re: SendPtr, + state_im: SendPtr, + chunk: usize, + target: usize, + cos: Float, + sin: Float, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + + // s1 = base + i + dist, for i \in \{0, 1, 2, \ldots, dist-1\} + for s1 in (base + dist)..(base + 2 * dist) { + unsafe { + let z_re = *state_re.get().add(s1); + let z_im = *state_im.get().add(s1); + *state_re.get().add(s1) = z_re.mul_add(cos, -z_im * sin); + *state_im.get().add(s1) = z_im.mul_add(cos, z_re * sin); + } + } +} + +fn p_apply(state: &mut State, target: usize, angle: Float) { + let (sin, cos) = Float::sin_cos(angle); + let end = state.len() >> 1; + let chunks = end >> target; + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + p_proc_chunk(state, chunk, target, cos, sin); + }); + } else { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + (0..chunks).into_par_iter().for_each(|chunk| { + p_proc_chunk_par(state_re, state_im, chunk, target, cos, sin); + }); + } +} + +fn p_c_apply(state: &mut State, control: usize, target: usize, angle: Float) { + let (sin, cos) = Float::sin_cos(angle); + let end = state.len() >> 2; + let marks = (target.min(control), target.max(control)); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + for i in 0..end { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let z_re = state.reals[s1]; + let z_im = state.imags[s1]; + state.reals[s1] = z_re.mul_add(cos, -z_im * sin); + state.imags[s1] = z_im.mul_add(cos, z_re * sin); + } + } else { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + unsafe { + let z_re = *state_re.get().add(s1); + let z_im = *state_im.get().add(s1); + *state_re.get().add(s1) = z_re.mul_add(cos, -z_im * sin); + *state_im.get().add(s1) = z_im.mul_add(cos, z_re * sin); + } + }); + } +} + +fn p_mc_apply(state: &mut State, mask: u64, target: usize, theta: Float) { + let (sin, cos) = Float::sin_cos(theta); + let mut i = 0; + let dist = 1 << target; + + while i < state.len() { + if (i as u64 & mask) == mask { + let s1 = i + dist; + let z_re = state.reals[s1]; + let z_im = state.imags[s1]; + state.reals[s1] = z_re.mul_add(cos, -z_im * sin); + state.imags[s1] = z_im.mul_add(cos, z_re * sin); + i += dist; + } + i += 1; + } +} + +fn rz_apply_strategy1(state: &mut State, target: usize, diag_matrix: &[Amplitude; 2]) { + let chunk_size = 1 << target; + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + state + .reals + .chunks_exact_mut(chunk_size) + .zip(state.imags.chunks_exact_mut(chunk_size)) + .enumerate() + .for_each(|(i, (c0, c1))| { + c0.iter_mut().zip(c1.iter_mut()).for_each(|(a, b)| { + let m = diag_matrix[i & 1]; + let c = *a; + let d = *b; + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + }); + }); + } else { + state + .reals + .par_chunks_exact_mut(chunk_size) + .zip(state.imags.par_chunks_exact_mut(chunk_size)) + .enumerate() + .with_min_len(1 << 11) + .for_each(|(i, (c0, c1))| { + c0.iter_mut().zip(c1.iter_mut()).for_each(|(a, b)| { + let m = diag_matrix[i & 1]; + let c = *a; + let d = *b; + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + }); + }); + } +} + +// NOTE: since we are checking pairs, rather than generating, we need to go through the *entire* +// state +fn rz_apply(state: &mut State, target: usize, angle: Float) { + let theta = angle * 0.5; + let (s, c) = Float::sin_cos(theta); + let d0 = Amplitude { re: c, im: -s }; + let d1 = Amplitude { re: c, im: s }; + let diag_matrix = [d0, d1]; + rz_apply_strategy1(state, target, &diag_matrix); +} + +fn rz_c_apply(state: &mut State, control: usize, target: usize, angle: Float) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let theta = angle * 0.5; + let (s, c) = Float::sin_cos(theta); + let d0 = Amplitude { re: c, im: -s }; + let d1 = Amplitude { re: c, im: s }; + let diag_matrix = [d0, d1]; + let end = state.len() >> 2; + + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..end).for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + + let m = diag_matrix[0]; + unsafe { + let a = state_re.get().add(s0); + let b = state_im.get().add(s0); + let c = *a; + let d = *b; + + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + } + let m = diag_matrix[1]; + unsafe { + let a = state_re.get().add(s1); + let b = state_im.get().add(s1); + let c = *a; + let d = *b; + + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + } + }); + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + let m = diag_matrix[0]; + unsafe { + let a = state_re.get().add(s0); + let b = state_im.get().add(s0); + let c = *a; + let d = *b; + + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + } + let m = diag_matrix[1]; + unsafe { + let a = state_re.get().add(s1); + let b = state_im.get().add(s1); + let c = *a; + let d = *b; + + *a = c.mul_add(m.re, -d * m.im); + *b = c.mul_add(m.im, d * m.re); + } + }); + } +} + +fn z_proc_chunk(state: &mut State, chunk: usize, target: usize) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in base + dist..base + 2 * dist { + unsafe { + *state.reals.get_unchecked_mut(i) = -state.reals.get_unchecked(i); + *state.imags.get_unchecked_mut(i) = -state.imags.get_unchecked(i); + }; + } +} + +fn z_proc_chunk_par( + state_re: SendPtr, + state_im: SendPtr, + chunk: usize, + target: usize, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in base + dist..base + 2 * dist { + unsafe { + *state_re.get().add(i) = -(*state_re.get().add(i)); + *state_im.get().add(i) = -(*state_im.get().add(i)); + }; + } +} + +pub(crate) fn z_apply(state: &mut State, target: usize) { + // NOTE: chunks == end >> target, where end == state.len() >> 1 + let chunks = state.len() >> (target + 1); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|c| z_proc_chunk(state, c, target)); + } else { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + (0..chunks) + .into_par_iter() + .for_each(|chunk| z_proc_chunk_par(state_re, state_im, chunk, target)); + } +} + +fn ry_apply_strategy2( + state_re: SendPtr, + state_im: SendPtr, + chunk: usize, + target: usize, + sin: Float, + cos: Float, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + + let (a, b, c, d) = unsafe { + // a + ib + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + + // c + id + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + (a, b, c, d) + }; + + unsafe { + *state_re.get().add(s0) = a * cos - c * sin; + *state_im.get().add(s0) = b * cos - d * sin; + *state_re.get().add(s1) = a * sin + c * cos; + *state_im.get().add(s1) = b * sin + d * cos; + } + } +} + +fn ry_apply(state: &mut State, target: usize, angle: Float) { + let theta = angle * 0.5; + let (sin, cos) = Float::sin_cos(theta); + + let end = state.len() >> 1; + let chunks = end >> target; + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + ry_apply_strategy2(state_re, state_im, chunk, target, sin, cos); + }); + } else { + (0..chunks).into_par_iter().for_each(|chunk| { + ry_apply_strategy2(state_re, state_im, chunk, target, sin, cos); + }); + } +} + +fn ry_c_apply_strategy3(state: &mut State, control: usize, target: usize, angle: Float) { + let theta = angle * 0.5; + let (sin, cos) = Float::sin_cos(theta); + let dist = 1 << target; + let marks = (target.min(control), target.max(control)); + let end = state.len() >> 2; + + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + for i in 0..end { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + + let (a, b, c, d) = unsafe { + // a + ib + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + + // c + id + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + (a, b, c, d) + }; + + unsafe { + *state_re.get().add(s0) = a * cos - c * sin; + *state_im.get().add(s0) = b * cos - d * sin; + *state_re.get().add(s1) = a * sin + c * cos; + *state_im.get().add(s1) = b * sin + d * cos; + } + } + } else { + (0..end).into_par_iter().for_each(|i| { + let x = i + (1 << (marks.1 - 1)) + ((i >> (marks.1 - 1)) << (marks.1 - 1)); + let s1 = x + (1 << marks.0) + ((x >> marks.0) << marks.0); + let s0 = s1 - dist; + + let (a, b, c, d) = unsafe { + // a + ib + let a = *state_re.get().add(s0); + let b = *state_im.get().add(s0); + + // c + id + let c = *state_re.get().add(s1); + let d = *state_im.get().add(s1); + (a, b, c, d) + }; + + unsafe { + *state_re.get().add(s0) = a * cos - c * sin; + *state_im.get().add(s0) = b * cos - d * sin; + *state_re.get().add(s1) = a * sin + c * cos; + *state_im.get().add(s1) = b * sin + d * cos; + } + }); + } +} + +fn ry_c_apply(state: &mut State, control: usize, target: usize, angle: Float) { + ry_c_apply_strategy3(state, control, target, angle); +} + +fn ry_mc_apply(state: &mut State, mask: u64, target: usize, angle: Float) { + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let theta = angle * 0.5; + let ct = Float::cos(theta); + let nst = -Float::sin(theta); + + let mut i = 0; + let dist = 1 << target; + + while i < state.len() { + if (i as u64 & mask) == mask { + let s0 = i; + let s1 = i + dist; + rx_apply_target(state_re, state_im, s0, s1, ct, nst); + i += dist; + } + i += 1; + } +} + +fn u_apply_target( + state_re: SendPtr, + state_im: SendPtr, + g: &[Amplitude; 4], + s0: usize, + s1: usize, +) { + let (c, d, m, n) = unsafe { + let z0_re = *state_re.get().add(s0); + let z0_im = *state_im.get().add(s0); + let z1_re = *state_re.get().add(s1); + let z1_im = *state_im.get().add(s1); + (z0_re, z0_im, z1_re, z1_im) + }; + + let a = g[0].re; + // let b = g[0].im; + let k = g[1].re; + let l = g[1].im; + + let q = g[2].re; + let r = g[2].im; + let s = g[3].re; + let t = g[3].im; + + let t0 = a.mul_add(c, k.mul_add(m, -l * n)); + let t1 = a.mul_add(d, k.mul_add(n, l * m)); + let t2 = q.mul_add(c, (-r).mul_add(d, s.mul_add(m, -t * n))); + let t3 = q.mul_add(d, r.mul_add(c, s.mul_add(n, t * m))); + + unsafe { + *state_re.get().add(s0) = t0; + *state_im.get().add(s0) = t1; + *state_re.get().add(s1) = t2; + *state_im.get().add(s1) = t3; + } +} + +fn u_apply_strategy2( + state_re: SendPtr, + state_im: SendPtr, + g: &[Amplitude; 4], + chunk: usize, + target: usize, +) { + let dist = 1 << target; + let base = (2 * chunk) << target; + for i in 0..dist { + let s0 = base + i; + let s1 = s0 + dist; + u_apply_target(state_re, state_im, g, s0, s1); + } +} + +fn u_apply(state: &mut State, target: usize, theta: Float, phi: Float, lambda: Float) { + let (st, ct) = Float::sin_cos(theta * 0.5); + let (sl, cl) = Float::sin_cos(lambda); + let (spl, cpl) = Float::sin_cos(phi + lambda); + let (sp, cp) = Float::sin_cos(phi); + + let c = Amplitude { re: ct, im: 0.0 }; + let ncs = Amplitude { + re: -cl * st, + im: -sl * st, + }; + let es = Amplitude { + re: cp * st, + im: sp * st, + }; + let ec = Amplitude { + re: cpl * ct, + im: spl * ct, + }; + let g = [c, ncs, es, ec]; + + // NOTE: chunks == end >> target, where end == state.len() >> 1 + let chunks = state.len() >> (target + 1); + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + if Config::global().threads < 2 || state.n < LOW_QUBIT_THRESHOLD { + (0..chunks).for_each(|chunk| { + u_apply_strategy2(state_re, state_im, &g, chunk, target); + }); + } else { + (0..chunks).into_par_iter().for_each(|chunk| { + u_apply_strategy2(state_re, state_im, &g, chunk, target); + }); + } +} + +fn u_c_apply( + state: &mut State, + theta: Float, + phi: Float, + lambda: Float, + control: usize, + target: usize, +) { + let (st, ct) = Float::sin_cos(theta * 0.5); + let (sl, cl) = Float::sin_cos(lambda); + let (spl, cpl) = Float::sin_cos(phi + lambda); + let (sp, cp) = Float::sin_cos(phi); + + let c = Amplitude { re: ct, im: 0.0 }; + let ncs = Amplitude { + re: -cl * st, + im: -sl * st, + }; + let es = Amplitude { + re: cp * st, + im: sp * st, + }; + let ec = Amplitude { + re: cpl * ct, + im: spl * ct, + }; + let g = [c, ncs, es, ec]; + + let state_re = SendPtr(state.reals.as_mut_ptr()); + let state_im = SendPtr(state.imags.as_mut_ptr()); + + let distance = 1 << target; + let mask = (1 << control) | (1 << target); + + for i in 0..state.len() { + if i & mask == mask { + let s1 = i; + let s0 = s1 - distance; + u_apply_target(state_re, state_im, &g, s0, s1); + } + } +} + +fn bit_flip_noise_apply(state: &mut State, prob: Float, target: usize) -> bool { + let mut rng = rand::thread_rng(); + let epsilon: Float = rng.gen(); + + if epsilon <= prob { + x_apply(state, target); + return true; + } + false +} + +fn swap_apply(state: &mut State, t0: usize, t1: usize) { + assert!(usize::from(state.n) > t0 && usize::from(state.n) > t1); + + for i in 0..state.len() { + if ((i >> t0) & 1) == 0 && ((i >> t1) & 1) == 1 { + let j = i + (1 << t0) - (1 << t1); + state.reals.swap(i, j); + state.imags.swap(i, j); + } + } +} + +fn unitary_col_vec_mul( + unitary: &Unitary, + vec_reals: &[Float], + vec_imags: &[Float], +) -> (Vec, Vec) { + assert!( + unitary.height == unitary.width + && vec_reals.len() == unitary.height + && vec_imags.len() == unitary.height + ); + let chunk_size = unitary.width; + + let mut reals = Vec::with_capacity(vec_reals.len()); + let mut imags = Vec::with_capacity(vec_imags.len()); + + // TODO(saveliy): parallelize this + unitary + .reals + .chunks_exact(chunk_size) + .zip(unitary.imags.chunks_exact(chunk_size)) + .for_each(|(row_reals, row_imags)| { + let mut dot_prod_re = 0.0; + let mut dot_prod_im = 0.0; + row_reals + .iter() + .zip(row_imags.iter()) + .zip(vec_reals.iter()) + .zip(vec_imags.iter()) + .for_each(|(((a, b), s_re), s_im)| { + dot_prod_re += *a * s_re - *b * s_im; + dot_prod_im += *a * s_im + *b * s_re; + }); + reals.push(dot_prod_re); + imags.push(dot_prod_im); + }); + (reals, imags) +} + +/// Apply a Unitary to the `State` +pub fn transform_u(state: &mut State, u: &Unitary, t: usize) { + assert_eq!(u.height, u.width); + let m: usize = usize::try_from(u.height.ilog2()).unwrap(); + let n: usize = usize::try_from(u.width.ilog2()).unwrap(); + + let mut vec_reals = vec![0.0; 1 << m]; + let mut vec_imags = vec![0.0; 1 << m]; + + for suffix in 0..(1 << t) { + for prefix in 0..(1 << (n - m - t)) { + for target in 0..(1 << m) { + let k = prefix * (1 << (t + m)) + target * (1 << t) + suffix; + vec_reals[target] = state.reals[k]; + vec_imags[target] = state.imags[k]; + } + + let (vec_reals_out, vec_imags_out) = unitary_col_vec_mul(u, &vec_reals, &vec_imags); + + for target in 0..(1 << m) { + let k = prefix * (1 << (t + m)) + target * (1 << t) + suffix; + state.reals[k] = vec_reals_out[target]; + state.imags[k] = vec_imags_out[target]; + } + } + } +} + +/// Apply a controlled Unitary to the `State` +pub fn c_transform_u(state: &mut State, u: &Unitary, c: usize, t: usize) { + assert_eq!(u.height, u.width); + let m: usize = usize::try_from(u.height.ilog2()).unwrap(); + let n: usize = usize::try_from(u.width.ilog2()).unwrap(); + + let mut vec_reals = vec![0.0; 1 << m]; + let mut vec_imags = vec![0.0; 1 << m]; + let mut targets = Vec::new(); + + for suffix in 0..(1 << t) { + for prefix in 0..(1 << (n - m - t)) { + targets.clear(); + for idx in 0..(1 << m) { + let k = prefix * (1 << (t + m)) + idx * (1 << t) + suffix; + if ((k >> c) & 1) == 1 { + vec_reals[idx] = state.reals[k]; + vec_imags[idx] = state.imags[k]; + targets.push(k) + } + } + + let (vec_reals_out, vec_imags_out) = unitary_col_vec_mul(u, &vec_reals, &vec_imags); + + for idx in 0..(1 << m) { + let k = prefix * (1 << (t + m)) + idx * (1 << t) + suffix; + if ((k >> c) & 1) == 1 { + state.reals[k] = vec_reals_out[idx]; + state.imags[k] = vec_imags_out[idx]; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + core::iqft, + math::{pow2f, PI}, + utils::{assert_float_closeness, gen_random_state, mat_mul_2x2, swap}, + }; + + use super::*; + + fn qcbm_functional(n: usize) -> State { + let mut state = State::new(n); + let pairs: Vec<_> = (0..n).map(|i| (i, (i + 1) % n)).collect(); + + for i in 0..n { + apply(Gate::RX(1.0), &mut state, i); + apply(Gate::RZ(1.0), &mut state, i); + } + + for (p0, p1) in pairs.iter().take(n - 1) { + c_apply(Gate::X, &mut state, *p0, *p1); + } + + for _ in 0..9 { + for i in 0..n { + apply(Gate::RZ(1.0), &mut state, i); + apply(Gate::RX(1.0), &mut state, i); + apply(Gate::RZ(1.0), &mut state, i); + } + + for (p0, p1) in pairs.iter().take(n - 1) { + c_apply(Gate::X, &mut state, *p0, *p1); + } + } + + for i in 0..n { + apply(Gate::RZ(1.0), &mut state, i); + apply(Gate::RX(1.0), &mut state, i); + } + state + } + + #[test] + fn h_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } + + for i in 0..(1 << n) { + assert_float_closeness(state.reals[i], 0.35355339059327384, 1e-10); + assert_float_closeness(state.imags[i], 0.0, 1e-10); + } + } + + #[test] + fn x_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::X, &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.0, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[7], 1.0, 1e-10); + assert_float_closeness(state.imags[7], 0.0, 1e-10); + } + + #[test] + fn x_gate_20_qubits() { + let n = 20; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::X, &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.0, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[(1 << n) - 1], 1.0, 1e-10); + assert_float_closeness(state.imags[(1 << n) - 1], 0.0, 1e-10); + } + + #[test] + fn y_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Y, &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.0, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[7], 0.0, 1e-10); + assert_float_closeness(state.imags[7], -1.0, 1e-10); + } + + #[test] + fn y_gate_20_qubits() { + let n = 20; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Y, &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.0, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[(1 << n) - 1], 1.0, 1e-10); + assert_float_closeness(state.imags[(1 << n) - 1], 0.0, 1e-10); + } + + #[test] + fn z_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::Z, &mut state, i); + } + + for i in 0..(1 << n) { + if i == 0 { + assert_float_closeness(state.reals[i], 1.0, 1e-10); + assert_float_closeness(state.imags[i], 0.0, 1e-10); + } else { + assert_float_closeness(state.reals[i], 0.0, 1e-10); + assert_float_closeness(state.imags[i], 0.0, 1e-10); + } + } + } + + #[test] + fn p_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::P(PI), &mut state, i); + } + + for i in 0..(1 << n) { + if i == 0 { + assert_float_closeness(state.reals[i], 1.0, 1e-10); + assert_float_closeness(state.imags[i], 0.0, 1e-10); + } else { + assert_float_closeness(state.reals[i], 0.0, 1e-10); + assert_float_closeness(state.imags[i], 0.0, 1e-10); + } + } + } + + #[test] + fn rx_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::RX(1.0), &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.6758712218347053, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[1], 0.0, 1e-10); + assert_float_closeness(state.imags[1], -0.3692301313020644, 1e-10); + + assert_float_closeness(state.reals[2], 0.0, 1e-10); + assert_float_closeness(state.imags[2], -0.3692301313020644, 1e-10); + + assert_float_closeness(state.reals[3], -0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[3], 0.0, 1e-10); + + assert_float_closeness(state.reals[4], 0.0, 1e-10); + assert_float_closeness(state.imags[4], -0.3692301313020644, 1e-10); + + assert_float_closeness(state.reals[5], -0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[5], 0.0, 1e-10); + + assert_float_closeness(state.reals[6], -0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[6], 0.0, 1e-10); + + assert_float_closeness(state.reals[7], 0.0, 1e-10); + assert_float_closeness(state.imags[7], 0.11019540730213864, 1e-10); + } + + #[test] + fn ry_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::RY(1.0), &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.6758712218347053, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[1], 0.3692301313020644, 1e-10); + assert_float_closeness(state.imags[1], 0.0, 1e-10); + + assert_float_closeness(state.reals[2], 0.3692301313020644, 1e-10); + assert_float_closeness(state.imags[2], 0.0, 1e-10); + + assert_float_closeness(state.reals[3], 0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[3], 0.0, 1e-10); + + assert_float_closeness(state.reals[4], 0.3692301313020644, 1e-10); + assert_float_closeness(state.imags[4], 0.0, 1e-10); + + assert_float_closeness(state.reals[5], 0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[5], 0.0, 1e-10); + + assert_float_closeness(state.reals[6], 0.20171134005566746, 1e-10); + assert_float_closeness(state.imags[6], 0.0, 1e-10); + + assert_float_closeness(state.reals[7], 0.11019540730213864, 1e-10); + assert_float_closeness(state.imags[7], 0.0, 1e-10); + } + + #[test] + fn rz_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::RZ(1.0), &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.07073720166770296, 1e-10); + assert_float_closeness(state.imags[0], -0.9974949866040546, 1e-10); + + assert_float_closeness(state.reals[1], 0.0, 1e-10); + assert_float_closeness(state.imags[1], 0.0, 1e-10); + + assert_float_closeness(state.reals[2], 0.0, 1e-10); + assert_float_closeness(state.imags[2], 0.0, 1e-10); + + assert_float_closeness(state.reals[3], 0.0, 1e-10); + assert_float_closeness(state.imags[3], 0.0, 1e-10); + + assert_float_closeness(state.reals[4], 0.0, 1e-10); + assert_float_closeness(state.imags[4], 0.0, 1e-10); + + assert_float_closeness(state.reals[5], 0.0, 1e-10); + assert_float_closeness(state.imags[5], 0.0, 1e-10); + + assert_float_closeness(state.reals[6], 0.0, 1e-10); + assert_float_closeness(state.imags[6], 0.0, 1e-10); + + assert_float_closeness(state.reals[7], 0.0, 1e-10); + assert_float_closeness(state.imags[7], 0.0, 1e-10); + } + + #[test] + fn value_encoding_3_qubits() { + let v = 2.4; + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } + for i in 0..n { + apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); + } + let targets: Vec = (0..n).rev().collect(); + + iqft(&mut state, &targets); + } + + #[test] + fn value_encoding_20_qubits() { + let v = 2.4; + let n = 20; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::H, &mut state, i); + } + for i in 0..n { + apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); + } + let targets: Vec = (0..n).rev().collect(); + + iqft(&mut state, &targets); + } + + #[test] + fn qcbm_3_qubits() { + let state = qcbm_functional(3); + + assert_float_closeness(state.reals[0], 0.18037770683997864, 1e-10); + assert_float_closeness(state.imags[0], -0.17626993141958947, 1e-10); + + assert_float_closeness(state.reals[7], 0.014503954556966365, 1e-10); + assert_float_closeness(state.imags[7], -0.11198008105074927, 1e-10); + } + + #[test] + fn qcbm_20_qubits() { + let state = qcbm_functional(20); + + assert_float_closeness(state.reals[0], -0.0022221321676945643, 1e-10); + assert_float_closeness(state.imags[0], 0.001743068112560825, 1e-10); + + assert_float_closeness(state.reals[7], -0.0031017461877124453, 1e-10); + assert_float_closeness(state.imags[7], -0.0034043237120339686, 1e-10); + + assert_float_closeness(state.reals[12], 0.0005494086136357235, 1e-10); + assert_float_closeness(state.imags[12], -0.00009827749580581964, 1e-10); + } + + #[test] + fn u_gate_3_qubits() { + let n = 3; + let mut state = State::new(n); + + for i in 0..n { + apply(Gate::U(1.0, 1.0, 1.0), &mut state, i); + } + + assert_float_closeness(state.reals[0], 0.6758712218347053, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[1], 0.19949589133850137, 1e-10); + assert_float_closeness(state.imags[1], 0.3106964422074971, 1e-10); + + assert_float_closeness(state.reals[2], 0.19949589133850137, 1e-10); + assert_float_closeness(state.imags[2], 0.3106964422074971, 1e-10); + + assert_float_closeness(state.reals[3], -0.08394153605985091, 1e-10); + assert_float_closeness(state.imags[3], 0.18341560247417849, 1e-10); + + assert_float_closeness(state.reals[2], 0.19949589133850137, 1e-10); + assert_float_closeness(state.imags[2], 0.3106964422074971, 1e-10); + + assert_float_closeness(state.reals[3], -0.08394153605985091, 1e-10); + assert_float_closeness(state.imags[3], 0.18341560247417849, 1e-10); + + assert_float_closeness(state.reals[3], -0.08394153605985091, 1e-10); + assert_float_closeness(state.imags[3], 0.18341560247417849, 1e-10); + + assert_float_closeness(state.reals[7], -0.1090926263889472, 1e-10); + assert_float_closeness(state.imags[7], 0.015550776766638148, 1e-10); + } + + #[test] + fn u_gate_1_qubit() { + let mut state = State::new(1); + + let lambda = 1.0; + let theta = 2.0; + let phi = 3.0; + + apply(Gate::U(theta, phi, lambda), &mut state, 0); + assert_float_closeness(state.reals[0], 0.5403023058681398, 1e-10); + assert_float_closeness(state.imags[0], 0.0, 1e-10); + + assert_float_closeness(state.reals[1], -0.833049961066805, 1e-10); + assert_float_closeness(state.imags[1], 0.11874839215823475, 1e-10); + + // Check other base vector + let mut state = State::new(1); + apply(Gate::X, &mut state, 0); + apply(Gate::U(theta, phi, lambda), &mut state, 0); + + assert_float_closeness(state.reals[0], -0.4546487134128409, 1e-10); + assert_float_closeness(state.imags[0], -0.7080734182735712, 1e-10); + + assert_float_closeness(state.reals[1], -0.35316515556860967, 1e-10); + assert_float_closeness(state.imags[1], -0.4089021333016357, 1e-10); + } + + #[test] + fn swap_9_qubits() { + const N: usize = 9; + let mut state_0 = gen_random_state(N); + let mut state_1 = state_0.clone(); + + let (t0, t1) = (0, 1); + swap(&mut state_0, t0, t1); + swap_apply(&mut state_1, t0, t1); + + assert_eq!(state_0.n, state_1.n); + assert_eq!(state_0.reals, state_1.reals); + assert_eq!(state_0.imags, state_1.imags); + } + + #[test] + fn swap_all_qubits() { + const N: usize = 3; + let mut state_0 = gen_random_state(N); + let mut state_1 = state_0.clone(); + + for i in 0..(N >> 1) { + swap(&mut state_0, i, N - 1 - i); + swap_apply(&mut state_1, i, N - 1 - i); + } + + assert_eq!(state_0.n, state_1.n); + assert_eq!(state_0.reals, state_1.reals); + assert_eq!(state_0.imags, state_1.imags); + } + + #[test] + fn h_inverse() { + let h = Gate::H.to_matrix(); + let h_inv = Gate::H.inverse().to_matrix(); + + let identity = mat_mul_2x2(h, h_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn x_inverse() { + let x = Gate::X.to_matrix(); + let x_inv = Gate::X.inverse().to_matrix(); + + let identity = mat_mul_2x2(x, x_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn y_inverse() { + let y = Gate::Y.to_matrix(); + let y_inv = Gate::Y.inverse().to_matrix(); + + let identity = mat_mul_2x2(y, y_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn z_inverse() { + let z = Gate::Z.to_matrix(); + let z_inv = Gate::Z.inverse().to_matrix(); + + let identity = mat_mul_2x2(z, z_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn p_inverse() { + let p = Gate::P(2.03).to_matrix(); + let p_inv = Gate::P(2.03).inverse().to_matrix(); + + let identity = mat_mul_2x2(p, p_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn rx_inverse() { + let rx = Gate::RX(2.03).to_matrix(); + let rx_inv = Gate::RX(2.03).inverse().to_matrix(); + + let identity = mat_mul_2x2(rx, rx_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn rz_inverse() { + let rz = Gate::RZ(3.03).to_matrix(); + let rz_inv = Gate::RZ(3.03).inverse().to_matrix(); + + let identity = mat_mul_2x2(rz, rz_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn ry_inverse() { + let ry = Gate::RY(3.03).to_matrix(); + let ry_inv = Gate::RY(3.03).inverse().to_matrix(); + + let identity = mat_mul_2x2(ry, ry_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn u_inverse() { + let u = Gate::U(1.0, 2.0, 3.0).to_matrix(); + let u_inv = Gate::U(1.0, 2.0, 3.0).inverse().to_matrix(); + + let identity = mat_mul_2x2(u, u_inv); + assert_float_closeness(identity[0].re, 1.0, 0.001); + assert_float_closeness(identity[0].im, 0.0, 0.001); + assert_float_closeness(identity[1].re, 0.0, 0.001); + assert_float_closeness(identity[1].im, 0.0, 0.001); + assert_float_closeness(identity[2].re, 0.0, 0.001); + assert_float_closeness(identity[2].im, 0.0, 0.001); + assert_float_closeness(identity[3].re, 1.0, 0.001); + assert_float_closeness(identity[3].im, 0.0, 0.001); + } + + #[test] + fn unitary_inverse() {} + + #[test] + #[should_panic] + fn m_inverse() { + let _m = Gate::M; + let _m_inv = Gate::M.inverse(); + } + + #[test] + #[should_panic] + fn swap_inverse() { + let _swap = Gate::SWAP(0, 1); + let _swap_inv = Gate::SWAP(0, 1).inverse().to_matrix(); + } + + #[test] + fn unitary_column_vector_multiplication() { + // Test case 1: Unitary matrix with real components + let u1 = Unitary { + height: 2, + width: 2, + reals: vec![1.0, 0.0, 0.0, 1.0], + imags: vec![0.0, 0.0, 0.0, 0.0], + }; + + let vec_reals1 = vec![2.0, 3.0]; + let vec_imags1 = vec![4.0, 5.0]; + + let (result_reals1, result_imags1) = unitary_col_vec_mul(&u1, &vec_reals1, &vec_imags1); + + assert_eq!(result_reals1, vec![2.0, 3.0]); + assert_eq!(result_imags1, vec![4.0, 5.0]); + + // Test case 2: Unitary matrix with complex components + let u2 = Unitary { + height: 2, + width: 2, + reals: vec![0.0, 1.0, 1.0, 0.0], + imags: vec![0.0, 0.0, 0.0, 0.0], + }; + + let vec_reals2 = vec![2.0, 3.0]; + let vec_imags2 = vec![4.0, 5.0]; + + let (result_reals2, result_imags2) = unitary_col_vec_mul(&u2, &vec_reals2, &vec_imags2); + + assert_eq!(result_reals2, vec![3.0, 2.0]); + assert_eq!(result_imags2, vec![5.0, 4.0]); + } + + #[test] + fn ch() { + let n = 3; + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::H, &mut state, t); + } + + state + .reals + .iter() + .zip(state.imags.iter()) + .for_each(|(z_re, z_im)| { + assert_float_closeness(*z_re, 0.353553391, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + }); + + c_apply(Gate::H, &mut state, 0, 1); + + let mut i = 0; + + while i < state.len() - 3 { + assert_float_closeness(state.reals[i], 0.353553391, 0.0001); + assert_float_closeness(state.reals[i + 1], 0.5, 0.0001); + assert_float_closeness(state.reals[i + 2], 0.353553391, 0.0001); + assert_float_closeness(state.reals[i + 3], 0.0, 0.0001); + + assert_float_closeness(state.imags[i], 0.0, 0.0001); + assert_float_closeness(state.imags[i + 1], 0.0, 0.0001); + assert_float_closeness(state.imags[i + 2], 0.0, 0.0001); + assert_float_closeness(state.imags[i + 3], 0.0, 0.0001); + i += 4; + } + } + + #[test] + fn crz() { + let n = 3; + let mut state = State::new(n); + + for t in 0..n { + apply(Gate::H, &mut state, t); + } + + state + .reals + .iter() + .zip(state.imags.iter()) + .for_each(|(z_re, z_im)| { + assert_float_closeness(*z_re, 0.353553391, 0.0001); + assert_float_closeness(*z_im, 0.0, 0.0001); + }); + + c_apply(Gate::RZ(PI / 2.0), &mut state, 0, 1); + + let mut i = 0; + + while i < state.len() - 3 { + assert_float_closeness(state.reals[i], 0.353553391, 0.0001); + assert_float_closeness(state.imags[i], 0.0, 0.0001); + + assert_float_closeness(state.reals[i + 1], 0.25, 0.0001); + assert_float_closeness(state.imags[i + 1], -0.25, 0.0001); + + assert_float_closeness(state.reals[i + 2], 0.353553391, 0.0001); + assert_float_closeness(state.imags[i + 2], 0.0, 0.0001); + + assert_float_closeness(state.reals[i + 3], 0.25, 0.0001); + assert_float_closeness(state.imags[i + 3], 0.25, 0.0001); + i += 4; + } + } + + #[test] + fn bit_flip_noise() { + const N: usize = 1; + let state0 = gen_random_state(N); + let mut state1 = state0.clone(); + + apply(Gate::BitFlipNoise(0.0), &mut state1, 0); + assert_eq!( + state0.reals, state1.reals, + "BitFlipNoise with prob=0.0 should have no effect" + ); + assert_eq!( + state0.imags, state1.imags, + "BitFlipNoise with prob=0.0 should have no effect" + ); + + // BitFlipNoise with prob=1.0 is equivalent to just applying the X gate to the provided target qubit + apply(Gate::BitFlipNoise(1.0), &mut state1, 0); + assert_float_closeness(state1.reals[0], state0.reals[1], 0.00001); + assert_float_closeness(state1.reals[1], state0.reals[0], 0.00001); + assert_float_closeness(state1.imags[0], state0.imags[1], 0.00001); + assert_float_closeness(state1.imags[1], state0.imags[0], 0.00001); + } + + #[test] + fn controlled_u() { + const N: usize = 3; + let mut state = State::new(N); + + for target in 0..N { + h_apply(&mut state, target); + } + u_c_apply(&mut state, 1.0, 2.0, 3.0, 0, 1); + + let expected_reals = [ + 0.35355339, 0.47807852, 0.35355339, 0.01747458, 0.35355339, 0.47807852, 0.35355339, + 0.01747458, + ]; + let expected_imags = [ + 0.0, + -0.0239202, + 0.0, + -0.14339942, + 0.0, + -0.0239202, + 0.0, + -0.14339942, + ]; + + state + .reals + .iter() + .zip(expected_reals.iter()) + .for_each(|(actual, expected)| { + assert_float_closeness(*actual, *expected, 0.00001); + }); + state + .imags + .iter() + .zip(expected_imags.iter()) + .for_each(|(actual, expected)| { + assert_float_closeness(*actual, *expected, 0.00001); + }); + } +} diff --git a/vendor/spinoza/src/lib.rs b/vendor/spinoza/src/lib.rs new file mode 100644 index 0000000..a87ecba --- /dev/null +++ b/vendor/spinoza/src/lib.rs @@ -0,0 +1,142 @@ +//! A High Performance Quantum State Simulator +//! +//! Spinoza is a fast and flexible quantum simulator written exclusively in Rust, with bindings available for Python +//! users. Spinoza simulates the evolution of a quantum system’s state by applying quantum gates, with the core design +//! principle being that a single-qubit gate applied to a target qubit preserves the probability of pairs of amplitudes +//! corresponding to measurement outcomes that differ only in the target qubit. Spinoza is intended to enable the +//! development of quantum computing solutions by offering researchers and quantum developers a simple, flexible, and +//! fast tool for classical simulation. +//! +//! # How to use Spinoza +//! +//! There are three ways to use Spinoza: +//! +//! - **Functional** the simplest way to mutate a quantum state, directly. +//! - [apply][gates::apply] for quantum transformations that do not need a control (qubit). +//! - [c_apply][gates::c_apply] for quantum transformations that have a single control. +//! - [mc_apply][gates::mc_apply] for quantum transformation that require multiple controls. +//! - **Object-Oriented** the [QuantumCircuit][circuit::QuantumCircuit] represents a [quantum circuit](https://en.wikipedia.org/wiki/Quantum_circuit). +//! Using the [QuantumCircuit][circuit::QuantumCircuit] one can create, mutate, and simulate quantum circuits with +//! various quantum [gates][gates::Gate], operators, etc. +//! - **Python Bindings** Spinoza has python bindings named `Spynoza` +//! - All functionality for [QuantumCircuit][circuit::QuantumCircuit] and other functions have corresponding bindings +//! created using PyO3. +//! +//! # Examples +//! Let's encode the value, 2.4 using the three aforementioned approaches: +//! +//! ### Functional +//! ``` +//! use spinoza::{ +//! core::{iqft, State}, +//! gates::{apply, Gate}, +//! math::{pow2f, PI}, +//! utils::{to_table}, +//! }; +//! +//! pub fn main() { +//! let n = 3; +//! let v = 2.4; +//! let mut state = State::new(n); +//! +//! for i in 0..n { +//! apply(Gate::H, &mut state, i); +//! } +//! for i in 0..n { +//! apply(Gate::P(2.0 * PI / (pow2f(i + 1)) * v), &mut state, i); +//! } +//! let targets: Vec = (0..n).rev().collect(); +//! +//! iqft(&mut state, &targets); +//! println!("{}", to_table(&state)); +//! } +//! ``` +//! ### Object Oriented (OO) +//! ``` +//! use spinoza::{ +//! core::{iqft, State}, +//! circuit::{QuantumCircuit, QuantumRegister}, +//! math::{pow2f, PI}, +//! utils::{to_table}, +//! }; +//! +//! pub fn main() { +//! let n = 3; +//! let v = 2.4; +//! let now = std::time::Instant::now(); +//! let mut q = QuantumRegister::new(n); +//! let mut qc = QuantumCircuit::new(&mut [&mut q]); +//! +//! for i in 0..n { +//! qc.h(i) +//! } +//! for i in 0..n { +//! qc.p(2.0 * PI / pow2f(i + 1) * v, i) +//! } +//! +//! let targets: Vec = (0..n).rev().collect(); +//! qc.iqft(&targets); +//! qc.execute(); +//! println!("{}", to_table(qc.get_statevector())); +//! } +//!``` +//! ### Spynoza +//! ```python +//! from math import pi +//! from spynoza import QuantumCircuit, QuantumRegister, show_table +//! +//! +//! def value_encoding(n, v): +//! q = QuantumRegister(n) +//! qc = QuantumCircuit(q) +//! +//! for i in range(n): +//! qc.h(i) +//! +//! for i in range(n): +//! qc.p(2 * pi / (2 ** (i + 1)) * v, i) +//! +//! qc.iqft(range(n)[::-1]) +//! +//! qc.execute() +//! return qc.get_statevector() +//! +//! +//! if __name__ == "__main__": +//! state = value_encoding(4, 2.4) +//! print(show_table(state)) +//!``` +//! +//! More complex examples can be found in the [Spinoza examples](https://github.com/QuState/spinoza/tree/main/spinoza/examples) +//! and the [Spynoza exmaples](https://github.com/QuState/spinoza/tree/main/spynoza/examples). +//! +//! # References +//! ```latex +//! @misc{yusufov2023designing, +//! title={Designing a Fast and Flexible Quantum State Simulator}, +//! author={Saveliy Yusufov and Charlee Stefanski and Constantin Gonciulea}, +//! year={2023}, +//! eprint={2303.01493}, +//! archivePrefix={arXiv}, +//! primaryClass={quant-ph} +//! } +//!``` + +#![warn(clippy::complexity)] +#![warn(missing_docs)] +#![warn(clippy::style)] +#![warn(clippy::correctness)] +#![warn(clippy::suspicious)] +#![warn(clippy::perf)] +#![deny(unsafe_op_in_unsafe_fn)] + +pub mod circuit; +pub mod config; +pub mod consts; +pub mod core; +pub mod gates; +pub mod math; +pub mod measurement; +pub mod openqasm; +pub mod unitaries; +pub mod utils; diff --git a/vendor/spinoza/src/math.rs b/vendor/spinoza/src/math.rs new file mode 100644 index 0000000..2d02b3d --- /dev/null +++ b/vendor/spinoza/src/math.rs @@ -0,0 +1,79 @@ +//! An assortment of mathematical structures, functions, and constants for quantum state +//! simulation. + +/// An alias for +pub const SQRT_ONE_HALF: Float = std::f64::consts::FRAC_1_SQRT_2 as Float; + +/// An alias for +pub const PI: Float = std::f64::consts::PI as Float; + +/// The type of floating point number to use for amplitudes +#[cfg(feature = "double")] +pub type Float = f64; + +#[cfg(feature = "single")] +pub type Float = f32; + +/// An amplitude that makes up a Quantum State +#[derive(Copy, Clone)] +pub struct Amplitude { + /// imaginary component + pub im: Float, + /// real component + pub re: Float, +} + +/// The absolute value of a complex number +/// See +#[inline] +pub fn modulus(z_re: Float, z_im: Float) -> Float { + (z_re.mul_add(z_re, z_im * z_im)).sqrt() +} + +/// Compute 2^n and convert it to a float +pub fn pow2f(n: usize) -> Float { + const BASE2: Float = 2.0; + BASE2.powi(n.try_into().unwrap()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::assert_float_closeness; + + fn linspace(start: Float, end: Float, num: Option) -> Vec { + let n = if let Some(num) = num { num } else { 50 }; + let step = (end - start) / n as Float; + let mut x = 0.0; + let mut res: Vec = Vec::with_capacity(n); + + while x < end { + res.push(x); + x += step; + } + res + } + + #[test] + fn modulus_unit_circle() { + let angles = linspace(0.0, 2.0 * PI, Some(100)); + for angle in angles.into_iter() { + let amplitude = Amplitude { + re: angle.cos(), + im: angle.sin(), + }; + assert_float_closeness(modulus(amplitude.re, amplitude.im), 1.0, 0.001); + } + } + + #[test] + fn pow() { + for i in 0..52 { + let mut res = 1.0; + for _ in 0..i { + res *= 2.0; + } + assert_float_closeness(pow2f(i), res, 0.001); + } + } +} diff --git a/vendor/spinoza/src/measurement.rs b/vendor/spinoza/src/measurement.rs new file mode 100644 index 0000000..ce1b0cc --- /dev/null +++ b/vendor/spinoza/src/measurement.rs @@ -0,0 +1,247 @@ +//! Functionality for measurement +use rand_distr::{Binomial, Distribution}; +use rayon::prelude::*; + +use crate::{ + core::State, + gates::{apply, Gate}, + math::Float, +}; + +/// Single qubit measurement +pub fn measure_qubit(state: &mut State, target: usize, reset: bool, v: Option) -> u8 { + let chunk_size = 1 << (target + 1); + let dist = 1 << target; + + let prob0 = state + .reals + .par_chunks_exact(chunk_size) + .zip_eq(state.imags.par_chunks_exact(chunk_size)) + .map(|(reals_chunk, imags_chunk)| { + reals_chunk + .par_iter() + .take(dist) + .zip_eq(imags_chunk.par_iter().take(dist)) + .with_min_len(1 << 16) + .map(|(re_s0, im_s0)| re_s0.powi(2) + im_s0.powi(2)) + .sum::() + }) + .sum::(); + + let val = if let Some(_v) = v { + assert!(_v == 0 || _v == 1); + _v + } else { + let bin = Binomial::new(1, 1.0 - prob0).unwrap(); + bin.sample(&mut rand::thread_rng()) as u8 + }; + + if val == 0 { + let prob0_sqrt_recip = prob0.sqrt().recip(); + state + .reals + .par_chunks_exact_mut(chunk_size) + .zip_eq(state.imags.par_chunks_exact_mut(chunk_size)) + .for_each(|(reals_chunk, imags_chunk)| { + let (reals_s0, reals_s1) = reals_chunk.split_at_mut(dist); + let (imags_s0, imags_s1) = imags_chunk.split_at_mut(dist); + + reals_s0 + .par_iter_mut() + .zip_eq(reals_s1.par_iter_mut()) + .zip_eq(imags_s0.par_iter_mut()) + .zip_eq(imags_s1.par_iter_mut()) + .with_min_len(1 << 16) + .for_each(|(((re_s0, re_s1), im_s0), im_s1)| { + *re_s0 *= prob0_sqrt_recip; + *im_s0 *= prob0_sqrt_recip; + *re_s1 = 0.0; + *im_s1 = 0.0; + }); + }); + } else { + let prob1 = 1.0 - prob0; + let prob1_sqrt_recip = prob1.sqrt().recip(); + + state + .reals + .par_chunks_exact_mut(chunk_size) + .zip_eq(state.imags.par_chunks_exact_mut(chunk_size)) + .for_each(|(reals_chunk, imags_chunk)| { + let (reals_s0, reals_s1) = reals_chunk.split_at_mut(dist); + let (imags_s0, imags_s1) = imags_chunk.split_at_mut(dist); + + reals_s0 + .par_iter_mut() + .zip_eq(reals_s1.par_iter_mut()) + .zip_eq(imags_s0.par_iter_mut()) + .zip_eq(imags_s1.par_iter_mut()) + .with_min_len(1 << 16) + .for_each(|(((re_s0, re_s1), im_s0), im_s1)| { + *re_s1 *= prob1_sqrt_recip; + *im_s1 *= prob1_sqrt_recip; + *re_s0 = 0.0; + *im_s0 = 0.0; + }); + }); + if reset { + apply(Gate::X, state, target); + } + } + val +} + +#[cfg(test)] +mod tests { + use crate::utils::{assert_float_closeness, gen_random_state}; + + use super::*; + + #[test] + fn test_measure_qubit() { + let mut state = gen_random_state(3); + println!("{state}"); + let sum = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(re, im)| re.powi(2) + im.powi(2)) + .sum(); + assert_float_closeness(sum, 1.0, 0.001); + + measure_qubit(&mut state, 0, true, Some(0)); + println!("{state}"); + let sum = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(re, im)| re.powi(2) + im.powi(2)) + .sum(); + assert_float_closeness(sum, 1.0, 0.001); + + measure_qubit(&mut state, 1, true, Some(0)); + println!("{state}"); + let sum = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(re, im)| re.powi(2) + im.powi(2)) + .sum(); + assert_float_closeness(sum, 1.0, 0.001); + + measure_qubit(&mut state, 2, true, Some(1)); + println!("{state}"); + let sum = state + .reals + .iter() + .zip(state.imags.iter()) + .map(|(re, im)| re.powi(2) + im.powi(2)) + .sum(); + + assert_float_closeness(sum, 1.0, 0.001); + } + + #[test] + fn test_measure_qubit_known_state() { + let n = 3; + let mut reals = Vec::with_capacity(1 << n); + let mut imags = Vec::with_capacity(1 << n); + + let vals = [ + 0.034172256444052966, + 0.29007027387615136, + -0.1300556493088507, + 0.47222164829858637, + -0.032338373524095645, + 0.26511510737291843, + 0.1259630181898572, + -0.09645897805840803, + -0.31931099330088214, + -0.24644972468157703, + -0.15963222942036193, + -0.14329373536970438, + -0.1564141838467382, + -0.4751067410290973, + 0.1034273381193853, + -0.32966556091031934, + ]; + + let mut i = 0; + while i < vals.len() - 1 { + reals.push(vals[i]); + imags.push(vals[i + 1]); + i += 2; + } + + let mut state = State { reals, imags, n }; + + let epsilon = 0.001; + + measure_qubit(&mut state, 0, true, Some(0)); + + assert_float_closeness(state.reals[0], 0.04528096797370981, epsilon); + assert_float_closeness(state.imags[0], 0.38436627101331156, epsilon); + assert_float_closeness(state.reals[1], 0.0, epsilon); + assert_float_closeness(state.imags[1], 0.0, epsilon); + + assert_float_closeness(state.reals[2], -0.042850926694402595, epsilon); + assert_float_closeness(state.imags[2], 0.3512986830692283, epsilon); + assert_float_closeness(state.reals[3], 0.0, epsilon); + assert_float_closeness(state.imags[3], 0.0, epsilon); + + assert_float_closeness(state.reals[4], -0.42311255872092046, epsilon); + assert_float_closeness(state.imags[4], -0.32656556082875193, epsilon); + assert_float_closeness(state.reals[5], 0.0, epsilon); + assert_float_closeness(state.imags[5], 0.0, epsilon); + + assert_float_closeness(state.reals[6], -0.2072612811212442, epsilon); + assert_float_closeness(state.imags[6], -0.6295543626114914, epsilon); + assert_float_closeness(state.reals[7], 0.0, epsilon); + assert_float_closeness(state.imags[7], 0.0, epsilon); + println!("{state}"); + + measure_qubit(&mut state, 1, true, Some(0)); + assert_float_closeness(state.reals[0], 0.06861878352538178, epsilon); + assert_float_closeness(state.imags[0], 0.5824686866330654, epsilon); + assert_float_closeness(state.reals[1], 0.0, epsilon); + assert_float_closeness(state.imags[1], 0.0, epsilon); + + assert_float_closeness(state.reals[2], 0.0, epsilon); + assert_float_closeness(state.imags[2], 0.0, epsilon); + assert_float_closeness(state.reals[3], 0.0, epsilon); + assert_float_closeness(state.imags[3], 0.0, epsilon); + + assert_float_closeness(state.reals[4], -0.6411848150109799, epsilon); + assert_float_closeness(state.imags[4], -0.49487748447346463, epsilon); + assert_float_closeness(state.reals[5], 0.0, epsilon); + assert_float_closeness(state.imags[5], 0.0, epsilon); + + assert_float_closeness(state.reals[6], 0.0, epsilon); + assert_float_closeness(state.imags[6], 0.0, epsilon); + assert_float_closeness(state.reals[7], 0.0, epsilon); + assert_float_closeness(state.imags[7], 0.0, epsilon); + println!("{state}"); + + measure_qubit(&mut state, 2, true, Some(1)); + assert_float_closeness(state.reals[0], -0.7916334352111761, epsilon); + assert_float_closeness(state.imags[0], -0.6109963209838112, epsilon); + assert_float_closeness(state.reals[1], 0.0, epsilon); + assert_float_closeness(state.imags[1], 0.0, epsilon); + + assert_float_closeness(state.reals[2], 0.0, epsilon); + assert_float_closeness(state.imags[2], 0.0, epsilon); + assert_float_closeness(state.reals[3], 0.0, epsilon); + assert_float_closeness(state.imags[3], 0.0, epsilon); + + assert_float_closeness(state.reals[4], 0.0, epsilon); + assert_float_closeness(state.imags[4], 0.0, epsilon); + assert_float_closeness(state.reals[5], 0.0, epsilon); + assert_float_closeness(state.imags[5], 0.0, epsilon); + + assert_float_closeness(state.reals[6], 0.0, epsilon); + assert_float_closeness(state.imags[6], 0.0, epsilon); + assert_float_closeness(state.reals[7], 0.0, epsilon); + assert_float_closeness(state.imags[7], 0.0, epsilon); + println!("{state}"); + } +} diff --git a/vendor/spinoza/src/openqasm.rs b/vendor/spinoza/src/openqasm.rs new file mode 100644 index 0000000..cf9543e --- /dev/null +++ b/vendor/spinoza/src/openqasm.rs @@ -0,0 +1,305 @@ +//! Functionality for creating a QuantumCircuit from an OpenQASM 2.0 program +use std::{collections::HashMap, env, fs::File, io::prelude::*, path::Path}; + +use evalexpr::*; +use qasm::{lex, parse, process, Argument, AstNode}; + +use crate::{ + circuit::{QuantumCircuit, QuantumRegister}, + math::{Float, PI}, +}; + +/// Parse an OpenQASM 2.0 program file, and convert it into a `QuantumCircuit`. +pub fn load(filename: &Path) -> QuantumCircuit { + let cwd = env::current_dir().unwrap(); + let mut source = String::new(); + let mut f = File::open(filename).expect("cannot find source file"); + f.read_to_string(&mut source).expect("couldn't read file"); + + let processed_source = process(&source, &cwd); + let mut tokens = lex(&processed_source); + let ast = parse(&mut tokens).unwrap(); + build_circuit(&ast) +} + +/// Parse an OpenQASM 2.0 program in `str` format, and convert it into a `QuantumCircuit`. +pub fn loads(qasm_as_str: &str) -> QuantumCircuit { + let cwd = env::current_dir().unwrap(); + let processed_source = process(qasm_as_str, &cwd); + let mut tokens = lex(&processed_source); + let ast = parse(&mut tokens).unwrap(); + build_circuit(&ast) +} + +/// Build a circuit from an AST +fn build_circuit(ast: &[AstNode]) -> QuantumCircuit { + let context = context_map! { + "pi" => PI, + "-pi" => -PI, + } + .unwrap(); + + let mut registers = HashMap::new(); + + // Find all registers + for node in ast.iter() { + if let AstNode::QReg(identifier, num_qubits) = node { + let n = (*num_qubits).try_into().unwrap(); + registers.insert(identifier, QuantumRegister::new(n)); + } + } + + let mut qrs: Vec<&mut QuantumRegister> = registers.iter_mut().map(|(_, r)| r).collect(); + let mut qc = QuantumCircuit::new(&mut qrs); + + for node in ast.iter() { + match node { + AstNode::ApplyGate(gate, args0, args1) => match gate.as_str() { + "h" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let register = registers.get(identifier).unwrap(); + qc.h(register[(*qubit_num).try_into().unwrap()]); + } + } + } + "x" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let register = registers.get(identifier).unwrap(); + qc.x(register[(*qubit_num).try_into().unwrap()]); + } + } + } + "y" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let register = registers.get(identifier).unwrap(); + qc.y(register[(*qubit_num).try_into().unwrap()]); + } + } + } + "z" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let register = registers.get(identifier).unwrap(); + qc.z(register[(*qubit_num).try_into().unwrap()]); + } + } + } + "rx" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let angle = args1[0].replace(' ', "").parse::().unwrap(); + let register = registers.get(identifier).unwrap(); + qc.rx(angle, register[(*qubit_num).try_into().unwrap()]); + } + } + } + "ry" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let angle = args1[0].replace(' ', "").parse::().unwrap(); + let register = registers.get(identifier).unwrap(); + qc.ry(angle, register[(*qubit_num).try_into().unwrap()]); + } + } + } + "rz" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let angle = args1[0].replace(' ', "").parse::().unwrap(); + let register = registers.get(identifier).unwrap(); + qc.rz(angle, register[(*qubit_num).try_into().unwrap()]); + } + } + } + "u" => { + for arg in args0.iter() { + if let Argument::Qubit(identifier, qubit_num) = arg { + let theta = args1[0].replace(' ', "").parse::().unwrap(); + let phi = args1[1].replace(' ', "").parse::().unwrap(); + let lambda = args1[2].replace(' ', "").parse::().unwrap(); + let register = registers.get(identifier).unwrap(); + qc.u( + theta, + phi, + lambda, + register[(*qubit_num).try_into().unwrap()], + ); + } + } + } + "cp" => { + let control = if let Argument::Qubit(identifier, qubit_num) = &args0[0] { + let register = registers.get(identifier).unwrap(); + register[(*qubit_num).try_into().unwrap()] + } else { + panic!("there's no argument 0 for the CP gate") + }; + let target = if let Argument::Qubit(identifier, qubit_num) = &args0[1] { + let register = registers.get(identifier).unwrap(); + register[(*qubit_num).try_into().unwrap()] + } else { + panic!("there's no argument 1 for the CP gate") + }; + let theta = eval_with_context(&args1[0], &context).unwrap(); + qc.cp(theta.as_float().unwrap(), control, target); + } + "cx" => { + let control = if let Argument::Qubit(identifier, qubit_num) = &args0[0] { + let register = registers.get(&identifier).unwrap(); + register[(*qubit_num).try_into().unwrap()] + } else { + panic!("there's no argument 0 for the CX gate") + }; + let target = if let Argument::Qubit(identifier, qubit_num) = &args0[1] { + let register = registers.get(&identifier).unwrap(); + register[(*qubit_num).try_into().unwrap()] + } else { + panic!("there's no argument 1 for the CX gate") + }; + qc.cx(control, target); + } + _ => todo!(), + }, + AstNode::QReg(_, _) => (), + _ => todo!(), + } + } + qc +} + +#[cfg(test)] +mod tests { + use crate::{circuit::QuantumRegister, utils::assert_float_closeness}; + + use super::*; + + #[test] + fn quantum_lstm_load() { + let filename: &Path = Path::new("../qasm/quantum_lstm.qasm"); + let mut qc1 = load(filename); + qc1.execute(); + + qc1.state + .reals + .iter() + .zip(qc1.state.imags.iter()) + .for_each(|(qc1_re, qc1_im)| { + println!("{} + i{}", *qc1_re, *qc1_im); + }); + } + + #[test] + fn iqft_load() { + let filename: &Path = Path::new("../qasm/iqft.qasm"); + let mut qc2 = load(filename); + qc2.execute(); + + let n: usize = qc2.state.n.into(); + let targets: Vec<_> = (0..n).rev().collect(); + let mut qr = QuantumRegister::new(n); + let mut qc1 = QuantumCircuit::new(&mut [&mut qr]); + qc1.iqft(&targets); + qc1.execute(); + + qc1.state + .reals + .iter() + .zip(qc1.state.imags.iter()) + .zip(qc2.state.reals.iter()) + .zip(qc2.state.imags.iter()) + .for_each(|(((qc1_re, qc1_im), qc2_re), qc2_im)| { + assert_float_closeness(*qc1_re, *qc2_re, 0.001); + assert_float_closeness(*qc1_im, *qc2_im, 0.001); + }); + } + + #[test] + fn iqft_loads() { + let qasm_as_str = include_str!("../../qasm/iqft.qasm"); + let mut qc2 = loads(qasm_as_str); + qc2.execute(); + + let n: usize = qc2.state.n.into(); + let targets: Vec<_> = (0..n).rev().collect(); + let mut qr = QuantumRegister::new(n); + let mut qc1 = QuantumCircuit::new(&mut [&mut qr]); + qc1.iqft(&targets); + qc1.execute(); + + qc1.state + .reals + .iter() + .zip(qc1.state.imags.iter()) + .zip(qc2.state.reals.iter()) + .zip(qc2.state.imags.iter()) + .for_each(|(((qc1_re, qc1_im), qc2_re), qc2_im)| { + assert_float_closeness(*qc1_re, *qc2_re, 0.001); + assert_float_closeness(*qc1_im, *qc2_im, 0.001); + }); + } + + #[test] + fn non_controlled_gates_load() { + let filename: &Path = Path::new("../qasm/test0.qasm"); + let mut qc2 = load(filename); + qc2.execute(); + + let n: usize = qc2.state.n.into(); + let mut qr = QuantumRegister::new(n); + let mut qc1 = QuantumCircuit::new(&mut [&mut qr]); + qc1.h(0); + qc1.x(1); + qc1.y(2); + qc1.z(3); + qc1.rx(1.0, 4); + qc1.ry(2.0, 5); + qc1.rz(3.0, 6); + qc1.u(1.0, 2.0, 3.0, 7); + qc1.execute(); + + qc1.state + .reals + .iter() + .zip(qc1.state.imags.iter()) + .zip(qc2.state.reals.iter()) + .zip(qc2.state.imags.iter()) + .for_each(|(((qc1_re, qc1_im), qc2_re), qc2_im)| { + assert_float_closeness(*qc1_re, *qc2_re, 0.001); + assert_float_closeness(*qc1_im, *qc2_im, 0.001); + }); + } + + #[test] + fn non_controlled_gates_loads() { + let qasm_as_str = include_str!("../../qasm/test0.qasm"); + let mut qc2 = loads(qasm_as_str); + qc2.execute(); + + let n: usize = qc2.state.n.into(); + let mut qr = QuantumRegister::new(n); + let mut qc1 = QuantumCircuit::new(&mut [&mut qr]); + qc1.h(0); + qc1.x(1); + qc1.y(2); + qc1.z(3); + qc1.rx(1.0, 4); + qc1.ry(2.0, 5); + qc1.rz(3.0, 6); + qc1.u(1.0, 2.0, 3.0, 7); + qc1.execute(); + + qc1.state + .reals + .iter() + .zip(qc1.state.imags.iter()) + .zip(qc2.state.reals.iter()) + .zip(qc2.state.imags.iter()) + .for_each(|(((qc1_re, qc1_im), qc2_re), qc2_im)| { + assert_float_closeness(*qc1_re, *qc2_re, 0.001); + assert_float_closeness(*qc1_im, *qc2_im, 0.001); + }); + } +} diff --git a/vendor/spinoza/src/unitaries.rs b/vendor/spinoza/src/unitaries.rs new file mode 100644 index 0000000..cd9d50a --- /dev/null +++ b/vendor/spinoza/src/unitaries.rs @@ -0,0 +1,374 @@ +//! Functionality for applying large 2^n * 2^n matrices to the state +//! Ideally, this should be a last resort +use crate::{core::State, gates::Gate, math::Float}; +use rayon::prelude::*; +use std::fmt; +use std::fmt::Formatter; + +/// A representation of a Unitary Matrix +#[derive(Clone)] +pub struct Unitary { + pub(crate) reals: Vec, + pub(crate) imags: Vec, + /// The number of rows in the matrix + pub height: usize, + /// The number of columns in the matrix + pub width: usize, +} + +impl fmt::Display for Unitary { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.reals + .chunks_exact(self.width) + .zip(self.imags.chunks_exact(self.width)) + .for_each(|(re, im)| { + re.iter().zip(im.iter()).for_each(|(z_re, z_im)| { + write!(f, "{z_re}+i{z_im} ").unwrap(); + }); + writeln!(f).unwrap(); + }); + Ok(()) + } +} + +impl Unitary { + // TODO(saveliy): look into using strategy 2 here + /// Construct a Unitary from a single qubit gate + pub fn from_single_qubit_gate(state: &State, gate: Gate, target: usize) -> Self { + let g = gate.to_matrix(); + let num_pairs = state.len() >> 1; + let distance = 1 << target; + + let width = state.len(); + let height = state.len(); + let mut reals = vec![0.0; height * width]; + let mut imags = vec![0.0; height * width]; + + for i in 0..num_pairs { + let s0 = i + ((i >> target) << target); + let s1 = s0 + distance; + + reals[width * s0 + s0] = g[0].re; + reals[width * s0 + s1] = g[1].re; + reals[width * s1 + s0] = g[2].re; + reals[width * s1 + s1] = g[3].re; + + imags[width * s0 + s0] = g[0].im; + imags[width * s0 + s1] = g[1].im; + imags[width * s1 + s0] = g[2].im; + imags[width * s1 + s1] = g[3].im; + } + + Self { + reals, + imags, + height, + width, + } + } + + /// Return the conjugate transpose of this `Unitary`, in-place + pub fn conj_t(&mut self) { + self.imags.par_iter_mut().for_each(|z_im| { + *z_im = -(*z_im); + }); + + for i in 0..self.height { + for j in i + 1..self.width { + self.reals.swap(i * self.width + j, j * self.width + i); + self.imags.swap(i * self.width + j, j * self.width + i); + } + } + } + + /// Multiply this unitary matrix by another unitary matrix + pub fn multiply(&self, other: &Unitary) -> Self { + assert_eq!(self.width, other.height); + + let mut result_reals = vec![0.0; self.height * other.width]; + let mut result_imags = vec![0.0; self.height * other.width]; + + // self.reals + // .chunks_exact(self.width) + // .zip(self.imags.chunks_exact(self.width)) + // .enumerate() + // .map(|(i, (z_re, z_im))| { + // for k in 0..other.height { + // + // } + // }); + + for i in 0..self.height { + for j in 0..other.width { + let mut sum_real = 0.0; + let mut sum_imag = 0.0; + + for k in 0..self.width { + let a_real = self.reals[i * self.width + k]; + let a_imag = self.imags[i * self.width + k]; + let b_real = other.reals[k * other.width + j]; + let b_imag = other.imags[k * other.width + j]; + + sum_real += a_real * b_real - a_imag * b_imag; + sum_imag += a_real * b_imag + a_imag * b_real; + } + + result_reals[i * other.width + j] = sum_real; + result_imags[i * other.width + j] = sum_imag; + } + } + + Unitary { + reals: result_reals, + imags: result_imags, + height: self.height, + width: other.width, + } + } +} + +/// Applies a unitary matrix to the Quantum State Vector +pub fn apply_unitary(state: &State, unitary: &Unitary) -> State { + assert!(state.len() == unitary.width && state.len() == unitary.height); + let chunk_size = unitary.width; + + let mut reals = Vec::with_capacity(state.len()); + let mut imags = Vec::with_capacity(state.len()); + + unitary + .reals + .chunks_exact(chunk_size) + .zip(unitary.imags.chunks_exact(chunk_size)) + .for_each(|(row_reals, row_imags)| { + let mut dot_prod_re = 0.0; + let mut dot_prod_im = 0.0; + row_reals + .iter() + .zip(row_imags.iter()) + .zip(state.reals.iter()) + .zip(state.imags.iter()) + .for_each(|(((a, b), s_re), s_im)| { + dot_prod_re += *a * s_re - *b * s_im; + dot_prod_im += *a * s_im + *b * s_re; + }); + reals.push(dot_prod_re); + imags.push(dot_prod_im); + }); + State { + reals, + imags, + n: state.n, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::{assert_float_closeness, gen_random_state}; + use crate::{ + gates::{apply, Gate}, + math::SQRT_ONE_HALF, + }; + + #[test] + fn test_hxi_from_single_qubit_gate() { + let mut reals = vec![ + 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0, -1.0, + ]; + reals.iter_mut().for_each(|a| *a *= SQRT_ONE_HALF); + + let imags = vec![0.0; reals.len()]; + + let u1 = Unitary { + reals, + imags, + height: 4, + width: 4, + }; + + let state = State::new(2); + let u2 = Unitary::from_single_qubit_gate(&state, Gate::H, 1); + assert_eq!(u1.height, u2.height); + assert_eq!(u1.width, u2.width); + assert_eq!(u1.reals, u2.reals); + assert_eq!(u1.imags, u2.imags); + } + + #[test] + fn test_hxi() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::H, 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::H, &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_x() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::X, 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::X, &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_y() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::Y, 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::Y, &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_z() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::Z, 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::Z, &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_p() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::P(3.0), 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::P(3.0), &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_rx() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::RX(3.0), 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::RX(3.0), &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_ry() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::RY(3.0), 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::RY(3.0), &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_rz() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::RZ(3.0), 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::RZ(3.0), &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn test_u() { + let n = 2; + let state = State::new(n); + let u = Unitary::from_single_qubit_gate(&state, Gate::U(1.0, 2.0, 3.0), 0); + + let s = apply_unitary(&state, &u); + + let mut s1 = State::new(n); + apply(Gate::U(1.0, 2.0, 3.0), &mut s1, 0); + + assert_eq!(s.n, s1.n); + assert_eq!(s.reals, s1.reals); + assert_eq!(s.imags, s1.imags); + } + + #[test] + fn display() { + const N: usize = 1; + let state = gen_random_state(N); + let u = Unitary::from_single_qubit_gate(&state, Gate::X, 0); + + let x_gate_as_str = "0+i0 1+i0 \n1+i0 0+i0 \n".to_string(); + let u_as_str = u.to_string(); + assert_eq!(u_as_str, x_gate_as_str); + } + + #[test] + fn conjugate_transpose() { + const N: usize = 2; + let state = State::new(N); + let u = Unitary::from_single_qubit_gate(&state, Gate::H, 0); + let mut u_ct = Unitary::from_single_qubit_gate(&state, Gate::H, 0); + u_ct.conj_t(); + + let identity = u_ct.multiply(&u); + + for i in 0..u.height { + for j in 0..u.width { + if i == j { + assert_float_closeness(identity.reals[i * identity.width + j], 1.0, 0.00001); + assert_float_closeness(identity.imags[i * identity.width + j], 0.0, 0.00001); + } else { + assert_float_closeness(identity.reals[i * identity.width + j], 0.0, 0.00001); + assert_float_closeness(identity.imags[i * identity.width + j], 0.0, 0.00001); + } + } + } + } +} diff --git a/vendor/spinoza/src/utils.rs b/vendor/spinoza/src/utils.rs new file mode 100644 index 0000000..e5931e3 --- /dev/null +++ b/vendor/spinoza/src/utils.rs @@ -0,0 +1,378 @@ +//! An assortment of utility functions for visualizing, benchmarking, and testing. +use comfy_table::{ + presets::UTF8_FULL, + Color::Rgb, + {Cell, Color, Table}, +}; +use rand::distributions::Uniform; +use rand::prelude::*; + +use crate::{ + core::State, + gates::{c_apply, Gate}, + math::{modulus, Amplitude, Float, PI}, +}; + +/// Formats an unsigned, 128 bit integer with commas, as a string. Used for readability +pub fn pretty_print_int(i: u128) -> String { + if i == 0 { + return "0".into(); + } + + // u128::MAX == 340_282_366_920_938_463_463_374_607_431_768_211_455 + // len("340_282_366_920_938_463_463_374_607_431_768_211_455") == 51 + let mut q = arrayvec::ArrayVec::::new(); + + let mut x = i; + let mut comma = 0; + + while x > 0 { + let r = x % 10; + x /= 10; + + if comma == 3 { + q.push(44); // 44 is ',' in ASCII + comma = 0; + } + q.push((0x30 + r) as u8); // ascii digits 0, 1, 2, ... start at value 0x30 + comma += 1; + } + + q.into_iter().map(|d| d as char).rev().collect() +} + +/// Convert a `usize` to its binary expansion, but padded with 0's. Padding is of size, width. +pub fn padded_bin(i: usize, width: usize) -> String { + format!("{:01$b}", i, width + 2) +} + +/// Display the `State` as a table +pub fn to_table(state: &State) -> String { + let n: usize = state.n.into(); + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + // .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(100) + .set_header(vec![ + Cell::new("Outcome"), + Cell::new("Amplitude"), + Cell::new("Magnitude"), + Cell::new("Amplitude Bar"), + Cell::new("Probability"), + Cell::new("Probability Bar"), + ]); + + (0..16.min(state.len())).for_each(|idx| { + let z_re = state.reals[idx]; + let z_im = state.imags[idx]; + table.add_row(vec![ + Cell::new(format!("{} = {}", idx, padded_bin(idx, n))), + Cell::new(format!("{:.5} + i{:.5}", z_re, z_im)), + Cell::new(format!("{:.5}", modulus(z_re, z_im))), + Cell::new(str::repeat( + " ", + (modulus(z_re, z_im) * 50.0).round() as usize, + )) + .bg(complex_to_rgb(z_re, z_im, false)), + Cell::new(format!("{:.5}", modulus(z_re, z_im).powi(2))), + Cell::new(str::repeat( + " ", + (modulus(z_re, z_im).powi(2) * 50.0).round() as usize, + )) + .bg(complex_to_rgb(1.0, 0.0, false)), + ]); + }); + table.force_no_tty().enforce_styling().style_text_only(); + table.to_string() +} + +fn complex_to_rgb(z_re: Float, z_im: Float, scaled_saturation: bool) -> Color { + let val = 100.0; + + let mut hue: f32 = (z_im.atan2(z_re) * 180.0 / PI) as f32; + if hue < 0.0 { + hue += 360.0; + } + + let sat: f32 = if scaled_saturation { + modulus(z_re, z_im) as f32 * 100.0 + } else { + 100.0 + }; + + let [r, g, b] = hsv_to_rgb(hue, sat, val); + Rgb { r, g, b } +} + +// https://gist.github.com/eyecatchup/9536706 Colors +fn hsv_to_rgb(hue: f32, sat: f32, val: f32) -> [u8; 3] { + // Make sure our arguments stay in-range + let (mut h, mut s, mut v) = ( + 0.0_f32.max(360.0_f32.min(hue)), + 0.0_f32.max(100.0_f32.min(sat)), + 0.0_f32.max(100.0_f32.min(val)), + ); + + // We accept saturation and value arguments from 0 to 100 because that's + // how Photoshop represents those values. Internally, however, the + // saturation and value are calculated from a range of 0 to 1. + // We make that conversion here. + s /= 100.0; + v /= 100.0; + + let (r, g, b) = ( + (v * 255.0).round(), + (v * 255.0).round(), + (v * 255.0).round(), + ); + + if s == 0.0 { + // Achromatic(grey) + return [r as u8, g as u8, b as u8]; + } + + h /= 60.0; // sector 0 to 5 + let i = h.floor() as i16; + let f = h - f32::from(i); // factorial part of h + let p = v * (1.0 - s); + let q = v * (1.0 - s * f); + let t = v * (1.0 - s * (1.0 - f)); + + let (r, g, b) = if i == 0 { + (v, t, p) + } else if i == 1 { + (q, v, p) + } else if i == 2 { + (p, v, t) + } else if i == 3 { + (p, q, v) + } else if i == 4 { + (t, p, v) + } else { + (v, p, q) + }; + [ + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + ] +} + +/// Asserts that two floating point numbers are approximately equal. +pub fn assert_float_closeness(actual: Float, expected: Float, epsilon: Float) { + assert!((actual - expected).abs() < epsilon); +} + +/// Generates a random quantum state +pub fn gen_random_state(n: usize) -> State { + assert!(n > 0); + let mut rng = thread_rng(); + let between = Uniform::from(0.0..1.0); + let angle_dist = Uniform::from(0.0..2.0 * PI); + let num_amps = 1 << n; + + let mut probs: Vec<_> = (0..num_amps).map(|_| between.sample(&mut rng)).collect(); + + let total: Float = probs.iter().sum(); + let total_recip = total.recip(); + + probs.iter_mut().for_each(|p| *p *= total_recip); + + let angles = (0..num_amps).map(|_| angle_dist.sample(&mut rng)); + + let mut reals = Vec::with_capacity(num_amps); + let mut imags = Vec::with_capacity(num_amps); + + probs.iter().zip(angles).for_each(|(p, a)| { + let p_sqrt = p.sqrt(); + let (sin_a, cos_a) = a.sin_cos(); + let re = p_sqrt * cos_a; + let im = p_sqrt * sin_a; + reals.push(re); + imags.push(im); + }); + + State { + reals, + imags, + n: n.try_into().unwrap(), + } +} + +/// Swap using controlled X gates. +pub fn swap(state: &mut State, first: usize, second: usize) { + c_apply(Gate::X, state, first, second); + c_apply(Gate::X, state, second, first); + c_apply(Gate::X, state, first, second); +} + +/// Utility function for multiplying two 2 x 2 gates +pub fn mat_mul_2x2(m0: [Amplitude; 4], m1: [Amplitude; 4]) -> [Amplitude; 4] { + let mut res: [Amplitude; 4] = [Amplitude { re: 0.0, im: 0.0 }; 4]; + + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + res[i * 2 + j].re += + m0[i * 2 + k].re * m1[k * 2 + j].re - m0[i * 2 + k].im * m1[k * 2 + j].im; + res[i * 2 + j].im += + m0[i * 2 + k].re * m1[k * 2 + j].im + m0[i * 2 + k].im * m1[k * 2 + j].re; + } + } + } + res +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + // color values taken from: https://www.rapidtables.com/convert/color/hsv-to-rgb.html + #[test] + fn hsv_rgb_conv() { + let hsv_colors = HashMap::from([ + ("Black", (0.0, 0.0, 0.0)), + ("White", (0.0, 0.0, 100.0)), + ("Red", (0.0, 100.0, 100.0)), + ("Lime", (120.0, 100.0, 100.0)), + ("Blue", (240.0, 100.0, 100.0)), + ("Yellow", (60.0, 100.0, 100.0)), + ("Cyan", (180.0, 100.0, 100.0)), + ("Magenta", (300.0, 100.0, 100.0)), + ("Silver", (0.0, 0.0, 75.0)), + ("Gray", (0.0, 0.0, 50.0)), + ("Maroon", (0.0, 100.0, 50.0)), + ("Olive", (60.0, 100.0, 50.0)), + ("Green", (120.0, 100.0, 50.0)), + ("Purple", (300.0, 100.0, 50.0)), + ("Teal", (180.0, 100.0, 50.0)), + ("Navy", (240.0, 100.0, 50.0)), + ]); + let rgb_colors = HashMap::from([ + ("Black", (0, 0, 0)), + ("White", (255, 255, 255)), + ("Red", (255, 0, 0)), + ("Lime", (0, 255, 0)), + ("Blue", (0, 0, 255)), + ("Yellow", (255, 255, 0)), + ("Cyan", (0, 255, 255)), + ("Magenta", (255, 0, 255)), + ("Silver", (191, 191, 191)), + ("Gray", (128, 128, 128)), + ("Maroon", (128, 0, 0)), + ("Olive", (128, 128, 0)), + ("Green", (0, 128, 0)), + ("Purple", (128, 0, 128)), + ("Teal", (0, 128, 128)), + ("Navy", (0, 0, 128)), + ]); + + for (color, value) in hsv_colors.into_iter() { + let expected_rgb_color = *rgb_colors.get(color).unwrap(); + let actual_rgb_color = hsv_to_rgb(value.0, value.1, value.2); + assert_eq!(actual_rgb_color[0], expected_rgb_color.0); + assert_eq!(actual_rgb_color[1], expected_rgb_color.1); + assert_eq!(actual_rgb_color[2], expected_rgb_color.2); + } + } + + // Compare the output to a complex number color map + // Positive real numbers always appear red. + // The primary colors appear at phase angles 2 pi/3 (green) and 4 pi/3 (blue). + // The subtractive colors yellow, cyan, and magenta have the phases pi/3, pi, and 5 pi/3. + //https://vqm.uni-graz.at/pages/colormap.html + #[test] + fn complex_num_color_map() { + let rgb_colors = HashMap::from([ + ("Red", Rgb { r: 255, g: 0, b: 0 }), + ("Blue", Rgb { r: 0, g: 0, b: 255 }), + ( + "Yellow", + Rgb { + r: 255, + g: 255, + b: 0, + }, + ), + ( + "Cyan", + Rgb { + r: 0, + g: 255, + b: 255, + }, + ), + ( + "Magenta", + Rgb { + r: 255, + g: 0, + b: 255, + }, + ), + ("Green", Rgb { r: 0, g: 255, b: 0 }), + ]); + + let z = Amplitude { re: 1.0, im: 0.0 }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + assert_eq!(rgb_val, *rgb_colors.get("Red").unwrap()); + + let z = Amplitude { + re: (2.0 * PI / 3.0).cos(), + im: (2.0 * PI / 3.0).sin(), + }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + assert_eq!(rgb_val, *rgb_colors.get("Green").unwrap()); + + let z = Amplitude { + re: (4.0 * PI / 3.0).cos(), + im: (4.0 * PI / 3.0).sin(), + }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + assert_eq!(rgb_val, *rgb_colors.get("Blue").unwrap()); + + let z = Amplitude { + re: (PI / 3.0).cos(), + im: (PI / 3.0).sin(), + }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + println!("{:?}", rgb_val); + assert_eq!(rgb_val, *rgb_colors.get("Yellow").unwrap()); + + let z = Amplitude { + re: PI.cos(), + im: PI.sin(), + }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + assert_eq!(rgb_val, *rgb_colors.get("Cyan").unwrap()); + + let z = Amplitude { + re: (5.0 * PI / 3.0).cos(), + im: (5.0 * PI / 3.0).sin(), + }; + let rgb_val = complex_to_rgb(z.re, z.im, false); + assert_eq!(rgb_val, *rgb_colors.get("Magenta").unwrap()); + } + + #[test] + fn test_pretty_print_int() { + for i in 0..1000 { + assert_eq!(pretty_print_int(i), i.to_string()); + } + + assert_eq!(pretty_print_int(1_000), "1,000"); + assert_eq!(pretty_print_int(10_000), "10,000"); + assert_eq!(pretty_print_int(100_000), "100,000"); + assert_eq!(pretty_print_int(1_000_000), "1,000,000"); + assert_eq!(pretty_print_int(1_000_000_000), "1,000,000,000"); + assert_eq!(pretty_print_int(1_000_000_000_000), "1,000,000,000,000"); + assert_eq!(pretty_print_int(100_000_000_000_000), "100,000,000,000,000"); + assert_eq!( + pretty_print_int(u128::MAX), + "340,282,366,920,938,463,463,374,607,431,768,211,455" + ); + } +}