wai-quantum 0.3.27: Gaussian boson sampling, exactly #145
14 changed files with 1168 additions and 23 deletions
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -589,7 +589,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
67
wai/wai-quantum/examples/gbs_check.rs
Normal file
67
wai/wai-quantum/examples/gbs_check.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
918
wai/wai-quantum/src/quantum_gbs.rs
Normal file
918
wai/wai-quantum/src/quantum_gbs.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -3900,7 +3900,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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:?}")));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -734,7 +734,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue