Rydberg arrays¶
The model these functions implement, with its units and conventions, is specified in The Rydberg feature model.
hqnn_forge.rydberg ¶
hqnn_forge.rydberg¶
A small simulated array of neutral atoms, evolving continuously in time under
the Rydberg Hamiltonian. The model, its units and every convention are
specified in docs/rydberg-model.md; this package implements it.
Units: ħ = 1, angular frequencies in rad/µs, lengths in µm, times in µs.
The matrices are dense and built in torch (float64 / complex128),
without a PennyLane device: PennyLane's own qml.pulse.rydberg_interaction
and rydberg_drive need JAX, which this library does not depend on.
Exported symbols
AtomRegister Atom positions in µm; chain and ring constructors, C6/r^6 couplings, blockade radius. rydberg_hamiltonian Dense 2^n x 2^n Hamiltonian of a register, for one or a batch of pulses. evolve ρ(t) from all atoms in |g⟩: exact without dephasing, Strang splitting with it. readout ⟨n_i⟩ of every atom, optionally ⟨n_i n_j⟩ of every pair, from the diagonal of ρ. PulseEncoding Inputs to bounded detunings, Δ_i = Δ_max · σ(x_i), with the fixed Ω and T. RydbergFeatureMap Fixed map from a batch of inputs to ⟨n_i⟩ (optionally ⟨n_i n_j⟩), exact or from shots. DEFAULT_C6 C6 of two 87Rb atoms in 70S, 2π × 862 690 MHz µm^6, in rad/µs · µm^6. MAX_ATOMS Largest register rydberg_hamiltonian, evolve, readout and the feature map accept (10).
DEFAULT_C6
module-attribute
¶
DEFAULT_C6: float = 2 * math.pi * 862690.0
C6 of two ⁸⁷Rb atoms in 70S₁/₂, in rad/µs · µm⁶:
2π × 862 690 MHz µm⁶ = 5 420 441, positive (repulsive). It is the
value fixed in docs/rydberg-model.md and the default of PennyLane's
qml.pulse.rydberg_interaction (which takes it in MHz µm⁶). No function
here defaults to it: c6 is always passed, so the atom is stated at the
call.
MAX_ATOMS
module-attribute
¶
MAX_ATOMS: int = 10
Largest register rydberg_hamiltonian builds a matrix for. N
atoms have 2^N basis states, so one Hamiltonian, like one density
matrix, holds 4^N complex128 entries, 16 · 4^N bytes: 16 MiB at
10 atoms, and 4 times more for each further atom (1 GiB at 13), per sample
of a batch. The model of docs/rydberg-model.md uses 4 to 6 atoms.
PulseEncoding ¶
The map from inputs to bounded pulse parameters: Δ_i = Δ_max · σ(x_i).
One input per atom sets that atom's detuning through the logistic
function σ(x) = 1/(1 + e^(−x)); the Rabi frequency Ω and the pulse
duration T are fixed, the same for every atom and sample. Input
column i gives the detuning of atom i. The module docstring
states the conventions and derives what the defaults give.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_features
|
int
|
Width of the inputs, an integer |
required |
omega
|
float
|
Rabi frequency Ω in rad/µs, finite and positive (the coefficient of
|
required |
t
|
float | None
|
Pulse duration |
None
|
delta_max
|
float | None
|
Upper bound |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
n_features |
int
|
|
omega |
float
|
|
t |
float
|
The resolved duration, |
delta_max |
float
|
The resolved bound, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import math
>>> from hqnn_forge.rydberg import PulseEncoding
>>> encoding = PulseEncoding(2, omega=4 * math.pi)
>>> encoding.t # ΩT = π
0.25
>>> round(encoding.delta_max / encoding.omega, 12) # √3
1.732050807569
>>> (encoding.detunings([[0.0, math.log(3)]]) / encoding.delta_max).tolist() # σ(x)
[[0.5, 0.75]]
detunings ¶
detunings(X: Any) -> torch.Tensor
The detunings Δ_i = Δ_max · σ(x_i) of a batch of inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
Any
|
Inputs of shape |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Notes
0 < Δ_i < Δ_max holds for every finite input in exact arithmetic.
In float64, σ(x) rounds to 1 for x above about 37 and to
0 for x below about −700, where Δ_i equals a bound. The exact
lower value depends on how the torch build evaluates σ: −709.8 where
e^(−x) overflows, −745.2 where e^x itself rounds to 0.
get_config ¶
get_config() -> dict[str, Any]
The constructor arguments, as a fresh JSON-serialisable dict.
t and delta_max are the resolved values, so
PulseEncoding(**encoding.get_config()) is the same pulse whatever
the defaults are.
RydbergFeatureMap ¶
A fixed map from a batch of inputs to the excitation probabilities of a Rydberg array.
Each sample sets the detunings of the atoms (encoding), the array
evolves for the pulse duration under the Hamiltonian of
docs/rydberg-model.md and dephasing at rate γ, and the features are
⟨n_i⟩ of every atom, optionally followed by ⟨n_i n_j⟩ of every
pair. The module docstring gives the pipeline, the conventions (input
i feeds atom i, the pair order) and the closed form without
interactions.
The map has no trainable parameters and nothing to fit: it is a
preprocessing step, like PCANormalizer,
and not an encoding layer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
register
|
Any
|
The atoms' positions: an |
required |
encoding
|
PulseEncoding | Mapping[str, Any]
|
The |
required |
c6
|
float
|
The |
required |
gamma
|
float
|
Dephasing rate γ in rad/µs, |
required |
n_steps
|
int | None
|
Number of splitting steps of the solver, an integer |
None
|
interactions
|
bool
|
|
True
|
correlations
|
bool
|
Append the pair correlations |
False
|
Attributes:
| Name | Type | Description |
|---|---|---|
n_features_in |
int
|
Width of the inputs, the number of atoms |
n_features_out |
int
|
Width of the features: |
evolution_time |
float
|
The pulse duration |
register, encoding, c6, gamma, n_steps, interactions, correlations |
The settings, read-only. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the register has more than
|
Examples:
>>> import math
>>> import torch
>>> from hqnn_forge.rydberg import DEFAULT_C6, AtomRegister, PulseEncoding, RydbergFeatureMap
>>> omega = 4 * math.pi
>>> spacing = AtomRegister.blockade_radius(DEFAULT_C6, omega) # V = Ω
>>> control = RydbergFeatureMap(
... AtomRegister.chain(4, spacing),
... PulseEncoding(4, omega=omega),
... c6=DEFAULT_C6,
... gamma=0.0,
... interactions=False,
... )
>>> X = torch.tensor([[-40.0, 0.0, 0.0, 40.0]])
>>> [round(v, 3) for v in control.transform(X)[0].tolist()] # f(1), f(√1.75), ·, f(2)
[1.0, 0.437, 0.437, 0.0]
>>> noisy = RydbergFeatureMap(
... AtomRegister.chain(4, spacing),
... PulseEncoding(4, omega=omega),
... c6=DEFAULT_C6,
... gamma=omega,
... n_steps=200,
... correlations=True,
... )
>>> noisy.n_features_out
10
>>> rebuilt = RydbergFeatureMap(**noisy.get_config())
>>> torch.equal(rebuilt.transform(X), noisy.transform(X))
True
correlations
property
¶
correlations: bool
Whether the pair correlations are appended to the features.
n_features_out
property
¶
n_features_out: int
Width of the features: N, or N + N(N−1)/2 with correlations.
transform ¶
transform(X: Any, *, shots: int | None = None, generator: Generator | None = None, chunk_size: int | None = None) -> torch.Tensor
The features of a batch of inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
Any
|
Inputs of shape |
required |
shots
|
int | None
|
|
None
|
generator
|
Generator | None
|
The |
None
|
chunk_size
|
int | None
|
Samples evolved at a time, an integer |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Notes
- Memory. The samples are evolved
chunk_sizeat a time and the features written into one preallocated result, so the working memory is that of one chunk: the solver holds a handful ofcomplex128tensors ofchunk_size · 4^Nentries (16 · 4^Nbytes per sample each; 16 MiB each at the default). - Chunked and unchunked results are equal to rounding, not bit for
bit. Every sample is evolved with the same
T, γ andn_stepswhatever the chunk, and bitstrings are drawn sample by sample in order, so no setting and no random draw depends onchunk_size. What is left is the rounding of the solver's batched matrix products, which a linear-algebra backend may sum in another order for another batch size. Measured on a CPU, features and states of two chunkings do not differ at all up to 4 atoms; from 5 atoms on they differ by up to1e-15atgamma = 0and by up to2e-16per solver step with dephasing (3e-13at 2000 steps), the size of the rounding error either result has anyway, and orders of magnitude below the splitting error. The same holds forstates. Withshotsthe drawn bitstrings were the same in every chunking tried, and the estimates are the correctly rounded value ofcount / S, the same bit for bit in every chunking as long as the drawn bitstrings are the same. For bit-for-bit repeatable features without shots, keepchunk_size(and the machine) fixed. - Precision.
Xis converted tofloat64whatever its dtype. Withshots, a population that rounding left slightly below 0 is taken as 0.
states ¶
states(X: Any, *, chunk_size: int | None = None) -> torch.Tensor
The density matrices ρ(T) the features are read from.
readout(feature_map.states(X), pairs=feature_map.correlations)
equals feature_map.transform(X).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
Any
|
Inputs of shape |
required |
chunk_size
|
int | None
|
Samples evolved at a time, as for |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
get_config ¶
get_config() -> dict[str, Any]
Every setting of the map, as a fresh JSON-serialisable dict.
The keys are the constructor's arguments, so
type(m)(**m.get_config()) builds a map with the same features,
also after a round trip through JSON:
register
The positions in µm, a list of [x, y] per atom.
encoding
PulseEncoding.get_config(): n_features, Ω (omega),
T (t) and the bound delta_max, in rad/µs and µs.
c6, gamma, n_steps, interactions, correlations
As passed; n_steps is the solver's step count, None if
none was given (gamma = 0).
The number of shots and the chunk size are arguments of
transform, not settings of the map, and are not recorded.
AtomRegister ¶
The positions of the atoms of an array, in µm, in a plane.
Atoms are counted from 0 in the order of the rows of positions. That
order is the order of everything derived from the register: row and column
i of interaction_matrix, and tensor factor (PennyLane wire)
i of rydberg_hamiltonian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positions
|
Any
|
Shape |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Notes
Only exact duplicates are refused. How close two atoms may sensibly be is
a statement about the atom, not the geometry: the C6 / r⁶ form is a
long-distance one, and docs/rydberg-model.md does not use it below
4 µm. The register has no upper limit on the number of atoms; the dense
Hamiltonian has (MAX_ATOMS).
Examples:
>>> register = AtomRegister.chain(3, spacing=5.0)
>>> register.positions.tolist()
[[0.0, 0.0], [5.0, 0.0], [10.0, 0.0]]
>>> v = register.interaction_matrix(c6=15625.0) # V = C6 / 5^6 = 1
>>> v[0, 1].item(), v[0, 2].item()
(1.0, 0.015625)
chain
classmethod
¶
chain(n_atoms: int, spacing: float) -> AtomRegister
An open chain: atom i at r_i = (i · spacing, 0).
This is the geometry of docs/rydberg-model.md. Atoms i and
j are |i − j| · spacing apart, so with V = C6 / spacing⁶
the nearest-neighbour coupling, V_ij = V / |i − j|⁶.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_atoms
|
int
|
Number of atoms, at least 1. |
required |
spacing
|
float
|
Distance between neighbouring atoms in µm, finite and positive. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ring
classmethod
¶
ring(n_atoms: int, spacing: float) -> AtomRegister
A closed ring: the corners of a regular polygon of side spacing.
Atom i sits at R · (cos θ_i, sin θ_i) with θ_i = 2π i / n,
counterclockwise from the positive x axis around the origin. Two
atoms k steps apart around the ring subtend the angle 2π k / n
and are the chord 2 R sin(π k / n) apart. Setting the k = 1
chord to spacing fixes the radius::
R = spacing / (2 sin(π / n))
so every atom has both ring neighbours at spacing, including atoms
n − 1 and 0. For n = 2 this is two atoms spacing
apart (R = spacing / 2). For n = 1 there is no chord and the
formula has no finite value; the single atom is placed at the origin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_atoms
|
int
|
Number of atoms, at least 1. |
required |
spacing
|
float
|
Distance between neighbouring atoms in µm (the chord, not the arc), finite and positive. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
interaction_matrix ¶
interaction_matrix(c6: float) -> torch.Tensor
The van der Waals couplings V_ij = C6 / |r_i − r_j|⁶ of every pair.
With d_ij = r_i − r_j the squared distance is
|d_ij|² = d_ij,x² + d_ij,y², and |d_ij|⁶ = (|d_ij|²)³, which is
how it is computed (no square root). d_ij = −d_ji enters only
squared, so the matrix is symmetric exactly, not just to rounding. The
diagonal is set to 0: an atom does not interact with itself, and the
Hamiltonian sums over pairs i < j only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
c6
|
float
|
The |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
blockade_radius
staticmethod
¶
blockade_radius(c6: float, omega: float) -> float
The blockade radius R_b = (C6/Ω)^(1/6), in µm.
It is the distance at which the coupling equals the Rabi frequency:
C6 / R_b⁶ = Ω. Closer pairs have V_ij > Ω and cannot both be
excited by a resonant drive; so V/Ω = (R_b/a)⁶ for a pair at
distance a. The radius depends on the atom and the drive, not on
the positions, so it can be called on the class as well as on a
register. For DEFAULT_C6 and
Ω = 4π rad/µs it is 8.69 µm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
c6
|
float
|
The |
required |
omega
|
float
|
The Rabi frequency Ω in rad/µs, positive (the coefficient of
|
required |
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
rydberg_hamiltonian ¶
rydberg_hamiltonian(register: AtomRegister, omega: Any, delta: Any, *, c6: float, interactions: bool = True) -> torch.Tensor
The dense Rydberg Hamiltonian of a register, for one or a batch of pulses.
::
H = Σ_i Ω_i/2 · X_i − Σ_i Δ_i n_i + Σ_{i<j} C6/|r_i − r_j|⁶ · n_i n_j
with n_i = |r⟩⟨r|_i, in rad/µs. The derivation of every matrix
element, the sign conventions and the basis ordering (atom i is tensor
factor and PennyLane wire i, atom 0 the most significant bit of the
index) are in the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
register
|
AtomRegister
|
The atoms' positions. |
required |
omega
|
Any
|
Rabi frequency Ω in rad/µs: a scalar (one global drive), or one value
per atom with the atoms on the last axis. Accepted shapes are
|
required |
delta
|
Any
|
Detuning Δ_i in rad/µs, one per atom: shape |
required |
c6
|
float
|
The |
required |
interactions
|
bool
|
|
True
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the register has more than |
Notes
- Precision.
omegaanddeltaare converted tofloat64whatever their dtype, and the result iscomplex128. - Gradients. The matrix is linear in
omegaanddeltaand built with differentiable operations:∂H/∂Δ_i = −n_iand∂H/∂Ω_i = X_i/2reach tensors that require them. The positions andc6are constants. - Batched and unbatched calls agree bit for bit. Each diagonal entry
is accumulated atom by atom in the same order either way, so sample
bof a batched call equals the unbatched call onomega[b]anddelta[b]exactly. - Drive phase. With a phase φ the drive term reads
Ω/2 · Σ_i (cos φ X_i − sin φ Y_i)(the trainable analog layer, #420). This function is the caseφ = 0. Everything afterdeltais keyword-only, so aphaseargument defaulting to 0 can be added without changing any existing call.
Examples:
>>> import torch
>>> from hqnn_forge.rydberg import AtomRegister, rydberg_hamiltonian
>>> one = AtomRegister([[0.0, 0.0]])
>>> rydberg_hamiltonian(one, 2.0, [0.5], c6=1.0).real.tolist()
[[0.0, 1.0], [1.0, -0.5]]
>>> pair = AtomRegister.chain(2, spacing=2.0) # V = C6 / 2^6
>>> h = rydberg_hamiltonian(pair, 2.0, [0.5, 0.25], c6=64.0)
>>> h.real.diagonal().tolist() # |gg>, |gr>, |rg>, |rr>
[0.0, -0.25, -0.5, 0.25]
>>> rydberg_hamiltonian(pair, 2.0, torch.zeros(8, 2), c6=64.0).shape
torch.Size([8, 4, 4])
evolve ¶
evolve(hamiltonian: Tensor, t: float, *, gamma: float = 0.0, n_steps: int | None = None) -> torch.Tensor
The state ρ(t) of an array that starts with every atom in |g⟩.
Solves, for one or a batch of time-independent Hamiltonians::
dρ/dt = −i [H, ρ] + γ Σ_i ( n_i ρ n_i − ½ { n_i , ρ } ), ρ(0) = |g…g⟩⟨g…g|
the Lindblad equation of docs/rydberg-model.md with the local
collapse operators L_i = √γ n_i. The module docstring derives every
formula used here, with the sign and ordering conventions.
gamma = 0:ρ(t) = ψψ†withψ = e^(−iHt) |g…g⟩, exact, from one eigendecomposition per sample and without time stepping.gamma > 0:n_stepssteps ofdt = t / n_stepsof the symmetric (Strang) splittinge^(𝓛_D dt/2) · e^(𝓛_H dt) · e^(𝓛_D dt/2). The unitary step isρ → U ρ U†withU = e^(−iH dt)from the same eigendecomposition; the dissipative step multipliesρ_abbyexp(−γ τ d_H(a, b) / 2),d_Hthe Hamming distance of the basis statesaandb. Both are exact; the splitting has a global error of second order indt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hamiltonian
|
Tensor
|
Shape |
required |
t
|
float
|
Evolution time in µs, |
required |
gamma
|
float
|
Dephasing rate γ in rad/µs, |
0.0
|
n_steps
|
int | None
|
Number of splitting steps, an integer |
None
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Notes
- Precision. The Hamiltonian is converted to
complex128whatever its dtype. - Hermiticity is checked per sample: the largest
|H − H†|entry may be at most1e-12of the sample's largest|H|entry, or 100 machine epsilons of the input's dtype where that is more (1.2e-5for single precision, and no more than that below it). - Choosing
n_steps. Halvingdtdivides the error by 4, so two runs withn_stepsand2 · n_stepsdiffer by about 3/4 of the coarser run's error.dtmust be small against1/γand against the inverse of the largest level spacing ofH. - Cost. One eigendecomposition per sample, then two
2^N × 2^Nmatrix products per step and sample. - Gradients. Every operation is differentiable and none is in
place on a tensor that carries a gradient (the only in-place write
sets the constant initial state), so gradients reach the tensors
hamiltonianwas built from, and they match finite differences where the spectrum ofHis not degenerate. They pass throughtorch.linalg.eigh, whose backward pass holds1/(E_m − E_k): where two eigenvalues coincide the gradient is wrong without any error, finite but not the derivative (#505). Two atoms with equal detunings and no interaction are enough. Nothing here needs a gradient; the features are precomputed. - Time-dependent pulses are out of scope. A piecewise-constant one would repeat the loop below per segment, starting from the previous segment's state.
Examples:
>>> import math
>>> from hqnn_forge.rydberg import AtomRegister, evolve, readout, rydberg_hamiltonian
>>> one = AtomRegister([[0.0, 0.0]])
>>> h = rydberg_hamiltonian(one, 2.0, [0.0], c6=1.0) # Ω = 2 rad/µs, resonant
>>> rho = evolve(h, math.pi / 2) # Ωt = π: a π pulse
>>> round(readout(rho).item(), 12)
1.0
>>> damped = evolve(h, math.pi / 2, gamma=0.2, n_steps=200)
>>> round(readout(damped).item(), 3) # 1 − πγ/(8Ω) = 0.961 to first order in γ
0.962
readout ¶
readout(rho: Tensor, *, pairs: bool = False) -> torch.Tensor
Excitation probabilities ⟨n_i⟩ of every atom, and optionally ⟨n_i n_j⟩.
With b_i(k) = (k >> (N−1−i)) & 1 the bit of atom i in the index
k of a basis state (atom 0 the most significant bit)::
⟨n_i⟩ = Tr[ρ n_i] = Σ_k b_i(k) ρ_kk
⟨n_i n_j⟩ = Tr[ρ n_i n_j] = Σ_k b_i(k) b_j(k) ρ_kk
Only the diagonal of ρ is read, the outcome probabilities of measuring
every atom in the (|g⟩, |r⟩) basis. These are exact expectation
values, not estimates from shots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rho
|
Tensor
|
Density matrix of shape |
required |
pairs
|
bool
|
Also return the pair correlations |
False
|
Returns:
| Type | Description |
|---|---|
Tensor
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Examples:
>>> import torch
>>> from hqnn_forge.rydberg import readout
>>> rho = torch.diag(torch.tensor([0.125, 0.125, 0.25, 0.5])) # |gg>, |gr>, |rg>, |rr>
>>> readout(rho).tolist() # atom 0 is excited in |rg> and |rr>
[0.75, 0.625]
>>> readout(rho, pairs=True).tolist()
[0.75, 0.625, 0.5]
The feature map: encoding, single-atom feature, readout order and shots¶
hqnn_forge.rydberg.feature_map ¶
hqnn_forge.rydberg.feature_map¶
The fixed feature map of docs/rydberg-model.md: a batch of classical
inputs goes in, a matrix of excitation probabilities comes out, and every
physical setting is recorded.
Units: ħ = 1, angular frequencies (Ω, Δ, γ, C6/r⁶) in rad/µs, lengths in
µm, times in µs. The inputs and the features are dimensionless.
The map, step by step
For one sample x ∈ ℝ^N and a register of N atoms::
x_i → Δ_i = Δ_max · σ(x_i) (PulseEncoding)
→ H = Ω/2 Σ_i X_i − Σ_i Δ_i n_i + Σ_{i<j} V_ij n_i n_j (rydberg_hamiltonian)
→ ρ(T), from every atom in |g⟩, with dephasing γ (evolve)
→ F_i = ⟨n_i⟩ = Tr[ρ(T) n_i] (readout)
- Which input feeds which atom. Column
iofXsets the detuning of atomi, the atom at rowiof the register's positions, which is tensor factor (PennyLane wire)iof the Hamiltonian and feature columniof the result. There is one input per atom, so the input width equals the number of atoms. - What carries data. Only the detunings. Ω,
T, the positions,C6and γ are fixed controls, the same for every sample. - Sign of Δ. Δ is the laser frequency minus the transition frequency
and enters
Has−Δ n; the encoding only producesΔ_i > 0.
Encoding
::
Δ_i(x_i) = Δ_max · σ(x_i), σ(x) = 1 / (1 + e^(−x))
σ is strictly increasing from 0 (x → −∞) to 1 (x → +∞), with
σ(0) = ½, so Δ_i lies in the open interval (0, Δ_max) for every
finite input. The bound is enforced by this squashing function and not by
clipping, which would send every input beyond the range to the same
detuning. There is no scale parameter: scaling the inputs is preprocessing.
The single-atom feature
Without interactions and dephasing each atom evolves alone under
H_1 = [[0, Ω/2], [Ω/2, −Δ]] in the basis (|g⟩, |r⟩). With
n = (1 − Z)/2::
H_1 = −(Δ/2) · 1 + ½ (Ω X + Δ Z), (Ω X + Δ Z)² = (Ω² + Δ²) · 1 = Ω_eff² · 1
so, with Ω_eff = √(Ω² + Δ²)::
e^(−i H_1 T) = e^(iΔT/2) [ cos(Ω_eff T/2) · 1 − i sin(Ω_eff T/2) · (Ω X + Δ Z)/Ω_eff ]
⟨r| e^(−i H_1 T) |g⟩ = −i e^(iΔT/2) · (Ω/Ω_eff) · sin(Ω_eff T/2)
⟨n⟩(T) = Ω²/Ω_eff² · sin²(Ω_eff T/2)
With the default pulse ΩT = π and s = Ω_eff/Ω = √(1 + (Δ/Ω)²) this is
f(s) = sin²(π s/2) / s². The default bound Δ_max = √3 Ω is the
detuning with s = 2, the first zero of f: f falls strictly from 1
at Δ = 0 to 0 at Δ_max, so the feature
::
F_i = f( √(1 + 3 σ(x_i)²) ) (interactions off, γ = 0, default pulse)
is strictly decreasing in x_i and depends on no other input. With
γ > 0 and no interactions the state is still a product of single-atom
states, and each feature solves, for its own Δ_i, with p = ρ_rr and
c = ρ_gr::
dp/dt = Ω · Im c, dc/dt = −(γ/2 + iΔ) c − i (Ω/2)(2p − 1)
Readout and its order
The features are ⟨n_0⟩, …, ⟨n_{N−1}⟩, in the order of the atoms. With
correlations=True the pair correlations ⟨n_i n_j⟩, i < j, are
appended in the order (0,1), (0,2), …, (0,N−1), (1,2), …, (N−2,N−1),
which gives N + N(N−1)/2 features. Both are sums over the diagonal of ρ
(readout).
Shots
By default the features are exact expectation values. With shots=S they
are estimated as a device would: S bitstrings b^(1), …, b^(S) are
drawn for each sample, each with probability ρ_kk for the basis state
k (bit b_i = 1 when atom i is found in |r⟩), and::
F̂_i = (1/S) Σ_s b_i^(s), F̂_ij = (1/S) Σ_s b_i^(s) b_j^(s)
b_i is 1 with probability Σ_k b_i(k) ρ_kk = ⟨n_i⟩ and 0 otherwise, so
F̂_i is the mean of S independent Bernoulli variables: unbiased, with
variance F_i (1 − F_i)/S, i.e. standard error
√(F_i (1 − F_i)/S) ≤ 1/(2√S). The same holds for b_i b_j with
⟨n_i n_j⟩. All features of a sample are computed from the same S
bitstrings, so their errors are correlated, as on a device.
Not an encoding layer
RydbergFeatureMap is a preprocessing step, like
PCANormalizer, followed by any classifier.
It is not an nn.Module, has no PennyLane circuit (qlayer) and no
trainable weights, so the encoding-layer contract
(hqnn_forge.encoding.EncodingLayer) does not apply to it, and neither do
the gate-based noise options (noise_level, apply_depolarizing_noise):
its noise is the dephasing rate γ. Nothing is differentiated; the results
carry no gradient.
Sign conventions, basis ordering and matrix elements¶
hqnn_forge.rydberg.hamiltonian ¶
hqnn_forge.rydberg.hamiltonian¶
The Hamiltonian of docs/rydberg-model.md as a dense matrix.
Each atom is a two-level system of its ground state |g⟩ = |0⟩ and one
Rydberg state |r⟩ = |1⟩. In the frame rotating at the laser frequency,
with the rotating-wave approximation and ħ = 1::
H = Σ_i Ω_i/2 · X_i − Σ_i Δ_i n_i + Σ_{i<j} V_ij n_i n_j
n_i = |r⟩⟨r|_i = (1 − Z_i)/2 V_ij = C6 / |r_i − r_j|⁶
Units: angular frequencies (Ω, Δ, V and H itself) in rad/µs, lengths in
µm, C6 in rad/µs · µm⁶.
Sign conventions
- Ω is the Rabi frequency, not half of it. The coefficient of
XisΩ/2: a resonant atom has⟨n⟩(t) = sin²(Ωt/2), and its two levels are split by Ω (eigenvalues±Ω/2). - Δ is the laser frequency minus the atomic transition frequency. The
atom's energy
ω₀ nminus the frame'sω_L nis−Δ n: a positive Δ lowers|r⟩. - A positive C6 is repulsive. It raises a doubly excited pair by
V_ij, so two atoms at distancerhave⟨rr|H|rr⟩ = −Δ_0 − Δ_1 + C6/r⁶.
For one atom, in the basis (|g⟩, |r⟩), H = [[0, Ω/2], [Ω/2, −Δ]].
Basis ordering
Atom i (counted from 0, the row of the register) is tensor factor i
from the left and PennyLane wire i. The basis state
|b_0 b_1 … b_{N−1}⟩, with b_i = 1 for |r⟩, has the index::
k = Σ_i b_i 2^(N−1−i) ⇔ b_i(k) = (k >> (N−1−i)) & 1
so atom 0 is the most significant bit. For two atoms the order is
|gg⟩, |gr⟩, |rg⟩, |rr⟩ and n_0 = diag(0, 0, 1, 1). This is the matrix
qml.matrix(op, wire_order=range(N)) returns for the same operator.
Matrix elements
In that basis n_i is diagonal with entry b_i(k), and X_i flips bit
N−1−i of the index. Term by term:
- detuning:
−Σ_i Δ_i b_i(k)on the diagonal; - interaction:
+Σ_{i<j} V_ij b_i(k) b_j(k)on the diagonal; - drive:
Ω_i/2at(k, k XOR 2^(N−1−i))for everykandi. No two of these coincide (differentiflip different bits) and none is on the diagonal, so every row holds exactlyNoff-diagonal entries.
The matrix is real and symmetric. It is returned as complex128 because
what consumes it (exp(−iHt), a density matrix) is complex, and because a
drive phase makes it complex (see the Notes of rydberg_hamiltonian).
References
- Bernien et al. (2017) "Probing many-body dynamics on a 51-atom quantum simulator", Nature 551, 579–584.
- Wurtz et al. (2023) "Aquila: QuEra's 256-qubit neutral-atom quantum computer", arXiv:2306.11727.
The master equation, the dissipator and the splitting¶
hqnn_forge.rydberg.lindblad ¶
hqnn_forge.rydberg.lindblad¶
Time evolution of a small Rydberg array under its Hamiltonian and Markovian
dephasing, and the readout of the resulting state. This is the solver of
docs/rydberg-model.md.
Units: ħ = 1, the Hamiltonian and γ in rad/µs, times in µs, so H t and
γ t are dimensionless. Basis ordering is that of
hqnn_forge.rydberg.hamiltonian: atom i is tensor factor i from
the left, and bit b_i(k) = (k >> (N−1−i)) & 1 of the index k of a
basis state is 1 when atom i is in |r⟩ (atom 0 the most significant
bit). Index 0 is |g…g⟩.
Master equation
::
dρ/dt = 𝓛_H ρ + 𝓛_D ρ
𝓛_H ρ = −i [H, ρ]
𝓛_D ρ = Σ_i ( L_i ρ L_i† − ½ { L_i† L_i , ρ } ), L_i = √γ n_i
with one collapse operator per atom, n_i = |r⟩⟨r|_i, and
ρ(0) = |g…g⟩⟨g…g|. There is no global variant of the operator and no
decay term.
The dissipator, elementwise
n_i is diagonal in the computational basis with entry a_i on the
state a, and n_i² = n_i, so L_i† L_i = γ n_i. On the matrix
element ρ_ab between basis states a and b::
(n_i ρ n_i)_ab = a_i b_i ρ_ab
−½ ({n_i, ρ})_ab = −½ (a_i + b_i) ρ_ab
sum = −½ (a_i + b_i − 2 a_i b_i) ρ_ab = −½ (a_i − b_i)² ρ_ab
(the last step uses a_i² = a_i for a bit). (a_i − b_i)² is 1 where
the two states differ on atom i and 0 otherwise, so the sum over the
atoms counts the differing atoms, the Hamming distance d_H(a, b)::
(𝓛_D ρ)_ab = −(γ/2) · d_H(a, b) · ρ_ab
𝓛_D is therefore diagonal in the elements of ρ, and its exponential is an
elementwise multiplication, exactly, for any τ::
(e^(𝓛_D τ) ρ)_ab = exp(−γ τ d_H(a, b) / 2) · ρ_ab
- What γ means. The coherence of a single atom (
d_H = 1) decays at rateγ/2, i.e.T₂ = 2/γwithout a drive. A coherence between states that differ ondatoms decays atd γ/2. Populations (d_H = 0) are untouched.√γ n_iand√(γ/4) Z_igive the same equation; a collapse operator√κ Z_icorresponds toγ = 4κ. - No ordering enters.
d_His symmetric in the atoms, so the factor does not depend on which bit belongs to which atom.
The unitary part
H is constant in time. With its eigendecomposition H = V E V†
(E real and diagonal, V unitary)::
U(τ) = e^(−iHτ) = V e^(−iEτ) V†, e^(𝓛_H τ) ρ = U(τ) ρ U(τ)†
The sign is that of the Schrödinger equation i dψ/dt = Hψ. For one
resonant atom it gives ψ(t) = (cos(Ωt/2), −i sin(Ωt/2)) in the basis
(|g⟩, |r⟩), so ⟨n⟩(t) = sin²(Ωt/2) and ρ_gr = +(i/2) sin(Ωt).
Without dephasing the state stays pure, ρ(t) = ψ(t) ψ(t)† with::
ψ(t) = U(t) e_0, ψ_k(t) = Σ_m V_km e^(−i E_m t) conj(V_0m)
e_0 being the basis vector of |g…g⟩. This is exact at any t.
Splitting
With dephasing the two parts do not commute, and the evolution over t is
taken in n steps of dt = t/n by the symmetric (Strang) splitting::
S(dt) = e^(𝓛_D dt/2) · e^(𝓛_H dt) · e^(𝓛_D dt/2), ρ(t) ≈ S(dt)^n ρ(0)
read from the right: damp for dt/2, rotate for dt, damp for dt/2.
Both factors are exact, so the only error is that of the splitting.
- Order.
S(dt)is symmetric,S(dt) S(−dt) = 1, so its logarithm holds only odd powers ofdt:S(dt) = exp((𝓛_H + 𝓛_D) dt + O(dt³)). The error of one step isO(dt³)and the global error aftern = t/dtsteps is of second order,O(t · dt²): halvingdtdivides it by 4. - Its size. The
dt³term is built from the double commutators[𝓛_D, [𝓛_D, 𝓛_H]]and[𝓛_H, [𝓛_H, 𝓛_D]], of orderγ² ‖H‖andγ ‖H‖².dttherefore has to resolve both1/γand the largest level spacing ofH, which in the blockade is the interactionV, not Ω. It vanishes whenHis diagonal (no drive):𝓛_Hthen also acts elementwise, the two parts commute and one step is exact. - Every step is a physical map at any
dt.U ρ U†is unitary conjugation. The damping factorexp(−γ τ d_H(a, b)/2)is the matrix of a product of single-atom dephasing channels: it has a unit diagonal and is positive semidefinite, so the elementwise product keeps trace, Hermiticity and non-negative eigenvalues (Schur product theorem). A step that is too large gives an inaccurate state, never an unphysical one. - The maximally mixed state is a fixed point of every step, as it is of
the equation: the factor is 1 on the diagonal and
U 1 U† = 1. - Adjacent half steps are merged. The damping factors of consecutive
steps multiply,
e^(𝓛_D dt/2) e^(𝓛_D dt/2) = e^(𝓛_D dt), exactly. The loop applies one half step at the start, a full step between rotations and one half step at the end, which isS(dt)^nterm by term.
Readout
⟨n_i⟩ = Tr[ρ n_i] = Σ_k b_i(k) ρ_kk and
⟨n_i n_j⟩ = Σ_k b_i(k) b_j(k) ρ_kk: both are sums over the diagonal of ρ,
the outcome probabilities of measuring every atom in the (|g⟩, |r⟩) basis.
The register's two uses¶
hqnn_forge.rydberg.register ¶
hqnn_forge.rydberg.register¶
The positions of a neutral-atom array and what follows from them alone: the pairwise van der Waals couplings and the blockade radius.
Units are those of docs/rydberg-model.md: ħ = 1, lengths in µm, angular
frequencies in rad/µs, so C6 is in rad/µs · µm⁶.
One register serves both ways of using the array:
- the analog model (
rydberg_hamiltonian) takes the continuous couplingsV_ij = C6 / |r_i − r_j|⁶fromAtomRegister.interaction_matrix; - a gate-based entangler connects the pairs closer than the blockade radius
R_b = (C6/Ω)^(1/6)(AtomRegister.blockade_radius). SinceV_ij > Ωexactly when|r_i − r_j| < R_b, that unit-disk graph is the set of pairs withV_ij > Ω: the two views are the same geometry. A pair at exactlyR_bhasV_ij = Ωand lies on the boundary of the graph; which side it falls on is for the entangler (#418) to fix, and the register does not decide it.
A register holds positions and nothing else. The atomic species (C6) and
the drive (Ω) are arguments of the methods that need them.
References
- Saffman, Walker & Mølmer (2010) "Quantum information with Rydberg atoms", Reviews of Modern Physics 82, 2313.
- Bernien et al. (2017) "Probing many-body dynamics on a 51-atom quantum simulator", Nature 551, 579–584.