Skip to main content

ruqu_wasm/
lib.rs

1//! # ruqu-wasm - WebAssembly Quantum Simulation
2//!
3//! Browser-compatible quantum circuit simulation.
4//! Supports up to 25 qubits in WASM (memory limit enforcement).
5//!
6//! This crate provides wasm-bindgen bindings over `ruqu-core` and `ruqu-algorithms`,
7//! exposing a JavaScript-friendly API for building quantum circuits, running simulations,
8//! and executing quantum algorithms (Grover's search, QAOA MaxCut) directly in the browser.
9//!
10//! ## Usage (JavaScript)
11//!
12//! ```javascript
13//! import { WasmQuantumCircuit, simulate, max_qubits, estimate_memory } from 'ruqu-wasm';
14//!
15//! // Check limits
16//! console.log(`Max qubits: ${max_qubits()}`);
17//! console.log(`Memory for 10 qubits: ${estimate_memory(10)} bytes`);
18//!
19//! // Build a Bell state circuit
20//! const circuit = new WasmQuantumCircuit(2);
21//! circuit.h(0);
22//! circuit.cnot(0, 1);
23//! circuit.measure_all();
24//!
25//! // Simulate
26//! const result = simulate(circuit);
27//! console.log(result.probabilities);
28//! ```
29//!
30//! ## Memory Limits
31//!
32//! WASM operates under 32-bit address space constraints (~4GB max).
33//! A quantum state vector for n qubits requires `2^n * 16` bytes
34//! (complex f64 amplitudes). At 25 qubits this is ~512MB, which is
35//! a practical upper bound for browser environments.
36
37use wasm_bindgen::prelude::*;
38use serde::{Serialize, Deserialize};
39
40/// Maximum qubits allowed in WASM environment.
41///
42/// 25 qubits produces a state vector of 2^25 = 33,554,432 complex amplitudes,
43/// requiring approximately 512MB (at 16 bytes per complex f64 pair).
44/// This is near the practical limit for 32-bit WASM address space.
45const WASM_MAX_QUBITS: u32 = 25;
46
47// ═══════════════════════════════════════════════════════════════════════════
48// WasmQuantumCircuit - JS-friendly circuit builder
49// ═══════════════════════════════════════════════════════════════════════════
50
51/// A JavaScript-friendly quantum circuit builder.
52///
53/// Wraps `ruqu_core::circuit::QuantumCircuit` with wasm-bindgen annotations.
54/// All gate methods validate qubit indices against the circuit size internally
55/// via the core library.
56///
57/// ## JavaScript Example
58///
59/// ```javascript
60/// const qc = new WasmQuantumCircuit(3);
61/// qc.h(0);           // Hadamard on qubit 0
62/// qc.cnot(0, 1);     // CNOT: control=0, target=1
63/// qc.rz(2, Math.PI); // Rz rotation on qubit 2
64/// qc.measure_all();
65///
66/// console.log(`Qubits: ${qc.num_qubits}`);
67/// console.log(`Gates:  ${qc.gate_count}`);
68/// console.log(`Depth:  ${qc.depth}`);
69/// ```
70#[wasm_bindgen]
71pub struct WasmQuantumCircuit {
72    inner: ruqu_core::circuit::QuantumCircuit,
73}
74
75#[wasm_bindgen]
76impl WasmQuantumCircuit {
77    /// Create a new quantum circuit with the given number of qubits.
78    ///
79    /// Returns an error if `num_qubits` exceeds the WASM limit (25).
80    #[wasm_bindgen(constructor)]
81    pub fn new(num_qubits: u32) -> Result<WasmQuantumCircuit, JsValue> {
82        if num_qubits > WASM_MAX_QUBITS {
83            return Err(JsValue::from_str(&format!(
84                "Qubit limit exceeded: {} requested, max {} in WASM",
85                num_qubits, WASM_MAX_QUBITS
86            )));
87        }
88        Ok(Self {
89            inner: ruqu_core::circuit::QuantumCircuit::new(num_qubits),
90        })
91    }
92
93    // ── Single-qubit gates ──────────────────────────────────────────────
94
95    /// Apply Hadamard gate to the target qubit.
96    pub fn h(&mut self, qubit: u32) {
97        self.inner.h(qubit);
98    }
99
100    /// Apply Pauli-X (NOT) gate to the target qubit.
101    pub fn x(&mut self, qubit: u32) {
102        self.inner.x(qubit);
103    }
104
105    /// Apply Pauli-Y gate to the target qubit.
106    pub fn y(&mut self, qubit: u32) {
107        self.inner.y(qubit);
108    }
109
110    /// Apply Pauli-Z gate to the target qubit.
111    pub fn z(&mut self, qubit: u32) {
112        self.inner.z(qubit);
113    }
114
115    /// Apply S (phase) gate to the target qubit.
116    pub fn s(&mut self, qubit: u32) {
117        self.inner.s(qubit);
118    }
119
120    /// Apply T gate to the target qubit.
121    pub fn t(&mut self, qubit: u32) {
122        self.inner.t(qubit);
123    }
124
125    /// Apply Rx rotation gate with the given angle (radians).
126    pub fn rx(&mut self, qubit: u32, angle: f64) {
127        self.inner.rx(qubit, angle);
128    }
129
130    /// Apply Ry rotation gate with the given angle (radians).
131    pub fn ry(&mut self, qubit: u32, angle: f64) {
132        self.inner.ry(qubit, angle);
133    }
134
135    /// Apply Rz rotation gate with the given angle (radians).
136    pub fn rz(&mut self, qubit: u32, angle: f64) {
137        self.inner.rz(qubit, angle);
138    }
139
140    // ── Two-qubit gates ─────────────────────────────────────────────────
141
142    /// Apply CNOT (controlled-X) gate.
143    pub fn cnot(&mut self, control: u32, target: u32) {
144        self.inner.cnot(control, target);
145    }
146
147    /// Apply controlled-Z gate.
148    pub fn cz(&mut self, q1: u32, q2: u32) {
149        self.inner.cz(q1, q2);
150    }
151
152    /// Apply SWAP gate.
153    pub fn swap(&mut self, q1: u32, q2: u32) {
154        self.inner.swap(q1, q2);
155    }
156
157    /// Apply Rzz (ZZ-rotation) gate with the given angle (radians).
158    pub fn rzz(&mut self, q1: u32, q2: u32, angle: f64) {
159        self.inner.rzz(q1, q2, angle);
160    }
161
162    // ── Measurement and control ─────────────────────────────────────────
163
164    /// Add a measurement operation on a single qubit.
165    pub fn measure(&mut self, qubit: u32) {
166        self.inner.measure(qubit);
167    }
168
169    /// Add measurement operations on all qubits.
170    pub fn measure_all(&mut self) {
171        self.inner.measure_all();
172    }
173
174    /// Reset a qubit to the |0> state.
175    pub fn reset(&mut self, qubit: u32) {
176        self.inner.reset(qubit);
177    }
178
179    /// Insert a barrier (prevents gate reordering across this point).
180    pub fn barrier(&mut self) {
181        self.inner.barrier();
182    }
183
184    // ── Circuit properties ──────────────────────────────────────────────
185
186    /// The number of qubits in this circuit.
187    #[wasm_bindgen(getter)]
188    pub fn num_qubits(&self) -> u32 {
189        self.inner.num_qubits()
190    }
191
192    /// The total number of gates applied so far.
193    #[wasm_bindgen(getter)]
194    pub fn gate_count(&self) -> usize {
195        self.inner.gate_count()
196    }
197
198    /// The circuit depth (longest path through the gate DAG).
199    #[wasm_bindgen(getter)]
200    pub fn depth(&self) -> u32 {
201        self.inner.depth()
202    }
203}
204
205// ═══════════════════════════════════════════════════════════════════════════
206// Simulation result types (serialized to JS via serde-wasm-bindgen)
207// ═══════════════════════════════════════════════════════════════════════════
208
209/// Simulation result returned as a plain JS object.
210///
211/// Contains the probability distribution, any measurement outcomes,
212/// and execution metadata.
213#[derive(Serialize, Deserialize)]
214pub struct WasmSimResult {
215    /// Probability of each computational basis state (length = 2^n).
216    pub probabilities: Vec<f64>,
217    /// Measurement outcomes for qubits that were explicitly measured.
218    pub measurements: Vec<WasmMeasurement>,
219    /// Number of qubits in the simulated circuit.
220    pub num_qubits: u32,
221    /// Total gate count of the simulated circuit.
222    pub gate_count: usize,
223    /// Wall-clock execution time in milliseconds.
224    pub execution_time_ms: f64,
225}
226
227/// A single qubit measurement outcome.
228#[derive(Serialize, Deserialize)]
229pub struct WasmMeasurement {
230    /// Which qubit was measured.
231    pub qubit: u32,
232    /// The measured classical bit (true = |1>, false = |0>).
233    pub result: bool,
234    /// The probability of this outcome (before collapse).
235    pub probability: f64,
236}
237
238// ═══════════════════════════════════════════════════════════════════════════
239// Top-level simulation function
240// ═══════════════════════════════════════════════════════════════════════════
241
242/// Run a quantum circuit simulation and return the results as a JS object.
243///
244/// The returned object has the shape:
245/// ```typescript
246/// {
247///   probabilities: number[],   // length = 2^num_qubits
248///   measurements: Array<{ qubit: number, result: boolean, probability: number }>,
249///   num_qubits: number,
250///   gate_count: number,
251///   execution_time_ms: number,
252/// }
253/// ```
254#[wasm_bindgen]
255pub fn simulate(circuit: &WasmQuantumCircuit) -> Result<JsValue, JsValue> {
256    let result = ruqu_core::simulator::Simulator::run(&circuit.inner)
257        .map_err(|e| JsValue::from_str(&e.to_string()))?;
258
259    let wasm_result = WasmSimResult {
260        probabilities: result.state.probabilities(),
261        measurements: result
262            .measurements
263            .iter()
264            .map(|m| WasmMeasurement {
265                qubit: m.qubit,
266                result: m.result,
267                probability: m.probability,
268            })
269            .collect(),
270        num_qubits: result.metrics.num_qubits,
271        gate_count: result.metrics.gate_count,
272        execution_time_ms: result.metrics.execution_time_ns as f64 / 1_000_000.0,
273    };
274
275    serde_wasm_bindgen::to_value(&wasm_result)
276        .map_err(|e| JsValue::from_str(&e.to_string()))
277}
278
279// ═══════════════════════════════════════════════════════════════════════════
280// Utility functions
281// ═══════════════════════════════════════════════════════════════════════════
282
283/// Estimate memory usage (in bytes) for a state vector of `num_qubits` qubits.
284///
285/// Each qubit doubles the state vector size. The formula is `2^n * 16` bytes
286/// (two f64 values per complex amplitude).
287#[wasm_bindgen]
288pub fn estimate_memory(num_qubits: u32) -> usize {
289    ruqu_core::state::QuantumState::estimate_memory(num_qubits)
290}
291
292/// Get the maximum number of qubits supported in the WASM environment.
293#[wasm_bindgen]
294pub fn max_qubits() -> u32 {
295    WASM_MAX_QUBITS
296}
297
298// ═══════════════════════════════════════════════════════════════════════════
299// Grover's search algorithm
300// ═══════════════════════════════════════════════════════════════════════════
301
302/// Run Grover's quantum search algorithm.
303///
304/// Searches for one or more target states in a space of `2^num_qubits` items.
305/// The optimal number of iterations is computed automatically when not specified.
306///
307/// ## Parameters
308///
309/// - `num_qubits` - Number of qubits (search space = 2^num_qubits).
310/// - `target_states` - Array of target state indices to search for (as u32 values).
311/// - `seed` - Optional RNG seed for reproducibility. Pass `null` or `undefined`
312///            for non-deterministic execution. If provided, interpreted as a
313///            floating-point number and truncated to a 64-bit unsigned integer.
314///
315/// ## Returns
316///
317/// A JS object:
318/// ```typescript
319/// {
320///   measured_state: number,
321///   target_found: boolean,
322///   success_probability: number,
323///   num_iterations: number,
324/// }
325/// ```
326#[wasm_bindgen]
327pub fn grover_search(
328    num_qubits: u32,
329    target_states: Vec<u32>,
330    seed: JsValue,
331) -> Result<JsValue, JsValue> {
332    if num_qubits > WASM_MAX_QUBITS {
333        return Err(JsValue::from_str(&format!(
334            "Qubit limit exceeded: {} requested, max {} in WASM",
335            num_qubits, WASM_MAX_QUBITS
336        )));
337    }
338
339    // Convert seed: JsValue -> Option<u64>
340    // Accept null/undefined as None, otherwise parse as f64 and truncate.
341    let seed_opt: Option<u64> = if seed.is_undefined() || seed.is_null() {
342        None
343    } else {
344        Some(
345            seed.as_f64()
346                .ok_or_else(|| JsValue::from_str("seed must be a number, null, or undefined"))?
347                as u64,
348        )
349    };
350
351    // Convert Vec<u32> -> Vec<usize> for the core API.
352    let target_states_usize: Vec<usize> = target_states
353        .into_iter()
354        .map(|s| s as usize)
355        .collect();
356
357    let config = ruqu_algorithms::grover::GroverConfig {
358        num_qubits,
359        target_states: target_states_usize,
360        num_iterations: None,
361        seed: seed_opt,
362    };
363
364    let result = ruqu_algorithms::grover::run_grover(&config)
365        .map_err(|e| JsValue::from_str(&e.to_string()))?;
366
367    #[derive(Serialize)]
368    struct GroverJs {
369        measured_state: usize,
370        target_found: bool,
371        success_probability: f64,
372        num_iterations: u32,
373    }
374
375    serde_wasm_bindgen::to_value(&GroverJs {
376        measured_state: result.measured_state,
377        target_found: result.target_found,
378        success_probability: result.success_probability,
379        num_iterations: result.num_iterations,
380    })
381    .map_err(|e| JsValue::from_str(&e.to_string()))
382}
383
384// ═══════════════════════════════════════════════════════════════════════════
385// QAOA MaxCut algorithm
386// ═══════════════════════════════════════════════════════════════════════════
387
388/// Build and simulate a QAOA (Quantum Approximate Optimization Algorithm)
389/// circuit for the MaxCut problem on an undirected graph.
390///
391/// ## Parameters
392///
393/// - `num_nodes` - Number of graph nodes (one qubit per node).
394/// - `edges_flat` - Flattened edge list as consecutive pairs: `[i1, j1, i2, j2, ...]`.
395///   Each `(i, j)` pair defines an undirected edge with unit weight.
396/// - `p` - Number of QAOA rounds (circuit depth parameter).
397/// - `gammas` - Problem-unitary angles, length must equal `p`.
398/// - `betas` - Mixer-unitary angles, length must equal `p`.
399/// - `seed` - Optional RNG seed. Pass `null` or `undefined` for non-deterministic
400///            execution.
401///
402/// ## Returns
403///
404/// A JS object:
405/// ```typescript
406/// {
407///   probabilities: number[],   // length = 2^num_nodes
408///   expected_cut: number,      // expected cut value from the output state
409/// }
410/// ```
411#[wasm_bindgen]
412pub fn qaoa_maxcut(
413    num_nodes: u32,
414    edges_flat: Vec<u32>,
415    p: u32,
416    gammas: Vec<f64>,
417    betas: Vec<f64>,
418    seed: JsValue,
419) -> Result<JsValue, JsValue> {
420    if num_nodes > WASM_MAX_QUBITS {
421        return Err(JsValue::from_str(&format!(
422            "Qubit limit exceeded: {} requested, max {} in WASM",
423            num_nodes, WASM_MAX_QUBITS
424        )));
425    }
426
427    if gammas.len() != p as usize {
428        return Err(JsValue::from_str(&format!(
429            "gammas length mismatch: expected {} (p), got {}",
430            p,
431            gammas.len()
432        )));
433    }
434    if betas.len() != p as usize {
435        return Err(JsValue::from_str(&format!(
436            "betas length mismatch: expected {} (p), got {}",
437            p,
438            betas.len()
439        )));
440    }
441    if edges_flat.len() % 2 != 0 {
442        return Err(JsValue::from_str(
443            "edges_flat must contain an even number of elements (pairs of node indices)",
444        ));
445    }
446
447    // Convert seed: JsValue -> Option<u64>
448    let seed_opt: Option<u64> = if seed.is_undefined() || seed.is_null() {
449        None
450    } else {
451        Some(
452            seed.as_f64()
453                .ok_or_else(|| JsValue::from_str("seed must be a number, null, or undefined"))?
454                as u64,
455        )
456    };
457
458    // Build graph from flattened edge pairs.
459    let mut graph = ruqu_algorithms::qaoa::Graph::new(num_nodes);
460    for chunk in edges_flat.chunks(2) {
461        if chunk.len() == 2 {
462            graph.add_edge(chunk[0], chunk[1], 1.0);
463        }
464    }
465
466    // Build and run the QAOA circuit.
467    let circuit = ruqu_algorithms::qaoa::build_qaoa_circuit(&graph, &gammas, &betas);
468    let result = ruqu_core::simulator::Simulator::run_with_config(
469        &circuit,
470        &ruqu_core::simulator::SimConfig {
471            seed: seed_opt,
472            noise: None,
473            shots: None,
474        },
475    )
476    .map_err(|e| JsValue::from_str(&e.to_string()))?;
477
478    let probs = result.state.probabilities();
479
480    // Compute the expected cut value: sum over edges of 0.5 * (1 - <Z_i Z_j>).
481    let mut expected_cut = 0.0;
482    for chunk in edges_flat.chunks(2) {
483        if chunk.len() == 2 {
484            let zz = result.state.expectation_value(&ruqu_core::types::PauliString {
485                ops: vec![
486                    (chunk[0], ruqu_core::types::PauliOp::Z),
487                    (chunk[1], ruqu_core::types::PauliOp::Z),
488                ],
489            });
490            expected_cut += 0.5 * (1.0 - zz);
491        }
492    }
493
494    #[derive(Serialize)]
495    struct QaoaJs {
496        probabilities: Vec<f64>,
497        expected_cut: f64,
498    }
499
500    serde_wasm_bindgen::to_value(&QaoaJs {
501        probabilities: probs,
502        expected_cut,
503    })
504    .map_err(|e| JsValue::from_str(&e.to_string()))
505}
506
507// ═══════════════════════════════════════════════════════════════════════════
508// WASM initialization
509// ═══════════════════════════════════════════════════════════════════════════
510
511/// Called automatically when the WASM module is instantiated.
512///
513/// Sets up `console_error_panic_hook` (when the feature is enabled) so that
514/// Rust panics produce readable stack traces in the browser console instead
515/// of opaque "unreachable" errors.
516#[wasm_bindgen(start)]
517pub fn init() {
518    #[cfg(feature = "console_error_panic_hook")]
519    console_error_panic_hook::set_once();
520}
521
522// ═══════════════════════════════════════════════════════════════════════════
523// Tests
524// ═══════════════════════════════════════════════════════════════════════════
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_max_qubits_constant() {
532        assert_eq!(max_qubits(), 25);
533    }
534
535    #[test]
536    fn test_circuit_rejects_too_many_qubits() {
537        let result = WasmQuantumCircuit::new(WASM_MAX_QUBITS + 1);
538        assert!(result.is_err());
539    }
540
541    #[test]
542    fn test_circuit_accepts_max_qubits() {
543        // Should not error at the boundary.
544        let result = WasmQuantumCircuit::new(WASM_MAX_QUBITS);
545        assert!(result.is_ok());
546    }
547
548    #[test]
549    fn test_circuit_accepts_small_count() {
550        let circuit = WasmQuantumCircuit::new(2).expect("2 qubits should succeed");
551        assert_eq!(circuit.num_qubits(), 2);
552        assert_eq!(circuit.gate_count(), 0);
553    }
554}