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 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
+ )