wai-quantum 0.3.27: Gaussian boson sampling, exactly #145

Merged
dcharlot merged 1 commit from wai-quantum-gbs into main 2026-10-04 20:41:21 -04:00
14 changed files with 1168 additions and 23 deletions

View file

@ -589,7 +589,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.3.26"
version = "0.3.27"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.3.26"
version = "0.3.27"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
version = "0.3.26"
version = "0.3.27"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
@ -59,6 +59,9 @@ quantum_frame = []
# Tensor-network contraction: hyper-optimised contraction trees, slicing, and
# amplitudes of circuits far past the state vector, reproducible bit for bit.
quantum_tn = []
# Gaussian boson sampling, exactly: hafnians, torontonians, lossy Gaussian states,
# pattern probabilities, exact samples and the marginals any sampler must match.
quantum_gbs = []
# Mixed states and the observables that distinguish quantum theory from a classical
# account: density matrices, the partial trace, exact Pauli expectations, CHSH.
quantum_info = ["quantum"]
@ -98,13 +101,17 @@ quantum_phasor_meter = ["_signing"]
# Everything.
full = [
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom",
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",
"quantum_phasor_meter",
]
[[example]]
name = "gbs_check"
required-features = ["quantum_gbs"]
[[example]]
name = "tn_path"
required-features = ["quantum_tn"]

View file

@ -71,6 +71,7 @@ never creates information.
| `quantum_qec` | CSS codes (distance verified by exhaustive search, not asserted) — **rotated surface** `[[d²,1,d]]`, toric, bivariate-bicycle, and the **`[[7,1,3]]` colour code**, whose self-duality buys a *transversal* logical Hadamard — one round, no ancillas, no surgery — verified on the simulator, with the surface code shown failing the same move — with **two decoders**: union-find (weighted matching, single-shot, space-time with faulty measurement, **and circuit-level with hook errors**; **imports and exports** Stim-syntax detector error models, so other tools' noise models decode here and ours decode there) and Relay-BP (for qLDPC) |
| `quantum_frame` | **noisy Clifford circuits at QEC scale**: reads the ecosystem's circuit text; samples detection events by bit-packed Pauli-frame simulation (64 shots per word); derives the detector error model by backward sensitivity, refusing non-deterministic detectors; generates the rotated surface-code memory experiment byte for byte as the reference layout; and decodes by **exact minimum-weight perfect matching** (Edmonds' blossom on integer weights). Measures logical error rates and fits the code model resource estimates assume |
| `quantum_tn` | **tensor-network contraction**: circuits (any unitary on any qubits) become networks; a seeded hyper-optimiser finds contraction trees from randomised-greedy and multilevel-partition families, reconfigures them optimally in subtrees, and slices them to a memory cap with a second search around the slices; slices contract in a fixed order, threaded, to the same bits everywhere. Single amplitudes or open-output batches |
| `quantum_gbs` | **Gaussian boson sampling, exactly**: hafnians (power traces, Hessenberg + characteristic polynomial, `O(n³ 2^{n/2})`), torontonians, lossy Gaussian states from squeezers and an interferometer, exact click and photon-number probabilities, exact samples drawn mode by mode, and a check of observed samples against the exact one- and two-mode click statistics |
| `quantum_compile` | Clifford routing + stabilizer-tableau equivalence proof |
| `quantum_atom` | neutral-atom register preparation (Hungarian / LSAP) |
| `quantum_qir` | **QIR export + ingest** — emit and parse QIR (the QIR Alliance's LLVM-based IR); emitted modules validated with `llvm-as`, and 9 of 10 catalog algorithms re-ingest to a bit-identical statevector |
@ -199,6 +200,25 @@ agrees with an independent contraction to 12 significant figures. The module
documentation states what slicing costs on deep circuits and what the scalar
executor can and cannot contract.
## Gaussian boson sampling
```sh
wai-quantum gbs --squeezing 0.9,0.7,0.8,0.6 --eta 0.5 --seed 3 # exact click statistics
wai-quantum gbs --squeezing 0.9,0.7,0.8,0.6 --eta 0.5 --seed 3 --sample 20000 > s.txt
wai-quantum gbs --squeezing 0.9,0.7,0.8,0.6 --eta 0.5 --seed 3 --check s.txt
```
A photonic sampler's outcome probabilities are hafnians (photon-number
detection) or torontonians (threshold detection) of its Gaussian state.
`quantum_gbs` computes both exactly. They agree with an independent
implementation:
- 10⁻¹² for probabilities and for hafnians up to `20 × 20`;
- 10⁻⁷ at `36 × 36`.
`--check` holds a file of observed click patterns to the state's exact one-
and two-mode statistics. Those are computable for any number of modes, and
every sampler of the state must reproduce them.
## Determinism
Simulation is **byte-exact**: amplitudes are dyadic fixed-point, so a circuit

View file

@ -0,0 +1,67 @@
//! Gaussian boson sampling numbers, as JSON, for checking against any other
//! implementation.
//!
//! ```text
//! cargo run --release --example gbs_check --features quantum_gbs -- state MODES SEED ETA R1,R2,…
//! cargo run --release --example gbs_check --features quantum_gbs -- hafnian N SEED
//! ```
use wai_quantum::quantum_gbs::{hafnian, random_unitary, GaussianState, Mat, C};
fn c(z: C) -> String {
format!("[{:e},{:e}]", z.re, z.im)
}
fn main() {
let a: Vec<String> = std::env::args().collect();
match a.get(1).map(String::as_str) {
Some("state") => {
let n: usize = a[2].parse().unwrap();
let seed: u64 = a[3].parse().unwrap();
let eta: f64 = a[4].parse().unwrap();
let r: Vec<f64> = a[5].split(',').map(|x| x.parse().unwrap()).collect();
let u = random_unitary(n, seed);
let st = GaussianState::squeezed(&r, &u, eta).unwrap();
let cov: Vec<String> = st.cov.a.iter().map(|z| c(*z)).collect();
let clicks: Vec<String> = (0..1u32 << n)
.map(|m| {
let p: Vec<bool> = (0..n).map(|i| m >> i & 1 == 1).collect();
format!("{:e}", st.click_probability(&p).unwrap())
})
.collect();
// Photon patterns with up to 4 photons in total.
let mut photons = Vec::new();
let mut counts = vec![0usize; n];
fn rec(i: usize, left: usize, counts: &mut Vec<usize>, st: &GaussianState, out: &mut Vec<String>) {
if i == counts.len() {
out.push(format!("[{:?},{:e}]", counts, st.photon_probability(counts).unwrap()));
return;
}
for k in 0..=left {
counts[i] = k;
rec(i + 1, left - k, counts, st, out);
}
counts[i] = 0;
}
rec(0, 4, &mut counts, &st, &mut photons);
println!("{{\"n\":{n},\"cov\":[{}],\"clicks\":[{}],\"photons\":[{}]}}", cov.join(","), clicks.join(","), photons.join(","));
}
Some("hafnian") => {
let n: usize = a[2].parse().unwrap();
let seed: u64 = a[3].parse().unwrap();
let u = random_unitary(n, seed);
let mut m = Mat::zeros(n);
for i in 0..n {
for j in 0..n {
let (x, y) = (u.at(i, j), u.at(j, i));
m.a[i * n + j] = C { re: x.re + y.re, im: x.im + y.im };
}
}
let t0 = std::time::Instant::now();
let h = hafnian(&m);
let dt = t0.elapsed().as_secs_f64();
let entries: Vec<String> = m.a.iter().map(|z| c(*z)).collect();
println!("{{\"n\":{n},\"m\":[{}],\"haf\":{},\"seconds\":{dt}}}", entries.join(","), c(h));
}
_ => eprintln!("usage: gbs_check state MODES SEED ETA R1,R2,… | hafnian N SEED"),
}
}

View file

@ -17,7 +17,7 @@
#[cfg(feature = "_signing")]
mod merkle;
#[cfg(any(feature = "quantum_mps", feature = "quantum_bptn", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_mps", feature = "quantum_bptn", feature = "quantum_tn", feature = "quantum_gbs"))]
mod linalg;
#[cfg(any(
@ -29,7 +29,8 @@ mod linalg;
feature = "quantum_kernel",
feature = "quantum_resource",
feature = "quantum_frame",
feature = "quantum_tn"
feature = "quantum_tn",
feature = "quantum_gbs"
))]
mod repro;
@ -102,6 +103,9 @@ pub mod quantum_match;
#[cfg(feature = "quantum_tn")]
pub mod quantum_tn;
#[cfg(feature = "quantum_gbs")]
pub mod quantum_gbs;
#[cfg(feature = "quantum_info")]
pub mod quantum_info;

View file

@ -0,0 +1,918 @@
//! Gaussian boson sampling, exactly — `wai.quantum.gbs`.
//!
//! A Gaussian boson sampler sends squeezed light through an interferometer
//! and counts photons at the outputs. The probability of each outcome is a
//! matrix function of the Gaussian state:
//! - a **hafnian** for photon-number-resolving detectors;
//! - a **torontonian** for threshold (click/no-click) detectors.
//!
//! Both are #P-hard in general. That hardness is the basis of every photonic
//! quantum-advantage claim, and computing them exactly is how a claim is
//! checked wherever it can be.
//!
//! This module computes them exactly and reproducibly.
//!
//! - [`hafnian`] uses power traces: a sum over `2^{n/2}` subsets of pairs, each
//! the `λ^{n/2}` coefficient of `exp(Σ tr(Mᵏ) λᵏ / 2k)`. The traces come from
//! a Hessenberg reduction and La Budde's characteristic polynomial, so the
//! total cost is `O(n³ 2^{n/2})`.
//! - [`torontonian`] is the inclusion–exclusion sum over `2^m` subsets of the
//! clicked modes, each a determinant.
//! - [`GaussianState`] is a state built from single-mode squeezers, an
//! interferometer and uniform loss. It is held as its complex covariance
//! matrix in the `(a, a†)` basis, with vacuum `I/2`.
//! - [`GaussianState::click_probability`] and
//! [`GaussianState::photon_probability`] give exact pattern probabilities.
//! - [`GaussianState::sample_clicks`] draws exact samples mode by mode from
//! marginal torontonians.
//! - [`GaussianState::click_marginals`] gives the one- and two-mode click
//! statistics that any sampler, a device included, must reproduce. They can
//! be computed for any number of modes.
//!
//! Every routine uses `+ − × ÷ √` only, in a fixed order. Hafnian and
//! torontonian sums are split into a fixed number of chunks, threaded natively
//! and run in sequence on targets without threads, and added in chunk order.
//! Probabilities and samples are therefore the same bits on every machine.
//!
//! # Checked against an independent implementation
//!
//! - **Click probabilities:** every pattern of three lossy states of four to six
//! modes agrees to 1.1·10⁻¹⁰, the worst being the smallest probabilities.
//! - **Photon-number probabilities:** every pattern of up to four photons
//! agrees to 2.3·10⁻¹³.
//! - **Hafnians** of random complex symmetric matrices agree to 1.5·10⁻¹² up to
//! `20 × 20` and to 4.5·10⁻⁸ at `36 × 36`, where the alternating
//! `2^{n/2}`-term sum loses precision in either implementation. A `36 × 36`
//! hafnian takes under two seconds on one machine.
//!
//! # What it checks
//!
//! [`GaussianState::check_samples`] holds observed click patterns, from a
//! device or from any sampler, to the state's exact one- and two-mode click
//! statistics. Exact samples of the state pass. The same samples tested against
//! a different interferometer fail by a wide margin.
//!
//! These statistics can be computed for any number of modes. Exact pattern
//! probabilities cost `2^{clicks}` determinants, so they are practical up to
//! a few dozen clicks.
pub use crate::linalg::C;
fn add(a: C, b: C) -> C {
C { re: a.re + b.re, im: a.im + b.im }
}
fn sub(a: C, b: C) -> C {
C { re: a.re - b.re, im: a.im - b.im }
}
fn mul(a: C, b: C) -> C {
C { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }
}
fn div(a: C, b: C) -> C {
let d = b.re * b.re + b.im * b.im;
C { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }
}
fn scale(a: C, s: f64) -> C {
C { re: a.re * s, im: a.im * s }
}
fn conj(a: C) -> C {
C { re: a.re, im: -a.im }
}
fn abs2(a: C) -> f64 {
a.re * a.re + a.im * a.im
}
const ZERO: C = C { re: 0.0, im: 0.0 };
const ONE: C = C { re: 1.0, im: 0.0 };
/// A dense complex matrix, row-major.
#[derive(Clone, Debug)]
pub struct Mat {
pub n: usize,
pub a: Vec<C>,
}
impl Mat {
pub fn zeros(n: usize) -> Mat {
Mat { n, a: vec![ZERO; n * n] }
}
pub fn identity(n: usize) -> Mat {
let mut m = Mat::zeros(n);
for i in 0..n {
m.a[i * n + i] = ONE;
}
m
}
#[inline]
pub fn at(&self, i: usize, j: usize) -> C {
self.a[i * self.n + j]
}
#[inline]
fn set(&mut self, i: usize, j: usize, v: C) {
self.a[i * self.n + j] = v;
}
fn mul(&self, o: &Mat) -> Mat {
let n = self.n;
let mut out = Mat::zeros(n);
for i in 0..n {
for k in 0..n {
let x = self.at(i, k);
if x.re == 0.0 && x.im == 0.0 {
continue;
}
for j in 0..n {
out.a[i * n + j] = add(out.a[i * n + j], mul(x, o.at(k, j)));
}
}
}
out
}
fn dagger(&self) -> Mat {
let n = self.n;
let mut out = Mat::zeros(n);
for i in 0..n {
for j in 0..n {
out.set(j, i, conj(self.at(i, j)));
}
}
out
}
/// The principal submatrix on `idx` (repeats allowed).
pub fn sub(&self, idx: &[usize]) -> Mat {
let m = idx.len();
let mut out = Mat::zeros(m);
for (r, &i) in idx.iter().enumerate() {
for (c, &j) in idx.iter().enumerate() {
out.a[r * m + c] = self.at(i, j);
}
}
out
}
/// Determinant by LU decomposition with partial pivoting.
pub fn det(&self) -> C {
let n = self.n;
let mut m = self.a.clone();
let mut d = ONE;
for col in 0..n {
let mut piv = col;
for r in col + 1..n {
if abs2(m[r * n + col]) > abs2(m[piv * n + col]) {
piv = r;
}
}
if abs2(m[piv * n + col]) == 0.0 {
return ZERO;
}
if piv != col {
for c in 0..n {
m.swap(piv * n + c, col * n + c);
}
d = scale(d, -1.0);
}
let p = m[col * n + col];
d = mul(d, p);
for r in col + 1..n {
let f = div(m[r * n + col], p);
if f.re == 0.0 && f.im == 0.0 {
continue;
}
for c in col..n {
m[r * n + c] = sub(m[r * n + c], mul(f, m[col * n + c]));
}
}
}
d
}
/// Inverse by Gauss–Jordan elimination with partial pivoting.
pub fn inverse(&self) -> Option<Mat> {
let n = self.n;
let mut m = self.a.clone();
let mut inv = Mat::identity(n).a;
for col in 0..n {
let mut piv = col;
for r in col + 1..n {
if abs2(m[r * n + col]) > abs2(m[piv * n + col]) {
piv = r;
}
}
if abs2(m[piv * n + col]) == 0.0 {
return None;
}
for c in 0..n {
m.swap(piv * n + c, col * n + c);
inv.swap(piv * n + c, col * n + c);
}
let p = m[col * n + col];
for c in 0..n {
m[col * n + c] = div(m[col * n + c], p);
inv[col * n + c] = div(inv[col * n + c], p);
}
for r in 0..n {
if r == col {
continue;
}
let f = m[r * n + col];
if f.re == 0.0 && f.im == 0.0 {
continue;
}
for c in 0..n {
m[r * n + c] = sub(m[r * n + c], mul(f, m[col * n + c]));
inv[r * n + c] = sub(inv[r * n + c], mul(f, inv[col * n + c]));
}
}
}
Some(Mat { n, a: inv })
}
}
/// `tr(Mᵏ)` for `k = 1..=kmax`: reduce to Hessenberg form by Householder
/// reflections, take the characteristic polynomial by La Budde's recurrence,
/// and turn its coefficients into power sums by Newton's identities.
fn power_traces(m: &Mat, kmax: usize) -> Vec<C> {
let n = m.n;
if n == 0 {
return vec![ZERO; kmax];
}
let mut h = m.a.clone();
// Householder reduction to upper Hessenberg form (similarity).
for col in 0..n.saturating_sub(2) {
let mut norm2 = 0.0;
for r in col + 1..n {
norm2 += abs2(h[r * n + col]);
}
let x0 = h[(col + 1) * n + col];
let alpha = norm2.sqrt();
if alpha == 0.0 {
continue;
}
let ax0 = abs2(x0).sqrt();
// v = x + e^{i arg x0} alpha e_1, normalised.
let phase = if ax0 == 0.0 { ONE } else { scale(x0, 1.0 / ax0) };
let mut v: Vec<C> = (col + 1..n).map(|r| h[r * n + col]).collect();
v[0] = add(v[0], scale(phase, alpha));
let vn = v.iter().map(|x| abs2(*x)).sum::<f64>().sqrt();
if vn == 0.0 {
continue;
}
for x in v.iter_mut() {
*x = scale(*x, 1.0 / vn);
}
// H ← (I − 2vv†) H (I − 2vv†), on rows/cols col+1..n.
for c in 0..n {
let mut s = ZERO;
for (k, vk) in v.iter().enumerate() {
s = add(s, mul(conj(*vk), h[(col + 1 + k) * n + c]));
}
for (k, vk) in v.iter().enumerate() {
let idx = (col + 1 + k) * n + c;
h[idx] = sub(h[idx], scale(mul(*vk, s), 2.0));
}
}
for r in 0..n {
let mut s = ZERO;
for (k, vk) in v.iter().enumerate() {
s = add(s, mul(h[r * n + col + 1 + k], *vk));
}
for (k, vk) in v.iter().enumerate() {
let idx = r * n + col + 1 + k;
h[idx] = sub(h[idx], scale(mul(s, conj(*vk)), 2.0));
}
}
}
let hh = |i: usize, j: usize| h[i * n + j];
// La Budde: p_k(x) = (x − h_kk) p_{k−1}(x) − Σ_{i=1}^{k−1} h_{k−i,k} (Π β) p_{k−i−1}(x),
// with β_j = h_{j,j−1}; 1-based k. Polynomials as coefficient vectors.
let mut polys: Vec<Vec<C>> = vec![vec![ONE]];
for k in 1..=n {
let prev = &polys[k - 1];
let mut p = vec![ZERO; k + 1];
// x · p_{k−1}
for (d, c) in prev.iter().enumerate() {
p[d + 1] = add(p[d + 1], *c);
}
// − h_kk · p_{k−1}
let hkk = hh(k - 1, k - 1);
for (d, c) in prev.iter().enumerate() {
p[d] = sub(p[d], mul(hkk, *c));
}
let mut prod = ONE;
for i in 1..k {
// β_{k−i+1} = h_{(k−i+1),(k−i)} in 1-based terms.
prod = mul(prod, hh(k - i, k - i - 1));
let coef = mul(hh(k - i - 1, k - 1), prod);
for (d, c) in polys[k - i - 1].iter().enumerate() {
p[d] = sub(p[d], mul(coef, *c));
}
}
polys.push(p);
}
// det(xI − M) = Σ_d p[d] x^d, monic; c_j = coefficient of x^{n−j}.
let p = &polys[n];
let c = |j: usize| -> C { if j <= n { p[n - j] } else { ZERO } };
// Newton's identities: p_k = −k c_k − Σ_{i=1}^{k−1} c_i p_{k−i}, c_i = 0 past n.
let mut traces: Vec<C> = Vec::with_capacity(kmax);
for k in 1..=kmax {
let mut s = if k <= n { scale(c(k), -(k as f64)) } else { ZERO };
for i in 1..k.min(n + 1) {
s = sub(s, mul(c(i), traces[k - i - 1]));
}
traces.push(s);
}
traces
}
/// The hafnian of a symmetric `n × n` matrix (`n` even; zero if odd): the sum
/// over perfect matchings of the products of matched entries.
///
/// The `2^{n/2}` subset terms are split into a fixed number of chunks, each
/// summed in order, and the chunk sums are added in order. Threads take whole
/// chunks, so the result is the same bits however many threads run, and the
/// same as a sequential run on a target without threads.
pub fn hafnian(a: &Mat) -> C {
let n = a.n;
if n == 0 {
return ONE;
}
if !n.is_multiple_of(2) {
return ZERO;
}
let half = n / 2;
let total_masks = 1usize << half;
let chunks = total_masks.min(HAFNIAN_CHUNKS);
let per = total_masks / chunks;
let chunk_sum = |c: usize| -> C {
let mut s = ZERO;
for mask in c * per..(c + 1) * per {
s = add(s, hafnian_term(a, half, mask));
}
s
};
#[cfg(not(target_arch = "wasm32"))]
let parts: Vec<C> = {
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(chunks).max(1);
let mut parts = vec![ZERO; chunks];
if threads == 1 || half < 12 {
for (c, p) in parts.iter_mut().enumerate() {
*p = chunk_sum(c);
}
} else {
std::thread::scope(|sc| {
let size = chunks.div_ceil(threads);
for (t, slot) in parts.chunks_mut(size).enumerate() {
let chunk_sum = &chunk_sum;
sc.spawn(move || {
for (k, p) in slot.iter_mut().enumerate() {
*p = chunk_sum(t * size + k);
}
});
}
});
}
parts
};
#[cfg(target_arch = "wasm32")]
let parts: Vec<C> = (0..chunks).map(chunk_sum).collect();
parts.into_iter().fold(ZERO, add)
}
/// Subset terms per hafnian are summed in this many fixed chunks.
const HAFNIAN_CHUNKS: usize = 64;
/// One subset's signed term: `(−1)^{n/2−|Z|}` times the `λ^{n/2}` coefficient
/// of `exp(Σ_k tr(Bᵏ) λᵏ / 2k)`, with `B` the rows and columns of `A·X` in the
/// pairs of `Z` (pairs are `(2j, 2j+1)`; `X` swaps each pair).
fn hafnian_term(a: &Mat, half: usize, mask: usize) -> C {
let pairs: Vec<usize> = (0..half).filter(|j| mask >> j & 1 == 1).collect();
let m = 2 * pairs.len();
let mut b = Mat::zeros(m);
for (r, &pi) in pairs.iter().enumerate() {
for (cpos, &pj) in pairs.iter().enumerate() {
for (dr, i) in [2 * pi, 2 * pi + 1].into_iter().enumerate() {
for (dc, j) in [2 * pj, 2 * pj + 1].into_iter().enumerate() {
// (A X)_{ij} = A_{i, j^1}
b.a[(2 * r + dr) * m + 2 * cpos + dc] = a.at(i, j ^ 1);
}
}
}
}
let traces = power_traces(&b, half);
let g: Vec<C> = (1..=half).map(|k| scale(traces[k - 1], 1.0 / (2.0 * k as f64))).collect();
let mut e = vec![ZERO; half + 1];
e[0] = ONE;
for k in 1..=half {
let mut s = ZERO;
for j in 1..=k {
s = add(s, scale(mul(g[j - 1], e[k - j]), j as f64));
}
e[k] = scale(s, 1.0 / k as f64);
}
if (half - pairs.len()).is_multiple_of(2) { e[half] } else { scale(e[half], -1.0) }
}
/// The torontonian of a `2m × 2m` matrix `O` (modes in `(a, a†)` blocks):
/// `Σ_{Z ⊆ [m]} (−1)^{m−|Z|} / √det(I − O_Z)`. Summed in fixed chunks like
/// [`hafnian`], so the bits do not depend on threading.
pub fn torontonian(o: &Mat) -> f64 {
let m = o.n / 2;
let total_masks = 1usize << m;
let chunks = total_masks.min(HAFNIAN_CHUNKS);
let per = total_masks / chunks;
let chunk_sum = |c: usize| -> f64 {
let mut s = 0.0;
for mask in c * per..(c + 1) * per {
s += torontonian_term(o, m, mask);
}
s
};
#[cfg(not(target_arch = "wasm32"))]
let parts: Vec<f64> = {
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(chunks).max(1);
let mut parts = vec![0.0; chunks];
if threads == 1 || m < 14 {
for (c, p) in parts.iter_mut().enumerate() {
*p = chunk_sum(c);
}
} else {
std::thread::scope(|sc| {
let size = chunks.div_ceil(threads);
for (t, slot) in parts.chunks_mut(size).enumerate() {
let chunk_sum = &chunk_sum;
sc.spawn(move || {
for (k, p) in slot.iter_mut().enumerate() {
*p = chunk_sum(t * size + k);
}
});
}
});
}
parts
};
#[cfg(target_arch = "wasm32")]
let parts: Vec<f64> = (0..chunks).map(chunk_sum).collect();
parts.into_iter().fold(0.0, |a, b| a + b)
}
/// One subset's signed term of the torontonian.
fn torontonian_term(o: &Mat, m: usize, mask: usize) -> f64 {
let z: Vec<usize> = (0..m).filter(|j| mask >> j & 1 == 1).collect();
let term = if z.is_empty() {
1.0
} else {
let idx: Vec<usize> = z.iter().copied().chain(z.iter().map(|&j| j + m)).collect();
let k = idx.len();
let mut d = o.sub(&idx);
for i in 0..k {
for j in 0..k {
let v = d.a[i * k + j];
d.a[i * k + j] = if i == j { sub(ONE, v) } else { scale(v, -1.0) };
}
}
1.0 / d.det().re.sqrt()
};
if (m - z.len()).is_multiple_of(2) { term } else { -term }
}
/// A Gaussian state of `modes` modes, as its complex covariance matrix in the
/// `(a₁…a_N, a₁†…a_N†)` basis (vacuum = `I/2`), with zero displacement.
#[derive(Clone, Debug)]
pub struct GaussianState {
pub modes: usize,
pub cov: Mat,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GbsError {
/// Squeezing, interferometer or transmission of the wrong size or range.
Invalid,
/// A pattern longer than the state, or a mode index outside it.
BadPattern,
}
impl GaussianState {
/// Single-mode squeezed vacua (`r[i]` per mode, 0 = vacuum) through the
/// interferometer `u` (`N × N` unitary, row-major), then uniform
/// transmission `eta` on every mode.
pub fn squeezed(r: &[f64], u: &Mat, eta: f64) -> Result<GaussianState, GbsError> {
let n = r.len();
if u.n != n || !(0.0..=1.0).contains(&eta) || r.iter().any(|x| !x.is_finite()) {
return Err(GbsError::Invalid);
}
// Squeezed covariance: per mode [[cosh 2r, −sinh 2r], [−sinh 2r, cosh 2r]] / 2.
let mut cov = Mat::zeros(2 * n);
for (i, &ri) in r.iter().enumerate() {
let e2 = crate::repro::exp(2.0 * ri);
let (ch, sh) = ((e2 + 1.0 / e2) / 2.0, (e2 - 1.0 / e2) / 2.0);
cov.set(i, i, C { re: ch / 2.0, im: 0.0 });
cov.set(n + i, n + i, C { re: ch / 2.0, im: 0.0 });
cov.set(i, n + i, C { re: -sh / 2.0, im: 0.0 });
cov.set(n + i, i, C { re: -sh / 2.0, im: 0.0 });
}
// Interferometer: S = U ⊕ U*, σ → S σ S†.
let mut s = Mat::zeros(2 * n);
for i in 0..n {
for j in 0..n {
s.set(i, j, u.at(i, j));
s.set(n + i, n + j, conj(u.at(i, j)));
}
}
let mut cov = s.mul(&cov).mul(&s.dagger());
// Loss: σ → η σ + (1 − η) I/2.
for v in cov.a.iter_mut() {
*v = scale(*v, eta);
}
for i in 0..2 * n {
cov.a[i * 2 * n + i] = add(cov.a[i * 2 * n + i], C { re: (1.0 - eta) / 2.0, im: 0.0 });
}
Ok(GaussianState { modes: n, cov })
}
/// The reduced state on `keep` (in that order).
pub fn reduced(&self, keep: &[usize]) -> GaussianState {
let n = self.modes;
let idx: Vec<usize> = keep.iter().copied().chain(keep.iter().map(|&k| k + n)).collect();
GaussianState { modes: keep.len(), cov: self.cov.sub(&idx) }
}
fn q(&self) -> Mat {
let mut q = self.cov.clone();
for i in 0..2 * self.modes {
q.a[i * 2 * self.modes + i] = add(q.a[i * 2 * self.modes + i], C { re: 0.5, im: 0.0 });
}
q
}
/// Probability that exactly the modes with `pattern[i] = true` click.
pub fn click_probability(&self, pattern: &[bool]) -> Result<f64, GbsError> {
if pattern.len() != self.modes {
return Err(GbsError::BadPattern);
}
let q = self.q();
let qinv = q.inverse().ok_or(GbsError::Invalid)?;
let n = self.modes;
let clicked: Vec<usize> = (0..n).filter(|&i| pattern[i]).collect();
let idx: Vec<usize> = clicked.iter().copied().chain(clicked.iter().map(|&k| k + n)).collect();
// O = I − Q⁻¹ on the clicked modes.
let mut o = qinv.sub(&idx);
let k = idx.len();
for i in 0..k {
for j in 0..k {
let v = o.a[i * k + j];
o.a[i * k + j] = if i == j { sub(ONE, v) } else { scale(v, -1.0) };
}
}
Ok(torontonian(&o) / q.det().re.sqrt())
}
/// Probability of exactly `counts[i]` photons in each mode
/// (photon-number-resolving detection): `Haf(A_n) / (Π nᵢ! √det Q)`, with
/// `A = X (I − Q⁻¹)*`.
pub fn photon_probability(&self, counts: &[usize]) -> Result<f64, GbsError> {
if counts.len() != self.modes {
return Err(GbsError::BadPattern);
}
let n = self.modes;
let q = self.q();
let qinv = q.inverse().ok_or(GbsError::Invalid)?;
// A = X (I − Q⁻¹)*.
let mut a = Mat::zeros(2 * n);
for i in 0..2 * n {
let xi = if i < n { i + n } else { i - n };
for j in 0..2 * n {
let v = conj(qinv.at(xi, j));
let id = if xi == j { ONE } else { ZERO };
a.set(i, j, sub(conj(id), v));
}
}
let mut idx = Vec::new();
for (i, &c) in counts.iter().enumerate() {
idx.extend(std::iter::repeat_n(i, c));
}
let half: Vec<usize> = idx.clone();
idx.extend(half.iter().map(|&i| i + n));
let h = hafnian(&a.sub(&idx));
let mut fact = 1.0;
for &c in counts {
for k in 2..=c {
fact *= k as f64;
}
}
Ok(h.re / (fact * q.det().re.sqrt()))
}
/// Exact samples of click patterns, drawn mode by mode: the probability of
/// each prefix is a torontonian of the reduced state on the modes so far.
pub fn sample_clicks(&self, shots: usize, seed: u64) -> Vec<Vec<bool>> {
let mut rng = seed;
let mut next = || {
rng = rng.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = rng;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
(((z ^ (z >> 31)) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0
};
let reduced: Vec<GaussianState> = (1..=self.modes).map(|k| self.reduced(&(0..k).collect::<Vec<_>>())).collect();
let mut out = Vec::with_capacity(shots);
for _ in 0..shots {
let mut pattern: Vec<bool> = Vec::with_capacity(self.modes);
let mut p_prefix = 1.0;
for st in &reduced {
pattern.push(true);
let p_click = st.click_probability(&pattern).unwrap_or(0.0);
let u = next();
if u * p_prefix < p_click {
p_prefix = p_click;
} else {
*pattern.last_mut().unwrap() = false;
p_prefix -= p_click;
}
}
out.push(pattern);
}
out
}
/// One-mode click probabilities `pᵢ` and two-mode joint click
/// probabilities `pᵢⱼ` (row-major, `pᵢᵢ = pᵢ`): the low-order statistics
/// every sampler must reproduce, polynomial in the number of modes.
pub fn click_marginals(&self) -> (Vec<f64>, Vec<f64>) {
let n = self.modes;
let mut p1 = vec![0.0; n];
for (i, p) in p1.iter_mut().enumerate() {
*p = self.reduced(&[i]).click_probability(&[true]).unwrap_or(f64::NAN);
}
let mut p2 = vec![0.0; n * n];
for i in 0..n {
p2[i * n + i] = p1[i];
for j in i + 1..n {
let v = self.reduced(&[i, j]).click_probability(&[true, true]).unwrap_or(f64::NAN);
p2[i * n + j] = v;
p2[j * n + i] = v;
}
}
(p1, p2)
}
}
/// How observed click patterns compare with a state's exact low-order
/// statistics.
#[derive(Clone, Debug, PartialEq)]
pub struct MarginalCheck {
pub shots: usize,
/// Largest |z| over the one-mode click frequencies.
pub worst_single: f64,
/// Largest |z| over the two-mode joint click frequencies.
pub worst_pair: f64,
/// Mean clicks per shot, observed and exact.
pub mean_clicks: (f64, f64),
}
impl GaussianState {
/// Compare observed click patterns with the exact one- and two-mode click
/// statistics: a z-score per mode and per pair (binomial standard error),
/// and the mean number of clicks. A sampler, a device included, that
/// samples this state reproduces all of them within sampling error.
pub fn check_samples(&self, samples: &[Vec<bool>]) -> Result<MarginalCheck, GbsError> {
let n = self.modes;
if samples.iter().any(|s| s.len() != n) || samples.is_empty() {
return Err(GbsError::BadPattern);
}
let shots = samples.len() as f64;
let (p1, p2) = self.click_marginals();
let z = |count: usize, p: f64| -> f64 {
let f = count as f64 / shots;
let se = (p * (1.0 - p) / shots).sqrt();
if se > 0.0 { (f - p) / se } else if f == p { 0.0 } else { f64::INFINITY }
};
let mut worst_single: f64 = 0.0;
for i in 0..n {
let c = samples.iter().filter(|s| s[i]).count();
worst_single = worst_single.max(z(c, p1[i]).abs());
}
let mut worst_pair: f64 = 0.0;
for i in 0..n {
for j in i + 1..n {
let c = samples.iter().filter(|s| s[i] && s[j]).count();
worst_pair = worst_pair.max(z(c, p2[i * n + j]).abs());
}
}
let observed = samples.iter().map(|s| s.iter().filter(|&&b| b).count()).sum::<usize>() as f64 / shots;
Ok(MarginalCheck { shots: samples.len(), worst_single, worst_pair, mean_clicks: (observed, p1.iter().sum()) })
}
}
/// A Haar-random unitary from a seed (QR of a complex Gaussian matrix, with
/// the phases of R's diagonal fixed), for building test interferometers.
#[allow(clippy::needless_range_loop)]
pub fn random_unitary(n: usize, seed: u64) -> Mat {
let mut s = seed;
let mut unit = || {
s = s.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
(((z ^ (z >> 31)) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0
};
// Box–Muller with in-crate logarithm and sine/cosine-free polar form.
let mut gauss = || loop {
let (x, y) = (2.0 * unit() - 1.0, 2.0 * unit() - 1.0);
let r2 = x * x + y * y;
if r2 > 0.0 && r2 < 1.0 {
let f = (-2.0 * crate::repro::ln(r2) / r2).sqrt();
return C { re: x * f, im: y * f };
}
};
let mut cols: Vec<Vec<C>> = (0..n).map(|_| (0..n).map(|_| gauss()).collect()).collect();
for j in 0..n {
for i in 0..j {
let mut dot = ZERO;
for r in 0..n {
dot = add(dot, mul(conj(cols[i][r]), cols[j][r]));
}
for r in 0..n {
let t = mul(cols[i][r], dot);
cols[j][r] = sub(cols[j][r], t);
}
}
let norm = cols[j].iter().map(|c| abs2(*c)).sum::<f64>().sqrt();
for x in cols[j].iter_mut() {
*x = scale(*x, 1.0 / norm);
}
}
let mut m = Mat::zeros(n);
for r in 0..n {
for c in 0..n {
m.a[r * n + c] = cols[c][r];
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol * (1.0 + b.abs())
}
/// Hafnian by exhaustion over perfect matchings.
fn brute_hafnian(a: &Mat) -> C {
fn go(a: &Mat, left: &mut Vec<usize>) -> C {
if left.is_empty() {
return ONE;
}
let i = left.remove(0);
let mut s = ZERO;
for k in 0..left.len() {
let j = left.remove(k);
s = add(s, mul(a.at(i, j), go(a, left)));
left.insert(k, j);
}
left.insert(0, i);
s
}
go(a, &mut (0..a.n).collect())
}
fn random_symmetric(n: usize, seed: u64) -> Mat {
let u = random_unitary(n, seed);
let mut m = Mat::zeros(n);
for i in 0..n {
for j in i..n {
let v = add(u.at(i, j), u.at(j, i));
m.set(i, j, v);
m.set(j, i, v);
}
}
m
}
#[test]
fn the_hafnian_matches_exhaustion_and_known_values() {
for n in [2, 4, 6, 8, 10] {
for seed in 0..5 {
let a = random_symmetric(n, 100 * n as u64 + seed);
let (h, b) = (hafnian(&a), brute_hafnian(&a));
assert!(close(h.re, b.re, 1e-9) && close(h.im, b.im, 1e-9), "n {n} seed {seed}: {h:?} vs {b:?}");
}
}
// haf(J_{2k}) = (2k − 1)!!
for (n, want) in [(2, 1.0), (4, 3.0), (6, 15.0), (8, 105.0), (10, 945.0), (12, 10395.0)] {
let j = Mat { n, a: vec![ONE; n * n] };
assert!(close(hafnian(&j).re, want, 1e-12), "J_{n}");
}
assert_eq!(hafnian(&Mat::zeros(3)).re, 0.0);
}
#[test]
fn determinant_and_inverse_agree() {
let a = random_symmetric(6, 9);
let inv = a.inverse().unwrap();
let id = a.mul(&inv);
for i in 0..6 {
for j in 0..6 {
let want = if i == j { 1.0 } else { 0.0 };
assert!((id.at(i, j).re - want).abs() < 1e-12 && id.at(i, j).im.abs() < 1e-12);
}
}
let (d, di) = (a.det(), inv.det());
let p = mul(d, di);
assert!((p.re - 1.0).abs() < 1e-10 && p.im.abs() < 1e-10);
}
#[test]
fn single_mode_squeezed_vacuum_has_the_textbook_statistics() {
let r = 0.7;
let st = GaussianState::squeezed(&[r], &Mat::identity(1), 1.0).unwrap();
let cosh = (crate::repro::exp(r) + crate::repro::exp(-r)) / 2.0;
let tanh = (crate::repro::exp(2.0 * r) - 1.0) / (crate::repro::exp(2.0 * r) + 1.0);
assert!(close(st.click_probability(&[true]).unwrap(), 1.0 - 1.0 / cosh, 1e-12));
assert!(close(st.photon_probability(&[0]).unwrap(), 1.0 / cosh, 1e-12));
assert!(close(st.photon_probability(&[2]).unwrap(), tanh * tanh / (2.0 * cosh), 1e-12));
assert!(st.photon_probability(&[1]).unwrap().abs() < 1e-14);
// P(4) = 4!/(2² 2!)² tanh⁴ / cosh = 3/8 tanh⁴ / cosh.
assert!(close(st.photon_probability(&[4]).unwrap(), 3.0 / 8.0 * tanh.powi(4) / cosh, 1e-12));
}
#[test]
fn click_probabilities_sum_to_one_with_and_without_loss() {
for (eta, seed) in [(1.0, 1u64), (0.6, 2), (0.3, 3)] {
let n = 5;
let r = [0.9, 0.4, 0.0, 0.7, 0.2];
let st = GaussianState::squeezed(&r, &random_unitary(n, seed), eta).unwrap();
let mut total = 0.0;
for mask in 0..1u32 << n {
let p: Vec<bool> = (0..n).map(|i| mask >> i & 1 == 1).collect();
let v = st.click_probability(&p).unwrap();
assert!(v > -1e-12, "negative probability {v}");
total += v;
}
assert!((total - 1.0).abs() < 1e-10, "eta {eta}: {total}");
}
}
#[test]
fn photon_probabilities_sum_towards_one() {
// Three modes, total photons up to a cutoff; the tail is tiny at low squeezing.
let st = GaussianState::squeezed(&[0.3, 0.2, 0.25], &random_unitary(3, 4), 0.8).unwrap();
let mut total = 0.0;
for a in 0..8 {
for b in 0..8 - a {
for c in 0..8 - a - b {
total += st.photon_probability(&[a, b, c]).unwrap();
}
}
}
assert!(total > 0.99999 && total < 1.0 + 1e-10, "{total}");
}
#[test]
fn exact_samples_reproduce_the_marginals() {
let st = GaussianState::squeezed(&[0.8, 0.5, 0.6, 0.3], &random_unitary(4, 7), 0.7).unwrap();
let shots = 40_000;
let samples = st.sample_clicks(shots, 11);
assert_eq!(samples, st.sample_clicks(shots, 11));
let (p1, p2) = st.click_marginals();
let n = 4;
for i in 0..n {
let f = samples.iter().filter(|s| s[i]).count() as f64 / shots as f64;
let sigma = (p1[i] * (1.0 - p1[i]) / shots as f64).sqrt();
assert!((f - p1[i]).abs() < 5.0 * sigma, "mode {i}: {f} vs {}", p1[i]);
for j in i + 1..n {
let f = samples.iter().filter(|s| s[i] && s[j]).count() as f64 / shots as f64;
let q = p2[i * n + j];
let sigma = (q * (1.0 - q) / shots as f64).sqrt();
assert!((f - q).abs() < 5.0 * sigma, "modes {i},{j}: {f} vs {q}");
}
}
// Full patterns too: each pattern's frequency against its exact probability.
for mask in 0..16u32 {
let p: Vec<bool> = (0..n).map(|i| mask >> i & 1 == 1).collect();
let want = st.click_probability(&p).unwrap();
let f = samples.iter().filter(|s| **s == p).count() as f64 / shots as f64;
let sigma = (want * (1.0 - want) / shots as f64).sqrt().max(1e-4);
assert!((f - want).abs() < 5.0 * sigma, "pattern {mask:04b}: {f} vs {want}");
}
}
#[test]
fn exact_samples_pass_the_check_and_a_wrong_state_fails_it() {
let st = GaussianState::squeezed(&[0.9, 0.7, 0.8, 0.6, 0.5], &random_unitary(5, 21), 0.5).unwrap();
let samples = st.sample_clicks(20_000, 3);
let ok = st.check_samples(&samples).unwrap();
assert!(ok.worst_single < 5.0 && ok.worst_pair < 5.0, "{ok:?}");
assert!((ok.mean_clicks.0 - ok.mean_clicks.1).abs() < 0.03, "{ok:?}");
// The same samples against a different interferometer must fail.
let other = GaussianState::squeezed(&[0.9, 0.7, 0.8, 0.6, 0.5], &random_unitary(5, 22), 0.5).unwrap();
let bad = other.check_samples(&samples).unwrap();
assert!(bad.worst_single > 5.0 || bad.worst_pair > 5.0, "{bad:?}");
}
#[test]
fn bad_input_is_refused() {
assert_eq!(GaussianState::squeezed(&[0.1, 0.2], &Mat::identity(3), 1.0).unwrap_err(), GbsError::Invalid);
assert_eq!(GaussianState::squeezed(&[0.1], &Mat::identity(1), 1.5).unwrap_err(), GbsError::Invalid);
let st = GaussianState::squeezed(&[0.1], &Mat::identity(1), 1.0).unwrap();
assert_eq!(st.click_probability(&[true, false]).unwrap_err(), GbsError::BadPattern);
}
}

View file

@ -30,28 +30,28 @@ pub use trig::sin_cos;
// Natural logarithm
// ---------------------------------------------------------------------------
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LN2_HI: f64 = 6.931_471_803_691_238_164_90e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LN2_LO: f64 = 1.908_214_929_270_587_700_02e-10;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG1: f64 = 6.666_666_666_666_735_130e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG2: f64 = 3.999_999_999_940_941_908e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG3: f64 = 2.857_142_874_366_239_149e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG4: f64 = 2.222_219_843_214_978_396e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG5: f64 = 1.818_357_216_161_805_012e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG6: f64 = 1.531_383_769_920_937_332e-1;
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const LG7: f64 = 1.479_819_860_511_658_591e-1;
/// The natural logarithm, within an ulp of the true value: `−∞` at zero, NaN
/// below it.
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
pub fn ln(x: f64) -> f64 {
let mut bits = x.to_bits();
let mut hx = (bits >> 32) as u32;
@ -95,7 +95,7 @@ pub fn ln(x: f64) -> f64 {
// Exponential
// ---------------------------------------------------------------------------
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
const EXP_P: [f64; 5] = [
1.666_666_666_666_660_190_37e-1,
-2.777_777_777_701_559_338_42e-3,
@ -107,7 +107,7 @@ const EXP_P: [f64; 5] = [
/// `eˣ`, bit-identical on every IEEE-754 platform: fdlibm's algorithm
/// (reduction by `ln 2` in two parts, a rational approximation on
/// `[−ln2/2, ln2/2]`), using only `+ − × ÷`. Within an ulp of the true value.
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
pub fn exp(x: f64) -> f64 {
const LN2_HI_E: f64 = 6.931_471_803_691_238_164_90e-1;
const LN2_LO_E: f64 = 1.908_214_929_270_587_700_02e-10;
@ -308,7 +308,7 @@ mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
#[test]
fn exp_is_within_an_ulp_of_the_platform() {
let mut s = 0x9e37_79b9_7f4a_7c15u64;
@ -351,7 +351,7 @@ mod tests {
})
}
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn"))]
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
#[test]
fn ln_is_within_an_ulp_of_the_platform() {
for x in inputs().filter(|&x| x > 0.0) {

View file

@ -17,7 +17,8 @@
feature = "quantum_bptn",
feature = "quantum_resource",
feature = "quantum_frame",
feature = "quantum_tn"
feature = "quantum_tn",
feature = "quantum_gbs"
))]
use wai_quantum::*;
@ -220,3 +221,33 @@ fn tensor_network_plan_and_amplitude() {
assert_eq!(digest(&v), 0x8954_0c3a_d1fc_a04d);
}
/// A 24 × 24 hafnian and a 14-click torontonian (both summed in fixed chunks,
/// threaded natively and sequential under WASI), lossy-state probabilities,
/// exact samples and marginals.
#[test]
fn gaussian_boson_sampling() {
use quantum_gbs::{hafnian, random_unitary, GaussianState, Mat, C};
let u = random_unitary(24, 5);
let mut m = Mat::zeros(24);
for i in 0..24 {
for j in 0..24 {
let (x, y) = (u.at(i, j), u.at(j, i));
m.a[i * 24 + j] = C { re: x.re + y.re, im: x.im + y.im };
}
}
let h = hafnian(&m);
let r: Vec<f64> = (0..16).map(|i| 0.3 + 0.04 * i as f64).collect();
let st = GaussianState::squeezed(&r, &random_unitary(16, 6), 0.7).unwrap();
let pattern: Vec<bool> = (0..16).map(|i| i % 8 != 3).collect();
let mut v = vec![h.re, h.im, st.click_probability(&pattern).unwrap()];
let small = GaussianState::squeezed(&[0.8, 0.5, 0.6, 0.4, 0.7], &random_unitary(5, 7), 0.6).unwrap();
v.push(small.photon_probability(&[2, 0, 1, 1, 0]).unwrap());
for s in small.sample_clicks(300, 9) {
v.push(s.iter().enumerate().map(|(i, &b)| if b { (1u32 << i) as f64 } else { 0.0 }).sum());
}
let (p1, p2) = small.click_marginals();
v.extend(p1);
v.extend(p2);
assert_eq!(digest(&v), 0x039d_e851_06c3_567d);
}

2
wai/wai-rs/Cargo.lock generated
View file

@ -3900,7 +3900,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.3.26"
version = "0.3.27"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -353,6 +353,8 @@ quantum_resource_receipt = ["quantum_resource", "wai-quantum/quantum_resource_re
quantum_frame = ["dep:wai-quantum", "wai-quantum/quantum_frame"]
# Tensor-network contraction: hyper-optimised trees, slicing, circuit amplitudes.
quantum_tn = ["dep:wai-quantum", "wai-quantum/quantum_tn"]
# Gaussian boson sampling: hafnians, torontonians, exact probabilities, samples, checks.
quantum_gbs = ["dep:wai-quantum", "wai-quantum/quantum_gbs"]
quantum_qir = ["quantum_toolchain", "wai-quantum/quantum_qir"]
# Variational quantum machine learning (extensions/quantum-sim § VML,
# `wai.quantum.vml`): the interference-based ML the thermodynamic-computing pivot
@ -483,6 +485,7 @@ required-features = [
"quantum_resource_receipt",
"quantum_frame",
"quantum_tn",
"quantum_gbs",
]
[[bin]]

View file

@ -161,6 +161,10 @@ fn usage() -> ! {
wai-quantum qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
wai-quantum qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
wai-quantum qec --fit [--shots N] (measure a and p* of the code model)\n\n\
GAUSSIAN BOSON SAMPLING:\n \
wai-quantum gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
wai-quantum gbs … --sample K (exact click patterns)\n \
wai-quantum gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
TENSOR NETWORKS:\n \
wai-quantum tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--plan-only] [--json]\n\n\
Channels: identity, bitflip, dephasing, depolarizing, damping.\n\
@ -854,6 +858,94 @@ fn main() {
}
}
// Gaussian boson sampling, exactly: squeezed light through an
// interferometer with loss, and the click statistics it must show.
"gbs" => {
use wai::quantum_gbs::{random_unitary, GaussianState, Mat, C};
let r: Vec<f64> = opt(args, "--squeezing", "")
.unwrap_or_else(|| die("--squeezing r1,r2,… is required (one value per mode)"))
.split(',')
.map(|x| x.trim().parse().unwrap_or_else(|_| die("bad --squeezing value")))
.collect();
let n = r.len();
let eta: f64 = num(args, "--eta", "", 1.0);
let u = match opt(args, "--unitary", "") {
Some(path) => {
// JSON: an array of N rows, each N [re, im] pairs.
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let v: serde_json::Value = serde_json::from_str(&text).unwrap_or_else(|_| die("--unitary is not JSON"));
let rows = v.as_array().unwrap_or_else(|| die("--unitary: an array of rows"));
if rows.len() != n {
die("--unitary needs one row per mode");
}
let mut m = Mat::zeros(n);
for (i, row) in rows.iter().enumerate() {
let row = row.as_array().filter(|r| r.len() == n).unwrap_or_else(|| die("--unitary rows need one entry per mode"));
for (j, z) in row.iter().enumerate() {
let re = z[0].as_f64().unwrap_or_else(|| die("--unitary entries are [re, im]"));
let im = z[1].as_f64().unwrap_or(0.0);
m.a[i * n + j] = C { re, im };
}
}
m
}
None => random_unitary(n, num(args, "--seed", "", 0)),
};
let st = GaussianState::squeezed(&r, &u, eta).unwrap_or_else(|e| die(&format!("{e:?}")));
let json = flag(args, "--json");
if let Some(path) = opt(args, "--check", "") {
// Lines of 0/1, one click pattern per line.
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let samples: Vec<Vec<bool>> = text
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.trim().chars().map(|c| c == '1').collect())
.collect();
let c = st.check_samples(&samples).unwrap_or_else(|e| die(&format!("{e:?} (every line needs {n} characters)")));
if json {
println!(
"{{\"mean_clicks_exact\":{},\"mean_clicks_observed\":{},\"shots\":{},\"worst_pair_z\":{},\"worst_single_z\":{}}}",
c.mean_clicks.1, c.mean_clicks.0, c.shots, c.worst_pair, c.worst_single
);
} else {
println!("shots {}", c.shots);
println!("mean clicks {:.4} observed, {:.4} exact", c.mean_clicks.0, c.mean_clicks.1);
println!("worst |z| {:.2} over single modes, {:.2} over pairs", c.worst_single, c.worst_pair);
let verdict = if c.worst_single < 5.0 && c.worst_pair < 5.0 { "consistent" } else { "INCONSISTENT" };
println!("verdict {verdict} with the state's exact one- and two-mode click statistics");
}
return;
}
if let Some(k) = opt(args, "--sample", "") {
let k: usize = k.parse().unwrap_or_else(|_| die("bad --sample count"));
for s in st.sample_clicks(k, num(args, "--seed", "", 0) ^ 0x9e37) {
println!("{}", s.iter().map(|&b| if b { '1' } else { '0' }).collect::<String>());
}
return;
}
let (p1, _) = st.click_marginals();
let none = st.click_probability(&vec![false; n]).unwrap_or(f64::NAN);
let all = if n <= 22 { st.click_probability(&vec![true; n]).ok() } else { None };
if json {
let p1s: Vec<String> = p1.iter().map(|p| p.to_string()).collect();
println!(
"{{\"click_marginals\":[{}],\"modes\":{n},\"p_all_click\":{},\"p_no_click\":{none}}}",
p1s.join(","),
all.map_or("null".to_string(), |p| p.to_string())
);
} else {
println!("modes {n}, transmission {eta}");
println!("mean clicks {:.4}", p1.iter().sum::<f64>());
println!("P(no click) {none:.6e}");
if let Some(p) = all {
println!("P(all click) {p:.6e}");
}
for (i, p) in p1.iter().enumerate() {
println!("mode {i:>3} P(click) = {p:.6}");
}
}
}
"entropy" => {
let c = circuit_from(args, algo);
let sv = c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}")));

View file

@ -627,6 +627,9 @@ pub use wai_quantum::quantum_match;
#[cfg(feature = "quantum_tn")]
pub use wai_quantum::quantum_tn;
#[cfg(feature = "quantum_gbs")]
pub use wai_quantum::quantum_gbs;
#[cfg(feature = "quantum_qir")]
pub use wai_quantum::quantum_qir;

View file

@ -734,7 +734,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.3.26"
version = "0.3.27"
dependencies = [
"blake3",
"ed25519-dalek",