pqls 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pqls/__init__.py +61 -0
- pqls/atoms.py +109 -0
- pqls/lindblad.py +421 -0
- pqls/types.py +215 -0
- pqls/units.py +159 -0
- pqls-0.1.0.dist-info/METADATA +209 -0
- pqls-0.1.0.dist-info/RECORD +10 -0
- pqls-0.1.0.dist-info/WHEEL +5 -0
- pqls-0.1.0.dist-info/licenses/LICENSE +674 -0
- pqls-0.1.0.dist-info/top_level.txt +1 -0
pqls/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PQLS: High-performance JAX-accelerated Lindblad steady-state solvers for quantum atomic systems.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .atoms import calculate_rabi_frequency, get_atom, get_ground_state
|
|
6
|
+
from .lindblad import (
|
|
7
|
+
solve_custom_network,
|
|
8
|
+
solve_ladder_system,
|
|
9
|
+
solve_quantum_ladder,
|
|
10
|
+
solve_steady_state,
|
|
11
|
+
)
|
|
12
|
+
from .types import (
|
|
13
|
+
ArrayLike,
|
|
14
|
+
ArrayOrScalar,
|
|
15
|
+
AtomicLevel,
|
|
16
|
+
AtomName,
|
|
17
|
+
Driver,
|
|
18
|
+
QuantumLadder,
|
|
19
|
+
QuantumState,
|
|
20
|
+
)
|
|
21
|
+
from .units import (
|
|
22
|
+
GHz_to_eV,
|
|
23
|
+
au_to_coulomb_metre,
|
|
24
|
+
coulomb_metre_to_au,
|
|
25
|
+
electric_field_to_rabi_freq,
|
|
26
|
+
eV_to_GHz,
|
|
27
|
+
hz_to_rad_s,
|
|
28
|
+
rabi_freq_to_electric_field,
|
|
29
|
+
rad_s_to_hz,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Core Solvers
|
|
36
|
+
"solve_steady_state",
|
|
37
|
+
"solve_custom_network",
|
|
38
|
+
"solve_ladder_system",
|
|
39
|
+
"solve_quantum_ladder",
|
|
40
|
+
# Data Models & Types
|
|
41
|
+
"ArrayLike",
|
|
42
|
+
"ArrayOrScalar",
|
|
43
|
+
"AtomicLevel",
|
|
44
|
+
"QuantumState",
|
|
45
|
+
"QuantumLadder",
|
|
46
|
+
"Driver",
|
|
47
|
+
"AtomName",
|
|
48
|
+
# Atom Helpers
|
|
49
|
+
"get_atom",
|
|
50
|
+
"get_ground_state",
|
|
51
|
+
"calculate_rabi_frequency",
|
|
52
|
+
# Units & Conversions
|
|
53
|
+
"au_to_coulomb_metre",
|
|
54
|
+
"coulomb_metre_to_au",
|
|
55
|
+
"eV_to_GHz",
|
|
56
|
+
"GHz_to_eV",
|
|
57
|
+
"electric_field_to_rabi_freq",
|
|
58
|
+
"rabi_freq_to_electric_field",
|
|
59
|
+
"hz_to_rad_s",
|
|
60
|
+
"rad_s_to_hz",
|
|
61
|
+
]
|
pqls/atoms.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atoms management and caching for ARC physics parameters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
from arc import Cesium, Rubidium85, Rubidium87
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
ArrayOrScalar,
|
|
11
|
+
AtomicLevel,
|
|
12
|
+
AtomName,
|
|
13
|
+
AtomType,
|
|
14
|
+
QuantumState,
|
|
15
|
+
)
|
|
16
|
+
from .units import au_to_coulomb_metre, electric_field_to_rabi_freq
|
|
17
|
+
|
|
18
|
+
_ATOM_MAP = {
|
|
19
|
+
"rb85": Rubidium85,
|
|
20
|
+
"rb87": Rubidium87,
|
|
21
|
+
"cs": Cesium,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@lru_cache(maxsize=3)
|
|
26
|
+
def get_atom(atom_name: AtomName) -> AtomType:
|
|
27
|
+
"""Returns a cached instance of the requested atom from the ARC package.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
atom_name : AtomName
|
|
32
|
+
The common name of the atom (e.g., 'Rb85', 'Cs').
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
AtomType
|
|
37
|
+
The instantiated atom object from the ARC library.
|
|
38
|
+
|
|
39
|
+
Raises
|
|
40
|
+
------
|
|
41
|
+
ValueError
|
|
42
|
+
If the provided atom_name is not in the supported list.
|
|
43
|
+
"""
|
|
44
|
+
atom_cls = _ATOM_MAP.get(atom_name.lower())
|
|
45
|
+
if atom_cls is None:
|
|
46
|
+
raise ValueError(f"Atom {atom_name} not supported.")
|
|
47
|
+
return atom_cls()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_ground_state(atom_name: AtomName) -> AtomicLevel:
|
|
51
|
+
"""Returns the ground state of the specified atom.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
atom_name : AtomName
|
|
56
|
+
The common name of the atom.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
AtomicLevel
|
|
61
|
+
An object representing the ``n``, ``l``, and ``j`` quantum numbers
|
|
62
|
+
of the ground state.
|
|
63
|
+
"""
|
|
64
|
+
arc_atom = get_atom(atom_name)
|
|
65
|
+
return AtomicLevel(n=arc_atom.groundStateN, l=0, j=0.5)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def calculate_rabi_frequency[T: ArrayOrScalar](
|
|
69
|
+
atom: AtomName,
|
|
70
|
+
lower: QuantumState,
|
|
71
|
+
upper: QuantumState,
|
|
72
|
+
e_field: T,
|
|
73
|
+
q: int | None = None,
|
|
74
|
+
) -> T:
|
|
75
|
+
r"""Calculates angular Rabi frequency (``rad/s``) between two atomic states for a given electric field.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
atom : str, AtomName, or ARC Atom object
|
|
80
|
+
The target alkali atom.
|
|
81
|
+
lower : QuantumState
|
|
82
|
+
Initial / lower energy state.
|
|
83
|
+
upper : QuantumState
|
|
84
|
+
Target / upper energy state.
|
|
85
|
+
e_field : ArrayOrScalar
|
|
86
|
+
Electric field amplitude in ``V/m`` (scalar or array).
|
|
87
|
+
q : int, optional
|
|
88
|
+
Laser polarization transition (``-1`` for :math:`\sigma^-`, ``0`` for :math:`\pi`, ``+1`` for :math:`\sigma^+`).
|
|
89
|
+
If None, inferred automatically from :math:`\Delta m_j`.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
ArrayOrScalar
|
|
94
|
+
Angular Rabi frequency :math:`\Omega` in ``rad/s`` matching the shape of `e_field`.
|
|
95
|
+
"""
|
|
96
|
+
arc_atom = get_atom(atom)
|
|
97
|
+
|
|
98
|
+
if q is None:
|
|
99
|
+
q = round(upper.mj - lower.mj)
|
|
100
|
+
|
|
101
|
+
dipole_au = abs(
|
|
102
|
+
arc_atom.getDipoleMatrixElement(
|
|
103
|
+
*lower.to_arc_args(),
|
|
104
|
+
*upper.to_arc_args(),
|
|
105
|
+
q=q,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
dipole_cm = au_to_coulomb_metre(dipole_au)
|
|
109
|
+
return electric_field_to_rabi_freq(e_field, dipole_cm)
|
pqls/lindblad.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance JAX-accelerated solvers for the Lindblad master equation steady-state.
|
|
3
|
+
Provides both raw matrix-level APIs and high-level ladder system functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .atoms import get_atom
|
|
13
|
+
from .types import ArrayOrScalar, Driver, QuantumLadder
|
|
14
|
+
from .units import au_to_coulomb_metre, electric_field_to_rabi_freq, hz_to_rad_s
|
|
15
|
+
|
|
16
|
+
jax.config.update("jax_enable_x64", True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- Private JAX Kernels ---
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_hamiltonian(
|
|
23
|
+
detunings: jnp.ndarray,
|
|
24
|
+
rabis: jnp.ndarray,
|
|
25
|
+
from_idx: jnp.ndarray,
|
|
26
|
+
to_idx: jnp.ndarray,
|
|
27
|
+
n_levels: int,
|
|
28
|
+
) -> jnp.ndarray:
|
|
29
|
+
"""Constructs the N x N Hamiltonian matrix."""
|
|
30
|
+
H = jnp.zeros((n_levels, n_levels), dtype=jnp.complex128)
|
|
31
|
+
|
|
32
|
+
H = H.at[jnp.diag_indices(n_levels)].set(detunings)
|
|
33
|
+
|
|
34
|
+
H = H.at[(from_idx, to_idx)].set(rabis / 2.0)
|
|
35
|
+
H = H.at[(to_idx, from_idx)].set(jnp.conj(rabis) / 2.0)
|
|
36
|
+
|
|
37
|
+
return H
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_collapse_operators(
|
|
41
|
+
decay_from: jnp.ndarray, decay_to: jnp.ndarray, gammas: jnp.ndarray, n_levels: int
|
|
42
|
+
) -> jnp.ndarray:
|
|
43
|
+
"""Constructs a stack of K sparse decay/jump operators."""
|
|
44
|
+
K = gammas.shape[0]
|
|
45
|
+
C_ops = jnp.zeros((K, n_levels, n_levels), dtype=jnp.complex128)
|
|
46
|
+
|
|
47
|
+
# C_k = sqrt(gamma) * |to><from|
|
|
48
|
+
sqrt_g = jnp.sqrt(jnp.maximum(gammas, 0.0))
|
|
49
|
+
C_ops = C_ops.at[jnp.arange(K), decay_to, decay_from].set(sqrt_g)
|
|
50
|
+
|
|
51
|
+
return C_ops
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_output(
|
|
55
|
+
rho_ss: jnp.ndarray,
|
|
56
|
+
transitions: list[tuple[int, int]],
|
|
57
|
+
coherence_index: int | None,
|
|
58
|
+
is_scalar: bool,
|
|
59
|
+
) -> ArrayOrScalar:
|
|
60
|
+
"""Extracts either target coherence or full density matrix and formats dimensions."""
|
|
61
|
+
if coherence_index is not None:
|
|
62
|
+
try:
|
|
63
|
+
from_s, to_s = transitions[coherence_index]
|
|
64
|
+
except IndexError as err:
|
|
65
|
+
raise IndexError(
|
|
66
|
+
f"coherence_index {coherence_index} out of range for {len(transitions)} transitions."
|
|
67
|
+
) from err
|
|
68
|
+
res = np.asarray(jnp.imag(rho_ss[:, to_s, from_s]))
|
|
69
|
+
return float(res[0]) if is_scalar else res
|
|
70
|
+
|
|
71
|
+
res = np.asarray(rho_ss)
|
|
72
|
+
return res[0] if is_scalar else res
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _solve_steady_state_single(H: jnp.ndarray, C_ops: jnp.ndarray) -> jnp.ndarray:
|
|
76
|
+
r"""Solves the steady-state Lindblad master equation for an unbatched system.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
H : jnp.ndarray
|
|
81
|
+
Hamiltonian matrix of shape ``(N, N)``.
|
|
82
|
+
C_ops : jnp.ndarray
|
|
83
|
+
Stack of collapse/jump operators of shape ``(K, N, N)``.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
jnp.ndarray
|
|
88
|
+
Steady-state density matrix :math:`\rho_{ss}` of shape ``(N, N)`` with unit trace.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
N = H.shape[0]
|
|
92
|
+
Ident = jnp.eye(N, dtype=jnp.complex128)
|
|
93
|
+
|
|
94
|
+
L = -1j * (jnp.kron(H, Ident) - jnp.kron(Ident, H.T))
|
|
95
|
+
|
|
96
|
+
def make_decay_superop(C):
|
|
97
|
+
CTC = C.T.conj() @ C
|
|
98
|
+
return (
|
|
99
|
+
jnp.kron(C, C.conj())
|
|
100
|
+
- 0.5 * jnp.kron(CTC, Ident)
|
|
101
|
+
- 0.5 * jnp.kron(Ident, CTC.T)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if C_ops.shape[0] > 0:
|
|
105
|
+
L_decay = jnp.sum(jax.vmap(make_decay_superop)(C_ops), axis=0)
|
|
106
|
+
L = L + L_decay
|
|
107
|
+
|
|
108
|
+
L = L.at[0, :].set(0.0)
|
|
109
|
+
diag_indices = jnp.arange(0, N**2, N + 1)
|
|
110
|
+
L = L.at[0, diag_indices].set(1.0)
|
|
111
|
+
|
|
112
|
+
b = jnp.zeros(N**2, dtype=jnp.complex128).at[0].set(1.0)
|
|
113
|
+
rho_vec = jnp.linalg.solve(L, b)
|
|
114
|
+
|
|
115
|
+
return rho_vec.reshape((N, N))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _single_point_network(
|
|
119
|
+
d_arr, r_arr, from_idx, to_idx, C_ops, n_levels
|
|
120
|
+
) -> jnp.ndarray:
|
|
121
|
+
"""Maps parameters to a Hamiltonian and solves the steady state."""
|
|
122
|
+
H = _build_hamiltonian(d_arr, r_arr, from_idx, to_idx, n_levels)
|
|
123
|
+
return _solve_steady_state_single(H, C_ops)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _single_point_ladder(det_vec, rab_vec, gammas_vec, n_levels) -> jnp.ndarray:
|
|
127
|
+
"""N-level ladder point solver composing shared JAX primitives."""
|
|
128
|
+
cum_dets = jnp.concatenate([jnp.zeros(1, dtype=jnp.float64), jnp.cumsum(det_vec)])
|
|
129
|
+
diagonals = -1.0 * cum_dets
|
|
130
|
+
|
|
131
|
+
from_idx = jnp.arange(n_levels - 1)
|
|
132
|
+
to_idx = jnp.arange(1, n_levels)
|
|
133
|
+
|
|
134
|
+
H = _build_hamiltonian(diagonals, rab_vec, from_idx, to_idx, n_levels)
|
|
135
|
+
C_ops = _build_collapse_operators(to_idx, from_idx, gammas_vec[1:], n_levels)
|
|
136
|
+
|
|
137
|
+
return _solve_steady_state_single(H, C_ops)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_compiled_solve_single = jax.jit(_solve_steady_state_single)
|
|
141
|
+
_compiled_solve_batch = jax.jit(jax.vmap(_solve_steady_state_single, in_axes=(0, None)))
|
|
142
|
+
_compiled_batched_network = jax.jit(
|
|
143
|
+
jax.vmap(_single_point_network, in_axes=(0, 0, None, None, None, None)),
|
|
144
|
+
static_argnums=(5,), # n_levels must be a static integer for matrix allocation
|
|
145
|
+
)
|
|
146
|
+
_compiled_batched_ladder = jax.jit(
|
|
147
|
+
jax.vmap(_single_point_ladder, in_axes=(0, 0, None, None)), static_argnums=(3,)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# --- Public APIs ---
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def solve_steady_state(hamiltonian: np.ndarray, c_ops: np.ndarray) -> np.ndarray:
|
|
154
|
+
r"""Solves for the steady state given the Hamiltonian and collapse operators.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
hamiltonian : np.ndarray
|
|
159
|
+
Hamiltonian matrix of shape ``(N, N)`` or batched ``(B, N, N)``.
|
|
160
|
+
c_ops : np.ndarray
|
|
161
|
+
Stack of collapse operators of shape ``(K, N, N)``. These are shared across the batch.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
np.ndarray
|
|
166
|
+
Steady-state density matrix :math:`\rho_{ss}` of shape ``(N, N)`` or ``(B, N, N)``.
|
|
167
|
+
"""
|
|
168
|
+
if hamiltonian.ndim == 2:
|
|
169
|
+
# Single unbatched matrix solve
|
|
170
|
+
res = _compiled_solve_single(jnp.asarray(hamiltonian), jnp.asarray(c_ops))
|
|
171
|
+
else:
|
|
172
|
+
# Batched across axis 0
|
|
173
|
+
res = _compiled_solve_batch(jnp.asarray(hamiltonian), jnp.asarray(c_ops))
|
|
174
|
+
|
|
175
|
+
return np.asarray(res)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def solve_custom_network(
|
|
179
|
+
transitions: list[tuple[int, int]],
|
|
180
|
+
decays: list[tuple[int, int, float]],
|
|
181
|
+
detunings: list[ArrayOrScalar],
|
|
182
|
+
rabis: list[ArrayOrScalar],
|
|
183
|
+
n_levels: int,
|
|
184
|
+
coherence_index: int | None = None,
|
|
185
|
+
) -> ArrayOrScalar:
|
|
186
|
+
r"""Solves the steady state for an arbitrary transition network.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
transitions : list of tuple of (int, int)
|
|
191
|
+
List of coupled transitions represented as ``(from_state, to_state)`` indices.
|
|
192
|
+
decays : list of tuple of (int, int, float)
|
|
193
|
+
List of decay channels represented as ``(from_state, to_state, gamma)``.
|
|
194
|
+
detunings : list of ArrayOrScalar
|
|
195
|
+
List of detunings for each transition in ``rad/s``.
|
|
196
|
+
Each scalar will be held constant throughout the batch.
|
|
197
|
+
rabis : list of ArrayOrScalar
|
|
198
|
+
List of angular Rabi frequencies for each transition in ``rad/s``.
|
|
199
|
+
Each scalar will be held constant throughout the batch.
|
|
200
|
+
n_levels : int
|
|
201
|
+
Total number of levels :math:`N` in the system.
|
|
202
|
+
coherence_index : int, optional
|
|
203
|
+
If specified (e.g., ``0``, ``1``), returns :math:`\text{Im}(\rho)` for ``transitions[coherence_index]``.
|
|
204
|
+
If ``None``, returns the full density matrix batch of shape ``(Batch, N, N)``.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
ArrayOrScalar
|
|
209
|
+
Extracted coherence or full steady-state density matrix.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
num_dets = len(detunings)
|
|
213
|
+
all_bcast = np.broadcast_arrays(*detunings, *rabis)
|
|
214
|
+
is_scalar = all_bcast[0].ndim == 0
|
|
215
|
+
|
|
216
|
+
if is_scalar:
|
|
217
|
+
all_bcast = [np.expand_dims(x, 0) for x in all_bcast]
|
|
218
|
+
|
|
219
|
+
dets_stacked = np.stack(all_bcast[:num_dets], axis=-1)
|
|
220
|
+
rabs_stacked = np.stack(all_bcast[num_dets:], axis=-1)
|
|
221
|
+
|
|
222
|
+
from_idx = jnp.array([t[0] for t in transitions])
|
|
223
|
+
to_idx = jnp.array([t[1] for t in transitions])
|
|
224
|
+
|
|
225
|
+
decay_from = jnp.array([d[0] for d in decays], dtype=jnp.int32)
|
|
226
|
+
decay_to = jnp.array([d[1] for d in decays], dtype=jnp.int32)
|
|
227
|
+
gamma_vals = jnp.array([d[2] for d in decays], dtype=jnp.float64)
|
|
228
|
+
|
|
229
|
+
# Pre-build collapse operators (these do not change during parameter sweeps)
|
|
230
|
+
C_ops = _build_collapse_operators(decay_from, decay_to, gamma_vals, n_levels)
|
|
231
|
+
|
|
232
|
+
rho_ss = _compiled_batched_network(
|
|
233
|
+
jnp.asarray(dets_stacked),
|
|
234
|
+
jnp.asarray(rabs_stacked),
|
|
235
|
+
from_idx,
|
|
236
|
+
to_idx,
|
|
237
|
+
C_ops,
|
|
238
|
+
n_levels,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return _extract_output(rho_ss, transitions, coherence_index, is_scalar)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def solve_ladder_system(
|
|
245
|
+
detunings: list[ArrayOrScalar],
|
|
246
|
+
rabis: list[ArrayOrScalar],
|
|
247
|
+
gammas: list[float],
|
|
248
|
+
coherence_index: int | None = 0,
|
|
249
|
+
) -> ArrayOrScalar:
|
|
250
|
+
r"""Solves the steady state for an :math:`N`-level ladder system.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
detunings : list of ArrayOrScalar
|
|
255
|
+
List of detunings for each adjacent transition in ``rad/s``.
|
|
256
|
+
Each scalar will be held constant throughout the batch.
|
|
257
|
+
rabis : list of ArrayOrScalar
|
|
258
|
+
List of angular Rabi frequencies for each adjacent transition in ``rad/s``.
|
|
259
|
+
Each scalar will be held constant throughout the batch.
|
|
260
|
+
gammas : list of float
|
|
261
|
+
List of decay rates :math:`\Gamma` for each level in ``rad/s``. Note that ``gammas[0]`` is typically ``0.0``.
|
|
262
|
+
coherence_index : int, optional
|
|
263
|
+
Index of the transition to extract the coherence :math:`\text{Im}(\rho)` from. Defaults to ``0``.
|
|
264
|
+
If ``None``, returns the full density matrix.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
ArrayOrScalar
|
|
269
|
+
Extracted coherence or full steady-state density matrix.
|
|
270
|
+
"""
|
|
271
|
+
N = len(rabis) + 1
|
|
272
|
+
num_dets = len(detunings)
|
|
273
|
+
all_bcast = np.broadcast_arrays(*detunings, *rabis)
|
|
274
|
+
is_scalar = all_bcast[0].ndim == 0
|
|
275
|
+
|
|
276
|
+
if is_scalar:
|
|
277
|
+
all_bcast = [np.expand_dims(x, 0) for x in all_bcast]
|
|
278
|
+
|
|
279
|
+
dets_stacked = jnp.asarray(
|
|
280
|
+
np.stack(all_bcast[:num_dets], axis=-1), dtype=jnp.float64
|
|
281
|
+
)
|
|
282
|
+
rabs_stacked = jnp.asarray(
|
|
283
|
+
np.stack(all_bcast[num_dets:], axis=-1), dtype=jnp.complex128
|
|
284
|
+
)
|
|
285
|
+
gammas_vec = jnp.asarray(gammas, dtype=jnp.float64)
|
|
286
|
+
|
|
287
|
+
rho_ss = _compiled_batched_ladder(dets_stacked, rabs_stacked, gammas_vec, N)
|
|
288
|
+
transitions = [(i, i + 1) for i in range(N - 1)]
|
|
289
|
+
|
|
290
|
+
return _extract_output(rho_ss, transitions, coherence_index, is_scalar)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def solve_quantum_ladder(
|
|
294
|
+
ladder: QuantumLadder,
|
|
295
|
+
drivers: Sequence[Driver],
|
|
296
|
+
detunings: Sequence[ArrayOrScalar | None] | None = None,
|
|
297
|
+
frequencies: Sequence[ArrayOrScalar | None] | None = None,
|
|
298
|
+
amplitudes: Sequence[ArrayOrScalar | None] | None = None,
|
|
299
|
+
temperature: float = 300.0,
|
|
300
|
+
max_n: int = 80,
|
|
301
|
+
coherence_index: int | None = 0,
|
|
302
|
+
) -> ArrayOrScalar:
|
|
303
|
+
r"""Solves the steady-state for a physical atomic ladder system using ARC and JAX.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
ladder : QuantumLadder
|
|
308
|
+
The quantum ladder system defining the atomic levels and transitions.
|
|
309
|
+
drivers : Sequence of Driver
|
|
310
|
+
Sequence of electromagnetic drivers for each transition.
|
|
311
|
+
detunings : Sequence of ArrayOrScalar or None, optional
|
|
312
|
+
Overriding detunings in ``Hz``. If provided, overrides driver settings.
|
|
313
|
+
Each scalar will be held constant throughout the batch.
|
|
314
|
+
frequencies : Sequence of ArrayOrScalar or None, optional
|
|
315
|
+
Overriding drive frequencies :math:`f` in ``Hz``. If provided, overrides driver settings.
|
|
316
|
+
Each scalar will be held constant throughout the batch.
|
|
317
|
+
amplitudes : Sequence of ArrayOrScalar or None, optional
|
|
318
|
+
Overriding electric field amplitudes :math:`E` in ``V/m``. If provided, overrides driver settings.
|
|
319
|
+
Each scalar will be held constant throughout the batch.
|
|
320
|
+
temperature : float, default=300.0
|
|
321
|
+
Environmental temperature in Kelvin for blackbody decay calculations.
|
|
322
|
+
max_n : int, default=80
|
|
323
|
+
Maximum principal quantum number :math:`n` to consider for decay pathways in ARC.
|
|
324
|
+
coherence_index : int, optional
|
|
325
|
+
Index of the transition to extract the coherence :math:`\text{Im}(\rho)` from. Defaults to ``0``.
|
|
326
|
+
If ``None``, returns the full density matrix.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
ArrayOrScalar
|
|
331
|
+
Extracted coherence or full steady-state density matrix.
|
|
332
|
+
|
|
333
|
+
Raises
|
|
334
|
+
------
|
|
335
|
+
ValueError
|
|
336
|
+
If the number of drivers does not match the number of transitions in the ladder.
|
|
337
|
+
"""
|
|
338
|
+
n_transitions = ladder.num_transitions
|
|
339
|
+
n_levels = ladder.num_levels
|
|
340
|
+
|
|
341
|
+
if len(drivers) != n_transitions:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"Expected {n_transitions} drivers for a {n_levels}-level ladder, got {len(drivers)}."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
atom = get_atom(ladder.atom)
|
|
347
|
+
|
|
348
|
+
# Compute gammas (decay rates in rad/s) from ARC
|
|
349
|
+
gammas = [0.0] * n_levels
|
|
350
|
+
for i in range(1, n_levels):
|
|
351
|
+
lifetime_s = atom.getStateLifetime(
|
|
352
|
+
*ladder.states[i].level.to_arc_args(),
|
|
353
|
+
temperature=temperature,
|
|
354
|
+
includeLevelsUpTo=max_n,
|
|
355
|
+
)
|
|
356
|
+
gammas[i] = 1.0 / lifetime_s if lifetime_s > 0 else 0.0
|
|
357
|
+
|
|
358
|
+
# Resolve Frequencies, Detunings, and Rabi Frequencies
|
|
359
|
+
detunings_list: list[ArrayOrScalar] = []
|
|
360
|
+
rabis_list: list[ArrayOrScalar] = []
|
|
361
|
+
|
|
362
|
+
for i in range(n_transitions):
|
|
363
|
+
lower = ladder.states[i]
|
|
364
|
+
upper = ladder.states[i + 1]
|
|
365
|
+
driver = drivers[i]
|
|
366
|
+
|
|
367
|
+
if detunings is not None and i < len(detunings) and detunings[i] is not None:
|
|
368
|
+
detuning_rad_s = hz_to_rad_s(detunings[i])
|
|
369
|
+
|
|
370
|
+
elif (
|
|
371
|
+
frequencies is not None
|
|
372
|
+
and i < len(frequencies)
|
|
373
|
+
and frequencies[i] is not None
|
|
374
|
+
):
|
|
375
|
+
freq_val = frequencies[i]
|
|
376
|
+
if driver.account_for_detuning:
|
|
377
|
+
f_res_hz = atom.getTransitionFrequency(
|
|
378
|
+
*lower.level.to_arc_args(),
|
|
379
|
+
*upper.level.to_arc_args(),
|
|
380
|
+
)
|
|
381
|
+
detuning_rad_s = hz_to_rad_s(freq_val - f_res_hz)
|
|
382
|
+
else:
|
|
383
|
+
detuning_rad_s = (
|
|
384
|
+
np.zeros_like(freq_val) if not np.isscalar(freq_val) else 0.0
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
else:
|
|
388
|
+
if driver.account_for_detuning:
|
|
389
|
+
f_res_hz = atom.getTransitionFrequency(
|
|
390
|
+
*lower.level.to_arc_args(),
|
|
391
|
+
*upper.level.to_arc_args(),
|
|
392
|
+
)
|
|
393
|
+
detuning_rad_s = hz_to_rad_s(driver.frequency - f_res_hz)
|
|
394
|
+
else:
|
|
395
|
+
detuning_rad_s = 0.0
|
|
396
|
+
|
|
397
|
+
detunings_list.append(detuning_rad_s)
|
|
398
|
+
|
|
399
|
+
if amplitudes is not None and i < len(amplitudes) and amplitudes[i] is not None:
|
|
400
|
+
amp_v_m = amplitudes[i]
|
|
401
|
+
else:
|
|
402
|
+
amp_v_m = driver.amplitude
|
|
403
|
+
|
|
404
|
+
# Calculate transition dipole moment via ARC (a.u. -> C*m)
|
|
405
|
+
dipole_au = atom.getDipoleMatrixElement(
|
|
406
|
+
*lower.to_arc_args(),
|
|
407
|
+
*upper.to_arc_args(),
|
|
408
|
+
q=round(upper.mj - lower.mj),
|
|
409
|
+
)
|
|
410
|
+
dipole_c_m = au_to_coulomb_metre(abs(dipole_au))
|
|
411
|
+
|
|
412
|
+
# Convert electric field to angular Rabi frequency (rad/s)
|
|
413
|
+
rabi_rad_s = electric_field_to_rabi_freq(amp_v_m, dipole_c_m)
|
|
414
|
+
rabis_list.append(rabi_rad_s)
|
|
415
|
+
|
|
416
|
+
return solve_ladder_system(
|
|
417
|
+
detunings=detunings_list,
|
|
418
|
+
rabis=rabis_list,
|
|
419
|
+
gammas=gammas,
|
|
420
|
+
coherence_index=coherence_index,
|
|
421
|
+
)
|