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}