Gaussian layer
qkd.gaussian: an
from qkd import gaussian as gStates are immutable
Every operation returns a new State; sampling leaves the state unchanged. mean and cov are copies.
st = (g.Vacuum(2)
.squeeze(0, r=0.8)
.bs(0, 1, t=0.5)
.thermal_loss(0, T=0.5, xi=0.01, ref="input"))
st.n_modes # 2
st.physical() # True
st.cov
# array([[ 0.402737, 0. , 0.141086, 0. ],
# [ 0. , 0.996629, 0. , -0.698804],
# [ 0.141086, 0. , 0.300474, 0. ],
# [ 0. , -0.698804, 0. , 1.488258]])Constructors
| Constructor | Modes |
|---|---|
Vacuum(n) | |
Coherent(x, p), Thermal(nbar), Squeezed(r) | 1 |
Epr(r) | 2 |
Moments(mean, cov) |
Covariances and means: reference states. No defaults.
| Parameter | Unit | Range | Description |
|---|---|---|---|
n | modes | Zero raises. | |
x, p | internal quadratures | finite | Displacement; |
nbar | photons | ||
r | nepers | finite, unbounded | |
mean, cov | internal | — | Length |
Epr(r) is two-mode squeezed vacuum: pure at every Moments takes a covariance from elsewhere — a Fock state's Gaussian shadow, a measured matrix.
Symplectic operations
| Method | Parameters | Effect |
|---|---|---|
displace(mode, x=0.0, p=0.0) | quadratures, finite | mean shift only; covariance untouched |
squeeze(mode, r) | ||
rotate(mode, theta) | phase-space rotation by | |
bs(m1, m2, t) | beamsplitter of transmittance |
| Action | |
| Checked | every mode index is bounds-checked; a beamsplitter given one mode twice raises |
| Closed intervals | bs(t), thermal_loss(T) and fock.State.loss(eta) accept q.Channel(T), q.Fiber(T) and every detector eta require |
Thermal loss, and the required ref= plane
Vacuum through the loss port, then excess noise. Pure loss is
| Parameter | Unit | Default | Description |
|---|---|---|---|
mode | — | required | Which mode the channel acts on. |
T | — | required | Transmittance |
xi | SNU | 0.0 | Excess noise ref names. The one argument here not in internal units: halved on the way in (SNU boundary). |
ref | — | required, keyword-only | "input" or "output". Required even at |
g.Vacuum(1).thermal_loss(0, T=0.5, xi=0.01)
# TypeError: State.thermal_loss() missing 1 required keyword-only argument: 'ref'
g.Vacuum(1).thermal_loss(0, T=0.5, xi=0.01, ref="bob")
# ValueError: ref must be 'input' or 'output'One channel, either plane:
a = g.Vacuum(1).thermal_loss(0, T=0.4, xi=0.02, ref="input")
b = g.Vacuum(1).thermal_loss(0, T=0.4, xi=0.008, ref="output")
a.cov[0, 0] == b.cov[0, 0] # True -- 0.504Gaussian · Loss pins that identity, the vacuum fixed point, the
Measurement
Sampling is repeated-preparation: i.i.d. draws, state unchanged.
x = g.Coherent(1.0, 0.0).homodyne(0, angle=0.0, shots=100_000, seed=3)
x.shape, x.mean(), x.var()
# ((100000,), 0.9986, 0.5020) -- mean 1, variance 1/2
h = g.Vacuum(1).heterodyne(0, shots=100_000, seed=1)
h.shape, h.var(axis=0)
# ((100000, 2), array([0.9958, 0.9947]))| Method | Parameters | Returns |
|---|---|---|
homodyne(mode, angle=0.0, shots=1, seed=0) | (shots,) outcomes of | |
heterodyne(mode, shots=1, seed=0) | — | (shots, 2), drawn from the Husimi distribution |
condition(mode, angle, outcome) | radians, internal quadrature | the remaining |
| Buffers | owned by the array |
| Heterodyne | samples the covariance plus one vacuum unit from the balanced splitter, so vacuum reads variance |
seed | default 0; the same seed reproduces the samples |
Conditioning
Homodyne on one mode — Schur complement with a pseudo-inverse — which removes that mode:
c = g.Epr(1.0).condition(1, 0.0, 0.7)
c.n_modes # 1
c.cov[0, 0] # 0.132901 == 1 / (2*cosh(2)), below the vacuum 1/2
c.mean[0] # 0.674819 == tanh(2) * 0.7The sub-vacuum
CV toolbox
Closed Gaussian forms, no numerical integration.
| Method | Returns |
|---|---|
wigner(mode, xs, ps) | Wigner grid, shape (len(ps), len(xs)) |
husimi(mode, xs, ps) | Husimi |
keep(modes) / drop(modes) | partial trace, order preserved; keep needs at least one mode and rejects duplicates |
evolve(G, t) | unitary evolution under G symmetric and ValueError |
purity() | |
entropy() | von Neumann entropy in bits |
spectrum() | symplectic eigenvalues, internal units, ascending |
negativity() | Wigner-negativity volume, 0.0 |
physical(atol=1e-9) | the bona fide check |
overlap(other) | Hilbert–Schmidt overlap |
fidelity(other) | fidelity, squared convention |
trace_distance(other) | |
trace_bounds(other) |
xs and ps each need at least two finite, strictly increasing points.
Wigner and Husimi
Both normalised in
import numpy as np
xs = ps = np.linspace(-5, 5, 201)
W = g.Coherent(1.5, -0.5).wigner(0, xs, ps)
W.sum() * (xs[1] - xs[0]) * (ps[1] - ps[0]) # 1.0000
W.max() # 0.31831 == 1/piPartial trace and entanglement
e = g.Epr(0.8)
e.entropy() # 0.0 -- globally pure
e.keep([0]).entropy() # 1.77069 -- entanglement entropy
e.keep([1]).cov[0, 0] # 1.288732 == cosh(1.6) / 2Half an EPR pair is Thermal(sinh(r)**2). drop([0]) is keep([1]).
Evolution under a quadratic Hamiltonian
G = np.array([[0.0, 1.0], [1.0, 0.0]])
ev = g.Vacuum(1).evolve(G, 0.3)
ev.cov[0, 0], ev.cov[1, 1] # 0.911059, 0.274406 == e^0.6/2, e^-0.6/2
ev.purity() # 1.0 -- unitary, so purity is preservedPurity, entropy, spectrum
th = g.Thermal(0.5)
th.spectrum() # array([1.])
th.purity() # 0.5 == 1 / (2*nbar + 1)
th.entropy() # 1.377444 == 1.5*log2(1.5) - 0.5*log2(0.5)spectrum()'s ceiling | |
|---|---|
| Refusal | a surviving imaginary part is refused, |
| Threshold | the backward error of a general eigensolve, |
| First refusal | |
Epr's own limits | bona fide refusal from |
| Usable range | treat |
Distinguishability
| Method | Exactness |
|---|---|
overlap | closed form at every purity() when the states coincide |
fidelity | squared, |
a, b = g.Coherent(1.0, 0.0), g.Coherent(0.0, 0.0) # alpha = 1/sqrt2 and 0
a.overlap(b), a.fidelity(b) # 0.606531, 0.606531 == exp(-|alpha|^2) = exp(-1/2)
a.trace_distance(b) # 0.627271 == sqrt(1 - F), both pure
g.Thermal(0.5).trace_distance(g.Thermal(1.0))
# NotImplementedError: trace distance is exact here only for a pure pair| Trace distance | |
|---|---|
| No general closed form | set by the eigenvalues of |
trace_distance | pure pair only, |
trace_bounds | trace_distance answers |
Thermal(0.5) vs Thermal(1.0) |
Negativity
negativity() returns
Derived states
keep, drop and evolve return an ordinary State through Moments' constructor — every method, in any order — re-checking finiteness, symmetry and the bona fide condition.
sub = g.Epr(0.8).keep([0])
sub.entropy() # 1.77069
sub.thermal_loss(0, T=0.5, xi=0.0, ref="input").cov[0, 0] # 0.894366
sub.homodyne(0, shots=3, seed=1) # array([-0.032070, -1.209714, -0.258740])