Skip to content

Commit 00cba9d

Browse files
authored
Merge pull request #38 from QTC-UMD/cell_fixes
Multiple bug fixes in Cell
2 parents 59d7fe3 + 6b7bb8a commit 00cba9d

7 files changed

Lines changed: 132 additions & 51 deletions

File tree

docs/source/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Bug Fixes
2323
It is expected that other plots and values will differ by small amounts when using `Cell` and ARC v3.9.0
2424
- Fixed bug where `solve_doppler_analytic` ignored parameter time-dependence.
2525
It now follows the behavior of other steady-state solvers by constructing the Hamiltonian and equations of motion at `t=0`.
26+
- Ensure that `Cell.eta`, `Cell.kappa`, and `Cell.probe_freq` properly cache their values as intended.
27+
- Fix bug where `Cell` would not add transit broadening automatically, as intended.
28+
- Fix errors in `gamma_mismatch` calculations when coupling groups are used.
29+
Also fix issue that prevented `gamma_mismatch='all'` from working if only 1 dephasing is present that needs modification.
2630

2731
Deprecations
2832
++++++++++++

src/rydiqule/atom_utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from scipy.constants import epsilon_0, hbar, c
66
import numpy as np
7+
import math
78
import re
89
from .sensor_utils import expand_statespec
910

@@ -667,7 +668,7 @@ def calc_eta(omega: float, dipole_moment:float, beam_area: float) -> float:
667668
The value of eta, in root(Hz)
668669
"""
669670

670-
eta = np.sqrt((omega*dipole_moment**2)/(2.0*c*epsilon_0*hbar*beam_area))
671+
eta = math.sqrt((omega*dipole_moment**2)/(2.0*c*epsilon_0*hbar*beam_area))
671672

672673
return eta
673674

@@ -793,7 +794,7 @@ def validate_qnums(qstate:A_QState, I: Optional[float]=None):
793794
#validate (n,l) int, j half int
794795
assert int(n)==n, f"invalid n quantum number {n}."
795796
assert (int(l)==l) and (l < n), f"invalid l quantum number {l}."
796-
assert j==l+1/2 or j==np.abs(l-1/2), f"invalid j quantum number {j}"
797+
assert j==l+1/2 or j==abs(l-1/2), f"invalid j quantum number {j}"
797798

798799
#test m_j, f, m_f are allowed values
799800
if m_j is not None:
@@ -967,7 +968,7 @@ def get_valid_f(state: A_QState, I: Optional[float]=None) -> List[float]:
967968
J_qnum=state[2]
968969
if not isinstance(J_qnum, (int, float)) or not isinstance(I, (int, float)):
969970
raise ValueError(f"Invalid I,J quantum number types {(type(I),type(J_qnum))}.")
970-
return np.arange(np.abs(J_qnum - I), J_qnum + I + 1).tolist()
971+
return np.arange(abs(J_qnum - I), J_qnum + I + 1).tolist()
971972

972973

973974
def get_valid_mf(state: A_QState, I: Optional[float]=None) -> List[float]:

src/rydiqule/cell.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import scipy
66
import numpy as np
77
import warnings
8-
import itertools
8+
import itertools
9+
import math
910

1011
import scipy.constants
1112
from scipy.constants import Boltzmann, e
@@ -185,25 +186,26 @@ def __init__(self, atom_flag: AtomFlags, atomic_states: List[A_QState],
185186
self.temp = temp # K
186187
self.beam_area = beam_area
187188
self.density = self.atom.arc_atom.getNumberDensity(self.temp)
188-
self.atom_mass = self.atom.arc_atom.mass
189+
self.atom_mass: float = self.atom.arc_atom.mass
189190
if beam_diam is None:
190-
self.beam_diameter = 2.0*np.sqrt(beam_area/np.pi)
191+
self.beam_diameter = 2.0*math.sqrt(beam_area/np.pi)
191192
else:
192193
self.beam_diameter = beam_diam
193194

194195
if gamma_transit is None:
195-
gamma_transit = 1E-6*np.sqrt(8*Boltzmann*self.temp/(self.atom_mass*np.pi)
196-
)/(self.beam_diameter/2*np.sqrt(2*np.log(2)))
196+
gamma_transit = 1E-6*math.sqrt(8*Boltzmann*self.temp/(self.atom_mass*np.pi)
197+
)/(self.beam_diameter/2*math.sqrt(2*math.log(2)))
197198
self.gamma_transit = gamma_transit
198199

199200
# most probable speed for a 3D Maxwell-Boltzmann distribution
200201
# used when defining doppler averaging
201-
self.vP = np.sqrt(2*Boltzmann*self.temp/self.atom_mass)
202+
self.vP = math.sqrt(2*Boltzmann*self.temp/self.atom_mass)
202203

203204
self._add_state_energies()
204205
self._add_state_lifetimes()
205206
self._add_decoherence_rates()
206207
self._add_gamma_mismatches(gamma_mismatch)
208+
self.add_transit_broadening(gamma_transit)
207209

208210

209211
def set_experiment_values(self,
@@ -341,7 +343,7 @@ def level_ordering(self) -> List[A_QState]:
341343

342344

343345
@property
344-
def kappa(self):
346+
def kappa(self) -> float:
345347
"""Property to calculate the kappa value of the system.
346348
347349
The value is computed with the following formula Eq. 5 of
@@ -369,7 +371,7 @@ def kappa(self):
369371
return self._kappa
370372

371373
if self.probe_tuple is None:
372-
raise RydiquleError("Cell.probe_tuple not set. Either set manually or add at least one coupling before calculation.")
374+
raise RydiquleError("Cell.probe_tuple not set. Add at least one coupling before calculation.")
373375

374376
ground_manifold = self.states_with_spec(self.probe_tuple[0])
375377
excited_manifold = self.states_with_spec(self.probe_tuple[1])
@@ -394,6 +396,7 @@ def kappa(self):
394396
dipole_moment = self.atom.get_dipole_matrix_element(probe_g_nlj, probe_e_nlj, q=q)*a0*e
395397

396398
kappa = calc_kappa(omega_rad, dipole_moment, self.density)
399+
self._kappa = kappa
397400

398401
return kappa
399402

@@ -433,7 +436,7 @@ def kappa(self):
433436

434437

435438
@property
436-
def eta(self):
439+
def eta(self) -> float:
437440
"""Get the eta value for the system.
438441
439442
The value is computed with the following formula Eq. 7 of
@@ -459,7 +462,7 @@ def eta(self):
459462
if hasattr(self, "_eta"):
460463
return self._eta
461464
if self.probe_tuple is None:
462-
raise RydiquleError("Cell.probe_tuple not set. Either set manually or add at least one coupling before calculation.")
465+
raise RydiquleError("Cell.probe_tuple not set. Add at least one coupling before calculation.")
463466

464467
ground_manifold = self.states_with_spec(self.probe_tuple[0])
465468
excited_manifold = self.states_with_spec(self.probe_tuple[1])
@@ -483,12 +486,13 @@ def eta(self):
483486
omega_rad = self.atom.get_transition_frequency(probe_g_nlj, probe_e_nlj)*2.0*np.pi
484487
dipole_moment = self.atom.get_dipole_matrix_element(probe_g_nlj, probe_e_nlj, q=q)*a0*e
485488
eta = calc_eta(omega_rad, dipole_moment, self.beam_area)
489+
self._eta = eta
486490

487491
return eta
488492

489493

490494
@eta.setter
491-
def eta(self, value):
495+
def eta(self, value: float):
492496
"""Setter for the eta attribute.
493497
494498
Updates the self._eta class attribute.
@@ -519,7 +523,7 @@ def eta(self):
519523

520524

521525
@property
522-
def probe_freq(self):
526+
def probe_freq(self) -> float:
523527
"""Get the probe transition frequency, in rad/s.
524528
525529
Note that for :class:`~.Cell`, probing transition frequency is calculated using only
@@ -535,6 +539,8 @@ def probe_freq(self):
535539

536540
if hasattr(self, '_probe_freq'):
537541
return self._probe_freq
542+
if self.probe_tuple is None:
543+
raise RydiquleError("Cell.probe_tuple not set. Add at least one coupling before calculation.")
538544

539545
probe_lower_manifold = self.states_with_spec(self.probe_tuple[0])
540546
probe_upper_manifold = self.states_with_spec(self.probe_tuple[1])
@@ -544,11 +550,14 @@ def probe_freq(self):
544550

545551
energy_lower = self.atom.get_state_energy(A_QState(n1, l1, j1), s=0.5)*2*np.pi
546552
energy_upper = self.atom.get_state_energy(A_QState(n2, l2, j2), s=0.5)*2*np.pi
553+
554+
probe_freq = abs(energy_upper - energy_lower)
555+
self._probe_freq = probe_freq
547556

548-
return np.abs(energy_upper - energy_lower)
557+
return probe_freq
549558

550559
@probe_freq.setter
551-
def probe_freq(self, value):
560+
def probe_freq(self, value: float):
552561
"""Setter for the probe_freq attribute.
553562
554563
Updates the self._probe_freq class attribute.
@@ -737,7 +746,8 @@ def add_single_coupling(
737746
Coherent Couplings:
738747
((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 2.0, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0}
739748
Decoherent Couplings:
740-
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11316}
749+
((5, 0, 0.5),(5, 0, 0.5)): {gamma_transit: 0.40697}
750+
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11316, gamma_transit: 0.40697}
741751
Energy Shifts:
742752
None
743753
@@ -755,7 +765,8 @@ def add_single_coupling(
755765
Coherent Couplings:
756766
((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 2.0, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0}
757767
Decoherent Couplings:
758-
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11}
768+
((5, 0, 0.5),(5, 0, 0.5)): {gamma_transit: 0.40696}
769+
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.113, gamma_transit: 0.40696}
759770
Energy Shifts:
760771
None
761772
@@ -772,7 +783,8 @@ def add_single_coupling(
772783
Coherent Couplings:
773784
((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 1.177, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0}
774785
Decoherent Couplings:
775-
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11}
786+
((5, 0, 0.5),(5, 0, 0.5)): {gamma_transit: 0.40696}
787+
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11, gamma_transit: 0.40696}
776788
Energy Shifts:
777789
None
778790
@@ -787,7 +799,8 @@ def add_single_coupling(
787799
Coherent Couplings:
788800
((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 4.3, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0}
789801
Decoherent Couplings:
790-
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11}
802+
((5, 0, 0.5),(5, 0, 0.5)): {gamma_transit: 0.4117}
803+
((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.113, gamma_transit: 0.4117}
791804
Energy Shifts:
792805
None
793806
@@ -904,7 +917,7 @@ def add_single_coupling(
904917
lam = abs(self.atom.get_transition_wavelength(state1, state2)) # in m
905918
kvec = 2*np.pi/lam*np.asarray(kunit)*1e-6 # scaled to Mrad/m
906919
else:
907-
raise RydiquleError(f'Coupling {states} has un-normalized |kunit|={np.sqrt(k_norm_sq):.2f}!=1')
920+
raise RydiquleError(f'Coupling {states} has un-normalized |kunit|={math.sqrt(k_norm_sq):.2f}!=1')
908921

909922
super().add_single_coupling(states=states,
910923
rabi_frequency=passed_rabi,
@@ -1066,7 +1079,7 @@ def _add_gamma_mismatches(self, method:Union[str, dict]="ground"):
10661079
`gamma_transition` values on edges leaving that state. However, it is not always
10671080
desirable to account for all states in this way for simplicity or computational
10681081
complexity reasons. This function allows the :class:`~.Cell` to account for any
1069-
differences in these values that arise as a result of excluding physicalstates from a
1082+
differences in these values that arise as a result of excluding physical states from a
10701083
:class:`~.Cell`. There are multiple ways to resolve these discrepancies, specified by
10711084
the `method` argument, which is detailed in the `Parameters` section.
10721085
@@ -1155,8 +1168,9 @@ def _add_gamma_mismatch_to_ground(self, state: A_QState):
11551168
m_j = None if g.m_j is None else "all"
11561169
(f, m_f) = (None, None) if g.f is None else ("all","all")
11571170
ground_manifold = A_QState(g.n, g.l, g.j, m_j=m_j, f=f, m_f=m_f)
1171+
degeneracy = len(self.states_with_spec(ground_manifold))
11581172

1159-
self.add_decoherence((state, ground_manifold), gamma=(lifetime-transition_total), label="mismatch")
1173+
self.add_decoherence((state, ground_manifold), gamma=(lifetime-transition_total)/degeneracy, label="mismatch")
11601174

11611175

11621176
def _add_gamma_mismatch_to_all(self, state:A_QState):
@@ -1180,18 +1194,21 @@ def _add_gamma_mismatch_to_all(self, state:A_QState):
11801194

11811195
transition_total = sum(e[2] for e in out_edges)
11821196

1183-
#if they dom't match, we add a decoherence to the entire ground state manifold
1184-
#that matches what remains
1197+
#if they dom't match, we proportionally increase existing decoherences to make up the difference
11851198
if not np.isclose(transition_total, lifetime):
1186-
#construct the dictionary of coupling coefficients coefficients
1187-
cc = {
1188-
(s1, s2):gamma/transition_total for s1, s2, gamma in out_edges
1189-
if gamma
1190-
}
1191-
1199+
gamma_total_mismatch = lifetime-transition_total
11921200
out_states_list = [s2 for _, s2, _ in out_edges]
1193-
self.add_decoherence_group([state], out_states_list, gamma = transition_total, coupling_coefficients=cc, label="mismatch")
1194-
1201+
if len(out_states_list) > 1:
1202+
#construct the dictionary of coupling coefficients
1203+
cc = {
1204+
(s1, s2):gamma/transition_total for s1, s2, gamma in out_edges
1205+
if gamma
1206+
}
1207+
1208+
self.add_decoherence_group([state], out_states_list, gamma = gamma_total_mismatch, coupling_coefficients=cc, label="mismatch")
1209+
else:
1210+
self.add_single_decoherence((state, out_states_list[0]), gamma_total_mismatch,
1211+
label='mismatch')
11951212

11961213
def _validate_input_states(self, atomic_states: List[A_QState]):
11971214
"""Helper function to check that input states are compatible and defined"""

src/rydiqule/experiments.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ def get_snr(sensor: Sensor,
9090
>>> c.add_coupling(states=(g,e), rabi_frequency=np.linspace(1e-6, 1, 5), detuning=1, label="probe")
9191
>>> snr, mesh = rq.get_snr(c, 'probe_rabi_frequency')
9292
>>> print(snr)
93-
[ 0. 13947396.7 27887614.4 41813486.6
94-
55717871.1]
93+
[ 0. 13654034.1 27301261.5 40934886.9
94+
54548137.6]
9595
>>> print(mesh) # doctest: +SKIP
9696
[array([0. , 0.25 , 0.499999, 0.749999, 0.999999])]
9797

src/rydiqule/sensor_solution.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def get_solution_element(self, idx: int) -> Result:
352352
(3,)
353353
>>> rho_01_im = sols.get_solution_element(0)
354354
>>> print(rho_01_im)
355-
0.0013711656
355+
0.00131399
356356
357357
"""
358358
basis_size = np.sqrt(self.rho.shape[-1] + 1)
@@ -382,7 +382,11 @@ def coupling_coefficient_matrix(self, coupling: sensor_utils.StateSpecs) -> np.n
382382
>>> sol = rq.solve_steady_state(my_cell)
383383
>>> for e in sol.couplings.edges(data="coherent_cc"):
384384
... print(*e)
385+
(5, 0, 0.5, m_j=-0.5) (5, 0, 0.5, m_j=-0.5) None
386+
(5, 0, 0.5, m_j=-0.5) (5, 0, 0.5, m_j=0.5) None
385387
(5, 0, 0.5, m_j=-0.5) (5, 1, 0.5, m_j=-0.5) -0.816496580927726
388+
(5, 0, 0.5, m_j=0.5) (5, 0, 0.5, m_j=-0.5) None
389+
(5, 0, 0.5, m_j=0.5) (5, 0, 0.5, m_j=0.5) None
386390
(5, 0, 0.5, m_j=0.5) (5, 1, 0.5, m_j=0.5) 0.816496580927726
387391
(5, 1, 0.5, m_j=-0.5) (5, 0, 0.5, m_j=-0.5) None
388392
(5, 1, 0.5, m_j=-0.5) (5, 0, 0.5, m_j=0.5) None
@@ -495,7 +499,7 @@ def get_susceptibility(self) -> ComplexResult:
495499
(3,)
496500
>>> sus = sols.get_susceptibility()
497501
>>> print(sus)
498-
(1.9734254e-05+0.000376067j)
502+
(1.891136e-05+0.00036817j)
499503
500504
"""
501505
probe_rabi = self.probe_rabi*1e6 #rad/s
@@ -539,11 +543,11 @@ def get_OD(self) -> Result:
539543
(3,)
540544
>>> OD = sols.get_OD()
541545
>>> print(OD)
542-
0.3028422896
546+
0.2964844
543547
544548
"""
545549

546-
probe_wavelength = np.abs(c/(self.probe_freq/(2*np.pi))) #meters
550+
probe_wavelength = abs(c/(self.probe_freq/(2*np.pi))) #meters
547551
probe_wavevector = 2*np.pi/probe_wavelength #1/meters
548552
OD = self.get_susceptibility().imag*self.cell_length*probe_wavevector
549553
if np.any(OD > 1.0):
@@ -577,7 +581,7 @@ def get_transmission_coef(self) -> Result:
577581
(3,)
578582
>>> t = sols.get_transmission_coef()
579583
>>> print(t)
580-
0.73871559029
584+
0.743427
581585
582586
"""
583587
OD = self.get_OD()
@@ -605,7 +609,7 @@ def get_phase_shift(self) -> Result:
605609
>>> print(sols.rho.shape)
606610
(3,)
607611
>>> print(sols.get_phase_shift())
608-
80.5295218956
612+
80.5294887
609613
610614
"""
611615
# reverse probe tuple order to get correct sign of imag

src/rydiqule/sensor_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,9 @@ def convert_to_full_dm(dm: np.ndarray) -> np.ndarray:
405405
>>> c.add_coupling(states=(g, e), rabi_frequency=1, detuning=1)
406406
>>> sols = rq.solve_steady_state(c)
407407
>>> print(sols.rho)
408-
[0.001371 0.02613 0.000686]
408+
[0.001313 0.02558 0.000664]
409409
>>> print(rq.sensor_utils.convert_to_full_dm(sols.rho))
410-
[9.993144e-01 1.371165e-03 2.612972e-02 6.855828e-04]
410+
[9.993359e-01 1.31399e-03 2.55811e-02 6.64016e-04]
411411
412412
"""
413413
b, r = divmod(math.sqrt(dm.shape[-1]+1), 1.0)
@@ -461,10 +461,10 @@ def convert_dm_to_complex(dm: np.ndarray) -> np.ndarray:
461461
>>> c.add_coupling(states=(g, e), rabi_frequency=1, detuning=1)
462462
>>> sols = rq.solve_steady_state(c)
463463
>>> print(sols.rho)
464-
[0.001371 0.02613 0.000686]
464+
[0.001313 0.02558 0.000664]
465465
>>> print(rq.sensor_utils.convert_dm_to_complex(sols.rho))
466-
[[9.993144e-01+0.j 1.371166e-03+0.02613j]
467-
[1.371166e-03-0.02613j 6.855828e-04+0.j ]]
466+
[[9.993359e-01+0.j 1.31399e-03+0.025581j]
467+
[1.313990e-03-0.025581j 6.64016e-04+0.j ]]
468468
469469
"""
470470
b, r = divmod(math.sqrt(dm.shape[-1]+1), 1.0)

0 commit comments

Comments
 (0)