Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

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);
}
}