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]] [[package]]
name = "wai-quantum" name = "wai-quantum"
version = "0.3.26" version = "0.3.27"
dependencies = [ dependencies = [
"blake3", "blake3",
"ed25519-dalek", "ed25519-dalek",

View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "wai-quantum" name = "wai-quantum"
version = "0.3.26" version = "0.3.27"
edition = "2024" edition = "2024"
rust-version = "1.97" rust-version = "1.97"
license = "Apache-2.0" license = "Apache-2.0"
@ -59,6 +59,9 @@ quantum_frame = []
# Tensor-network contraction: hyper-optimised contraction trees, slicing, and # Tensor-network contraction: hyper-optimised contraction trees, slicing, and
# amplitudes of circuits far past the state vector, reproducible bit for bit. # amplitudes of circuits far past the state vector, reproducible bit for bit.
quantum_tn = [] 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 # Mixed states and the observables that distinguish quantum theory from a classical
# account: density matrices, the partial trace, exact Pauli expectations, CHSH. # account: density matrices, the partial trace, exact Pauli expectations, CHSH.
quantum_info = ["quantum"] quantum_info = ["quantum"]
@ -98,13 +101,17 @@ quantum_phasor_meter = ["_signing"]
# Everything. # Everything.
full = [ 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_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom",
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",
"quantum_phasor_meter", "quantum_phasor_meter",
] ]
[[example]]
name = "gbs_check"
required-features = ["quantum_gbs"]
[[example]] [[example]]
name = "tn_path" name = "tn_path"
required-features = ["quantum_tn"] 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_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_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_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_compile` | Clifford routing + stabilizer-tableau equivalence proof |
| `quantum_atom` | neutral-atom register preparation (Hungarian / LSAP) | | `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 | | `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 documentation states what slicing costs on deep circuits and what the scalar
executor can and cannot contract. 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 ## Determinism
Simulation is **byte-exact**: amplitudes are dyadic fixed-point, so a circuit 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")] #[cfg(feature = "_signing")]
mod merkle; 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; mod linalg;
#[cfg(any( #[cfg(any(
@ -29,7 +29,8 @@ mod linalg;
feature = "quantum_kernel", feature = "quantum_kernel",
feature = "quantum_resource", feature = "quantum_resource",
feature = "quantum_frame", feature = "quantum_frame",
feature = "quantum_tn" feature = "quantum_tn",
feature = "quantum_gbs"
))] ))]
mod repro; mod repro;
@ -102,6 +103,9 @@ pub mod quantum_match;
#[cfg(feature = "quantum_tn")] #[cfg(feature = "quantum_tn")]
pub mod quantum_tn; pub mod quantum_tn;
#[cfg(feature = "quantum_gbs")]
pub mod quantum_gbs;
#[cfg(feature = "quantum_info")] #[cfg(feature = "quantum_info")]
pub mod 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 // 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; 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; 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; 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; 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; 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; 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; 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; 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; const LG7: f64 = 1.479_819_860_511_658_591e-1;
/// The natural logarithm, within an ulp of the true value: `−∞` at zero, NaN /// The natural logarithm, within an ulp of the true value: `−∞` at zero, NaN
/// below it. /// 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 { pub fn ln(x: f64) -> f64 {
let mut bits = x.to_bits(); let mut bits = x.to_bits();
let mut hx = (bits >> 32) as u32; let mut hx = (bits >> 32) as u32;
@ -95,7 +95,7 @@ pub fn ln(x: f64) -> f64 {
// Exponential // 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] = [ const EXP_P: [f64; 5] = [
1.666_666_666_666_660_190_37e-1, 1.666_666_666_666_660_190_37e-1,
-2.777_777_777_701_559_338_42e-3, -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 /// `eˣ`, bit-identical on every IEEE-754 platform: fdlibm's algorithm
/// (reduction by `ln 2` in two parts, a rational approximation on /// (reduction by `ln 2` in two parts, a rational approximation on
/// `[−ln2/2, ln2/2]`), using only `+ − × ÷`. Within an ulp of the true value. /// `[−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 { pub fn exp(x: f64) -> f64 {
const LN2_HI_E: f64 = 6.931_471_803_691_238_164_90e-1; 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; const LN2_LO_E: f64 = 1.908_214_929_270_587_700_02e-10;
@ -308,7 +308,7 @@ mod tests {
#[allow(unused_imports)] #[allow(unused_imports)]
use super::*; use super::*;
#[cfg(any(feature = "quantum_frame", feature = "quantum_tn"))] #[cfg(any(feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs"))]
#[test] #[test]
fn exp_is_within_an_ulp_of_the_platform() { fn exp_is_within_an_ulp_of_the_platform() {
let mut s = 0x9e37_79b9_7f4a_7c15u64; 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] #[test]
fn ln_is_within_an_ulp_of_the_platform() { fn ln_is_within_an_ulp_of_the_platform() {
for x in inputs().filter(|&x| x > 0.0) { for x in inputs().filter(|&x| x > 0.0) {

View file

@ -17,7 +17,8 @@
feature = "quantum_bptn", feature = "quantum_bptn",
feature = "quantum_resource", feature = "quantum_resource",
feature = "quantum_frame", feature = "quantum_frame",
feature = "quantum_tn" feature = "quantum_tn",
feature = "quantum_gbs"
))] ))]
use wai_quantum::*; use wai_quantum::*;
@ -220,3 +221,33 @@ fn tensor_network_plan_and_amplitude() {
assert_eq!(digest(&v), 0x8954_0c3a_d1fc_a04d); 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]] [[package]]
name = "wai-quantum" name = "wai-quantum"
version = "0.3.26" version = "0.3.27"
dependencies = [ dependencies = [
"blake3", "blake3",
"ed25519-dalek", "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"] quantum_frame = ["dep:wai-quantum", "wai-quantum/quantum_frame"]
# Tensor-network contraction: hyper-optimised trees, slicing, circuit amplitudes. # Tensor-network contraction: hyper-optimised trees, slicing, circuit amplitudes.
quantum_tn = ["dep:wai-quantum", "wai-quantum/quantum_tn"] 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"] quantum_qir = ["quantum_toolchain", "wai-quantum/quantum_qir"]
# Variational quantum machine learning (extensions/quantum-sim § VML, # Variational quantum machine learning (extensions/quantum-sim § VML,
# `wai.quantum.vml`): the interference-based ML the thermodynamic-computing pivot # `wai.quantum.vml`): the interference-based ML the thermodynamic-computing pivot
@ -483,6 +485,7 @@ required-features = [
"quantum_resource_receipt", "quantum_resource_receipt",
"quantum_frame", "quantum_frame",
"quantum_tn", "quantum_tn",
"quantum_gbs",
] ]
[[bin]] [[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 --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
wai-quantum qec [--circuit | --dem] [-d D | --file CIRCUIT]\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\ 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 \ 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\ 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\ 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" => { "entropy" => {
let c = circuit_from(args, algo); let c = circuit_from(args, algo);
let sv = c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}"))); 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")] #[cfg(feature = "quantum_tn")]
pub use wai_quantum::quantum_tn; pub use wai_quantum::quantum_tn;
#[cfg(feature = "quantum_gbs")]
pub use wai_quantum::quantum_gbs;
#[cfg(feature = "quantum_qir")] #[cfg(feature = "quantum_qir")]
pub use wai_quantum::quantum_qir; pub use wai_quantum::quantum_qir;

View file

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