Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
[package]
name = "ruqu-wasm"
version = "2.0.5"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
readme = "README.md"
description = "Run quantum simulations in the browser - WebAssembly bindings for quantum circuits with 25-qubit support, VQE, Grover, QAOA"
keywords = ["quantum", "wasm", "webassembly", "quantum-computing", "browser"]
categories = ["wasm", "science", "simulation"]
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
[dependencies]
ruqu-core = { version = "2.0.5", path = "../ruqu-core" }
ruqu-algorithms = { version = "2.0.5", path = "../ruqu-algorithms" }
wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde-wasm-bindgen = "0.6"
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
wasm-bindgen-test = "0.3"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

221
crates/ruqu-wasm/README.md Normal file
View File

@@ -0,0 +1,221 @@
# ruqu-wasm
[![Crates.io](https://img.shields.io/crates/v/ruqu-wasm.svg)](https://crates.io/crates/ruqu-wasm)
[![npm](https://img.shields.io/npm/v/@ruvector/ruqu-wasm.svg)](https://www.npmjs.com/package/@ruvector/ruqu-wasm)
[![License](https://img.shields.io/crates/l/ruqu-wasm.svg)](https://github.com/ruvnet/ruvector)
**Run quantum simulations in the browser** — WebAssembly bindings for ruqu-core and ruqu-algorithms with 25-qubit support.
## Features
- **Browser-Native** — Run quantum circuits directly in JavaScript/TypeScript
- **5 Simulation Backends** — StateVector, Stabilizer, Clifford+T, TensorNetwork, Hardware
- **25-Qubit Limit** — Optimized for browser memory constraints (~1GB for 25 qubits)
- **Full Algorithm Suite** — VQE, Grover, QAOA, Surface Code available
- **OpenQASM 3.0** — Export circuits to standard quantum assembly format
- **Zero Dependencies** — Pure WASM, no server required
- **TypeScript Types** — Full type definitions included
## Installation
### npm
```bash
npm install @ruvector/ruqu-wasm
```
### Rust (for building)
```bash
cargo add ruqu-wasm
wasm-pack build --target web
```
## Quick Start (JavaScript)
```javascript
import init, { Circuit, Simulator } from '@ruvector/ruqu-wasm';
await init();
// Create a Bell state
const circuit = new Circuit(2);
circuit.h(0); // Hadamard on qubit 0
circuit.cnot(0, 1); // CNOT: entangle qubits
// Run simulation
const sim = new Simulator();
const state = sim.run(circuit);
// Measure
const result = state.measureAll();
console.log(`Measured: ${result.toString(2).padStart(2, '0')}`);
// Output: "00" or "11" with 50% probability each
```
## React Example
```tsx
import { useEffect, useState } from 'react';
import init, { Circuit, Simulator } from '@ruvector/ruqu-wasm';
function QuantumDemo() {
const [result, setResult] = useState<string | null>(null);
useEffect(() => {
async function runQuantum() {
await init();
const circuit = new Circuit(3);
circuit.h(0);
circuit.cnot(0, 1);
circuit.cnot(1, 2); // GHZ state
const sim = new Simulator();
const state = sim.run(circuit);
setResult(state.measureAll().toString(2).padStart(3, '0'));
}
runQuantum();
}, []);
return <div>Quantum result: {result ?? 'Computing...'}</div>;
}
```
## API Reference
### Circuit
```typescript
class Circuit {
constructor(nQubits: number);
// Single-qubit gates
h(qubit: number): void; // Hadamard
x(qubit: number): void; // Pauli-X (NOT)
y(qubit: number): void; // Pauli-Y
z(qubit: number): void; // Pauli-Z
rx(qubit: number, theta: number): void; // X-rotation
ry(qubit: number, theta: number): void; // Y-rotation
rz(qubit: number, theta: number): void; // Z-rotation
// Two-qubit gates
cnot(control: number, target: number): void;
cz(control: number, target: number): void;
swap(q1: number, q2: number): void;
// Three-qubit gates
toffoli(c1: number, c2: number, target: number): void;
}
```
### Simulator
```typescript
class Simulator {
constructor();
run(circuit: Circuit): QuantumState;
}
```
### QuantumState
```typescript
class QuantumState {
measureAll(): number;
measure(qubit: number): number;
probability(bitstring: number): number;
amplitudes(): Float64Array; // Complex interleaved [re, im, re, im, ...]
}
```
## Algorithms
### Grover's Search
```javascript
import { Grover } from '@ruvector/ruqu-wasm';
const grover = new Grover(4); // 4 qubits = search space of 16
grover.setTarget(0b1010); // Search for |1010⟩
const result = grover.search();
console.log(`Found: ${result.toString(2).padStart(4, '0')}`);
```
### VQE
```javascript
import { VQE, Hamiltonian } from '@ruvector/ruqu-wasm';
const h = new Hamiltonian();
h.addTerm("ZZ", 0.5);
h.addTerm("XX", 0.3);
const vqe = new VQE(h, nQubits: 4);
const energy = vqe.optimize({ maxIter: 100 });
console.log(`Ground state energy: ${energy}`);
```
## Performance
| Qubits | Memory | Init Time | Gate Time |
|--------|--------|-----------|-----------|
| 10 | 16 KB | 1ms | 0.01ms |
| 15 | 512 KB | 5ms | 0.1ms |
| 20 | 16 MB | 50ms | 5ms |
| 25 | 512 MB | 500ms | 150ms |
**Note**: 25 qubits requires ~1GB browser memory. Use Web Workers for heavy simulations.
## Web Worker Example
```javascript
// worker.js
import init, { Circuit, Simulator } from '@ruvector/ruqu-wasm';
self.onmessage = async (e) => {
await init();
const { gates, nQubits } = e.data;
const circuit = new Circuit(nQubits);
gates.forEach(g => circuit[g.name](...g.args));
const sim = new Simulator();
const state = sim.run(circuit);
self.postMessage({ result: state.measureAll() });
};
```
## Bundle Size
| Build | Size (gzip) |
|-------|-------------|
| Core only | 45 KB |
| With algorithms | 120 KB |
| Full bundle | 180 KB |
## Browser Support
- Chrome 89+
- Firefox 89+
- Safari 15+
- Edge 89+
Requires WebAssembly SIMD for optimal performance (available in all modern browsers).
## Related Packages
- [`ruqu-core`](https://crates.io/crates/ruqu-core) — Rust quantum simulator
- [`ruqu-algorithms`](https://crates.io/crates/ruqu-algorithms) — Algorithm implementations
- [`ruqu-exotic`](https://crates.io/crates/ruqu-exotic) — Experimental hybrids
## Documentation
- [WASM Strategy (ADR-QE-003)](https://github.com/ruvnet/ruvector/blob/main/docs/adr/quantum-engine/ADR-QE-003-wasm-compilation-strategy.md)
- [Performance (ADR-QE-004)](https://github.com/ruvnet/ruvector/blob/main/docs/adr/quantum-engine/ADR-QE-004-performance-optimization-benchmarks.md)
## License
MIT OR Apache-2.0

552
crates/ruqu-wasm/src/lib.rs Normal file
View File

@@ -0,0 +1,552 @@
//! # ruqu-wasm - WebAssembly Quantum Simulation
//!
//! Browser-compatible quantum circuit simulation.
//! Supports up to 25 qubits in WASM (memory limit enforcement).
//!
//! This crate provides wasm-bindgen bindings over `ruqu-core` and `ruqu-algorithms`,
//! exposing a JavaScript-friendly API for building quantum circuits, running simulations,
//! and executing quantum algorithms (Grover's search, QAOA MaxCut) directly in the browser.
//!
//! ## Usage (JavaScript)
//!
//! ```javascript
//! import { WasmQuantumCircuit, simulate, max_qubits, estimate_memory } from 'ruqu-wasm';
//!
//! // Check limits
//! console.log(`Max qubits: ${max_qubits()}`);
//! console.log(`Memory for 10 qubits: ${estimate_memory(10)} bytes`);
//!
//! // Build a Bell state circuit
//! const circuit = new WasmQuantumCircuit(2);
//! circuit.h(0);
//! circuit.cnot(0, 1);
//! circuit.measure_all();
//!
//! // Simulate
//! const result = simulate(circuit);
//! console.log(result.probabilities);
//! ```
//!
//! ## Memory Limits
//!
//! WASM operates under 32-bit address space constraints (~4GB max).
//! A quantum state vector for n qubits requires `2^n * 16` bytes
//! (complex f64 amplitudes). At 25 qubits this is ~512MB, which is
//! a practical upper bound for browser environments.
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
/// Maximum qubits allowed in WASM environment.
///
/// 25 qubits produces a state vector of 2^25 = 33,554,432 complex amplitudes,
/// requiring approximately 512MB (at 16 bytes per complex f64 pair).
/// This is near the practical limit for 32-bit WASM address space.
const WASM_MAX_QUBITS: u32 = 25;
// ═══════════════════════════════════════════════════════════════════════════
// WasmQuantumCircuit - JS-friendly circuit builder
// ═══════════════════════════════════════════════════════════════════════════
/// A JavaScript-friendly quantum circuit builder.
///
/// Wraps `ruqu_core::circuit::QuantumCircuit` with wasm-bindgen annotations.
/// All gate methods validate qubit indices against the circuit size internally
/// via the core library.
///
/// ## JavaScript Example
///
/// ```javascript
/// const qc = new WasmQuantumCircuit(3);
/// qc.h(0); // Hadamard on qubit 0
/// qc.cnot(0, 1); // CNOT: control=0, target=1
/// qc.rz(2, Math.PI); // Rz rotation on qubit 2
/// qc.measure_all();
///
/// console.log(`Qubits: ${qc.num_qubits}`);
/// console.log(`Gates: ${qc.gate_count}`);
/// console.log(`Depth: ${qc.depth}`);
/// ```
#[wasm_bindgen]
pub struct WasmQuantumCircuit {
inner: ruqu_core::circuit::QuantumCircuit,
}
#[wasm_bindgen]
impl WasmQuantumCircuit {
/// Create a new quantum circuit with the given number of qubits.
///
/// Returns an error if `num_qubits` exceeds the WASM limit (25).
#[wasm_bindgen(constructor)]
pub fn new(num_qubits: u32) -> Result<WasmQuantumCircuit, JsValue> {
if num_qubits > WASM_MAX_QUBITS {
return Err(JsValue::from_str(&format!(
"Qubit limit exceeded: {} requested, max {} in WASM",
num_qubits, WASM_MAX_QUBITS
)));
}
Ok(Self {
inner: ruqu_core::circuit::QuantumCircuit::new(num_qubits),
})
}
// ── Single-qubit gates ──────────────────────────────────────────────
/// Apply Hadamard gate to the target qubit.
pub fn h(&mut self, qubit: u32) {
self.inner.h(qubit);
}
/// Apply Pauli-X (NOT) gate to the target qubit.
pub fn x(&mut self, qubit: u32) {
self.inner.x(qubit);
}
/// Apply Pauli-Y gate to the target qubit.
pub fn y(&mut self, qubit: u32) {
self.inner.y(qubit);
}
/// Apply Pauli-Z gate to the target qubit.
pub fn z(&mut self, qubit: u32) {
self.inner.z(qubit);
}
/// Apply S (phase) gate to the target qubit.
pub fn s(&mut self, qubit: u32) {
self.inner.s(qubit);
}
/// Apply T gate to the target qubit.
pub fn t(&mut self, qubit: u32) {
self.inner.t(qubit);
}
/// Apply Rx rotation gate with the given angle (radians).
pub fn rx(&mut self, qubit: u32, angle: f64) {
self.inner.rx(qubit, angle);
}
/// Apply Ry rotation gate with the given angle (radians).
pub fn ry(&mut self, qubit: u32, angle: f64) {
self.inner.ry(qubit, angle);
}
/// Apply Rz rotation gate with the given angle (radians).
pub fn rz(&mut self, qubit: u32, angle: f64) {
self.inner.rz(qubit, angle);
}
// ── Two-qubit gates ─────────────────────────────────────────────────
/// Apply CNOT (controlled-X) gate.
pub fn cnot(&mut self, control: u32, target: u32) {
self.inner.cnot(control, target);
}
/// Apply controlled-Z gate.
pub fn cz(&mut self, q1: u32, q2: u32) {
self.inner.cz(q1, q2);
}
/// Apply SWAP gate.
pub fn swap(&mut self, q1: u32, q2: u32) {
self.inner.swap(q1, q2);
}
/// Apply Rzz (ZZ-rotation) gate with the given angle (radians).
pub fn rzz(&mut self, q1: u32, q2: u32, angle: f64) {
self.inner.rzz(q1, q2, angle);
}
// ── Measurement and control ─────────────────────────────────────────
/// Add a measurement operation on a single qubit.
pub fn measure(&mut self, qubit: u32) {
self.inner.measure(qubit);
}
/// Add measurement operations on all qubits.
pub fn measure_all(&mut self) {
self.inner.measure_all();
}
/// Reset a qubit to the |0> state.
pub fn reset(&mut self, qubit: u32) {
self.inner.reset(qubit);
}
/// Insert a barrier (prevents gate reordering across this point).
pub fn barrier(&mut self) {
self.inner.barrier();
}
// ── Circuit properties ──────────────────────────────────────────────
/// The number of qubits in this circuit.
#[wasm_bindgen(getter)]
pub fn num_qubits(&self) -> u32 {
self.inner.num_qubits()
}
/// The total number of gates applied so far.
#[wasm_bindgen(getter)]
pub fn gate_count(&self) -> usize {
self.inner.gate_count()
}
/// The circuit depth (longest path through the gate DAG).
#[wasm_bindgen(getter)]
pub fn depth(&self) -> u32 {
self.inner.depth()
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Simulation result types (serialized to JS via serde-wasm-bindgen)
// ═══════════════════════════════════════════════════════════════════════════
/// Simulation result returned as a plain JS object.
///
/// Contains the probability distribution, any measurement outcomes,
/// and execution metadata.
#[derive(Serialize, Deserialize)]
pub struct WasmSimResult {
/// Probability of each computational basis state (length = 2^n).
pub probabilities: Vec<f64>,
/// Measurement outcomes for qubits that were explicitly measured.
pub measurements: Vec<WasmMeasurement>,
/// Number of qubits in the simulated circuit.
pub num_qubits: u32,
/// Total gate count of the simulated circuit.
pub gate_count: usize,
/// Wall-clock execution time in milliseconds.
pub execution_time_ms: f64,
}
/// A single qubit measurement outcome.
#[derive(Serialize, Deserialize)]
pub struct WasmMeasurement {
/// Which qubit was measured.
pub qubit: u32,
/// The measured classical bit (true = |1>, false = |0>).
pub result: bool,
/// The probability of this outcome (before collapse).
pub probability: f64,
}
// ═══════════════════════════════════════════════════════════════════════════
// Top-level simulation function
// ═══════════════════════════════════════════════════════════════════════════
/// Run a quantum circuit simulation and return the results as a JS object.
///
/// The returned object has the shape:
/// ```typescript
/// {
/// probabilities: number[], // length = 2^num_qubits
/// measurements: Array<{ qubit: number, result: boolean, probability: number }>,
/// num_qubits: number,
/// gate_count: number,
/// execution_time_ms: number,
/// }
/// ```
#[wasm_bindgen]
pub fn simulate(circuit: &WasmQuantumCircuit) -> Result<JsValue, JsValue> {
let result = ruqu_core::simulator::Simulator::run(&circuit.inner)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let wasm_result = WasmSimResult {
probabilities: result.state.probabilities(),
measurements: result
.measurements
.iter()
.map(|m| WasmMeasurement {
qubit: m.qubit,
result: m.result,
probability: m.probability,
})
.collect(),
num_qubits: result.metrics.num_qubits,
gate_count: result.metrics.gate_count,
execution_time_ms: result.metrics.execution_time_ns as f64 / 1_000_000.0,
};
serde_wasm_bindgen::to_value(&wasm_result).map_err(|e| JsValue::from_str(&e.to_string()))
}
// ═══════════════════════════════════════════════════════════════════════════
// Utility functions
// ═══════════════════════════════════════════════════════════════════════════
/// Estimate memory usage (in bytes) for a state vector of `num_qubits` qubits.
///
/// Each qubit doubles the state vector size. The formula is `2^n * 16` bytes
/// (two f64 values per complex amplitude).
#[wasm_bindgen]
pub fn estimate_memory(num_qubits: u32) -> usize {
ruqu_core::state::QuantumState::estimate_memory(num_qubits)
}
/// Get the maximum number of qubits supported in the WASM environment.
#[wasm_bindgen]
pub fn max_qubits() -> u32 {
WASM_MAX_QUBITS
}
// ═══════════════════════════════════════════════════════════════════════════
// Grover's search algorithm
// ═══════════════════════════════════════════════════════════════════════════
/// Run Grover's quantum search algorithm.
///
/// Searches for one or more target states in a space of `2^num_qubits` items.
/// The optimal number of iterations is computed automatically when not specified.
///
/// ## Parameters
///
/// - `num_qubits` - Number of qubits (search space = 2^num_qubits).
/// - `target_states` - Array of target state indices to search for (as u32 values).
/// - `seed` - Optional RNG seed for reproducibility. Pass `null` or `undefined`
/// for non-deterministic execution. If provided, interpreted as a
/// floating-point number and truncated to a 64-bit unsigned integer.
///
/// ## Returns
///
/// A JS object:
/// ```typescript
/// {
/// measured_state: number,
/// target_found: boolean,
/// success_probability: number,
/// num_iterations: number,
/// }
/// ```
#[wasm_bindgen]
pub fn grover_search(
num_qubits: u32,
target_states: Vec<u32>,
seed: JsValue,
) -> Result<JsValue, JsValue> {
if num_qubits > WASM_MAX_QUBITS {
return Err(JsValue::from_str(&format!(
"Qubit limit exceeded: {} requested, max {} in WASM",
num_qubits, WASM_MAX_QUBITS
)));
}
// Convert seed: JsValue -> Option<u64>
// Accept null/undefined as None, otherwise parse as f64 and truncate.
let seed_opt: Option<u64> = if seed.is_undefined() || seed.is_null() {
None
} else {
Some(
seed.as_f64()
.ok_or_else(|| JsValue::from_str("seed must be a number, null, or undefined"))?
as u64,
)
};
// Convert Vec<u32> -> Vec<usize> for the core API.
let target_states_usize: Vec<usize> = target_states.into_iter().map(|s| s as usize).collect();
let config = ruqu_algorithms::grover::GroverConfig {
num_qubits,
target_states: target_states_usize,
num_iterations: None,
seed: seed_opt,
};
let result = ruqu_algorithms::grover::run_grover(&config)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
#[derive(Serialize)]
struct GroverJs {
measured_state: usize,
target_found: bool,
success_probability: f64,
num_iterations: u32,
}
serde_wasm_bindgen::to_value(&GroverJs {
measured_state: result.measured_state,
target_found: result.target_found,
success_probability: result.success_probability,
num_iterations: result.num_iterations,
})
.map_err(|e| JsValue::from_str(&e.to_string()))
}
// ═══════════════════════════════════════════════════════════════════════════
// QAOA MaxCut algorithm
// ═══════════════════════════════════════════════════════════════════════════
/// Build and simulate a QAOA (Quantum Approximate Optimization Algorithm)
/// circuit for the MaxCut problem on an undirected graph.
///
/// ## Parameters
///
/// - `num_nodes` - Number of graph nodes (one qubit per node).
/// - `edges_flat` - Flattened edge list as consecutive pairs: `[i1, j1, i2, j2, ...]`.
/// Each `(i, j)` pair defines an undirected edge with unit weight.
/// - `p` - Number of QAOA rounds (circuit depth parameter).
/// - `gammas` - Problem-unitary angles, length must equal `p`.
/// - `betas` - Mixer-unitary angles, length must equal `p`.
/// - `seed` - Optional RNG seed. Pass `null` or `undefined` for non-deterministic
/// execution.
///
/// ## Returns
///
/// A JS object:
/// ```typescript
/// {
/// probabilities: number[], // length = 2^num_nodes
/// expected_cut: number, // expected cut value from the output state
/// }
/// ```
#[wasm_bindgen]
pub fn qaoa_maxcut(
num_nodes: u32,
edges_flat: Vec<u32>,
p: u32,
gammas: Vec<f64>,
betas: Vec<f64>,
seed: JsValue,
) -> Result<JsValue, JsValue> {
if num_nodes > WASM_MAX_QUBITS {
return Err(JsValue::from_str(&format!(
"Qubit limit exceeded: {} requested, max {} in WASM",
num_nodes, WASM_MAX_QUBITS
)));
}
if gammas.len() != p as usize {
return Err(JsValue::from_str(&format!(
"gammas length mismatch: expected {} (p), got {}",
p,
gammas.len()
)));
}
if betas.len() != p as usize {
return Err(JsValue::from_str(&format!(
"betas length mismatch: expected {} (p), got {}",
p,
betas.len()
)));
}
if edges_flat.len() % 2 != 0 {
return Err(JsValue::from_str(
"edges_flat must contain an even number of elements (pairs of node indices)",
));
}
// Convert seed: JsValue -> Option<u64>
let seed_opt: Option<u64> = if seed.is_undefined() || seed.is_null() {
None
} else {
Some(
seed.as_f64()
.ok_or_else(|| JsValue::from_str("seed must be a number, null, or undefined"))?
as u64,
)
};
// Build graph from flattened edge pairs.
let mut graph = ruqu_algorithms::qaoa::Graph::new(num_nodes);
for chunk in edges_flat.chunks(2) {
if chunk.len() == 2 {
graph.add_edge(chunk[0], chunk[1], 1.0);
}
}
// Build and run the QAOA circuit.
let circuit = ruqu_algorithms::qaoa::build_qaoa_circuit(&graph, &gammas, &betas);
let result = ruqu_core::simulator::Simulator::run_with_config(
&circuit,
&ruqu_core::simulator::SimConfig {
seed: seed_opt,
noise: None,
shots: None,
},
)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let probs = result.state.probabilities();
// Compute the expected cut value: sum over edges of 0.5 * (1 - <Z_i Z_j>).
let mut expected_cut = 0.0;
for chunk in edges_flat.chunks(2) {
if chunk.len() == 2 {
let zz = result
.state
.expectation_value(&ruqu_core::types::PauliString {
ops: vec![
(chunk[0], ruqu_core::types::PauliOp::Z),
(chunk[1], ruqu_core::types::PauliOp::Z),
],
});
expected_cut += 0.5 * (1.0 - zz);
}
}
#[derive(Serialize)]
struct QaoaJs {
probabilities: Vec<f64>,
expected_cut: f64,
}
serde_wasm_bindgen::to_value(&QaoaJs {
probabilities: probs,
expected_cut,
})
.map_err(|e| JsValue::from_str(&e.to_string()))
}
// ═══════════════════════════════════════════════════════════════════════════
// WASM initialization
// ═══════════════════════════════════════════════════════════════════════════
/// Called automatically when the WASM module is instantiated.
///
/// Sets up `console_error_panic_hook` (when the feature is enabled) so that
/// Rust panics produce readable stack traces in the browser console instead
/// of opaque "unreachable" errors.
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
// ═══════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_max_qubits_constant() {
assert_eq!(max_qubits(), 25);
}
#[test]
fn test_circuit_rejects_too_many_qubits() {
let result = WasmQuantumCircuit::new(WASM_MAX_QUBITS + 1);
assert!(result.is_err());
}
#[test]
fn test_circuit_accepts_max_qubits() {
// Should not error at the boundary.
let result = WasmQuantumCircuit::new(WASM_MAX_QUBITS);
assert!(result.is_ok());
}
#[test]
fn test_circuit_accepts_small_count() {
let circuit = WasmQuantumCircuit::new(2).expect("2 qubits should succeed");
assert_eq!(circuit.num_qubits(), 2);
assert_eq!(circuit.gate_count(), 0);
}
}