├── .gitignore
├── LICENSE
├── README.md
├── rydcalc
├── .gitignore
├── MQDTclass.py
├── Yb171_NIST.txt
├── Yb174_NIST.txt
├── __init__.py
├── alkali.py
├── alkali_data.py
├── alkaline.py
├── alkaline_data.py
├── alkaline_hyperfine.py
├── analysis.py
├── arc_c_extensions.c
├── bib.bib
├── constants.py
├── database_constructor.py
├── db_manager.py
├── defects.py
├── hydrogen.py
├── pair_basis.py
├── ponderomotive.py
├── rydcalc.py
├── setupc.py
├── single_basis.py
└── utils.py
├── tests
└── unit_tests.py
└── tutorial.ipynb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.ipynb
2 | !tutorial.ipynb
3 | *.db
4 | *.so
5 | *.o
6 | .ipynb_checkpoints
7 | .idea
8 | .DS_Store
9 | *.npy
10 | *.pyc
11 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2024, Jeff Thompson, Michael Peper, Sam Cohen
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted
6 | provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions
9 | and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of
12 | conditions and the following disclaimer in the documentation and/or other materials provided with
13 | the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to
16 | endorse or promote products derived from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
19 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
20 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
25 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 | POSSIBILITY OF SUCH DAMAGE.
27 |
28 |
29 | Portions of this code in arc_c_extensions.c and AlkaliAtom.numerov_py() are incorporated from the Alkali Rydberg Calculator, under the following license:
30 |
31 | BSD 3-Clause License
32 |
33 | Copyright (c) 2016, Nikola Šibalić, Jonathan D. Pritchard, Charles S. Adams, Kevin J. Weatherill
34 | All rights reserved.
35 |
36 | Redistribution and use in source and binary forms, with or without modification, are permitted
37 | provided that the following conditions are met:
38 |
39 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions
40 | and the following disclaimer.
41 |
42 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of
43 | conditions and the following disclaimer in the documentation and/or other materials provided with
44 | the distribution.
45 |
46 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to
47 | endorse or promote products derived from this software without specific prior written permission.
48 |
49 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
50 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
51 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
52 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
53 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
54 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
55 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
56 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
57 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Rydberg calculator
2 |
3 | Thompson Lab
4 | Contributors: Jeff Thompson, Michael Peper, Sam Cohen
5 |
6 | This package computes quantities related to Rydberg states in an MDQT framework, including state energies, Stark shifts and pair interaction potentials. This software was used to implement the calculations in [M. Peper et al arXiv:2406.01482](http://arxiv.org/abs/2406.01482) for 174Yb and 171Yb. An earlier version was used to implement the calculations in [Cohen and Thompson, PRX 2 030322 (2022)](https://link.aps.org/doi/10.1103/PRXQuantum.2.030322).
7 |
8 | The basic approach to computing pair potentials follows [pairinteraction](https://github.com/pairinteraction/pairinteraction) and the [Alkali Rydberg Calculator (ARC)](https://github.com/nikolasibalic/ARC-Alkali-Rydberg-Calculator). In particular, we have directly incorporated functions for computing wavefunctions numerically using the Numerov method from ARC.
9 |
10 | `rydcalc` also has the necessary atomic data to perform calculations for Alkali atoms, which is intended mainly for debugging and comparison to results obtained with other programs that have been more extensively tested and documented. If you can do your calculation with other programs, we recommend that! But if you need MQDT, please try `rydcalc`.
11 |
12 | Documentation illustrating the basic functionality of the code can be found in the tutorial.ipynb notebook, and in the comments throughout the code.
13 |
14 | Note: to compile the ARC C numerov integrator, run this from a console (in MacOS/Linux):
15 | 'python setupc.py build_ext --inplace'
16 |
17 | [Note: if you are using Anaconda with multiple environments, you must run this
18 | with the correct environment activated on the command line!]
19 |
20 | See [ARC documentation](https://arc-alkali-rydberg-calculator.readthedocs.io/en/latest/installation.html#compiling-c-extension) for other platforms.
--------------------------------------------------------------------------------
/rydcalc/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .ipynb_checkpoints
3 | __pycache__
4 | *.so
5 | .DS_Store
6 | .idea
--------------------------------------------------------------------------------
/rydcalc/MQDTclass.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import scipy as scp
3 | import sys
4 | from .constants import *
5 |
6 | class mqdt_class:
7 | def __init__(self, channels, rot_order,
8 | rot_angles, eig_defects, nulims, Uiabar, atom):
9 | self.channels = channels
10 | self.numchannels = len(channels)
11 | self.rotchannels = rot_order
12 | self.rotangles = rot_angles
13 | self.muvec = eig_defects
14 |
15 | #self.Vrot = self.Vrotfunc()
16 | self.Uiabar = Uiabar#self.Uiabarfunc()
17 |
18 | #self.Uia = np.dot(self.Uiabar, self.Vrot)
19 |
20 | self.ionizationlimits_invcm=[]
21 | for ch in channels:
22 | self.ionizationlimits_invcm.append(0.01*ch.core.Ei_Hz/cs.c)
23 |
24 | [self.nulima,self.nulimb] = nulims
25 | self.i = atom.I
26 | self.RydConst_invcm = 0.01*atom.RydConstHz / cs.c
27 |
28 | self.mateps = sys.float_info.epsilon
29 |
30 | def getCofactorMatrix(self,A):
31 | ''' Returns the cofactor matrix of matrix A using SVD. See e.g.
32 | Linear Algebra Its Appl. 283 (998) 5-64.
33 | '''
34 | U, sigma, Vt = np.linalg.svd(A)
35 | N = len(sigma)
36 | g = np.tile(sigma, N)
37 | g[::(N + 1)] = 1
38 | G = np.diag(np.product(np.reshape(g, (N, N)), 1))
39 | return np.linalg.det(U)*np.linalg.det(Vt.T)*U @ G @ Vt
40 |
41 | def sinfunc(self, x, A, phi, b):
42 | return A * np.sin(2 * np.pi * x + phi) + b
43 |
44 | def customhalley(self,fwithdiff, x0, eps=10 ** (-16), tol=10 ** (-20), maxfunctioncalls=500):
45 |
46 | ''' Root finding algorithm based on Halley's method. It iteratively approximates a root of f(x) from an initial guess x0
47 | using f(x_n), f'(x_n) and f"(x_n) by
48 |
49 | x_(n+1) = x_n - ( 2*f(x_n)*f'(x_n) ) / (2*(f'(x_n))**2 - f(x_n)*f"(x_n)).
50 |
51 | If f"() == False, the function uses Newton's method
52 |
53 | x_(n+1) = x_n - f(x_n) / f'(x_n).
54 |
55 | '''
56 |
57 | x = x0
58 |
59 | functioncalls = 1
60 | while True:
61 | (fx, fpx, fpx2) = fwithdiff(x)
62 |
63 | if fpx2 == False:
64 | xnew = x - fx / fpx
65 | else:
66 | xnew = x - (2.0 * fx * fpx) / (2.0 * fpx ** 2 - fx * fpx2)
67 |
68 | if fx < tol and abs(xnew - x) <= eps:
69 | solution = [x, True, functioncalls]
70 | break
71 |
72 | if functioncalls > maxfunctioncalls:
73 | solution = [x, False, functioncalls]
74 | break
75 |
76 | x = xnew
77 | functioncalls += 1
78 |
79 | return solution
80 |
81 | def nux(self, Ia, Ib, nub):
82 | ''' Converts effective principal quantum nub with respect to threshold Ib
83 | to effective principal quantum number nua.
84 | '''
85 | return ((Ia - Ib) / self.RydConst_invcm + 1 / nub ** 2) ** (-1 / 2)
86 |
87 | def rotmat(self, i, j, thetaij):
88 | ''' Returns rotation matrix around channels i and j with angle thetaij.
89 | '''
90 | rotmat = np.identity(self.numchannels)
91 | rotmat[i - 1, i - 1] = np.cos(thetaij)
92 | rotmat[j - 1, j - 1] = np.cos(thetaij)
93 | rotmat[i - 1, j - 1] = -np.sin(thetaij)
94 | rotmat[j - 1, i - 1] = np.sin(thetaij)
95 | return rotmat
96 |
97 | def Vrotfunc(self,nub):
98 | ''' Performs consecutive rotations specified in the MQDT model in rot_order with angles specified in rot_angles.
99 | '''
100 | th=self.rotangles[0][0]
101 |
102 | for k,ii in enumerate(self.rotangles[0][1:],1):
103 | th+=ii/(nub**(2*k))
104 |
105 | Vrot = self.rotmat(self.rotchannels[0][0], self.rotchannels[0][1], th)
106 |
107 | for j,i in enumerate(self.rotchannels[1:],1):
108 |
109 | th = self.rotangles[j][0]
110 | for k,ii in enumerate(self.rotangles[j][1:],1):
111 | th += ii / (nub ** (2 * k))
112 |
113 | Vrot = np.dot(Vrot, self.rotmat(i[0], i[1], th))
114 |
115 | return Vrot
116 |
117 | def nufunc(self, nu, mu):
118 | ''' Generates matrix sin(pi*(nu_i+mu_a)) from vectors nu and mu.
119 | '''
120 | numat = np.zeros((self.numchannels, self.numchannels))
121 | for i in range(self.numchannels):
122 | for j in range(self.numchannels):
123 | numat[i, j] = np.sin(np.pi * (nu[i] + mu[j]))
124 |
125 | return numat
126 |
127 | def nudifffunc(self,nu,nudiff,mu,mudiff):
128 | ''' Generates derivative of matrix sin(pi*(nu_i+mu_a)), from vectors nu and mu and their derivatives.
129 | '''
130 | nudiffmat = np.zeros((self.numchannels, self.numchannels))
131 |
132 | for i in range(self.numchannels):
133 | for j in range(self.numchannels):
134 | nudiffmat[i, j] = np.pi*np.cos(np.pi * (nu[i] + mu[j])) * (nudiff[i] + mudiff[j])
135 |
136 | return nudiffmat
137 |
138 | def nudiff2func(self,nu,nudiff,nudiff2,mu,mudiff,mudiff2):
139 | ''' Generates second derivative of matrix sin(pi*(nu_i+mu_a)), from vectors nu and mu and their first and second derivatives.
140 | '''
141 | nudiff2mat = np.zeros((self.numchannels, self.numchannels))
142 |
143 | for i in range(self.numchannels):
144 | for j in range(self.numchannels):
145 | nudiff2mat[i, j] = -(np.pi**2)*np.sin(np.pi*(nu[i]+mu[j]))*(mudiff[j]+nudiff[i])**2+np.pi*np.cos(np.pi * (nu[i] + mu[j]))*(mudiff2[j]+nudiff2[i])
146 |
147 | return nudiff2mat
148 |
149 | def mqdtmodel(self, nuvec):
150 | ''' For a given set of effective quantum numbers with respect to two ionization limits a and b, this function returns the value of
151 |
152 | det (U_ia*sin(pi*(nu_i+mu_a)))
153 |
154 | '''
155 | [nua, nub] = nuvec
156 |
157 | nu=[]
158 | for i in np.arange(self.numchannels):
159 | if i in self.nulima:
160 | nu.append(nua)
161 | elif i in self.nulimb:
162 | nu.append(nub)
163 | else:
164 | nu.append(self.nux(self.ionizationlimits_invcm[i],self.ionizationlimits_invcm[self.nulima[0]],nua))
165 |
166 | mu = []
167 |
168 | for i in np.arange(self.numchannels):
169 | mue=0
170 |
171 | for k,muk in enumerate(self.muvec[i]):
172 | if k==0:
173 | mue += muk
174 | else:
175 | mue += muk/self.nux(self.ionizationlimits_invcm[i],self.ionizationlimits_invcm[self.nulima[0]],nua)**(k*2)
176 |
177 | mu.append(mue)
178 |
179 | self.Uia = np.dot(self.Uiabar, self.Vrotfunc(nub))
180 |
181 | nmat = self.nufunc(nu, mu)
182 |
183 | return np.linalg.det(np.multiply(self.Uia, nmat))
184 |
185 |
186 | def diff2mqdtmodel(self,nub):
187 |
188 | '''
189 | Calculates the determinant
190 |
191 | det|U_{ialpha} sin(pi(mu_alpha(E)+nu_i(E)))|
192 |
193 | along Ia-Rk/nua**2==Ib-Rk/nub**2, and it's first two derivatives. The derivates of the determinant are obtained by application of Jacobi's formula
194 |
195 | (1) d/dt (det A(t)) = tr (adj(A(t)) dA(t)/dt).
196 |
197 | and further for invertible matrices A(t),
198 |
199 | (2) d/dt (det A(t)) = det(A(t))*tr(inv(A(t))*dA(t)/dt)
200 |
201 | (3) d^2 / dt^2 (det A(t)) = det (A(t)) *(tr(inv(A(t))*(dA/dt))^2 - tr((inv(A(t))*(dA/dt))^2-inv(A(t))*(d^2A/dt^2))) )
202 |
203 | '''
204 |
205 | nu = []
206 | nudiff = []
207 | nudiff2 =[]
208 |
209 | for i in np.arange(self.numchannels):
210 | if i in self.nulimb:
211 | nu.append(nub)
212 | nudiff.append(1)
213 | nudiff2.append(0)
214 | else:
215 | nu.append(self.nux(self.ionizationlimits_invcm[i], self.ionizationlimits_invcm[self.nulimb[0]], nub))
216 | nudiff.append(1 / ((nub ** 3) * ((1 / nub ** 2) + (self.ionizationlimits_invcm[i] - self.ionizationlimits_invcm[self.nulimb[0]]) / self.RydConst_invcm) ** (3 / 2)))
217 | nudiff2.append((3/((nub**6)*(1/nub**2 + (self.ionizationlimits_invcm[i] - self.ionizationlimits_invcm[self.nulimb[0]]) / self.RydConst_invcm)**(5/2))-3/((nub**4)*(1/nub**2 + (self.ionizationlimits_invcm[i] - self.ionizationlimits_invcm[self.nulimb[0]]) / self.RydConst_invcm)**(3/2))))
218 |
219 | mu = []
220 | mudiff = []
221 | mudiff2 = []
222 |
223 | for i in np.arange(self.numchannels):
224 | mue = 0
225 | muediff = 0
226 | muediff2 = 0
227 |
228 | for k,muk in enumerate(self.muvec[i]):
229 | if k == 0:
230 | mue += muk
231 | else:
232 | mue += muk / self.nux(self.ionizationlimits_invcm[i], self.ionizationlimits_invcm[self.nulimb[0]], nub) ** (k * 2)
233 | muediff += (- muk * (2*k) *(1/nub**2+(self.ionizationlimits_invcm[i]-self.ionizationlimits_invcm[self.nulimb[0]])/self.RydConst_invcm)**(k-1))/nub**3
234 | muediff2 += (4*(k-1)*k*muk*(1/nub**2+(self.ionizationlimits_invcm[i]-self.ionizationlimits_invcm[self.nulimb[0]])/self.RydConst_invcm)**(k-2))/nub**6 + (6*k*muk*(1/nub**2+(self.ionizationlimits_invcm[i]-self.ionizationlimits_invcm[self.nulimb[0]])/self.RydConst_invcm)**(k-1))/nub**4
235 |
236 | mu.append(mue)
237 | mudiff.append(muediff)
238 | mudiff2.append(muediff2)
239 |
240 | self.Uia = np.dot(self.Uiabar, self.Vrotfunc(nub))
241 |
242 | ndiffmat = self.nudifffunc(nu,nudiff, mu,mudiff)
243 | ndiff2mat = self.nudiff2func(nu,nudiff,nudiff2,mu,mudiff,mudiff2)
244 | numat = self.nufunc(nu, mu)
245 |
246 | Umat = np.multiply(self.Uia, numat)
247 | detUmat = np.linalg.det(Umat)
248 |
249 | diffUmat = np.multiply(self.Uia, ndiffmat)
250 |
251 | # Check if matrix is singular. If not, use Eq. (2) and (3), if (close to) singular use Eq. (1) and don't return second derivative
252 | if np.linalg.cond(Umat) < 1 / self.mateps:
253 | invUmat = np.linalg.inv(Umat)
254 | diff2Umat = np.multiply(self.Uia, ndiff2mat)
255 | inVdiff = np.dot(invUmat, diffUmat)
256 | trinVdiff = np.trace(inVdiff)
257 | return [detUmat, detUmat*trinVdiff, detUmat*(trinVdiff**2 - np.trace(np.dot(inVdiff,inVdiff)-np.dot(invUmat,diff2Umat)))]
258 | else:
259 | adjUmat = self.getCofactorMatrix(Umat).T
260 | return [detUmat, np.trace(np.dot(adjUmat, diffUmat)), False]
261 |
262 | def lufano(self, na):
263 |
264 | if len(self.nulimb)==1:
265 |
266 | nuavec = [0, 0.5]
267 |
268 | fitvals = np.array([])
269 |
270 | for i in nuavec:
271 | fitvals = np.append(fitvals, self.mqdtmodel([na,i]))
272 |
273 | y0 = fitvals[0]
274 | y12 = fitvals[1]
275 |
276 | phi = np.arctan(y0 / y12)
277 |
278 | return [np.mod(1 - phi / np.pi, 1)]
279 |
280 | elif len(self.nulimb)==2:
281 | nubvec = np.arange(0.1, 1.1, 1 / 5)
282 |
283 | fitvals = np.array([])
284 |
285 | for i in nubvec:
286 | fitvals = np.append(fitvals, self.mqdtmodel([na,i]))
287 |
288 | params, params_covariance = scp.optimize.curve_fit(self.sinfunc, nubvec, fitvals, p0=[-0.0001, 1.24, 0.0001])
289 |
290 | A = params[0]
291 | phi = params[1]
292 | b = params[2]
293 |
294 | root = [np.mod((-phi - np.arcsin(b / A)) / (2 * np.pi), 1),np.mod((-phi + np.pi + np.arcsin(b / A)) / (2 * np.pi), 1)]
295 |
296 | return root
297 | else:
298 | print("Implement three channels converging to Ia")
299 |
300 |
301 | def boundstatesminimizer(self,nub):
302 | # solve for determinant along function Ea = Eb = Ia - R/nua**2 = Ib - R/nub**2
303 | [A,B,C]=self.diff2mqdtmodel(nub)
304 |
305 | return A,B,C
306 |
307 | def boundstates(self,nubguess,accuracy = 8):
308 | ''' calculates the theoretical bound state of the mqdt model close to guess of an effective quantum number with respect to the higher (Ia,Ib) ionization limit.
309 | Currently uses Halley's method (like Newton's method, but with additional information from second derivative) without brackets around the root.
310 | Accuracy 8 corresponds to 1 MHz at nub = 4, 8 kHz at nub = 20, and 0.065 kHz at nub = 100.
311 | For bracketed root search use scp.optimize.brentq'''
312 |
313 | sol = self.customhalley(self.boundstatesminimizer, x0=nubguess,eps=10**(-10),tol=10**(-accuracy),maxfunctioncalls=1000)
314 |
315 | if sol[1]==True:
316 | return np.round(sol[0], decimals=accuracy )
317 | else:
318 | #print("I did not converge")
319 | return 1
320 |
321 | #return np.round(sol, decimals=accuracy - 1)
322 |
323 |
324 | def boundstatesinrange(self,range,accuracy=8):
325 | '''Finds bound states in given range with given accuracy in nub. For nub converging to first ionization limit.
326 | '''
327 |
328 | nutheorb = []
329 | nutheora = []
330 |
331 | for nubguess in np.arange(range[0], range[1], 0.01):
332 | nutheorb.append(np.round(self.boundstates(nubguess), decimals=accuracy))
333 |
334 | sav = np.array(nutheorb)
335 | idx = np.unique(sav.round(decimals=accuracy-1), return_index=True)
336 |
337 | nutheorb = np.array(nutheorb)[idx[1]]
338 | nutheorb = np.round(nutheorb,decimals=accuracy-1)
339 |
340 |
341 |
342 | for i in nutheorb:
343 | nutheora.append(self.nux(self.ionizationlimits_invcm[self.nulima[0]], self.ionizationlimits_invcm[self.nulimb[0]], i))
344 |
345 | return [np.sort(nutheora), np.sort(nutheorb)]
346 |
347 | def channelcontributions(self,nub):
348 |
349 | ''' Calculates the channel contributions A_i for a given bound state with effective principal quantum number nub,
350 | using Eq. (5) and Eq. (24) from 0. ROBAUX, and M. AYMAR, Comp. Phys. Commun. 25, 223—236 (1982).
351 | The A_i's are normalized following Lee and Lu (Eq. (A5) from , Phys. Rev. A 8, 1241 (1973))
352 | '''
353 |
354 | def Uiafunc(nub):
355 | return np.dot(self.Uiabar, self.Vrotfunc(nub))
356 |
357 | self.Uia = Uiafunc(nub)
358 |
359 | nu = []
360 | for i in np.arange(self.numchannels):
361 | if i in self.nulimb:
362 | nu.append(nub)
363 | else:
364 | nu.append(self.nux(self.ionizationlimits_invcm[i], self.ionizationlimits_invcm[self.nulimb[0]], nub))
365 |
366 | mu = []
367 | for i in np.arange(self.numchannels):
368 | mue = 0
369 | for k,muk in enumerate(self.muvec[i]):
370 | if k == 0:
371 | mue += muk
372 | else:
373 | mue += muk / self.nux(self.ionizationlimits_invcm[i], self.ionizationlimits_invcm[self.nulimb[0]], nub) ** (k * 2)
374 | mu.append(mue)
375 |
376 | dmu = []
377 |
378 | for i in np.arange(self.numchannels):
379 | dmue = 0
380 | for k,muk in enumerate(self.muvec[i]):
381 | if k != 0:
382 | dmue += - 2*k * muk / self.nux(self.ionizationlimits_invcm[i], self.ionizationlimits_invcm[self.nulimb[0]], nub) ** ((k-1) * 2)
383 | dmu.append(dmue)
384 |
385 | eps = 10**(-6)
386 | dUia_dnu = (Uiafunc(nub+eps/2)-Uiafunc(nub-eps/2)) / eps
387 | dUia_dE = (nub**3)*dUia_dnu
388 |
389 | nmat = self.nufunc(nu, mu)
390 | Fialpha = np.multiply(self.Uia, nmat)
391 |
392 | cofacjalpha = self.getCofactorMatrix(Fialpha)
393 |
394 | Aalpha = []
395 | # Eq. (5) from 0. ROBAUX, and M. AYMAR, Comp. Phys. Commun. 25, 223—236 (1982)
396 | for i in np.arange(self.numchannels):
397 | Aalpha.append(cofacjalpha[0, i] / np.sqrt(np.sum(cofacjalpha[0, :] ** 2)))
398 |
399 | Ai = np.array([])
400 |
401 | # Eq. (24) from 0. ROBAUX, and M. AYMAR, Comp. Phys. Commun. 25, 223—236 (1982)
402 | for i in np.arange(self.numchannels):
403 | sumalpha = 0
404 | for alpha in np.arange(self.numchannels):
405 | sumalpha += (-1)**(self.channels[i].l+1) * nu[i]**(3 / 2) * self.Uia[i, alpha] * np.cos(np.pi * (nu[i] + mu[alpha])) * Aalpha[alpha]
406 |
407 | Ai=np.append(Ai,sumalpha)
408 |
409 | # simple normalization of channels contributions to np.sum(Ai ** 2) == 1, neglects energy dependence of mu and Uia
410 | #Ai_norm = Ai / np.sqrt(np.sum(Ai ** 2))
411 |
412 |
413 | # Eq. (A5) from Lee and Lu, Phys. Rev. A 8, 1241 (1973)
414 | Nsq = 0
415 |
416 | for i in np.arange(self.numchannels):
417 | Ni = 0
418 | for alpha in np.arange(self.numchannels):
419 |
420 | Ni+=(self.Uia[i,alpha]*np.cos(np.pi*(nu[i]+mu[alpha]))*Aalpha[alpha])
421 | Nsq += (nu[i]**3)*Ni**2
422 |
423 | for alpha in np.arange(self.numchannels):
424 | Nsq += dmu[alpha]*Aalpha[alpha]**2
425 |
426 | for i in np.arange(self.numchannels):
427 | for alpha in np.arange(self.numchannels):
428 | for beta in np.arange(self.numchannels):
429 | Nsq += (1/np.pi)*dUia_dE[i,alpha]*self.Uia[i,beta]*np.sin(np.pi*(mu[alpha]-mu[beta]))*Aalpha[alpha]*Aalpha[beta]
430 |
431 | Ai_norm = Ai / np.sqrt(Nsq)
432 |
433 | # channel contributions in alpha channels
434 |
435 | Aalpha_norm = np.array([])
436 |
437 | for alpha in np.arange(self.numchannels):
438 | Aalpha = 0
439 | for i in np.arange(self.numchannels):
440 | Aalpha += self.Uiabar[i, alpha] * Ai_norm[i]
441 |
442 | Aalpha_norm = np.append(Aalpha_norm, Aalpha)
443 |
444 | return [Ai_norm,Aalpha_norm]
445 |
446 |
447 | class mqdt_class_rydberg_ritz(mqdt_class):
448 | ''' Adaptation of the MQDT class for single-channel channel quantum defect theory.
449 | '''
450 |
451 | def __init__(self, channels,deltas,atom,HFlimit = None):
452 | self.channels = [channels]
453 | self.deltas=deltas
454 | self.HFlimit = HFlimit
455 |
456 | self.atom = atom
457 |
458 | self.nulima = [0]
459 | self.nulimb = [0]
460 |
461 |
462 | self.ionizationlimits_invcm=[]
463 |
464 | self.ionizationlimits_invcm.append(0.01 * channels.core.Ei_Hz / cs.c)
465 |
466 | if HFlimit == "upper":
467 | self.nulima = [1]
468 | self.nulimb = [0]
469 | self.ionizationlimits_invcm.append(0.01 *( channels.core.Ei_Hz - self.atom.ion_hyperfine_6s_Hz)/ cs.c)
470 | elif HFlimit == "lower":
471 | self.nulima = [0]
472 | self.nulimb = [1]
473 | self.ionizationlimits_invcm.append(0.01 * (channels.core.Ei_Hz + self.atom.ion_hyperfine_6s_Hz) / cs.c)
474 |
475 | self.RydConst_invcm = 0.01*atom.RydConstHz / cs.c
476 |
477 |
478 |
479 | def boundstates(self, nubguess,accuracy=8):
480 |
481 | if self.HFlimit == "upper" or self.HFlimit == None:
482 | #searchrange = [np.ceil(nubguess - 1.5), np.floor(nubguess + 1.5)]
483 | searchrange = [nubguess,nubguess]
484 | elif self.HFlimit == "lower":
485 | nuaguess = self.nux(0,0.01*self.atom.ion_hyperfine_6s_Hz/ cs.c,nubguess)
486 | #print(nuaguess)
487 | #searchrange = [np.ceil(nuaguess - 1.5), np.floor(nuaguess + 1.5)]
488 | searchrange = [ nuaguess,nuaguess]
489 | else:
490 | print("Unspecified HF limit!")
491 |
492 | approxdelta = 0
493 | approxnu = (searchrange[0]+searchrange[1])/2
494 |
495 | for k,di in enumerate(self.deltas):
496 | approxdelta += di / (approxnu) ** (2 * k)
497 |
498 | n=np.round(approxnu+approxdelta)
499 |
500 | nutheor=n
501 |
502 | for k,di in enumerate(self.deltas):
503 | nutheor += - di / (n-self.deltas[0]) ** (2 * k)
504 |
505 | if self.HFlimit == "upper" or self.HFlimit == None:
506 | return np.round(nutheor, decimals=accuracy-1)
507 | elif self.HFlimit == "lower":
508 | return np.round(self.nux(self.ionizationlimits_invcm[self.nulimb[0]],self.ionizationlimits_invcm[self.nulima[0]],nutheor), decimals=accuracy-1)
509 |
510 |
511 | def channelcontributions(self, nub):
512 | ''' Channel contributions for single channel model set to be 1.0.
513 | '''
514 | Ai = [1.0]
515 | Aalpha = [1.0]
516 | return [Ai,Aalpha]
517 |
--------------------------------------------------------------------------------
/rydcalc/Yb171_NIST.txt:
--------------------------------------------------------------------------------
1 | [{"Ref.":"1. 2. PHYSICAL REVIEW A 100, 042505 (2019), 3. Porsev PRA 2004"},
2 | {"n": 6, "l": 1, "S": 1, "J": 0, "E_cm": 17288.439, "A_GHz": 0 , "Ref.": [1]},
3 | {"n": 6, "l": 1, "S": 1, "J": 1, "E_cm": 17992.007, "A_GHz": 3957754, "Ref.": [1,2]},
4 | {"n": 6, "l": 1, "S": 1, "J": 2, "E_cm": 19710.388, "A_GHz": 2.677, "Ref.":[1,3]},
5 | {"n": 6, "l": 1, "S": 0, "J": 1, "E_cm": 19710.388, "A_GHz":0, "Ref.": [1]}]
6 |
--------------------------------------------------------------------------------
/rydcalc/Yb174_NIST.txt:
--------------------------------------------------------------------------------
1 | [{"Ref.":"1. NIST tables"},
2 | {"n": 6, "l": 0, "S": 0, "J": 0, "E_cm": 0.0, "Ref.": [1]},
3 | {"n": 6, "l": 1, "S": 1, "J": 0, "E_cm": 17288.439, "Ref.": [1]},
4 | {"n": 6, "l": 1, "S": 1, "J": 1, "E_cm": 17992.007, "Ref.": [1]},
5 | {"n": 6, "l": 1, "S": 1, "J": 2, "E_cm": 19710.388, "Ref.": [1]},
6 | {"n": 5, "l": 2, "S": 1, "J": 1, "E_cm": 24489.102, "Ref.": [1]},
7 | {"n": 5, "l": 2, "S": 1, "J": 2, "E_cm": 24751.948, "Ref.": [1]},
8 | {"n": 5, "l": 2, "S": 1, "J": 3, "E_cm": 25270.902, "Ref.": [1]},
9 | {"n": 6, "l": 1, "S": 0, "J": 1, "E_cm": 25068.222, "Ref.": [1]},
10 | {"n": 5, "l": 2, "S": 0, "J": 2, "E_cm": 27677.665, "Ref.": [1]},
11 | {"n": 7, "l": 0, "S": 1, "J": 1, "E_cm": 32694.692, "Ref.": [1]},
12 | {"n": 7, "l": 0, "S": 0, "J": 0, "E_cm": 34350.65, "Ref.": [1]},
13 | {"n": 7, "l": 1, "S": 1, "J": 0, "E_cm": 38090.71, "Ref.": [1]},
14 | {"n": 7, "l": 1, "S": 1, "J": 1, "E_cm": 38174.17, "Ref.": [1]},
15 | {"n": 7, "l": 1, "S": 1, "J": 2, "E_cm": 38551.93, "Ref.": [1]},
16 | {"n": 6, "l": 2, "S": 1, "J": 1, "E_cm": 39808.72, "Ref.": [1]},
17 | {"n": 6, "l": 2, "S": 1, "J": 2, "E_cm": 39838.04, "Ref.": [1]},
18 | {"n": 6, "l": 2, "S": 1, "J": 3, "E_cm": 39966.09, "Ref.": [1]},
19 | {"n": 6, "l": 2, "S": 0, "J": 2, "E_cm": 40061.51, "Ref.": [1]},
20 | {"n": 7, "l": 1, "S": 0, "J": 1, "E_cm": 40563.97, "Ref.": [1]},
21 | {"n": 8, "l": 0, "S": 1, "J": 1, "E_cm": 41615.04, "Ref.": [1]},
22 | {"n": 8, "l": 0, "S": 0, "J": 0, "E_cm": 41939.9, "Ref.": [1]},
23 | {"n": 5, "l": 3, "S": 1, "J": 2, "E_cm": 43433.85, "Ref.": [1]},
24 | {"n": 8, "l": 1, "S": 1, "J": 0, "E_cm": 43614.27, "Ref.": [1]},
25 | {"n": 8, "l": 1, "S": 1, "J": 1, "E_cm": 43659.38, "Ref.": [1]},
26 | {"n": 8, "l": 1, "S": 1, "J": 2, "E_cm": 43805.69, "Ref.": [1]},
27 | {"n": 8, "l": 1, "S": 0, "J": 1, "E_cm": 44017.6, "Ref.": [1]},
28 | {"n": 7, "l": 2, "S": 1, "J": 1, "E_cm": 44311.38, "Ref.": [1]},
29 | {"n": 7, "l": 2, "S": 1, "J": 2, "E_cm": 44313.05, "Ref.": [1]},
30 | {"n": 7, "l": 2, "S": 1, "J": 3, "E_cm": 44380.82, "Ref.": [1]},
31 | {"n": 7, "l": 2, "S": 0, "J": 2, "E_cm": 44357.6, "Ref.": [1]},
32 | {"n": 9, "l": 0, "S": 1, "J": 1, "E_cm": 45121.37, "Ref.": [1]},
33 | {"n": 6, "l": 3, "S": 1, "J": 2, "E_cm": 45956.27, "Ref.": [1]},
34 | {"n": 9, "l": 1, "S": 1, "J": 1, "E_cm": 46078.91, "Ref.": [1]},
35 | {"n": 9, "l": 1, "S": 1, "J": 0, "E_cm": 46082.17, "Ref.": [1]},
36 | {"n": 9, "l": 1, "S": 1, "J": 2, "E_cm": 46184.15, "Ref.": [1]},
37 | {"n": 9, "l": 1, "S": 0, "J": 1, "E_cm": 46370.3, "Ref.": [1]},
38 | {"n": 8, "l": 2, "S": 1, "J": 1, "E_cm": 46444.96, "Ref.": [1]},
39 | {"n": 8, "l": 2, "S": 1, "J": 2, "E_cm": 46467.7, "Ref.": [1]},
40 | {"n": 8, "l": 2, "S": 1, "J": 3, "E_cm": 46480.65, "Ref.": [1]},
41 | {"n": 10, "l": 0, "S": 1, "J": 1, "E_cm": 46877.1, "Ref.": [1]},
42 | {"n": 7, "l": 3, "S": 1, "J": 2, "E_cm": 47326.65, "Ref.": [1]},
43 | {"n": 11, "l": 0, "S": 1, "J": 1, "E_cm": 47885.81, "Ref.": [1]},
44 | {"n": 12, "l": 0, "S": 1, "J": 1, "E_cm": 48519.71, "Ref.": [1]},
45 | {"n": 13, "l": 0, "S": 1, "J": 1, "E_cm": 48943.42, "Ref.": [1]}]
46 |
--------------------------------------------------------------------------------
/rydcalc/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .ponderomotive import *
3 | from .db_manager import *
4 | from .constants import *
5 |
6 | from .single_basis import *
7 | from .pair_basis import *
8 |
9 | from .hydrogen import *
10 | from .alkali import *
11 | from .alkaline import *
12 |
13 | from .alkali_data import *
14 | from .alkaline_data import *
15 | from .alkaline_hyperfine import *
16 |
17 | from .utils import *
18 |
19 | from .rydcalc import *
20 | from .analysis import *
--------------------------------------------------------------------------------
/rydcalc/alkali_data.py:
--------------------------------------------------------------------------------
1 | from .alkali import *
2 |
3 | class Hydrogen_n(AlkaliAtom):
4 | """ Hydrogen, but using numerical techniques from alkali atoms """
5 |
6 | name = 'Hydrogen_n'
7 | mass = 1
8 |
9 | Z = 1
10 |
11 | ground_state_n = 1
12 |
13 | def __init__(self,**kwargs):
14 |
15 | self.model_pot = model_potential(0, [0] * 4, [0] * 4, [0] * 4, [0] * 4, [1e-3] * 4,
16 | self.Z, include_so=True, use_model=False)
17 |
18 | self.core = core_state((0,0,0,0,0),0,tt='sljif',config='p',potential = self.model_pot)
19 |
20 | self.channels = []
21 | self.defects = []
22 | self.defects.append(defect_model(0,polcorrection=False,relcorrection=False,SOcorrection=False))
23 |
24 | super().__init__(**kwargs)
25 |
26 | class Rubidium87(AlkaliAtom):
27 | """
28 | Properites of rubidium 87 atoms
29 | """
30 |
31 | name = 'Rb'
32 | dipole_data_file = 'rb_dipole_matrix_elements.npy'
33 | dipole_db_file = 'rb_dipole.db'
34 |
35 | # ALL PARAMETERES ARE IN ATOMIC UNITS (HARTREE)
36 | mass = 86.9091805310
37 | Z = 37
38 |
39 | ground_state_n = 5
40 |
41 | citations = ['Mack2011Measurement','Li2003Millimeter','Han2006Rb','Berl2020Core','Marinescu1994Dispersion']
42 |
43 | defects = []
44 | #defects.append(defect_Rydberg_Ritz([3.1311804, 0.1784], condition = lambda qns: qns['j']==1/2 and qns['l']==0)) , #85Rb from PHYSICAL REVIEW A 83, 052515 (2011)
45 | defects.append(defect_Rydberg_Ritz([3.1311807, 0.1787], condition=lambda qns: qns['j'] == 1/2 and qns['l'] == 0)), #87Rb from PHYSICAL REVIEW A 83, 052515 (2011)
46 | defects.append(defect_Rydberg_Ritz([2.6548849, 0.2900], condition = lambda qns: qns['j']==1/2 and qns['l']==1)) # from PHYSICAL REVIEW A 67, 052502 (2003)
47 | defects.append(defect_Rydberg_Ritz([2.6416737, 0.2950], condition = lambda qns: qns['j']==3/2 and qns['l']==1)) # from PHYSICAL REVIEW A 67, 052502 (2003)
48 | defects.append(defect_Rydberg_Ritz([1.34809171, -0.60286], condition = lambda qns: qns['j']==3/2 and qns['l']==2)) # from PHYSICAL REVIEW A 67, 052502 (2003)
49 | defects.append(defect_Rydberg_Ritz([1.34646572, -0.59600], condition = lambda qns: qns['j']==5/2 and qns['l']==2)) # from PHYSICAL REVIEW A 67, 052502 (2003)
50 | defects.append(defect_Rydberg_Ritz([0.0165192, -0.085], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 3)) # from 85Rb PHYSICAL REVIEW A 74, 054502 (2006)
51 | defects.append(defect_Rydberg_Ritz([0.0165437, -0.086], condition=lambda qns: qns['j'] == 7 / 2 and qns['l'] == 3)) # from 85Rb PHYSICAL REVIEW A 74, 054502 (2006)
52 | defects.append(defect_Rydberg_Ritz([0.004007, -0.02742], condition=lambda qns: qns['l'] == 4,SOcorrection=True)) # PHYSICAL REVIEW A 102, 062818 (2020)
53 | defects.append(defect_Rydberg_Ritz([0.001423, -0.01438], condition=lambda qns: qns['l'] == 5,SOcorrection=True)) # PHYSICAL REVIEW A 102, 062818 (2020)
54 | defects.append(defect_Rydberg_Ritz([0.0006074, -0.008550], condition=lambda qns: qns['l'] == 6,SOcorrection=True)) # PHYSICAL REVIEW A 102, 062818 (2020)
55 | defects.append(defect_model(0,polcorrection=True,relcorrection=True,SOcorrection=True)) # assume ''hydrogenic'' behaviour for remaining states
56 |
57 | def __init__(self,**kwargs):
58 |
59 | model_pot = model_potential(9.0760,[3.69628474, 4.44088978, 3.78717363, 2.39848933],
60 | [1.64915255, 1.92828831, 1.57027864, 1.76810544],
61 | [-9.86069196, -16.79597770, -11.65588970, -12.07106780],
62 | [0.19579987, -0.8163314, 0.52942835, 0.77256589],
63 | [1.66242117, 1.50195124, 4.86851938, 4.79831327],
64 | self.Z,include_so = True)#Phys. Rev. A 49, 982 (1994)
65 |
66 | self.core = core_state((0,0,0,0,0),0,tt='sljif',config='Kr+',potential = model_pot,alpha_d_a03 = 9.116, alpha_q_a05 = 38.4)# PHYSICAL REVIEW A 102, 062818 (2020)
67 |
68 | I = 1.5 # 3/2
69 |
70 | self.channels = []
71 |
72 | super().__init__(**kwargs)
73 |
74 |
75 | class Potassium39(AlkaliAtom):
76 | """
77 | Properites of potassium 39 atoms
78 | """
79 |
80 | name = 'K'
81 | dipole_data_file = 'k_dipole_matrix_elements.npy'
82 | dipole_db_file = 'k_dipole.db'
83 |
84 | # ALL PARAMETERES ARE IN ATOMIC UNITS (HARTREE)
85 | mass = 38.9637064864
86 | Z = 19
87 |
88 | ground_state_n = 4
89 |
90 | citations = ['Peper2019Precision','Risberg1956A','Johansson1972An','Lorenzen1981Precise','Lorenzen1983Quantum','Marinescu1994Dispersion']
91 |
92 | defects = []
93 | defects.append(defect_Rydberg_Ritz([2.18020826, 0.134534, 0.0952, 0.0021], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 0)) # PHYSICAL REVIEW A 100, 012501 (2019)
94 | defects.append(defect_Rydberg_Ritz([1.71392626, 0.23114, 0.1948, 0.3683], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 100, 012501 (2019)
95 | defects.append(defect_Rydberg_Ritz([1.71087854, 0.23233, 0.1961, 0.3716], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 100, 012501 (2019)
96 | defects.append(defect_Rydberg_Ritz([0.27698453, -1.02691, -0.665, 10.9], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 2)) # PHYSICAL REVIEW A 100, 012501 (2019)
97 | defects.append(defect_Rydberg_Ritz([0.27715665, -1.02493, -0.640, 10.0], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 2)) # PHYSICAL REVIEW A 100, 012501 (2019)
98 | defects.append(defect_Rydberg_Ritz([0.0094576, -0.0446], condition=lambda qns: qns['l'] == 3)) # PHYSICAL REVIEW A 100, 012501 (2019)
99 | defects.append(defect_Rydberg_Ritz([0.0024080, -0.0209], condition=lambda qns: qns['l'] == 4,SOcorrection=True)) # PHYSICAL REVIEW A 100, 012501 (2019)
100 | defects.append(defect_model(0,polcorrection=True,relcorrection=True,SOcorrection=True)) # assume ''hydrogenic'' behaviour for remaining states
101 |
102 | def __init__(self, **kwargs):
103 | model_pot = model_potential(5.3310, [3.56079437, 3.65670429, 4.12713694, 1.42310446],
104 | [1.83909642, 1.67520788, 1.79837462, 1.27861156],
105 | [-1.74701102, -2.07416615, -1.69935174, 4.77441476],
106 | [-1.03237313, -0.89030421, -0.98913582, -0.94829262],
107 | [0.83167545, 0.85235381, 0.83216907, 6.50294371],
108 | self.Z, include_so=True)#Phys. Rev. A 49, 982 (1994)
109 |
110 | self.core = core_state((0, 0, 0, 0, 0), 0, tt='sljif', config='Ar+', potential=model_pot,alpha_d_a03 = 5.4880, alpha_q_a05 = 17.89)# PHYSICAL REVIEW A 100, 012501 (2019)
111 |
112 | I = 1.5 # 3/2
113 |
114 | self.channels = []
115 |
116 | super().__init__(**kwargs)
117 |
118 |
119 |
120 | class Cesium133(AlkaliAtom):
121 | """
122 | Properites of cesium 133 atoms
123 | """
124 |
125 | name = 'Cs'
126 | dipole_data_file = 'cs_dipole_matrix_elements.npy'
127 | dipole_db_file = 'cs_dipole.db'
128 |
129 | # ALL PARAMETERES ARE IN ATOMIC UNITS (HARTREE)
130 | mass = 132.9054519610
131 | Z = 55
132 |
133 | ground_state_n = 6
134 |
135 | defects = []
136 | defects.append(defect_Rydberg_Ritz([4.0493532, 0.239, 0.06, 11,-209], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 0)) # PHYSICAL REVIEW A 93, 013424 (2016)
137 | defects.append(defect_Rydberg_Ritz([3.5915871, 0.36273], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 93, 013424 (2016)
138 | defects.append(defect_Rydberg_Ritz([3.5590676, 0.37469], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 93, 013424 (2016)
139 | defects.append(defect_Rydberg_Ritz([2.475365, 0.5554], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 2)) # PHYSICAL REVIEW A 26, 2733 (1982)
140 | defects.append(defect_Rydberg_Ritz([2.4663144, 0.01381, -0.392, -1.9], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 2)) # PHYSICAL REVIEW A 93, 013424 (2016)
141 | defects.append(defect_Rydberg_Ritz([0.033392, -0.191], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 3)) # PHYSICAL REVIEW A 26, 2733 (1982)
142 | defects.append(defect_Rydberg_Ritz([0.033537, -0.191], condition=lambda qns: qns['j'] == 7 / 2 and qns['l'] == 3)) # PHYSICAL REVIEW A 26, 2733 (1982)
143 | defects.append(defect_Rydberg_Ritz([0.00703865, -0.049252,0.0129], condition=lambda qns: qns['l'] == 4,SOcorrection=True)) # PHYSICAL REVIEW A 35, 4650 (1987)
144 | defects.append(defect_model(0,polcorrection=True,relcorrection=True,SOcorrection=True)) # assume ''hydrogenic'' behaviour for remaining states
145 |
146 |
147 |
148 | def __init__(self, **kwargs):
149 | model_pot = model_potential(15.6440, [3.495463, 4.69366096, 4.32466196, 3.01048361],
150 | [1.47533800, 1.71398344, 1.61365288, 1.40000001],
151 | [-9.72143084, -24.65624280, -6.70128850, -3.20036138],
152 | [0.02629242, -0.09543125, -0.74095193, 0.00034538],
153 | [1.92046930, 2.13383095, 0.93007296, 1.99969677],
154 | self.Z, include_so=True)#Phys. Rev. A 49, 982 (1994)
155 |
156 | self.core = core_state((0, 0, 0, 0, 0), 0, tt='sljif', config='Ar+', potential=model_pot,alpha_d_a03 = 15.5440, alpha_q_a05 = 70.7) # Phys. Rev. A 22, 2672 (1980)
157 |
158 | I = 3.5 # 7/2
159 |
160 | self.channels = []
161 |
162 | super().__init__(**kwargs)
163 |
164 |
165 |
166 | class Lithium7(AlkaliAtom):
167 | """
168 | Properites of lithium 7 atoms
169 | """
170 |
171 | name = 'Li'
172 | dipole_data_file = 'li_dipole_matrix_elements.npy'
173 | dipole_db_file = 'li_dipole.db'
174 |
175 | # ALL PARAMETERES ARE IN ATOMIC UNITS (HARTREE)
176 | mass = 7.0160034366
177 | Z = 3
178 |
179 | ground_state_n = 2
180 |
181 | defects = []
182 | defects.append(defect_Rydberg_Ritz([0.3995101, 0.029], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 0)) # PHYSICAL REVIEW A 34, 2889 (1986)
183 | defects.append(defect_Rydberg_Ritz([0.0471835, -0.024], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 34, 2889 (1986)
184 | defects.append(defect_Rydberg_Ritz([0.0471720, -0.024], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 1)) # PHYSICAL REVIEW A 34, 2889 (1986)
185 | defects.append(defect_Rydberg_Ritz([0.002129, -0.01491,0.1759,-0.8507], condition=lambda qns: qns['l'] == 2)) # Ark. f. Fysik 15,169 (1958), Phys. Scr. 27 300 (1983)
186 | defects.append(defect_Rydberg_Ritz([-0.000077, 0.021856, -0.4211, 2.3891], condition=lambda qns: qns['l'] == 3)) # Ark. f. Fysik 15,169 (1958), Phys. Scr. 27 300 (1983)
187 | defects.append(defect_model(0,polcorrection=True,relcorrection=True,SOcorrection=True)) # assume ''hydrogenic'' behaviour for remaining states
188 |
189 |
190 |
191 | def __init__(self, **kwargs):
192 | model_pot = model_potential(0.1923, [2.47718079, 3.45414648, 2.51909839, 2.51909839],
193 | [1.84150932, 2.55151080, 2.43712450, 2.43712450],
194 | [-0.02169712, -0.21646561, 0.32505524, 0.32505524],
195 | [-0.11988362, -0.06990078, 0.10602430, 0.10602430],
196 | [0.61340824, 0.61566441, 2.34126273, 2.34126273],
197 | self.Z, include_so=True)#Phys. Rev. A 49, 982 (1994)
198 |
199 | self.core = core_state((0, 0, 0, 0, 0), 0, tt='sljif', config='He+', potential=model_pot,alpha_d_a03 = 0.1884, alpha_q_a05 = 0.046) # Phys. Rev. A 16, 1141 (1977)
200 |
201 | I = 1.5 # 3/2
202 |
203 | self.channels = []
204 |
205 | super().__init__(**kwargs)
206 |
207 | class Sodium23(AlkaliAtom):
208 | """
209 | Properites of sodium 23 atoms
210 | """
211 |
212 | name = 'Na'
213 | dipole_data_file = 'na_dipole_matrix_elements.npy'
214 | dipole_db_file = 'na_dipole.db'
215 |
216 | # ALL PARAMETERES ARE IN ATOMIC UNITS (HARTREE)
217 | mass = 22.9897692820
218 | Z = 11
219 |
220 | ground_state_n = 3
221 |
222 | defects = []
223 | defects.append(defect_Rydberg_Ritz([1.34796938, 0.0609892,0.0196743,-0.001045], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 0)) # PHYSICAL REVIEW A 45, 4720 (1992)
224 | defects.append(defect_Rydberg_Ritz([0.85544502, 0.112067,0.0479,0.0457], condition=lambda qns: qns['j'] == 1 / 2 and qns['l'] == 1)) # Quantum Electron. 25 914 (1995)
225 | defects.append(defect_Rydberg_Ritz([0.85462615, 0.112344,0.0497,0.0406], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 1)) # Quantum Electron. 25 914 (1995)
226 | defects.append(defect_Rydberg_Ritz([0.014909286, -0.042506,0.00840], condition=lambda qns: qns['j'] == 3 / 2 and qns['l'] == 2)) # Quantum Electron. 25 914 (1995)
227 | defects.append(defect_Rydberg_Ritz([0.01492422, -.042585,0.00840], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 2)) # Quantum Electron. 25 914 (1995)
228 | defects.append(defect_Rydberg_Ritz([0.001632977, -0.0069906, 0.00423], condition=lambda qns: qns['j'] == 5 / 2 and qns['l'] == 3)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
229 | defects.append(defect_Rydberg_Ritz([0.001630875, -0.0069824, 0.00352], condition=lambda qns: qns['j'] == 7 / 2 and qns['l'] == 3)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
230 | defects.append(defect_Rydberg_Ritz([0.00043825, -0.00283], condition=lambda qns: qns['j'] == 7 / 2 and qns['l'] == 4)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
231 | defects.append(defect_Rydberg_Ritz([0.00043740, -0.00297], condition=lambda qns: qns['j'] == 9 / 2 and qns['l'] == 4)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
232 | defects.append(defect_Rydberg_Ritz([0.00016114, -0.00185], condition=lambda qns: qns['j'] == 9 / 2 and qns['l'] == 5)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
233 | defects.append(defect_Rydberg_Ritz([0.00015796, -0.00148], condition=lambda qns: qns['j'] == 11 / 2 and qns['l'] == 5)) # J. Phys. B: At. Mol. Opt. Phys. 30 2345 (1997)
234 | defects.append(defect_model(0,polcorrection=True,relcorrection=True,SOcorrection=True)) # assume ''hydrogenic'' behaviour for remaining states
235 |
236 |
237 |
238 | def __init__(self, **kwargs):
239 | model_pot = model_potential(0.9448, [4.82223117, 5.08382502, 3.53324124, 1.11056646],
240 | [2.45449865, 2.18226881, 2.48697936, 1.05458759],
241 | [-1.12255048, -1.19534623, -0.75688448, 1.73203428],
242 | [-1.42631393, -1.03142861, -1.27852357, -0.09265696],
243 | [0.45489422, 0.45798739, 0.71875312, 28.6735059],
244 | self.Z, include_so=True) #Phys. Rev. A 49, 982 (1994)
245 |
246 | self.core = core_state((0, 0, 0, 0, 0), 0, tt='sljif', config='Ne+', potential=model_pot,alpha_d_a03 = 0.9980, alpha_q_a05 = 0.351) # Phys. Rev. A 38, 4985 (1988)
247 |
248 | I = 1.5 # 3/2
249 |
250 | self.channels = []
251 |
252 | super().__init__(**kwargs)
253 |
254 |
--------------------------------------------------------------------------------
/rydcalc/alkaline.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import sqlite3
3 |
4 | from .alkali import *
5 |
6 |
7 | class AlkalineAtom(AlkaliAtom):
8 |
9 | transitions_ion = []
10 |
11 | def potential_ion(self,lam_nm):
12 | return self.potential(lam_nm,transitions=self.transitions_ion)
13 |
14 | def scattering_rate_ion(self,lam_nm):
15 | return self.scattering_rate(lam_nm,transitions=self.transitions_ion)
16 |
17 | def repr_state(self,st):
18 | return st.pretty_str
19 |
20 | # """ generate a nice ket for printing """
21 | # if st.tt == 'nljm':
22 | # if st.l <= 5:
23 | # return "|%s:%d,%d,%s,%.1f,%.1f>" % (self.name,st.n,2*st.s+1,['S','P','D','F','G','H'][st.l],st.j,st.m)
24 | # else:
25 | # return "|%s:%d,%d,%d,%.1f,%.1f>" % (self.name,st.n,2*st.s+1,st.l,st.j,st.m)
26 | # if st.tt == 'composite':
27 | # out = ""
28 | # #FIXME: trailing + sign
29 | # for p,c in zip(st.parts,st.wf_coeffs):
30 | # out += "%.2e %s + " % (c,p.__repr__())
31 |
32 | # return out
33 |
34 | # FIXME: put in proper Lande g factor
35 | # def get_g(self,st):
36 | # if st.tt == 'composite':
37 | # g = 0
38 | # for p,c in zip(st.parts,st.wf_coeffs):
39 | # g += np.abs(c)**2 * p.get_g()
40 | # else:
41 | # g = super().get_g(st)
42 |
43 | # return g
44 |
45 | def get_state(self,qn,tt='nsljm'):
46 | # first, find suitable channel
47 |
48 | if tt == 'nsljm' and len(qn)==5:
49 |
50 | n = qn[0]
51 | s = qn[1]
52 | l = qn[2]
53 | j = qn[3]
54 | m = qn[4]
55 |
56 | # for defect model
57 | qns = {'n': n, 's':s, 'l': l, 'j': j}
58 |
59 | if s < 0 or l < 0 or l >= n or np.abs(m) > j or j < np.abs(l-s) or j > l+s:
60 | return None
61 |
62 | if l <= 5:
63 | pretty_str = "|%s:%d,%d,%s,%.1f,%.1f>" % (self.name,n,2*s+1,['S','P','D','F','G','H'][l],j,m)
64 | else:
65 | pretty_str = "|%s:%d,%d,%d,%.1f,%.1f>" % (self.name,n,2*s+1,l,j,m)
66 |
67 | else:
68 | print("tt=",tt," not supported by H.get_state")
69 |
70 | my_ch = None
71 |
72 | # now decide what our core channels should be
73 | # here, we take core electron to be in S=1/2, L=1/2, J=1/2 state
74 | # so we really just have to decide on j of Rydberg electron
75 |
76 | sr = 1/2
77 | lr = l
78 | coeffs = [1]
79 |
80 | if s==1 and j == l+1:
81 | # ie, 3P2
82 | jr = [l + 1/2]
83 | elif s==1 and j == l-1:
84 | # ie, 3P0
85 | jr = [l - 1/2]
86 | else:
87 | # ie, 3P1 or 1P1
88 | jr = [l+1/2,l-1/2]
89 |
90 | theta = self.get_spin_mixing(l)
91 |
92 | rot = np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])
93 |
94 | ls_to_jj = np.array([[np.sqrt(l/(2*l+1)), -np.sqrt((l+1)/(2*l+1))], [np.sqrt((l+1)/(2*l+1)), np.sqrt(l/(2*l+1))]])
95 |
96 | ls_to_jj_rot = rot@ls_to_jj
97 |
98 | # # Eq. 4 in Lurio 1962 [had to change sign to make it work... try to figure out why this is]
99 | # if s==1:
100 | # coeffs = [np.sqrt(l/(2*l+1)), -np.sqrt((l+1)/(2*l+1))]
101 | # else:
102 | # coeffs = [np.sqrt((l+1)/(2*l+1)), np.sqrt(l/(2*l+1))]
103 |
104 | if s==1:
105 | coeffs = ls_to_jj_rot[0]
106 | else:
107 | coeffs = ls_to_jj_rot[1]
108 |
109 | chs = []
110 |
111 | for ij,ic in zip(jr,coeffs):
112 |
113 | my_ch = None
114 |
115 | # search through existing channels
116 | for ch in self.channels:
117 | if ch.l == lr and ch.j == ij and ch.s == sr:
118 | my_ch = ch
119 | break
120 |
121 | # if we didn't find a channel, make a new one
122 | if my_ch is None:
123 | my_ch = channel(self.core,(sr,lr,ij),tt='slj')
124 | self.channels.append(my_ch)
125 |
126 | chs.append(my_ch)
127 |
128 | defect = self.get_quantum_defect(qns)
129 | energy_Hz = self.get_energy_Hz_from_defect(n,defect)
130 |
131 | #__init__(self,atom,qn,channel,energy = None,tt='npfm'):
132 | st = state_mqdt(self,(n,(-1)**l,j,m),coeffs,chs,energy_Hz = energy_Hz)
133 | st.pretty_str = pretty_str
134 | return st
135 |
136 | def get_nearby(self,st,include_opts={}):
137 | """ generate a list of quantum number tuples specifying nearby states for sb.fill().
138 | include_opts can override options in terms of what states are included.
139 |
140 | It's a little messy to decide which options should be handled here vs. in single_basis
141 | deecision for now is to have all quantum numbers here but selection rules/energy cuts
142 | in single_basis to avoid duplication of code.
143 |
144 | Does not create states or check valid qn, just returns list of tuples. """
145 |
146 | ret = []
147 |
148 | o = {'dn': 2, 'dl': 2, 'dm': 1, 'ds': 0}
149 |
150 | for k,v in include_opts.items():
151 | o[k] = v
152 |
153 | for n in np.arange(st.n-o['dn'],st.n+o['dn']+1):
154 | for s in np.arange(max(0,st.channels[0].s-o['ds']), st.channels[0].s+o['ds']+1):
155 | for l in np.arange(st.channels[0].l-o['dl'],st.channels[0].l+o['dl']+1):
156 | for j in np.arange(st.f-o['dl'],st.f+o['dl']+1):
157 | for m in np.arange(st.m-o['dm'],st.m+o['dm']+1):
158 |
159 | ret.append((n,s,l,j,m))
160 |
161 | return ret
162 |
163 | def get_magnetic_me(self, st, other, cutoffnu=10):
164 | """ Return the magnetic dipole matrix element between st and other.
165 |
166 | Calculated using Eq. 21 of Robichaux et al, 10.1103/PhysRevA.97.022508
167 | """
168 |
169 | def lam(x):
170 | return np.sqrt((2 * x + 1) * (x + 1) * x)
171 |
172 | reduced_me = 0
173 | Ft = st.f
174 | Ftdash = other.f
175 |
176 | muB = cs.physical_constants['Bohr magneton'][0]
177 | muN = cs.physical_constants['nuclear magneton'][0]
178 | gs = -cs.physical_constants['electron g factor'][0]
179 | muI = self.gI * muN
180 |
181 | if st.m != other.m or np.abs(st.f - other.f) > 1 or st.parity != other.parity:
182 | return 0
183 |
184 | prefactor = (-1) ** (st.f - st.m) * wigner_3j(st.f, 1, other.f, -st.m, 0, other.m)
185 |
186 | for ii, (Ai, chi) in enumerate(zip(st.Ai, st.channels)):
187 | for jj, (Aj, chj) in enumerate(zip(other.Ai, other.channels)):
188 |
189 | chinu = 1 / np.sqrt((chi.core.Ei_Hz - st.energy_Hz) / (self.RydConstHz))
190 | chjnu = 1 / np.sqrt((chj.core.Ei_Hz - other.energy_Hz) / (self.RydConstHz))
191 |
192 | if np.abs(chinu - chjnu) > cutoffnu: # cut-off for small overlap
193 | continue
194 |
195 | if chi.no_me or chj.no_me:
196 | continue
197 |
198 | if chi.core.l < 0 or chi.l != chj.l or chi.s != chj.s or chi.core.i != chj.core.i or chi.core.l != chj.core.l or chi.core.s != chj.core.s:
199 | # chi.core.l>=0 to exclude unknown effective core states. implemented with l=-1
200 | continue
201 |
202 | ll = self.G1(chi, chj, Ft, Ftdash) * self.G2(chi, chj) * lam(chi.l)
203 | ss = self.G1(chi, chj, Ft, Ftdash) * self.G3(chi, chj) * lam(chi.s)
204 | II = self.G4(chi, chj, Ft, Ftdash) * self.G5(chi, chj) * lam(chi.core.i)
205 | LL = self.G4(chi, chj, Ft, Ftdash) * self.G6(chi, chj) * self.G7(chi, chj) * lam(chi.core.l)
206 | SS = self.G4(chi, chj, Ft, Ftdash) * self.G6(chi, chj) * self.G8(chi, chj) * lam(chi.core.s)
207 |
208 | if chinu == chjnu:
209 | overlap = 1
210 | else:
211 | overlap = (2 * np.sqrt(chinu * chjnu) / (chinu + chjnu)) * (
212 | np.sin(np.pi * (chinu - chi.l) - np.pi * (chjnu - chj.l)) / (
213 | np.pi * (chinu - chi.l) - np.pi * (chjnu - chj.l)))
214 |
215 | reduced_me += overlap * Ai * np.conjugate(Aj) * (muB * (LL + ll + gs * (SS + ss)) - muI * II)
216 |
217 | # need to implement other q for coupling
218 | # mu+=overlap*Ai*np.conjugate(Aj)*np.conjugate(((-1)**(st.f-st.m))*wigner_3j(st.f,1,other.f,-st.m,0,other.m))*(muB*(LL+ll+gs*(SS+ss))-muI*II)
219 |
220 | me = prefactor * reduced_me
221 | return me / muB
222 |
223 | def _dipole_db_query(self,s1,s2,rwi,me=0):
224 |
225 | # Provide strings to query to/from dipole matrix element database.
226 | # Ideally, this is agnostic about which type of matrix element is being stored,
227 | # and is the only thing that needs to be modified for different types of atoms
228 | # with different quantum numbers.
229 |
230 | # if we re-imported ARC databases with spin, we could probably get away
231 | # without this since all (conceivably interesting) atoms could be described
232 | # by nslj
233 |
234 | # Given the need to have different strings for creating tables and loading
235 | # from files, this function has become a bit ugly
236 |
237 | # length of this should be 2*(number of quantum numbers) + 1 for matrix element
238 | insert_str = "(?,?,?,?,?,?,?,?,?)"
239 |
240 | if rwi=='i':
241 | # query for initializing database
242 | query_str = '''(n1 TINYINT UNSIGNED, s1 TINYINT UNSIGNED, l1 TINYINT UNSIGNED,
243 | j1_x2 TINYINT UNSIGNED,
244 | n2 TINYINT UNSIGNED, s2 TINYINT UNSIGNED, l2 TINYINT UNSIGNED,
245 | j2_x2 TINYINT UNSIGNED,
246 | dme DOUBLE,
247 | PRIMARY KEY (n1,s1,l1,j1_x2,n2,s2,l2,j2_x2 )
248 | )'''
249 |
250 | return query_str
251 |
252 | if rwi=='wf':
253 | # query for writing to db from file
254 | query_str = insert_str
255 |
256 | return query_str
257 |
258 | if rwi=='rf':
259 | # query for read from db to save to file
260 | query_str = 'n1,s1,l1,j1_x2,n2,s2,l2,j2_x2'
261 | return query_str
262 |
263 | # database is ordered by energy
264 | if self.get_energy_au(s1) < self.get_energy_au(s2):
265 | s1_o = s1
266 | s2_o = s2
267 | else:
268 | s1_o = s2
269 | s2_o = s1
270 |
271 | if rwi=='r':
272 | # query for reading from database
273 | query_str = "n1= ? AND s1 = ? AND l1 = ? AND j1_x2 = ? AND n2 = ? AND s2 = ? AND l2 = ? AND j2_x2 = ?"
274 | query_dat = (s1_o.n, s1_o.s, s1_o.l, int(2*s1_o.j), s2_o.n, s2_o.s, s2_o.l, int(2*s2_o.j))
275 |
276 | if rwi=='w':
277 | # query for writing to database (storing matrix element me)
278 | query_str = insert_str
279 | query_dat = (s1_o.n, s1_o.s, s1_o.l, int(2*s1_o.j), s2_o.n, s2_o.s, s2_o.l, int(2*s2_o.j),me)
280 |
281 | return query_str, query_dat
282 |
283 |
284 |
285 |
--------------------------------------------------------------------------------
/rydcalc/alkaline_data.py:
--------------------------------------------------------------------------------
1 | from .alkaline import *
2 | from .MQDTclass import *
3 | from .utils import model_params
4 | import json
5 |
6 | import csv, importlib.resources
7 |
8 | class Ytterbium174(AlkalineAtom):
9 |
10 | I = 0
11 | Z = 70
12 |
13 | name = '174Yb'
14 |
15 | mass = 173.9388664
16 |
17 | model_pot = model_potential(0, [0] * 4, [0] * 4, [0] * 4, [0] * 4, [1e-3] * 4,
18 | Z, include_so=True, use_model=False)
19 |
20 | gI = 0
21 |
22 | Elim_cm = 50443.070417
23 | Elim_THz = Elim_cm * (cs.c * 100 * 1e-12)
24 |
25 | RydConstHz = cs.physical_constants["Rydberg constant times c in Hz"][0] * \
26 | (1 - cs.physical_constants["electron mass"][0] / (mass * cs.physical_constants["atomic mass constant"][0]))
27 |
28 |
29 | def __init__(self, params=None, **kwargs):
30 |
31 | self.citations = ['Peper2024Spectroscopy', 'Aymar1984three', 'Meggers1970First', 'Camus1980Highly', 'Camus1969spectre', 'Meggers1970First', 'Wyart1979Extended', 'Aymar1980Highly', 'Camus1980Highly', 'Aymar1984three', 'Martin1978Atomic', 'BiRu1991The', 'Maeda1992Optical', 'zerne1996lande', 'Ali1999Two', 'Lehec2017PhD', 'Lehec2018Laser']
32 |
33 | my_params = {
34 | }
35 |
36 | if params is not None:
37 | my_params.update(params)
38 |
39 | self.p = model_params(my_params)
40 |
41 |
42 | self.mqdt_models = []
43 | self.channels = []
44 |
45 | self.Elim_cm = 50443.070393
46 | self.Elim_THz = self.Elim_cm*cs.c*10**(-10) + self.p.value('174Yb_Elim_offset_MHz', 0, 1) * 1e-6
47 |
48 |
49 |
50 | # For 1S0 MQDT model
51 |
52 | mqdt_1S0 = {'cores': [
53 | core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
54 | config='6s1/2', potential=self.model_pot),
55 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
56 | config='4f135d6s (a)', potential=self.model_pot),
57 | core_state((1 / 2, 1, 3 / 2, 0, 0), Ei_Hz=(80835.39 - self.Elim_cm) * cs.c * 100, tt='sljif',
58 | config='6p3/2', potential=self.model_pot),
59 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
60 | config='4f135d6s (b)', potential=self.model_pot),
61 | core_state((1 / 2, 1, 1 / 2, 0, 0), Ei_Hz=(77504.98 - self.Elim_cm) * cs.c * 100, tt='sljif',
62 | config='6p1/2', potential=self.model_pot),
63 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
64 | config='4f135d6s (c)', potential=self.model_pot)
65 | ]}
66 |
67 | mqdt_1S0.update({
68 | 'channels': [
69 | channel(mqdt_1S0['cores'][0], (1 / 2, 0, 1 / 2), tt='slj'),
70 | channel(mqdt_1S0['cores'][1], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
71 | channel(mqdt_1S0['cores'][2], (1 / 2, 1, 3 / 2), tt='slj', no_me=True),
72 | channel(mqdt_1S0['cores'][3], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
73 | channel(mqdt_1S0['cores'][4], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
74 | channel(mqdt_1S0['cores'][5], (1 / 2, 1, 1 / 2), tt='slj', no_me=True)
75 | ]})
76 |
77 | # From LS-jj transformation.
78 | Uiabar1S0 = np.identity(6)
79 | Uiabar1S0[2, 2] = -np.sqrt(2 / 3)
80 | Uiabar1S0[4, 4] = np.sqrt(2 / 3)
81 | Uiabar1S0[2, 4] = np.sqrt(1 / 3)
82 | Uiabar1S0[4, 2] = np.sqrt(1 / 3)
83 |
84 | self.p.set_prefix('174Yb_1s0')
85 |
86 | MQDT_1S0 = mqdt_class(channels=mqdt_1S0['channels'],
87 | eig_defects=[[self.p.value('mu0',0.355097325), self.p.value('mu0_1',0.278368431)], [self.p.value('mu1',0.204537279)],
88 | [self.p.value('mu2',0.116394359)], [self.p.value('mu3',0.295432196)], [self.p.value('mu4',0.25765161)],
89 | [self.p.value('mu5',0.155807042)]],
90 | rot_order=[[1, 2], [1, 3], [1, 4], [3, 4], [3, 5], [1, 6]],
91 | rot_angles=[[self.p.value('th12',0.126548585)], [self.p.value('th13',0.300107437)], [self.p.value('th14',0.0570338119)],
92 | [self.p.value('th34',0.114398046)],[self.p.value('th35',0.0986437454)], [self.p.value('th16',0.142482098)]],
93 | Uiabar=Uiabar1S0, nulims=[[2], [0]],atom=self)
94 |
95 | self.mqdt_models.append({'L': 0, 'F': 0, 'model': MQDT_1S0})
96 | self.channels.extend(mqdt_1S0['channels'])
97 |
98 | self.p.set_prefix('174Yb')
99 |
100 | # 3S1 Rydberg Ritz formula
101 | QDT_3S1 = mqdt_class_rydberg_ritz(channels=mqdt_1S0['channels'][0],
102 | deltas=[self.p.value('3s1_rr_%d'%it,val) for it,val in enumerate([4.4382, 4, -10000, 8 * 10 ** 6, -3 * 10 ** 9])],atom=self)
103 |
104 |
105 | self.mqdt_models.append({'L': 0, 'F': 1, 'model': QDT_3S1})
106 |
107 | # For 3P0 MQDT model
108 | mqdt_3P0 = {'cores': [
109 | core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
110 | config='6s1/2', potential=self.model_pot),
111 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
112 | config='4f135d6s (a)', potential=self.model_pot),
113 | ]}
114 |
115 | mqdt_3P0.update({
116 | 'channels': [
117 | channel(mqdt_3P0['cores'][0], (1 / 2, 1, 1 / 2), tt='slj'),
118 | channel(mqdt_3P0['cores'][1], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
119 | ]})
120 |
121 | self.p.set_prefix('174Yb')
122 |
123 | MQDT_3P0 = mqdt_class(channels=mqdt_3P0['channels'],
124 | eig_defects=[[self.p.value('3p0_mu0',0.95356884), self.p.value('3p0_mu0_1',-0.28602498)], [self.p.value('3p0_mu1',0.19845903)]],
125 | rot_order=[[1, 2]],
126 | rot_angles=[[self.p.value('3p0_th12',0.16328854)]],
127 | Uiabar=np.identity(2), nulims=[[1],[0]],atom=self)
128 |
129 | self.mqdt_models.append({'L': 1, 'F': 0, 'model': MQDT_3P0})
130 | self.channels.extend(mqdt_3P0['channels'])
131 |
132 |
133 | # For 1,3P1 MQDT model
134 | mqdt_13P1 = {'cores': [
135 | core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
136 | config='6s1/2', potential=self.model_pot),
137 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
138 | config='4f135d6s (a)', potential=self.model_pot),
139 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
140 | config='4f135d6s (b)', potential=self.model_pot),
141 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
142 | config='4f135d6s (c)', potential=self.model_pot),
143 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
144 | config='4f135d6s (d)', potential=self.model_pot),
145 | ]}
146 |
147 | mqdt_13P1.update({
148 | 'channels': [
149 | channel(mqdt_13P1['cores'][0], (1 / 2, 1, 3 / 2), tt='slj'),
150 | channel(mqdt_13P1['cores'][0], (1 / 2, 1, 1 / 2), tt='slj'),
151 | channel(mqdt_13P1['cores'][1], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
152 | channel(mqdt_13P1['cores'][2], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
153 | channel(mqdt_13P1['cores'][3], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
154 | channel(mqdt_13P1['cores'][4], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
155 | ]})
156 |
157 | # From LS-jj transformation from F. ROBICHEAUX et al. PRA (2018), Eq. (11)
158 | Uiabar13P1 = np.identity(6)
159 | Uiabar13P1[0, 0] = np.sqrt(2 / 3)
160 | Uiabar13P1[0, 1] = 1 / np.sqrt(3)
161 | Uiabar13P1[1, 0] = -1 / np.sqrt(3)
162 | Uiabar13P1[1, 1] = np.sqrt(2 / 3)
163 |
164 | self.p.set_prefix('174Yb')
165 |
166 | MQDT_13P1 = mqdt_class(channels=mqdt_13P1['channels'], # self.p.value('13p1_th13',-0.058),#
167 | eig_defects=[[self.p.value('13p1_mu0', 0.922710983), self.p.value('13p1_mu0_1', 2.60362571)],
168 | [self.p.value('13p1_mu1', 0.982087193), self.p.value('13p1_mu1_1', -5.4562725)], [self.p.value('13p1_mu2', 0.228517204)],
169 | [self.p.value('13p1_mu3', 0.206077587)], [self.p.value('13p1_mu4', 0.193527511)], [self.p.value('13p1_mu5', 0.181530935)]],
170 | rot_order=[[1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [2, 3], [2, 4], [2, 5], [2, 6]],
171 | rot_angles=[[self.p.value('13p1_th12_0', -8.41087098e-02), self.p.value('13p1_th12_2', 1.20375554e+02), self.p.value('13p1_th12_4', -9.31423120e+03)], [self.p.value('13p1_th13', -0.073181559)], [self.p.value('13p1_th14', -0.06651977)], [self.p.value('13p1_th15', -0.0221098858)], [self.p.value('13p1_th16', -0.104516976)], [self.p.value('13p1_th23', 0.0247704758)], [self.p.value('13p1_th24', 0.0576580705)], [self.p.value('13p1_th25', 0.0860627643)], [self.p.value('13p1_th26', 0.0499436344)]],
172 | Uiabar=Uiabar13P1, nulims=[[2, 3, 4, 5], [0, 1]], atom=self)
173 |
174 | self.mqdt_models.append({'L': 1, 'F': 1, 'model': MQDT_13P1})
175 | self.channels.extend(mqdt_13P1['channels'])
176 |
177 | mqdt_3P2 = {'cores': [
178 | core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
179 | config='6s1/2', potential=self.model_pot),
180 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
181 | config='4f135d6s (a)', potential=self.model_pot),
182 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
183 | config='4f135d6s (b)', potential=self.model_pot),
184 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
185 | config='4f135d6s (c)', potential=self.model_pot),
186 | ]}
187 |
188 | mqdt_3P2.update({
189 | 'channels': [
190 | channel(mqdt_3P2['cores'][0], (1 / 2, 1, 3 / 2), tt='slj'),
191 | channel(mqdt_3P2['cores'][1], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
192 | channel(mqdt_3P2['cores'][2], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
193 | channel(mqdt_3P2['cores'][3], (1 / 2, 2, 3 / 2), tt='slj', no_me=True),
194 | ]})
195 |
196 | # From LS-jj transformation from F. ROBICHEAUX et al. PRA (2018), Eq. (11)
197 | Uiabar3P2 = np.identity(4)
198 |
199 | self.p.set_prefix('174Yb')
200 |
201 | MQDT_3P2 = mqdt_class(channels=mqdt_3P2['channels'], # self.p.value('13p1_th13',-0.058),
202 | eig_defects=[[self.p.value('3p2_mu0', 0.925121305), self.p.value('3p2_mu0_1', -2.73247165), self.p.value('3p2_mu0_2', 74.664989)],
203 | [self.p.value('3p2_mu1', 0.230133261)],
204 | [self.p.value('3p2_mu2', 0.209638118)],
205 | [self.p.value('3p2_mu3', 0.186228192)]],
206 | rot_order=[[1, 2], [1, 3],[1,4]],
207 | rot_angles=[[self.p.value('3p2_th12', 0.0706666127)], [self.p.value('3p2_th13', 0.0232711158)], [self.p.value('3p2_th14', -0.0292153659)]],
208 | Uiabar=Uiabar3P2, nulims=[[1,2,3], [0]], atom=self)
209 |
210 | self.mqdt_models.append({'L': 1, 'F': 2, 'model': MQDT_3P2})
211 |
212 | # For 1,3D2 MQDT model
213 | mqdt_13D2 = {'cores': [
214 | core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
215 | config='6s1/2', potential=self.model_pot),
216 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
217 | config='4f135d6s (a)', potential=self.model_pot),
218 | core_state((-1, -1, 1 / 2, 0, 0), Ei_Hz=(83967.7 - self.Elim_cm) * cs.c * 100, tt='sljif',
219 | config='4f135d6s (b)', potential=self.model_pot),
220 | core_state((1 / 2, 1, 1 / 2, 0, 1 / 2), Ei_Hz=(79725.35 - self.Elim_cm) * cs.c * 100, tt='sljif',
221 | config='4f135d6s (c)', potential=self.model_pot),
222 | ]}
223 |
224 | mqdt_13D2.update({
225 | 'channels': [
226 | channel(mqdt_13D2['cores'][0], (1 / 2, 2, 5 / 2), tt='slj'),
227 | channel(mqdt_13D2['cores'][0], (1 / 2, 2, 3 / 2), tt='slj'),
228 | channel(mqdt_13D2['cores'][1], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
229 | channel(mqdt_13D2['cores'][2], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
230 | channel(mqdt_13D2['cores'][3], (1 / 2, 1, 1 / 2), tt='slj', no_me=True),
231 | ]})
232 |
233 | # From LS-jj transformation from F. ROBICHEAUX et al. PRA (2018), Eq. (11)
234 | Uiabar13D2 = np.identity(5)
235 | Uiabar13D2[0, 0] = np.sqrt(3 / 5)
236 | Uiabar13D2[0, 1] = np.sqrt(2 / 5)
237 | Uiabar13D2[1, 0] = - np.sqrt(2 / 5)
238 | Uiabar13D2[1, 1] = np.sqrt(3 / 5)
239 |
240 | self.p.set_prefix('174Yb')
241 |
242 | MQDT_13D2 = mqdt_class(channels=mqdt_13D2['channels'],
243 | eig_defects=[[self.p.value('13d2_mu0',0.729500971), self.p.value('13d2_mu0_1',-0.0284447537)],
244 | [self.p.value('13d2_mu1',0.75229161), self.p.value('13d2_mu1_1',0.0967044398)],
245 | [self.p.value('13d2_mu2', 0.196120406)], [self.p.value('13d2_mu3',0.233821165)],[self.p.value('13d2_mu4',0.152890218)]],
246 | rot_order=[[1,2],[1, 3], [1, 4], [2, 4], [1, 5], [2, 5]],
247 | rot_angles=[[self.p.value('13d2_th12_0',0.21157531),self.p.value('13d2_th12_2',-15.38440215)],[self.p.value('13d2_th13',0.00522534111)], [self.p.value('13d2_th14',0.0398754262)],
248 | [self.p.value('13d2_th24',-0.00720265975)], [self.p.value('13d2_th15',0.104784389)], [self.p.value('13d2_th25',0.0721775002)]],
249 | Uiabar=Uiabar13D2, nulims=[[4],[0, 1]],atom=self)
250 |
251 | self.mqdt_models.append({'L': 2, 'F': 2, 'model': MQDT_13D2})
252 | self.channels.extend(mqdt_13D2['channels'])
253 |
254 | # 3D1 Rydberg Ritz formula
255 | QDT_3D1 = mqdt_class_rydberg_ritz(channels=mqdt_13D2['channels'][1],
256 | deltas=[self.p.value('3d1_rr_0',2.75258093), self.p.value('3d1_rr_1',0.382628525,1), self.p.value('3d1_rr_2',-483.120633,100)],atom=self)
257 |
258 | self.mqdt_models.append({'L': 2, 'F': 1, 'model': QDT_3D1})
259 |
260 | # 3D3 Rydberg Ritz formula
261 | QDT_3D3 = mqdt_class_rydberg_ritz(channels=mqdt_13D2['channels'][0],
262 | deltas=[self.p.value('3d3_rr_0',2.72895315), self.p.value('3d3_rr_1',-0.20653489,1), self.p.value('3d3_rr_2',220.484722,100)],atom=self)
263 |
264 | self.mqdt_models.append({'L': 2, 'F': 3, 'model': QDT_3D3})
265 |
266 | super().__init__(**kwargs)
267 |
268 |
269 | def get_state(self, qn, tt='vlfm', energy_exp_Hz=None, energy_only=False):
270 | """
271 | Retrieves the state for a given set of quantum numbers (nu, l, f, m).
272 |
273 | Note that nu is used as a guess to find an exact nu from the relevant MQDT model. The energy is calculated from the
274 | exact nu corresponding to the bound state, while st.v is rounded to two decimal places to serve as a unique but not overly complex
275 | label for the state.
276 |
277 | Parameters:
278 | qn (list): A list containing the quantum numbers [nu, l, f, m].
279 | tt (str): The type of calculation to perform. Default is 'vlfm'.
280 | energy_exp_Hz (float): The experimental energy in Hz with respect to the lowest ionization limit. Default is None.
281 | energy_only (bool): If True, only the energy is calculated without finding channel contributions. Default is False. Useful for spectrum fitting.
282 |
283 | Returns:
284 | state_mqdt: An instance of the state_mqdt class representing the state information.
285 |
286 | Raises:
287 | ValueError: If the MQDT model for the given quantum numbers is not found.
288 | """
289 | if tt == 'vlfm' and len(qn) == 4:
290 |
291 | n = qn[0]
292 | v = qn[0]
293 | l = qn[1]
294 | f = qn[2]
295 | m = qn[3]
296 |
297 | # NB: l >= v is not exactly right...
298 | if l < 0 or l >= v or np.abs(m) > f:
299 | return None
300 |
301 | elif tt == 'NIST':
302 | st = self.get_state_nist(qn, tt='nsljm')
303 | return st
304 | else:
305 | print("tt=", tt, " not supported by H.get_state")
306 |
307 | # choose MQDT model
308 | try:
309 | solver = [d for d in self.mqdt_models if d['L'] == l and d['F'] == f][0]['model']
310 | except:
311 | #raise ValueError(f"Could not find MQDT model for qn={qn}")
312 | return None
313 | #continue
314 |
315 | Ia = solver.ionizationlimits_invcm[solver.nulima[0]]
316 | Ib = solver.ionizationlimits_invcm[solver.nulimb[0]]
317 |
318 | # calculate experimental effective quantum number
319 | if energy_exp_Hz is not None:
320 | nuexp = ((- 0.01 * energy_exp_Hz / cs.c) / solver.RydConst_invcm) ** (-1 / 2)
321 | else:
322 | nuexp = v
323 |
324 | nub = solver.boundstates(nuexp)
325 | nuapprox = round(nub * 100) / 100
326 |
327 | # calculate energy of state
328 | E_rel_Hz = (-solver.RydConst_invcm / nub ** 2 + Ib) * 100 * cs.c
329 |
330 | if energy_only:
331 | [coeffs_i, coeffs_alpha] = [len(solver.channels) * [0],len(solver.channels) * [0]]
332 | else:
333 | [coeffs_i, coeffs_alpha] = solver.channelcontributions(nub)
334 |
335 | # define sate
336 | st = state_mqdt(self, (nuapprox, (-1) ** l, f, m), coeffs_i, coeffs_alpha, solver.channels, energy_Hz=E_rel_Hz, tt='vpfm')
337 | st.pretty_str = "|%s:%.2f,L=%d,F=%.1f,%.1f>" % (self.name, nuapprox, l, f, m)
338 |
339 | # effective quantum numbers with respect to two ionization limits Ia and Ib
340 | st.nua = solver.nux(Ia,Ib,nub)
341 | st.nub = nub
342 | st.v_exact = nub
343 |
344 | return st
345 |
346 | def get_state_nist(self, qn, tt='nsljm'):
347 |
348 | if tt == 'nsljm':
349 | # this is what we use to specify states near the ground state, that are LS coupled
350 |
351 | n = qn[0]
352 | s = qn[1]
353 | l = qn[2]
354 | j = qn[3]
355 | m = qn[4]
356 |
357 | if l < 0 or l >= n or np.abs(m) > j:
358 | return None
359 |
360 | pretty_str = "|%s:%d,S=%d,L=%d,j=%d,%.1f>" % (self.name, n, s, l, j, m)
361 |
362 |
363 |
364 | # defining core states
365 | mqdt_LS = {'cores': [core_state((1 / 2, 0, 1 / 2, 0, 1 / 2), Ei_Hz=0, tt='sljif',
366 | config='6s1/2', potential=self.model_pot),]}
367 |
368 | # generate channels by iterating over two core hyperfine states and Rydberg quantum numbers
369 | mqdt_LS.update({'channels': [channel(mqdt_LS['cores'][0], (1 / 2, l, j), tt='slj') for j in np.arange(np.abs(l-1/2),l+1/2+0.1) ]})
370 |
371 | datadir = importlib.resources.files('rydcalc')
372 |
373 | with open(datadir.joinpath('Yb174_NIST.txt'), 'r') as json_file:
374 | nist_data = json.load(json_file)
375 |
376 | nist_data = nist_data[1:] # drop references
377 |
378 | dat = list(filter(lambda x: x['n']== n and x['l']== l and x['S']== s and x['J'] == j, nist_data))
379 |
380 | if len(dat) == 0:
381 | return None
382 |
383 | dat = dat[0]
384 |
385 | # we are going to express this in terms of our mqdt_LS system, which will cover all of the 3PJ states (some will have zero weight)
386 |
387 | energy_Hz = (dat['E_cm'] - self.Elim_cm) * 100 * cs.c
388 |
389 | Ais = []
390 |
391 | for ch in mqdt_LS['channels']:
392 | # now go through the frame transformations in 10.1103/PhysRevA.97.022508 Eq. 11,
393 |
394 | # Eq 11
395 | # print((ch.core.s, ch.s, s, ch.core.l, ch.l, l, ch.core.j, ch.j, j))
396 | ls_to_jj = np.sqrt(2 * s + 1) * np.sqrt(2 * l + 1) * np.sqrt(2 * ch.core.j + 1) * np.sqrt(2 * ch.j + 1) * wigner_9j(ch.core.s, ch.s, s, ch.core.l, ch.l, l, ch.core.j, ch.j, j)
397 |
398 | # print(jj_to_f,ls_to_jj)
399 | Ais.append(ls_to_jj)
400 |
401 | Aalphas = []
402 |
403 | st = state_mqdt(self, (n, s, l, j, j, m), Ais, Aalphas, mqdt_LS['channels'], energy_Hz=energy_Hz, tt='nsljfm')
404 | st.pretty_str = pretty_str
405 | return st
406 |
407 | else:
408 | print("tt=", tt, " not supported by H.get_state")
409 |
410 | def get_nearby(self, st, include_opts={}, energy_only = False):
411 | """ generate a list of quantum number tuples specifying nearby states for sb.fill().
412 | include_opts can override options in terms of what states are included.
413 |
414 | It's a little messy to decide which options should be handled here vs. in single_basis
415 | decision for now is to have all quantum numbers here but selection rules/energy cuts
416 | in single_basis to avoid duplication of code.
417 |
418 | In contrast to get_nearby, this function actually returns a list of states """
419 |
420 | ret = []
421 |
422 | o = {'dn': 2, 'dl': 2, 'dm': 1, 'ds': 0}
423 |
424 | for k, v in include_opts.items():
425 | o[k] = v
426 |
427 | if 'df' not in o.keys():
428 | o['df'] = o['dl']
429 |
430 | # calculate experimental effective quantum number
431 | nu0 = st.nub
432 |
433 | for l in np.arange(st.channels[0].l - o['dl'], st.channels[0].l + o['dl'] + 1):
434 | if l < 0:
435 | continue
436 | for f in np.arange(st.f - o['df'], st.f + o['df'] + 1):
437 | if f < 0 or f>l+1 or f 1:
302 | c6d = (self.interactionFits[0][0] + self.interactionFits[1][0])/2
303 | c6e = (self.interactionFits[0][0] - self.interactionFits[1][0])/2
304 | c3d = (self.interactionFits[0][1] + self.interactionFits[1][1])/2
305 | c3e = (self.interactionFits[0][1] - self.interactionFits[1][1])/2
306 | else:
307 | c6d = self.interactionFits[0][0]
308 | c6e = 0
309 | c3d = self.interactionFits[0][1]
310 | c3e = 0
311 |
312 | return np.array([c6d,c6e,c3d,c3e])
313 |
314 |
315 | def pa_plot(self,include_plot_opts ={}):
316 | """ Plot the results of the pair interaction analysis, including energy shifts and overlaps with asymptotic pair state. """
317 |
318 | self.plot_opts = {"ov_norm": 'linear',"s":5,"lin_norm":[0,1],"log_norm":[0.1,1],"gamma":0.5,"show_overlap": False,'cb_loc':'right','special_colors':None,'highlight_idx':0}
319 | self.overlapsAll = []
320 |
321 | for k, v in include_plot_opts.items():
322 | self.plot_opts[k] = v
323 |
324 |
325 | if self.plot_opts["show_overlap"] == True:
326 | fig,axs = plt.subplots(1,3,figsize=(12,4),gridspec_kw={'wspace':0.3})
327 | else:
328 | fig, axs = plt.subplots(1, 2, figsize=(12, 4), gridspec_kw={'wspace': 0.3})
329 |
330 |
331 | if self.plot_opts["special_colors"] == None:
332 | # red, blue, orange, teal, magenta, cyan
333 | colorschemes = [['#DDDDDD', '#CC3311'], ['#DDDDDD', '#0077BB'], ['#DDDDDD', '#EE7733'], ['#DDDDDD', '#009988'], ['#DDDDDD', '#EE3377'], ['#DDDDDD', '#33BBEE']]
334 | else:
335 | colorschemes = self.plot_opts["special_colors"]
336 |
337 | for ii in range(len(self.pb.highlight)):
338 | posidx = np.argwhere(self.energies[:,ii] >=0)
339 | negidx = np.argwhere(self.energies[:,ii] < 0)
340 | axs[0].plot(self.rList_um[posidx],np.abs(self.energies[posidx,ii])*1e-6,'o',color=colorschemes[ii % len(colorschemes)][1],label=repr(self.pb.highlight[ii][:2]))
341 | axs[0].plot(self.rList_um[negidx],np.abs(self.energies[negidx,ii])*1e-6,'o',color=colorschemes[ii % len(colorschemes)][1])
342 | #axs[0].plot(self.rList_um,np.abs(self.intfn(self.rList_um,*self.interactionFits[ii]))*1e-6,'-',color='C'+str(ii))
343 |
344 | axs[0].set_xlabel(r'$R$ ($\mu$m)')
345 | axs[0].set_ylabel(r'Pair Energy ($h\cdot$MHz)')
346 | #axs[0].legend()
347 | axs[0].set_xscale('log')
348 | axs[0].set_yscale('log')
349 | #axs[0].set_ylim([-10,10])
350 | axs[0].grid(axis='both')
351 |
352 | if self.plot_opts["show_overlap"] == True:
353 | for ii in range(len(self.pb.highlight)):
354 | axs[1].plot(self.rList_um,self.overlaps[:,ii],'-',label=repr(self.pb.highlight[ii][:1]))
355 |
356 | axs[1].set_xlabel(r'$R$ ($\mu$m)')
357 | axs[1].set_ylabel(r'Overlap')
358 | #plt.legend()
359 | axs[1].set_xscale('log')
360 | #plt.yscale('log')
361 | axs[1].set_ylim([-0.1,1.2])
362 | axs[1].grid(axis='both')
363 |
364 | # now we want to plot energies of pair states and highlight using overlap
365 |
366 |
367 | minE = 0
368 | maxE = 0
369 |
370 |
371 |
372 | cmap0 = matplotlib.colors.LinearSegmentedColormap.from_list('testCmap', colorschemes[self.plot_opts["highlight_idx"] % len(colorschemes)], N=256)
373 |
374 | # determine range of initial target state
375 | newMinE = min(self.energies[:,self.plot_opts["highlight_idx"]]*1e-6)
376 | newMaxE = max(self.energies[:,self.plot_opts["highlight_idx"]]*1e-6)
377 |
378 | minE = min(newMinE,minE)
379 | maxE = max(newMaxE,maxE)
380 |
381 | ov = []
382 | Rlist = []
383 | Elist = []
384 |
385 | for jj in range(len(self.rList_um)):
386 | # self.evAll[jj] is list of eigenvalues for this r
387 | # take overlap with pb.highlight[3] which is ket for this highlight state
388 | ov = np.append(ov, ([np.abs(np.sum(self.pb.highlight[self.plot_opts["highlight_idx"]][3] * self.evAll[jj, :, kk])) ** 2 for kk in range(self.pb.dim())]))
389 | Rlist = np.append(Rlist, (list(self.rList_um[jj] * np.ones_like(self.energiesAll[jj]))))
390 | Elist = np.append(Elist, list((self.energiesAll[jj] * 1e-6)))
391 |
392 |
393 |
394 | # get order of points by overlap to plot points with high overlap on top of points with low overlap
395 | order = np.argsort(ov)
396 |
397 | self.overlapsAll.append([ov])
398 |
399 | # normalization overlap:
400 | if self.plot_opts["ov_norm"] == 'log':
401 | norm = matplotlib.colors.LogNorm(vmin=self.plot_opts["log_norm"][0], vmax=self.plot_opts["log_norm"][1],clip=True)
402 | elif self.plot_opts["ov_norm"] == 'linear':
403 | norm = matplotlib.colors.Normalize(vmin=self.plot_opts["lin_norm"][0], vmax=self.plot_opts["lin_norm"][1])
404 | elif self.plot_opts["ov_norm"] == 'power':
405 | norm = matplotlib.colors.PowerNorm(gamma=self.plot_opts["gamma"])
406 |
407 | sc = axs[-1].scatter(Rlist[order], Elist[order], s=self.plot_opts["s"], c=ov[order], cmap=cmap0,norm = norm)
408 | axs[-1].clb = fig.colorbar(sc, label=r'Overlap', ticks=[0, 0.2, 0.4, 0.6, 0.8, 1],location = self.plot_opts['cb_loc'])
409 |
410 | axs[-1].set_xlabel(r'$R$ ($\mu$m)')
411 | axs[-1].set_ylabel(r'Pair Energy ($h\cdot$MHz)')
412 | #plt.legend()
413 | #axs[2].set_xscale('log')
414 | #plt.yscale('log')
415 | axs[-1].set_ylim([1.2*minE,1.2*maxE])
416 | axs[-1].grid(axis='both')
417 | axs[-1].set_axisbelow(True)
418 |
419 | return fig,axs # to allow later figure modification
420 |
421 |
422 |
423 |
424 |
425 |
--------------------------------------------------------------------------------
/rydcalc/arc_c_extensions.c:
--------------------------------------------------------------------------------
1 | /*
2 | This code is incorporated from the Alkali Rydberg Calculator project. Please see the copyright and license attribution in the LICENSE file.
3 | */
4 | #include
5 | // http://docs.scipy.org/doc/numpy/reference/c-api.deprecations.html
6 | #define NPY_NO_DEPRECATED_API NPY_1_9_API_VERSION
7 | #include
8 |
9 | #include
10 | #include
11 | #include
12 |
13 | double innerLimit;
14 | double outerLimit;
15 | double step;
16 | double init1;
17 | double init2;
18 |
19 | int l;
20 | double s,j;
21 | double stateEnergy;
22 | double alphaC;
23 | double alpha;
24 | int Z;
25 | double a1,a2,a3,a4,rc; // depends on l - determined by Python in advance
26 | double mu;
27 |
28 | //#define DEBUG_OUTPUT
29 |
30 | static PyObject *NumerovWavefunction(PyObject *self, PyObject *args);
31 |
32 | static PyMethodDef module_methods[] = {
33 | {"NumerovWavefunction", NumerovWavefunction, METH_VARARGS,
34 | "Numerov wavefunction"},
35 | {NULL, NULL, 0, NULL}};
36 |
37 | #if PY_MAJOR_VERSION >= 3
38 | static struct PyModuleDef moduledef = {
39 | PyModuleDef_HEAD_INIT, "arc_c_extensions",
40 | "C extensions of ARC (Numerov integration)", -1, module_methods, NULL, NULL, NULL, NULL, };
41 |
42 | PyMODINIT_FUNC PyInit_arc_c_extensions(void) {
43 | // import Numpy API
44 | import_array();
45 |
46 | return PyModule_Create(&moduledef);
47 | }
48 | #else
49 | PyMODINIT_FUNC initarc_c_extensions(void) {
50 | if (!(Py_InitModule3("arc_c_extensions", module_methods,
51 | "C extensions of ARC (Numerov integration)"))) return;
52 | // import Numpy API
53 | import_array();
54 | }
55 | #endif
56 |
57 |
58 | // =========== variable definition ===========
59 | int divergencePoint;
60 | int totalLength;
61 | int br ;
62 | double* sol;
63 | double x,step2,maxValue,checkPoint,fromLastMax,r;
64 | int i;
65 | npy_intp dims[2];
66 | PyObject* narray;
67 |
68 | double commonTerm1=0;
69 | double commonTerm2=0;
70 |
71 | // =========== Numerov integration implementation ===========
72 |
73 | __inline double EffectiveCharge(double r){
74 | // returns effective charge of the core
75 | return 1.0+((double)Z-1.)*exp(-a1*r)-r*(a3+a4*r)*exp(-a2*r);
76 | }
77 |
78 | __inline double CorePotential(double r){
79 | // l dependent core potential (angular momentum of e) at radius r
80 | // returns Core potential
81 | return -EffectiveCharge(r)/r-alphaC/(2.*pow(r,4))*(1.-exp(-pow(r/rc,6)));
82 | }
83 |
84 |
85 | __inline double Potenital(double r){
86 | // l<4
87 | return CorePotential(r)+pow(alpha,2)/(2.0*pow(r,3))*commonTerm1;
88 |
89 | }
90 |
91 | __inline double Potenital2(double r){
92 | // l>=4
93 | // act as if it is a Hydrogen atom, include spin-orbit coupling
94 | return -1./r+pow(alpha,2)/(2.0*pow(r,3))*commonTerm1;
95 | }
96 |
97 |
98 | __inline double kfun(double x){
99 | // with potential for l<4
100 | r = x*x; // x = sqrt(r)
101 | return -3./(4.*r)+4*r*( 2.*mu*(stateEnergy-Potenital(r))-commonTerm2/pow(r,2) );
102 | }
103 |
104 | __inline double kfun2(double x){
105 | // with potential for l>4
106 | r = x*x; // x = sqrt(r)
107 | return -3./(4.*r)+4*r*( 2.*mu*(stateEnergy-Potenital2(r))-commonTerm2/pow(r,2) );
108 | }
109 |
110 | static PyObject *NumerovWavefunction(PyObject *self, PyObject *args) {
111 | // Numerov arguments: innerLimit,outerLimit,kfun,step,init1,init2
112 | double innerLimit,outerLimit,step,init1,init2;
113 |
114 |
115 | if (!(PyArg_ParseTuple(args, "dddddidddddidddddd", &innerLimit, &outerLimit, &step,
116 | &init1, &init2,
117 | &l, &s, &j, &stateEnergy, &alphaC, &alpha,
118 | &Z, &a1, &a2, &a3, &a4, &rc, &mu))) return NULL;
119 |
120 |
121 | #ifdef DEBUG_OUTPUT
122 | printf("innerLimit\t=\t%.3f\nouterLimit\t=\t%.3f\nstep\t=\t%.3f\ninit1\t=\t%.3f\ninit2\t=\t%.3f\n",innerLimit,outerLimit,step,init1,init2);
123 | printf("l\t=\t%i\ns\t=\t%.1f\nj\t=\t%.1f\n",l,s,j);
124 | printf("stateEnergy\t=\t%.7f\nalphaC\t=\t%.3f\nalpha\t=\t%.3f\nZ\t=\t%i\n",stateEnergy,alphaC,alpha,Z);
125 | printf("a1\t\t%.4f\na2\t\t%.4f\na3\t\t%.4f\na4\t\t%.4f\nrc\t\t%.4f\n",a1,a2,a3,a4,rc);
126 | printf("mu\t\t%.4f",mu);
127 | #endif
128 |
129 | // let's speed up calculation by calculating some common terms beforehand
130 | commonTerm1 = (j*(j+1.0)-((double)l)*(l+1.0)-s*(s+1.))/2.0;
131 | commonTerm2 = ((double)l)*(l+1.);
132 |
133 | totalLength = (int)((sqrt(outerLimit)-sqrt(innerLimit))/step);
134 |
135 | #ifdef DEBUG_OUTPUT
136 | printf("Index = %i\n",totalLength);
137 | printf("Index should be about = %.2f\n",(sqrt(outerLimit)-sqrt(innerLimit)/step));
138 | #endif
139 |
140 | br = totalLength;
141 | sol = (double*) malloc(2*br*sizeof(double));
142 |
143 | if (!sol){
144 | #ifdef DEBUG_OUTPUT
145 | printf("Memory allocaiton failed! Aborting.");
146 | #endif
147 | return NULL;
148 | }
149 |
150 | // for l<4
151 |
152 | if (l<4){
153 |
154 | br = br-1;
155 | x = sqrt(innerLimit)+step*(totalLength-1);
156 | step2 = step*step;
157 | sol[br] = (2*(1-5.0/12.0*step2*kfun(x))*init1-(1+1/12.0*step2*kfun(x+step))*init2)/(1+1/12.0*step2*kfun(x-step));
158 | sol[br+totalLength]=x;
159 | x = x-step;
160 | br = br-1;
161 |
162 | sol[br] = (2*(1-5.0/12.0*step2*kfun(x))*sol[br+1]-(1+1/12.0*step2*kfun(x+step))*init1)/(1+1/12.0*step2*kfun(x-step));
163 | sol[br+totalLength]=x;
164 |
165 | maxValue = 0;
166 |
167 | checkPoint = 0;
168 | fromLastMax = 0;
169 |
170 | while (br>checkPoint){
171 | br = br-1;
172 | x = x-step;
173 | sol[br] = (2*(1-5.0/12.0*step2*kfun(x))*sol[br+1]-(1+1/12.0*step2*kfun(x+step))*sol[br+2])/(1+1/12.0*step2*kfun(x-step));
174 | sol[br+totalLength]=x;
175 | if (fabs(sol[br]*sqrt(x))>maxValue){
176 | maxValue = fabs(sol[br]*sqrt(x));
177 | }
178 | else{
179 | fromLastMax += 1;
180 | if (fromLastMax>50){
181 | checkPoint = br;
182 | }
183 | }
184 | }
185 |
186 | divergencePoint = 0;
187 | while ((br>0)&&(divergencePoint == 0)){
188 | br = br-1;
189 | x = x-step;
190 | sol[br] = (2*(1-5.0/12.0*step2*kfun(x))*sol[br+1]-(1+1/12.0*step2*kfun(x+step))*sol[br+2])/(1+1/12.0*step2*kfun(x-step));
191 | sol[br+totalLength]=x;
192 |
193 | if ((divergencePoint==0)&&(fabs(sol[br]*sqrt(x))>maxValue)){
194 | divergencePoint = br;
195 | while ((fabs(sol[divergencePoint])>fabs(sol[divergencePoint+1])) && (divergencePointcheckPoint){
199 | #ifdef DEBUG_OUTPUT
200 | printf("ERROR: Numerov error\n");
201 | #endif
202 | return NULL;
203 | }
204 | }
205 | }
206 |
207 |
208 | } // end of if l<4
209 | else{ //l>=4
210 |
211 | br = br-1;
212 | x = sqrt(innerLimit)+step*(totalLength-1);
213 | step2 = step*step;
214 | sol[br] = (2*(1-5.0/12.0*step2*kfun2(x))*init1-(1+1/12.0*step2*kfun2(x+step))*init2)/(1+1/12.0*step2*kfun2(x-step));
215 | sol[br+totalLength]=x;
216 | x = x-step;
217 | br = br-1;
218 |
219 | sol[br] = (2*(1-5.0/12.0*step2*kfun2(x))*sol[br+1]-(1+1/12.0*step2*kfun2(x+step))*init1)/(1+1/12.0*step2*kfun2(x-step));
220 | sol[br+totalLength]=x;
221 |
222 | maxValue = 0;
223 |
224 | checkPoint = 0;
225 | fromLastMax = 0;
226 |
227 | while (br>checkPoint){
228 | br = br-1;
229 | x = x-step;
230 | sol[br] = (2*(1-5.0/12.0*step2*kfun2(x))*sol[br+1]-(1+1/12.0*step2*kfun2(x+step))*sol[br+2])/(1+1/12.0*step2*kfun2(x-step));
231 | sol[br+totalLength]=x;
232 |
233 | if (fabs(sol[br]*sqrt(x))>maxValue){
234 | maxValue = fabs(sol[br]*sqrt(x));
235 | }
236 | else{
237 | fromLastMax += 1;
238 | if (fromLastMax>50){
239 | checkPoint = br;
240 | }
241 | }
242 | }
243 |
244 | divergencePoint = 0;
245 | while ((br>0)&&(divergencePoint == 0)){
246 | br = br-1;
247 | x = x-step;
248 | sol[br] = (2*(1-5.0/12.0*step2*kfun2(x))*sol[br+1]-(1+1/12.0*step2*kfun2(x+step))*sol[br+2])/(1+1/12.0*step2*kfun2(x-step));
249 | sol[br+totalLength]=x;
250 |
251 | if ((divergencePoint==0)&&(fabs(sol[br]*sqrt(x))>maxValue)){
252 | divergencePoint = br;
253 | while ((fabs(sol[divergencePoint])>fabs(sol[divergencePoint+1])) && (divergencePointcheckPoint){
257 | #ifdef DEBUG_OUTPUT
258 | printf("ERROR: Numerov error\n");
259 | #endif
260 | return NULL;
261 | }
262 | }
263 | }
264 |
265 | }
266 |
267 | // RETURN RESULT - but set to zero divergent part (to prevent integration there)
268 | for (i =0; i= 0 ; i--) sol[i+totalLength] = sol[i+totalLength+1]-step;
271 |
272 | // convert sol that is at the moment R(r)*r^{3/4} into R(r)*r
273 | for (i=0; i 1:
50 | print("Error--more than one result variable not currently supported")
51 | return None
52 |
53 | self.results = results
54 |
55 | #print(self._create_str())
56 |
57 | # check to see if our table exists
58 | self.c.execute("""SELECT COUNT(*) FROM sqlite_master where type='table' AND name='%s'""" % self.table_name)
59 |
60 | if (self.c.fetchone()[0] == 0):
61 | # create it
62 | self.c.execute(self._create_str())
63 |
64 | self.load()
65 |
66 | else:
67 | # check that it is the right table by comparing the create string stored in sqlite to the one we would have created
68 | # this seems inelegant but not sure how else to do it
69 | self.c.execute("""SELECT sql FROM sqlite_master WHERE tbl_name = '%s' and type='table'""" % self.table_name)
70 | prev_str = self.c.fetchone()[0]
71 |
72 | if prev_str != self._create_str():
73 | print("Error in db_table() -- wanted create_str %s, got %s" % (self._create_str(),prev_str))
74 | return None
75 |
76 | self.conn.commit()
77 |
78 | def _create_str(self):
79 | # string for creating a table -- for now all of the keys are tinyint unsigned and results are doubles, and
80 | # all keys are part of the primary key
81 |
82 | keys_typed = [x + ' TINYINT UNSIGNED' for x in self.keys]
83 | results_typed = [x + ' DOUBLE' for x in self.results]
84 |
85 | create_str = "CREATE TABLE %s (" % self.table_name
86 |
87 | return create_str + ','.join(keys_typed + results_typed + ['PRIMARY KEY (%s)' % ','.join(self.keys)]) + ')'
88 |
89 | def load(self,altfile = None):
90 | """ Load values from npy file into database. This will give an error and undefined behavior
91 | if there are any key collisions, I think. Not obivously safe to run on existing, populated database. """
92 |
93 | file = altfile if altfile is not None else self.npy_file
94 |
95 | try:
96 | data = np.load(file)
97 | self.insert(data,many=True)
98 | print("Restored database table %s from %s (%d records)" % (self.table_name,file,self.db_size()))
99 | except:
100 | print("Error reloading database table %s from %s" % (self.table_name,file))
101 |
102 | def save(self,altfile = None):
103 | """ Save values from table to file, for later loading with load(). """
104 |
105 | file = altfile if altfile is not None else self.npy_file
106 |
107 | data = self.getall()
108 | np.save(file,data)
109 |
110 | print("Saved database table %s to %s (%d records)" % (self.table_name,file,len(data)))
111 |
112 |
113 | def insert(self,val,many=False):
114 | """ Add new items to table. Val should be tuple/array of length M = len(keys) + len(results).
115 | If many=True, Val can be NxM array to load N entries."""
116 |
117 | st = 'insert into %s values %s' % (self.table_name,'(' + ','.join(['?' for x in self.keys+self.results]) + ')')
118 | #print(st)
119 |
120 | if many:
121 | self.c.executemany(st, np.array(val,dtype=float))
122 | else:
123 | self.c.execute(st, np.array(val,dtype=float))
124 |
125 | self.conn.commit()
126 |
127 | def query(self,val):
128 | """ Find value in table. Val should be tuple/array of length len(keys) """
129 |
130 | st = 'select %s from %s where %s' % (self.results[0],self.table_name,' AND '.join([x + '=?' for x in self.keys]))
131 | #print(st)
132 | self.c.execute(st, np.array(val,dtype=float))
133 |
134 | ret = self.c.fetchone()
135 |
136 | if (ret):
137 | return ret[0]
138 | else:
139 | return None
140 |
141 | def getall(self):
142 | """ Get NxM array of all key,results in table """
143 |
144 | st = 'select %s from %s' % (','.join(self.keys+self.results),self.table_name)
145 |
146 | self.c.execute(st)
147 |
148 | return self.c.fetchall()
149 |
150 | def db_size(self):
151 | """ Get number of entries in table """
152 |
153 | self.c.execute('select count(*) from %s' % self.table_name)
154 | return self.c.fetchone()[0]
--------------------------------------------------------------------------------
/rydcalc/defects.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import scipy as sp
3 | import scipy.integrate
4 | import scipy.interpolate
5 | import scipy.constants as cs
6 | import scipy.optimize
7 | # whether to silence warnings about validity of quantum defect ranges
8 | qd_quiet = False
9 |
10 | class defect_model:
11 | ''' Parent class for quantum defect model. Has the following methods:
12 |
13 | - is_valid(qns)
14 | Checks whether the defect model is valid for the given quantum numbers (returns True/False).
15 |
16 | - get_defect(qns)
17 | Returns the defect for the given quantum numbers.
18 |
19 | This model is intended to be subclassed by other classes, such as a Rydberg-Ritz model or a model based on experimental energy levels.
20 |
21 | The corrections options specify whether various corrections to the energy, beyond the quantum defect, should be included.
22 |
23 | The intended usage of defect_model is to populate the atomic class (ie, Rubidium87) with a list of models, which
24 | will be sequentially checked to determine the first one that is valid for a given state.
25 | '''
26 |
27 | def __init__(self, delta, condition=lambda qns: True, polcorrection=False, SOcorrection=False, relcorrection=False):
28 | """
29 | Initializes an instance of a quantum defect model.
30 |
31 | Args:
32 | delta (float): Quantum defect
33 | condition (function, optional): A lambda function taking quantum numbers as input and returning True or False to determine whether this defect model is valid for the given state. Defaults to lambda qns: True.
34 | polcorrection (bool, optional): Flag indicating whether polarization correction is applied. Defaults to False.
35 | SOcorrection (bool, optional): Flag indicating whether spin-orbit correction is applied. Defaults to False.
36 | relcorrection (bool, optional): Flag indicating whether relativistic correction is applied. Defaults to False.
37 | """
38 | self.delta = delta
39 | self.condition = condition
40 | self.corrections = {"polcorrection": polcorrection, "SOcorrection": SOcorrection, "relcorrection": relcorrection}
41 |
42 | def is_valid(self,qns):
43 | ''' test if this defect model is valid for this state '''
44 | return self.condition(qns)
45 |
46 | def get_defect(self,qns):
47 | return self.delta
48 |
49 | class defect_Rydberg_Ritz(defect_model):
50 | '''Subclass of defect_model implementing the modified Rydberg-Ritz formula,
51 |
52 | delta = delta_0 + delta_2 / (n-delta_0)^2 + delta_4 / (n-delta_0)^4 ... '''
53 |
54 | # it is a little unclear whether we want to have both condition function
55 | # and n_range. The idea of n_range is that it is a 'soft' error as opposed
56 | # to throwing a zero by default
57 |
58 | def __init__(self,deltas,n_range = None,condition=lambda qns: True, polcorrection= False, SOcorrection= False, relcorrection= False):
59 | self.deltas = deltas
60 | self.order = len(self.deltas)
61 | self.n_range = n_range
62 | self.condition=condition
63 | self.corrections = {"polcorrection": polcorrection,"SOcorrection":SOcorrection,"relcorrection":relcorrection}
64 |
65 | if self.order == 0:
66 | print("Error--tried to initialize empty defect_Rydberg_Ritz")
67 |
68 | def get_defect(self,qns):
69 |
70 | if self.n_range is not None and (self.n_range[0] > qns['n'] or self.n_range[1] < qns['n']) and not qd_quiet:
71 | print("Warning--using defect_Rydberg_Ritz outside of specified n_range for st: ",qns,"(range is ",self.n_range,")")
72 |
73 | defect = self.deltas[0]
74 |
75 | nord = 1
76 | while nord < self.order:
77 | defect += self.deltas[nord] / (qns['n'] - self.deltas[0])**(2*nord)
78 | nord+=1
79 |
80 | return defect
81 |
82 | class defect_from_energy(defect_model):
83 | """ Class to define quantum defect based on experimentally known energy. This uses the reduced mass of the atom so that the correct energy will be returned when converting back to Hz. """
84 |
85 | def __init__(self,energy,unit='Hz',condition=lambda qns: True):
86 | """
87 | Initialize the defect_from_energy class.
88 |
89 | Parameters:
90 | - energy: The experimentally known energy, relative to the threshold.
91 | - unit: The unit of the energy, '1/cm' or 'Hz' (default is 'Hz').
92 | - condition: A lambda function that defines a condition for the defect calculation (default is lambda qns: True).
93 | """
94 |
95 | if unit == 'Hz':
96 | self.energy_Hz = energy
97 |
98 | elif unit == '1/cm':
99 | self.energy_Hz = energy * cs.c * 100
100 |
101 | else:
102 | print("Error--unsupported unit", unit, 'in defect_from_energy')
103 |
104 | self.energy_au = self.energy_Hz / (2 * cs.Rydberg * cs.c)
105 | self.condition=condition
106 |
107 | def get_defect(self,qns):
108 |
109 | return qns['n'] - np.sqrt(-1/(2*self.energy_au))
110 |
111 |
--------------------------------------------------------------------------------
/rydcalc/pair_basis.py:
--------------------------------------------------------------------------------
1 | import scipy as sp
2 |
3 | from rydcalc import *
4 |
5 | class pair:
6 |
7 | def __init__(self,s1,s2):
8 | self.s1 = s1
9 | self.s2 = s2
10 |
11 | self.energy_Hz = s1.get_energy_Hz() + s2.get_energy_Hz()
12 |
13 | self.flag = ''
14 |
15 | def __repr__(self):
16 | # print a nice ket
17 | return repr(self.s1) + " " + repr(self.s2) + " " + self.flag
18 |
19 | def __eq__(self,other):
20 | # check for equality
21 |
22 | if self.s1 == other.s1 and self.s2 == other.s2:
23 | return True
24 | else:
25 | return False
26 |
27 | def __ne__(self,other):
28 | return not self.__eq__(other)
29 |
30 |
31 | class pair_basis:
32 | class pair_basis:
33 | """
34 | A class to manage a basis of pair states for calculations involving interactions between pairs of states.
35 | """
36 |
37 | def __init__(self):
38 | self.pairs = []
39 | self.highlight = []
40 |
41 | def add(self,p):
42 | if self.find(p) is None:
43 | self.pairs.append(p)
44 | else:
45 | pass
46 | #print("Error--duplicate pair ",p)
47 |
48 | def dim(self):
49 | return len(self.pairs)
50 |
51 | def find(self,p):
52 | match = -1
53 |
54 | for ii in range(len(self.pairs)):
55 | if self.pairs[ii] == p:
56 | return ii
57 |
58 | return None
59 |
60 | def getClosest(self,ev):
61 | """ return the state that has maximum overlap with the given eigenvalue """
62 | idx = np.argmax(np.abs(ev)**2)
63 | return self.pairs[idx]
64 |
65 | def getVec(self,p):
66 | """ get vector corresponding to state p """
67 | idx = self.find(p)
68 |
69 | vec = np.zeros(self.dim())
70 | vec[idx] = 1
71 | return vec
72 |
73 | def fill(self, p0, include_opts={}, dm_tot=[-2, -1, 0, 1, 2]):
74 | """
75 | Fills the pair basis for the given pair state `p0` with options specified in `include_opts`.
76 |
77 | This method initializes the pair basis states considering the options provided for dipole transitions,
78 | exchange interactions, and other specified conditions. It populates the basis with all possible state
79 | combinations from single_basis instances `sb1` and `sb2`, which are filled based on the states `s1` and `s2`
80 | of `p0`.
81 |
82 | Parameters:
83 | p0 (PairState): The pair state for which the basis is to be filled.
84 | include_opts (dict): Options to include specific conditions while filling the basis.
85 | Keys can include 'dn', 'dl', 'dm', 'dipole_allowed', 'force_exchange', and 'pair_include_fn'.
86 | - 'force_exchange' forces the inclusion of pairs with states swapped
87 | - 'pair_include_fn' is a lambda function taking two pairs and returning True/False (ie, lambda p,p0: np.abs(p.energy_Hz-p0.energy_Hz) < 1e9)
88 | dm_tot (list of int): List of allowed changes in the total magnetic quantum number (delta m).
89 |
90 | Notes:
91 | - The method checks for dipole allowed transitions if 'dipole_allowed' is True.
92 | - Exchange interactions are considered if the states belong to the same atom and other conditions
93 | specified in `force_exchange` or delta m conditions are met.
94 | - The method also handles highlighting of states if exchange is considered.
95 | """
96 | """ fill in basis of states that are needed to calculate interactions of p0.
97 |
98 | dipole_allowed option specifies that only states that are e1 allowed from initial
99 | states should be included. Note that this restriction is not acceptable for circular
100 | states in electric fields, where the linear DC stark shift between other states
101 | must be taken into account.
102 |
103 | If dipole_allowed is True, dm_tot specifies which total deltaM should be included.
104 | This is useful to restrict computation basis for certain angles, ie, when th=0
105 | only dM = 0 is relevant.
106 | """
107 |
108 | self.p0 = p0
109 |
110 | self.add(p0)
111 |
112 | self.opts = {'dn': 2, 'dl': 2, 'dm': 1, 'dipole_allowed': True, 'force_exchange': False, 'pair_include_fn': lambda p, p0: True}
113 |
114 | for k, v in include_opts.items():
115 | self.opts[k] = v
116 |
117 | # if we are dealing with different states where exchange is allowed,
118 | # ofen need to add flipped state explicitly
119 | if p0.s1.atom == p0.s2.atom: # and p0.s1.tt == p0.s2.tt:
120 | # if the two states are the same atomic species and specified with the same
121 | # kind of term, then we will want to consider processes where (s1,s2) -> (s2,s1)
122 | # if that transition is allowed (ie, if deltaM is less than two for second-order
123 | # E1 transitions). If we add quadrupole, etc., will need to relax this constraint
124 | if (np.abs(p0.s1.m - p0.s2.m) <= 2 or self.opts['force_exchange']) and p0.s1 != p0.s2:
125 | self.consider_exchange = True
126 | else:
127 | self.consider_exchange = False
128 | else:
129 | # if the two states belong to different atoms or have different term symbols,
130 | # then we do not consider exchange. This would be the case for inter-species
131 | # interactions (ie, Rb-Cs) or low-L - circular interactions, if we specify
132 | # the circular states using 'nlm' terms to use analytic wavefunctions
133 | self.consider_exchange = False
134 |
135 | self.sb1 = single_basis()
136 | self.sb1.fill(p0.s1, include_opts=self.opts)
137 |
138 | self.sb2 = single_basis()
139 | self.sb2.fill(p0.s2, include_opts=self.opts)
140 |
141 | for s1 in self.sb1.states:
142 | for s2 in self.sb2.states:
143 | # if self.sb1.find(s2) is not None:
144 | # s2_temp = self.sb1.states[self.sb1.find(s2)]
145 | # else:
146 | # s2_temp = s2
147 |
148 | if self.opts['dipole_allowed']:
149 | if not (p0.s1.allowed_multipole(s1, k=1) and p0.s2.allowed_multipole(s2, k=1)):
150 | continue
151 |
152 | # implement restriction on change in total m
153 | delta_m_tot = s1.m + s2.m - (p0.s1.m + p0.s2.m)
154 | if self.opts['dipole_allowed'] and not (delta_m_tot in dm_tot):
155 | continue
156 |
157 | if not self.opts['pair_include_fn'](pair(s1, s2), self.p0):
158 | continue
159 |
160 | self.add(pair(s1, s2))
161 | if self.consider_exchange:
162 | # if we allow exchange interactions, explicitly add reversed
163 | # states to the Hamiltonian. This significantly increases
164 | # basis size
165 | self.add(pair(s2, s1))
166 |
167 | # highlight is a list of tuples, specifing (p1,'g'/'u'/'n',p2,targetstate)
168 | if self.consider_exchange: # p0.s1 == p0.s2 or p0.s1.atom != p0.s2.atom:
169 | # self.highlight = [(p0,'g',pair(p0.s2,p0.s1)),(p0,'u',pair(p0.s2,p0.s1))]
170 | self.addHighlight(p0, 'g', pair(p0.s2, p0.s1))
171 | self.addHighlight(p0, 'u', pair(p0.s2, p0.s1))
172 | else:
173 | self.addHighlight(p0, 'n')
174 | # self.highlight = [(p0,'n',None,self.getVec(p1))]
175 |
176 |
177 | def addHighlight(self,p1,sym='n',p2=None):
178 | """
179 | Adds a highlighted pair to the highlight list with a specified symmetry.
180 |
181 | Args:
182 | p1 (Pair): The primary pair involved in the highlight.
183 | sym (str): The symmetry type of the highlight ('g', 'u', or 'n').
184 | p2 (Pair, optional): The secondary pair involved in the highlight. Defaults to None.
185 |
186 | Notes:
187 | - If `sym` is 'n' and `p2` is None, the highlight is added with no secondary pair and a vector derived from `p1`.
188 | - If `sym` is 'g' or 'u', the highlight is added with both pairs and a vector calculated as a symmetric or antisymmetric combination of the vectors from `p1` and `p2`.
189 | """
190 |
191 | if sym=='n' and type(p2) is type(None):
192 | self.highlight.append((p1,'n',None,self.getVec(p1)))
193 |
194 | if type(p2) is type(None):
195 | return # shouldn't get here
196 |
197 | if sym == 'g':
198 | vec = (self.getVec(p1) + self.getVec(p2))/np.sqrt(2)
199 | elif sym == 'u':
200 | vec = (self.getVec(p1) - self.getVec(p2))/np.sqrt(2)
201 |
202 | self.highlight.append((p1,sym,p2,vec))
203 |
204 |
205 | def computeHamiltonians(self, multipoles=[[1,1]]):
206 | """
207 | Compute the Hamiltonians for the system considering the specified multipoles.
208 |
209 | This function initializes and computes the Hamiltonians for electric fields, magnetic fields, diamagnetic interaction, and the multipole interaction.
210 |
211 | All hamiltonians (including the different q values for the multipole interaction) are stored separately, so they can be resummed with appropriate
212 | coefficients to rapidly compute eigenvalues at different field strengths, distances and orientations.
213 |
214 | Args:
215 | multipoles (list of list of int): A list of pairs specifying the orders of the multipoles to include.
216 | Each pair is of the form [k1, k2] where k1 and k2 are the orders of the multipoles on each atom. When including [1,2], also need to include [2,1]
217 |
218 | """
219 |
220 | self.multipoles = multipoles
221 |
222 | Nb = self.dim()
223 |
224 | self.HEz = np.zeros((Nb,Nb))
225 | self.HBz = np.zeros((Nb,Nb))
226 | self.HBdiam = np.zeros((Nb,Nb))
227 | self.H0 = np.zeros((Nb,Nb))
228 |
229 | # HInt is 5xNxN, where the first index is the total change
230 | # in magnetic quantum number. Keeping track of it this way allows
231 | # the angular dependence to be easily worked out later
232 | #self.HInt = np.zeros((5,Nb,Nb))
233 |
234 | self.HInt = []
235 |
236 | for mm in self.multipoles:
237 | # need to have an Nb x Nb matrix for each possible deltaMtotal
238 | dm_max = mm[0] + mm[1]
239 | self.HInt.append(np.zeros((2*dm_max+1,Nb,Nb)))
240 |
241 |
242 | # first, compue H0 and HBz, which are diagonal
243 | for ii in range(Nb):
244 | self.H0[ii,ii] = self.pairs[ii].energy_Hz - self.p0.energy_Hz
245 | #self.HBz[ii,ii] = self.pairs[ii].s1.get_g()*self.pairs[ii].s1.m + self.pairs[ii].s2.get_g()*self.pairs[ii].s2.m
246 |
247 | self._compute_HBz()
248 | self._compute_HBdiam()
249 | self._compute_HEz_Hint_fast()
250 |
251 | def _compute_HBz(self):
252 | """ Compute the Zeeman Hamiltonian. """
253 |
254 | # if we don't have an MQDT model, can just enter diagonal matrix elements
255 | if (not isinstance(self.pairs[0].s1,state_mqdt)) or (not isinstance(self.pairs[0].s2,state_mqdt)):
256 | for ii in range(self.dim()):
257 | self.HBz[ii,ii] = self.pairs[ii].s1.get_g()*self.pairs[ii].s1.m + self.pairs[ii].s2.get_g()*self.pairs[ii].s2.m
258 |
259 | return
260 |
261 | # if we do, put in off-diagonal matrix elements as well
262 | # NB: this might fail for interactions between MQDT and non-MQDT, because we don't have get_magnetic_me defined for non-MQDT states.
263 | for ii in range(self.dim()):
264 | for jj in range(self.dim()):
265 |
266 | pii = self.pairs[ii]
267 | pjj = self.pairs[jj]
268 |
269 | if pii.s2 == pjj.s2:
270 | self.HBz[ii,jj] += pii.s1.atom.get_magnetic_me(self.pairs[ii].s1,self.pairs[jj].s1)
271 |
272 | if pii.s1 == pjj.s1:
273 | self.HBz[ii,jj] += pii.s2.atom.get_magnetic_me(self.pairs[ii].s2,self.pairs[jj].s2)
274 |
275 | return
276 |
277 | def _compute_HBdiam(self):
278 | """ Compute the Diamagnetic Hamiltonian. """
279 |
280 | # if we do, put in off-diagonal matrix elements as well
281 | # NB: this might fail for interactions between MQDT and non-MQDT, because we don't have get_magnetic_me defined for non-MQDT states.
282 | for ii in range(self.dim()):
283 | for jj in range(self.dim()):
284 |
285 | pii = self.pairs[ii]
286 | pjj = self.pairs[jj]
287 |
288 | if pii.s2 == pjj.s2:
289 | self.HBdiam[ii, jj] += pii.s1.atom.diamagnetic_int(self.pairs[ii].s1, self.pairs[jj].s1)
290 |
291 | if pii.s1 == pjj.s1:
292 | self.HBdiam[ii, jj] += pii.s2.atom.diamagnetic_int(self.pairs[ii].s2, self.pairs[jj].s2)
293 |
294 | return
295 |
296 |
297 |
298 | def _compute_HEz_Hint(self):
299 | printDebug = False
300 | computeHInt = True
301 |
302 | #ii (row idx) is final state
303 | #jj (col idx) is initial state
304 | for ii in range(self.dim()):
305 | for jj in range(ii,self.dim()): # only do the upper right triangle + diagonal
306 |
307 | pii = self.pairs[ii]
308 | pjj = self.pairs[jj]
309 |
310 | #print(pii,pjj)
311 |
312 | # if pairs are connected by a single-atom E1 transition with q=0,
313 | # add it to HEz
314 |
315 | # NB: putting the == first keeps this from evaluating the second part
316 | # when it's not needed, which saves a lot of time
317 | if pii.s2 == pjj.s2 and pii.s1.allowed_multipole(pjj.s1,k=1,qIn=(0,)):
318 | #if pii.s1.allowed_e1(pjj.s1,qIn=[0]) and pii.s2 == pjj.s2:
319 | #self.HEz[ii,jj] += pii.s1.get_dipole_me(pjj.s1,qIn=[0])
320 | self.HEz[ii,jj] += pii.s1.get_multipole_me(pjj.s1,qIn=(0,),k=1)
321 |
322 | if pii.s1 == pjj.s1 and pii.s2.allowed_multipole(pjj.s2,k=1,qIn=(0,)):
323 | #if pii.s2.allowed_e1(pjj.s2,qIn=[0]) and pii.s1 == pjj.s1:
324 | #self.HEz[ii,jj] += pii.s2.get_dipole_me(pjj.s2,qIn=[0])
325 | self.HEz[ii,jj] += pii.s2.get_multipole_me(pjj.s2,qIn=(0,),k=1)
326 |
327 | if self.HEz[ii,jj] != 0 and printDebug:
328 | print(pii, "<-(Ez)-> ", pjj)
329 |
330 | # if pairs are connected by allowed E1 transitions in both atoms, add to Hint:
331 |
332 | if computeHInt:
333 |
334 | for mm,HInt in zip(self.multipoles, self.HInt):
335 |
336 | # first check that the pair states reflect allowed transitions for given multipole
337 | #if pii.s1.allowed_e1(pjj.s1) and pii.s2.allowed_e1(pjj.s2):
338 |
339 | if pii.s1.allowed_multipole(pjj.s1,k=mm[0]) and pii.s2.allowed_multipole(pjj.s2,k=mm[1]):
340 |
341 | # q = final-initial, ie ii-jj
342 | q1 = -(pii.s1.m - pjj.s1.m)
343 | q2 = -(pii.s2.m - pjj.s2.m)
344 |
345 | qtot = int(q1+q2)
346 | qidx = qtot + (mm[0] + mm[1]) #index for HInt arr.
347 |
348 | d1 = pii.s1.get_multipole_me(pjj.s1,k=mm[0])
349 | d2 = pii.s2.get_multipole_me(pjj.s2,k=mm[1])
350 |
351 | #cg = CG(mm[0],q1,mm[1],q2,mm[0]+mm[1],q1+q2).doit().evalf()
352 | cg = CG(mm[0],q1,mm[1],q2,mm[0]+mm[1],q1+q2)
353 |
354 | me = cg*d1*d2
355 |
356 | if me != 0:
357 | # will multiply later by other factors which are not state-dependent
358 | HInt[qidx,ii,jj] = me
359 |
360 | if me != 0 and printDebug:
361 | print(pii, "<-(",qtot,mm,")-> ", pjj)
362 |
363 | # since we only did upper-right triangle, add transposed version
364 | self.HEz = self.HEz + np.conjugate(np.transpose(self.HEz))
365 |
366 | for mm,HInt in zip(self.multipoles, self.HInt):
367 | for qidx in range(len(HInt)):
368 | HInt[qidx] = HInt[qidx] + np.conjugate(np.transpose(HInt[qidx])) - np.diagflat(np.diagonal(HInt[qidx]))
369 |
370 | def _compute_HEz_Hint_fast(self):
371 | """ This fast version of computing HEz and Hint works by iterating over the pair basis
372 | in blocks grouped by the first state, s1. For each block, we check if the required
373 | s1 transition is allowed (ie, dipole-dipole in the case we are considering [1,1] multipole).
374 |
375 | If it is not, we skip the entire block (this is the main time saving)
376 |
377 | If it is, we compute the upper diagonal of cthe block, as before.
378 |
379 | Note that with caching enabled, the main thing that we are trying to minimize
380 | is calls to allowed_multipole().
381 | """
382 |
383 | printDebug = False
384 | computeHInt = True
385 |
386 | # generate a list of all unique states s1
387 | unique_s1 = list(set([p.s1 for p in self.pairs]))
388 |
389 | # for each state, make a list of the pair indexes where they occur
390 | unique_s1_pair_idx = []
391 |
392 | ttot=0
393 |
394 | for us1 in unique_s1:
395 | indices = [en[0] for en in filter(lambda p: p[1].s1==us1, enumerate(self.pairs))]
396 | unique_s1_pair_idx.append(indices)
397 |
398 |
399 | # now we loop over blocks of pairs with same s1
400 | for s1_i,pair_idx_i in zip(unique_s1,unique_s1_pair_idx):
401 |
402 | for s1_j,pair_idx_j in zip(unique_s1,unique_s1_pair_idx):
403 |
404 | # Ez
405 | # compute Ez if s1_i == s1_j or if there is a ME for s1_i->j
406 | if s1_i == s1_j or s1_i.allowed_multipole(s1_j,k=1,qIn=(0,)):
407 |
408 | s1_same = True if s1_i == s1_j else False
409 |
410 | # loop over upper diagonal
411 |
412 | for ii in pair_idx_i:
413 | for jj in filter(lambda x: x>= ii, pair_idx_j):
414 |
415 | pii = self.pairs[ii]
416 | pjj = self.pairs[jj]
417 |
418 | if (not s1_same) and pii.s2 == pjj.s2:
419 | self.HEz[ii,jj] += pii.s1.get_multipole_me(pjj.s1,qIn=(0,),k=1)
420 |
421 | if s1_same and pii.s2.allowed_multipole(pjj.s2,k=1,qIn=(0,)):
422 | self.HEz[ii,jj] += pii.s2.get_multipole_me(pjj.s2,qIn=(0,),k=1)
423 |
424 |
425 | # now look at interactions
426 |
427 | for mm,HInt in zip(self.multipoles, self.HInt):
428 |
429 | # if there is the right transition allowed on s1
430 |
431 | if s1_i.allowed_multipole(s1_j,k=mm[0]):
432 |
433 | for ii in pair_idx_i:
434 | for jj in filter(lambda x: x>= ii, pair_idx_j):
435 |
436 | pii = self.pairs[ii]
437 | pjj = self.pairs[jj]
438 |
439 | # already checked s1
440 | #if pii.s1.allowed_multipole(pjj.s1,k=mm[0]) and pii.s2.allowed_multipole(pjj.s2,k=mm[1]):
441 | if pii.s2.allowed_multipole(pjj.s2,k=mm[1]):
442 |
443 | # q = final-initial, ie ii-jj
444 | q1 = -(pii.s1.m - pjj.s1.m)
445 | q2 = -(pii.s2.m - pjj.s2.m)
446 |
447 | qtot = int(q1+q2)
448 | qidx = qtot + (mm[0] + mm[1]) #index for HInt arr.
449 |
450 | d1 = pii.s1.get_multipole_me(pjj.s1,k=mm[0])
451 | d2 = pii.s2.get_multipole_me(pjj.s2,k=mm[1])
452 |
453 | #cg = CG(mm[0],q1,mm[1],q2,mm[0]+mm[1],q1+q2).doit().evalf()
454 | cg = CG(mm[0],q1,mm[1],q2,mm[0]+mm[1],q1+q2)
455 |
456 | me = cg*d1*d2
457 |
458 | if me != 0:
459 | # will multiply later by other factors which are not state-dependent
460 | HInt[qidx,ii,jj] = me
461 |
462 | if me != 0 and printDebug:
463 | print(pii, "<-(",qtot,mm,")-> ", pjj)
464 |
465 |
466 | # since we only did upper-right triangle, add transposed version
467 | self.HEz = self.HEz + np.conjugate(np.transpose(self.HEz))
468 |
469 | for mm,HInt in zip(self.multipoles, self.HInt):
470 | for qidx in range(len(HInt)):
471 | HInt[qidx] = HInt[qidx] + np.conjugate(np.transpose(HInt[qidx])) - np.diagflat(np.diagonal(HInt[qidx]))
472 |
473 |
474 |
475 |
476 | def computeHtot(self,env,rum,th=np.pi/2,phi=0,interactions=True,multipoles=None):
477 | """ this computes the eigenvalues of the total hamiltonian for environment
478 | specified in env (E,B, etc.), for two atoms with relative axis (r,th,phi).
479 |
480 | Interactions can be turned off with option interactions.
481 |
482 | Returns energies/overlaps of eigenstates with maximum overlap on pairs in
483 | self.highlights. Also saves eigenstates/values for further analysis.
484 |
485 | If multipoles option is not specified, will use all avaialble hamiltonians from compute_hamiltonians.
486 | """
487 | Nb = self.dim()
488 |
489 | # save time if we can
490 | dtype = 'float64' if phi==0 else 'complex128'
491 |
492 | self.Htot = np.zeros((Nb,Nb),dtype=dtype)
493 |
494 | ub = cs.physical_constants['Bohr magneton in Hz/T'][0]*1e-4 # in Hz/Gauss
495 | self.Htot += self.H0 + env.Ez_Vcm*100*cs.e*a0*self.HEz/cs.h + ub*env.Bz_Gauss*self.HBz
496 |
497 | if env.diamagnetism == True:
498 | self.Htot+= self.HBdiam*(env.Bz_Gauss/10000)**2/cs.h
499 |
500 | if interactions:
501 |
502 | if multipoles is not None:
503 | for mm in multipoles:
504 | if mm not in self.multipoles:
505 | print("Warning--trying to compute interactions with %s multipole, but hamiltonian has only been computed for %s" % (repr(mm),repr(self.multipoles)))
506 |
507 |
508 | self.HIntAll = np.zeros((Nb,Nb),dtype=dtype)
509 |
510 | for mm,HInt in zip(self.multipoles, self.HInt):
511 |
512 | if multipoles is not None and not(mm in multipoles):
513 | continue
514 |
515 | dq_max = mm[0] + mm[1]
516 | for qidx in range(len(HInt)):
517 | self.HIntAll += HInt[qidx]*self.interaction_prefactor(qidx-dq_max,rum,th,phi,mm=mm)
518 |
519 | self.Htot += self.HIntAll
520 |
521 | # NB: eigh is about 4x faster than eig
522 | self.es,self.ev = np.linalg.eigh(self.Htot)
523 |
524 | # eig() does not sort eigenvalues, so do that first
525 | sort_idx = np.argsort(self.es)
526 | self.es = self.es[sort_idx]
527 | self.ev = self.ev[:,sort_idx]
528 |
529 | # now find highlighted pairs, and return (energy,overlap)
530 |
531 | ret = []
532 | for p1,sym,p2,targetState in self.highlight:
533 |
534 | targetState = targetState / np.linalg.norm(targetState)
535 |
536 | ov = [np.abs(np.sum(targetState*self.ev[:,ii]))**2 for ii in range(Nb)]
537 | idx = np.argmax(ov)
538 |
539 | ret.append([self.es[idx],ov[idx],idx])
540 |
541 | return np.array(ret)
542 |
543 |
544 | def interaction_prefactor(self,qtot,r,th,phi,mm=[1,1]):
545 | """ returns coefficient to give interaction in Hz for r in microns """
546 | # recall that the scipy sph_harm function has both the order of l,m and th,phi reversed from Mathematica
547 | unit_scale = (cs.e)**2 * a0**(mm[0]+mm[1]) / (4*np.pi*cs.epsilon_0*cs.h)
548 |
549 | #return -unit_scale * np.sqrt(24*np.pi/5) * 1/r**(3) * np.conjugate(sp.special.sph_harm(qtot,2,phi,th))
550 |
551 | number_factor = (4*np.pi)**3 * np.math.factorial(2*mm[0] + 2*mm[1]) / ( np.math.factorial(2*mm[0]+1) * np.math.factorial(2*mm[1]+1) * (2*mm[0] + 2*mm[1] + 1) )
552 | number_factor *= (2*mm[0]+1)/(4*np.pi)*(2*mm[1]+1)/(4*np.pi)
553 |
554 | # number_factor values
555 | # Note that this is equal to sqrt term in Vaillant 2012 eq. 6, but multiplied by 4pi/(2k+1) because
556 | # our reduced matrix elements are of normalized spherical harmonics.
557 | # [1,1]: 24*pi/5
558 | # [1,2]: 60*pi/7
559 | # [2,2]: 280*pi/9
560 | # [3,3]: 3696*pi/13
561 |
562 | ret = (-1)**mm[1] * unit_scale * np.sqrt(number_factor) * 1/(r*1e-6)**(mm[0]+mm[1]+1) * np.conjugate(sp.special.sph_harm(qtot,mm[0]+mm[1],phi,th))
563 |
564 | if phi==0:
565 | # in this case, we can save time by using real datatypes
566 | return np.real(ret)
567 | else:
568 | return ret
569 |
570 | def compute_Heff(self,subspace_pairs):
571 | """ compute the effective hamiltonian of the subspace_pairs to second order in the interaction Hamiltonian.
572 | Needs to be run after computeHtot """
573 |
574 | subspace_idx = [self.find(p) for p in subspace_pairs]
575 |
576 | # first get the restriction of Htot onto the subspace
577 | Heff_0 = self.Htot[np.ix_(subspace_idx,subspace_idx)]
578 |
579 | #N now sum over intermediate states
580 | for ii in range(self.dim()):
581 | if ii not in subspace_idx:
582 |
583 | for nn in range(len(subspace_idx)):
584 | for mm in range(len(subspace_idx)):
585 | dE_nn = self.pairs[subspace_idx[nn]].energy_Hz - self.pairs[ii].energy_Hz
586 |
587 | dE_mm = self.pairs[subspace_idx[mm]].energy_Hz - self.pairs[ii].energy_Hz
588 |
589 | Heff_0[nn,mm] += 0.5*(self.HIntAll[subspace_idx[nn],ii]*self.HIntAll[ii,subspace_idx[mm]]/dE_nn + self.HIntAll[subspace_idx[mm],ii]*self.HIntAll[ii,subspace_idx[nn]]/dE_mm)
590 |
591 | return Heff_0
--------------------------------------------------------------------------------
/rydcalc/ponderomotive.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Fri Jun 5 09:36:07 2020
5 |
6 | @author: jt11
7 | """
8 |
9 | import numpy as np
10 | import functools
11 | import scipy.constants as cs
12 |
13 | from .constants import *
14 |
15 |
16 | class ponderomotive:
17 | # base class that defines f_kq and lays out necessary functions.
18 | # can pass in external complex amplitude function
19 |
20 | def __init__(self,intensity_fn):
21 | self.intensity_fn = intensity_fn
22 |
23 | def intensity(self,z,r):
24 | return self.intensity_fn(z,r)
25 |
26 | def __hash__(self):
27 | # this is necessary for lru_cache to work--it needs some way to represent "self" as an integer
28 | return id(self)
29 |
30 | @functools.lru_cache(maxsize=1024)
31 | def f_kq(self,k,q,r):
32 | # This calculates the f_kq, but for most
33 | angularIntegral, error = scipy.integrate.quad(lambda theta: theta_SpHarm(k,q,theta)*self.intensity(r*np.cos(theta), r*np.sin(theta))*np.sin(theta), 0, np.pi)
34 | return(2*np.pi*angularIntegral)
35 |
36 |
37 | class Ponderomotive3DLattice(ponderomotive):
38 |
39 | def __init__(self,kx,ky,kz,lambda_nm=1000,dx=0,dy=0,dz=0):
40 |
41 | self.kx = kx
42 | self.ky = ky
43 | self.kz = kz
44 |
45 | self.dx = dx
46 | self.dy = dy
47 | self.dz = dz
48 |
49 | # scale factors allowing turning off certain directions
50 | self.sx = 0 if kx==0 else 1
51 | self.sy = 0 if ky==0 else 1
52 | self.sz = 0 if kz==0 else 1
53 |
54 | if dx ==0 and dy==0 and dz==0:
55 | self.krange = range(0,30,2)
56 | else:
57 | self.krange = range(0,30)
58 |
59 | self.lambda_nm = lambda_nm
60 |
61 | def unit_scale(self):
62 | # converts integral to units of energy. Scaling to Hz is done in compute_energies
63 | self.omega = 2*np.pi*cs.c/(self.lambda_nm * 1e-9)
64 | return -0.5*cs.e**2/(cs.electron_mass * self.omega**2 * cs.epsilon_0 * cs.c)
65 |
66 | def intensity_cart(self,x,y,z):
67 |
68 | #return np.cos(2*self.kx*(x-self.dx))*np.cos(2*self.ky*(y-self.dy))*np.cos(2*self.kz*(z-self.dz))
69 |
70 | return self.sx*np.cos(2*self.kx*(x-self.dx)) + self.sy*np.cos(2*self.ky*(y-self.dy)) + self.sz*np.cos(2*self.kz*(z-self.dz))
71 |
72 | def intensity_sph(self,r,th,phi):
73 |
74 | return self.intensity_cart(r*np.sin(th)*np.cos(phi), r*np.sin(th)*np.sin(phi), r*np.cos(th))
75 |
76 | def sph2cart(self,r,th,phi):
77 | # takes xyz, returns (r,th,phi)
78 | return (r*np.sin(th)*np.cos(phi), r*np.sin(th)*np.sin(phi), r*np.cos(th))
79 |
80 | def sph2cyl(self,r,z,phi):
81 |
82 | return (r*np.sin(th)/nm, r*np.cos(th)/nm, phi)
83 |
84 | def cart2sph(self,x,y,z):
85 | return (np.sqrt(x**2+y**2+z**2), np.arctan2(y,x), np.arctan2(np.sqrt(x**2+y**2)/z))
86 |
87 | def cart2cyl(self,x,y,z):
88 | return (np.sqrt(x**2+y**2), z, np.arctan2(y,x))
89 |
90 | def cyl2cart(self,r,z,phi):
91 | return (r*np.cos(phi),r*np.sin(phi),z)
92 |
93 | def f_kq(self,k,q,r):
94 | if type(r) == type(np.zeros(2)):
95 | return np.array([self.f_kq_single(k,q,rr) for rr in r])
96 | else:
97 | return self.f_kq_single(k,q,r)
98 |
99 | @functools.lru_cache(maxsize=1000000)
100 | def f_kq_single(self,k,q,r):
101 |
102 | if np.abs(q) > k:
103 | return 0
104 |
105 | nquad = 50
106 |
107 | intfn = lambda th,phi: self.intensity_sph(r,th,phi) * np.conjugate(scipy.special.sph_harm(q,k,phi,th))*np.sin(th)
108 | val,err = my_fixed_dblquad(intfn,0,2*np.pi,0,np.pi,nx=nquad,ny=nquad)
109 |
110 | return val*np.sqrt((2*k+1)/(4*np.pi))
111 |
112 | def get_me(self,s1,s2):
113 |
114 | q = s2.m - s1.m
115 |
116 | me = 0
117 | for k in self.krange:
118 | me += s1.get_multipole_me(s2,k=k,operator=lambda r,k: self.f_kq(k,q,r*a0))
119 |
120 | return self.unit_scale()*me
121 |
122 | # note that these are not tested
123 | class PonderomotiveLG(Ponderomotive3DLattice):
124 |
125 | def __init__(self,p,l,lambda_nm,w0_nm,dr_nm=0,dz_nm=0):
126 |
127 | self.l = l
128 | self.p = p
129 | self.lambda_nm = lambda_nm
130 | self.w0_nm = w0_nm
131 |
132 | self.zR_nm = np.pi*self.w0_nm**2/self.lambda_nm
133 |
134 | self.dr_nm = dr_nm
135 | self.dz_nm = dz_nm
136 |
137 | self.krange = range(10)
138 |
139 | self.laguerre = scipy.special.genlaguerre(self.p,np.abs(self.l))
140 | self.norm = np.sqrt(2*np.math.factorial(self.p)/(np.pi*np.math.factorial(self.p + np.abs(self.l))))
141 |
142 | def efield_cyl(self,r_nm,z_nm,phi):
143 |
144 | if self.dr_nm != 0 or self.dz_nm != 0:
145 | x_nm,y_nm,z_nm = self.cyl2cart(r_nm,z_nm,phi)
146 | r_nm,z_nm,phi = self.cart2cyl(x_nm - self.dr_nm,y_nm,z_nm - self.dz_nm)
147 |
148 | # putting this first forces datatype of ret to complex right away
149 | ret = np.exp(-1.j*self.l*phi) * np.exp(1.j*self.psi(z_nm))
150 |
151 | ret *= self.norm
152 |
153 | wz = self.w(z_nm)
154 |
155 | ret *= (self.w0_nm / wz) * (r_nm * np.sqrt(2) / wz)**np.abs(self.l) * np.exp(-r_nm**2/wz**2)
156 | ret *= self.laguerre(2*r_nm**2/wz**2)
157 | #ret *= np.exp(-1.j*r_nm**2/(2*self.R(z_nm)))
158 | #ret *= np.exp(-1.j*self.l*phi) * np.exp(1.j*self.psi(z_nm))
159 |
160 | return ret
161 |
162 | def intensity_sph(self,r,th,phi):
163 | # in meters
164 |
165 | nm = 1e-9
166 |
167 | return np.abs(self.efield_cyl(r*np.sin(th)/nm, r*np.cos(th)/nm, phi))**2
168 |
169 | def w(self,z_nm):
170 |
171 | return self.w0_nm * np.sqrt(1+(z_nm/self.zR_nm)**2)
172 |
173 | def R(self,z_nm):
174 |
175 | return z_nm * (1 + (self.zR_nm/z_nm)**2)
176 |
177 | def psi(self,z_nm):
178 |
179 | return np.arctan(z_nm/self.zR_nm)
180 |
181 | class PonderomotiveLG2(PonderomotiveLG):
182 |
183 | def __init__(self,LG1,LG2):
184 |
185 | self.LG1 = LG1
186 | self.LG2 = LG2
187 |
188 | self.krange = range(10)
189 |
190 | self.lambda_nm = self.LG1.lambda_nm
191 |
192 | def intensity_sph(self,r,th,phi):
193 |
194 | nm = 1e-9
195 | cyl_coords = (r*np.sin(th)/nm, r*np.cos(th)/nm, phi)
196 | return self.LG1.efield_cyl(*cyl_coords)*np.conjugate(self.LG2.efield_cyl(*cyl_coords))
197 |
198 |
199 | # Note, if we want to make this mergeable into scipy, need it to support integration
200 | # over vector-valued functions, see https://github.com/scipy/scipy/pull/6885
201 | @functools.lru_cache(maxsize=1024)
202 | def roots_legendre_cached(n):
203 | return scipy.special.roots_legendre(n)
204 |
205 | def my_fixed_dblquad(func, ax, bx, ay, by, args=(), nx=5, ny=5):
206 | # f(y,x)
207 | x_x, w_x = roots_legendre_cached(nx)
208 | x_y, w_y = roots_legendre_cached(ny)
209 |
210 | x_x = np.real(x_x)
211 | x_y = np.real(x_y)
212 |
213 | if np.isinf(ax) or np.isinf(bx) or np.isinf(ay) or np.isinf(by):
214 | raise ValueError("Gaussian quadrature is only available for "
215 | "finite limits.")
216 |
217 | x_x_scaled = (bx-ax)*(x_x+1)/2.0 + ax
218 | x_y_scaled = (by-ay)*(x_y+1)/2.0 + ay
219 |
220 | xpts,ypts = np.meshgrid(x_x_scaled,x_y_scaled)
221 | wts = np.outer(w_y,w_x)
222 |
223 | #return xpts,ypts,wts
224 |
225 | return (bx-ax)/2.0 * (by-ay)/2.0 * np.sum(wts*func(ypts,xpts, *args)), None
226 | #return w_x*w_y*func(ypts,xpts, *args)
227 |
228 |
229 | class transition:
230 |
231 | def __init__(self,lam_nm,lifetime_ns,ji,jf,name=''):
232 |
233 | self.lam_nm = lam_nm
234 | self.lifetime_ns = lifetime_ns
235 | self.name = name
236 |
237 | # lower state j
238 | self.ji = ji
239 |
240 | # upper state j
241 | self.jf = jf
242 |
243 | self.omega_au = 2*np.pi/(lam_nm*1e-9*cs.alpha/a0)
244 | self.omega_sec = 2*np.pi*cs.c/(lam_nm*1e-9)
245 |
246 | def __repr__(self):
247 |
248 | return "%s: %.1f nm, tau=%.2f" % (self.name, self.lam_nm, self.lifetime_ns)
--------------------------------------------------------------------------------
/rydcalc/rydcalc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Sun Feb 2 11:29:18 2020
5 |
6 | @author: jt11
7 | """
8 | #test
9 | import numpy as np
10 | #from sympy.physics.wigner import wigner_3j,wigner_6j,wigner_9j
11 | # from sympy.physics.wigner import wigner_3j as wigner_3j_sympy
12 | # from sympy.physics.wigner import wigner_6j as wigner_6j_sympy
13 | # from sympy.physics.wigner import wigner_9j
14 | # from sympy.physics.hydrogen import R_nl
15 | # from sympy.physics.quantum.cg import CG as CG_sympy
16 |
17 | from sympy.utilities import lambdify
18 | #from sympy.functions.special.spherical_harmonics import Ynm
19 | import scipy as sp
20 | import scipy.integrate
21 | import scipy.interpolate
22 | import scipy.constants as cs
23 | import scipy.optimize
24 |
25 | import sympy
26 |
27 | import time,os
28 |
29 | import functools, hashlib
30 |
31 | #a0 = cs.physical_constants['Bohr radius'][0]
32 |
33 | # from .ponderomotive import *
34 | # from .db_manager import *
35 | # from .constants import *
36 |
37 | # from .hydrogen import *
38 | # from .alkali import *
39 | # from .alkaline import *
40 |
41 | # from .alkali_data import *
42 | # from .alkaline_data import*
43 |
44 | # from .utils import *
45 |
46 |
47 | class environment:
48 | """ class that specifies environmental parameters, including E, B, and DOS """
49 |
50 | def __init__(self,Bz_Gauss=0,Ez_Vcm=0,T_K=0,Intensity_Wm2=0,diamagnetism=False):
51 |
52 | self.Bz_Gauss = Bz_Gauss # Z-directed B-field in Gauss
53 | self.Ez_Vcm = Ez_Vcm #Z-directed E-field in V/cm
54 | self.T_K = T_K # temperature in Kelvin
55 |
56 | self.Intensity_Wm2 = Intensity_Wm2 # Peak light intensity in W/m^2 for ponderomotive potential
57 |
58 | # this should take freq. in Hz and polarization +/-1, 0 and return
59 | # normalized LDOS (ie, Purcell factor)
60 | self.ldos = lambda f,q: 1
61 |
62 | # diamagnetism is a flag to include the diamagnetic term in the Hamiltonian
63 | self.diamagnetism = diamagnetism
64 |
65 | # potential addition: two-point DOS to describe interactions in structured environment
66 |
67 | def __repr__(self):
68 | return "Bz=%.2f G, Ez=%.2f V/cm, T=%.2f K" % (self.Bz_Gauss, self.Ez_Vcm, self.T_K)
69 |
70 |
71 |
72 |
73 |
74 | def getCs(pb,env,th=np.pi/2,phi=0,rList_um=np.logspace(0.3,2,20),plot=True):
75 | ''' Compute C6 and C3 coefficients for the highlighted pairs in pb.
76 |
77 | Returns [C6d,C6e,C3d,C3e] where e denotes exchange (off-diagonal) interation
78 | and d is diagonal interaction.
79 |
80 | In computing these, only looks at first two highlighted pairs, and takes
81 | them to be in the order [g,u] as implemented in pb.fill()
82 |
83 | '''
84 |
85 | Nb = pb.dim()
86 |
87 | rumList = rList_um
88 |
89 | energies = []
90 | energiesAll = []
91 | overlaps = []
92 |
93 | en0 = pb.computeHtot(env,0,th=th,phi=phi,interactions=False)[0,0]
94 |
95 | for rum in rumList:
96 |
97 | ret = pb.computeHtot(env,rum,th=th,phi=phi,interactions=True)
98 |
99 | energiesAll.append(pb.es)
100 |
101 | energies.append(ret[:,0]-en0)
102 | overlaps.append(ret[:,1])
103 |
104 | energies = np.array(energies)
105 | overlaps = np.array(overlaps)
106 | energiesAll = np.array(energiesAll)
107 |
108 | #return energies
109 |
110 | def intfn(r,c6,c3):
111 | return c6/r**6 + c3/r**3
112 |
113 | # rMinFit = 5
114 | # eMaxFit_Hz = 1e9
115 | # rFitIdx = np.argwhere(rumList > rMinFit).flatten()
116 | # eFitIdx = np.argwhere(np.abs(energies) < eMaxFit_Hz).flatten() # sort of arbitrary, should probably scale with n
117 | # fitIdx = np.intersect1d (rFitIdx,eFitIdx)
118 |
119 |
120 | #return (rumList[fitIdx],np.real(energies[fitIdx,0]))
121 | interactionFits = []
122 |
123 | for ii in range(len(pb.highlight)):
124 | popt,pcov = sp.optimize.curve_fit(intfn,rumList,np.real(energies[:,ii]),p0=(1e9,1e7))
125 | # fitting on a log scale does better when the r range is large, although it's not clear
126 | #popt,pcov = sp.optimize.curve_fit(lambda r,c6,c3: np.log(intfn(np.exp(r),c6,c3)),np.log(rumList[:]),np.log(np.real(energies[:,ii])),p0=(1e9,1e7))
127 | interactionFits.append(popt)
128 |
129 | if len(interactionFits) > 1:
130 | c6d = (interactionFits[0][0] + interactionFits[1][0])/2
131 | c6e = (interactionFits[0][0] - interactionFits[1][0])/2
132 | c3d = (interactionFits[0][1] + interactionFits[1][1])/2
133 | c3e = (interactionFits[0][1] - interactionFits[1][1])/2
134 | else:
135 | c6d = interactionFits[0][0]
136 | c6e = 0
137 | c3d = interactionFits[0][1]
138 | c3e = 0
139 |
140 | if plot:
141 | plt.figure(figsize=(12,4))
142 | plt.subplot(1,2,1)
143 |
144 | for ii in range(len(pb.highlight)):
145 | posidx = np.argwhere(energies[:,ii] >=0)
146 | negidx = np.argwhere(energies[:,ii] < 0)
147 | plt.plot(rumList[posidx],np.abs(energies[posidx,ii])*1e-6,'^',color='C'+str(ii),label=repr(pb.highlight[ii][:2]))
148 | plt.plot(rumList[negidx],np.abs(energies[negidx,ii])*1e-6,'v',color='C'+str(ii))
149 | plt.plot(rumList,np.abs(intfn(rumList,*interactionFits[ii]))*1e-6,'-',color='C'+str(ii))
150 |
151 | plt.xlabel('r [um]')
152 | plt.ylabel('Pair Energy [MHz]')
153 | plt.legend()
154 | plt.xscale('log')
155 | plt.yscale('log')
156 | #plt.ylim([-10,10])
157 | plt.grid(axis='both')
158 |
159 | plt.subplot(1,2,2)
160 |
161 | for ii in range(len(pb.highlight)):
162 | plt.plot(rumList,overlaps[:,ii],'-',label=repr(pb.highlight[ii][:1]))
163 |
164 | plt.xlabel('r [um]')
165 | plt.ylabel('Overlap')
166 | #plt.legend()
167 | plt.xscale('log')
168 | #plt.yscale('log')
169 | plt.ylim([-0.1,1.2])
170 | plt.grid(axis='both')
171 |
172 | return c6d,c6e,c3d,c3e
173 |
174 |
--------------------------------------------------------------------------------
/rydcalc/setupc.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup, Extension
2 | from numpy.distutils.misc_util import get_numpy_include_dirs
3 |
4 | setup(ext_modules=[Extension("arc_c_extensions", ["arc_c_extensions.c"],
5 | extra_compile_args = ['-Wall', '-O3'],
6 | include_dirs=get_numpy_include_dirs())])
7 |
--------------------------------------------------------------------------------
/rydcalc/single_basis.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | from .utils import *
4 | from .constants import *
5 |
6 | class model_potential:
7 | """
8 | Class to represent model potential for use in python numerov wavefunction calculations.
9 | This code is adapted directly from the Alkali Rydberg Calculator (ARC).
10 | """
11 |
12 | def __init__(self,alphaC,a1,a2,a3,a4,rc,Z,include_so=True, use_model = True):
13 | #FIXME -- this construction and its takes parameters for up to l=3, but this is implicit and should be made explicit
14 |
15 | self.alphaC = alphaC
16 | self.a1 = a1
17 | self.a2 = a2
18 | self.a3 = a3
19 | self.a4 = a4
20 | self.rc = rc
21 |
22 | self.Z = Z
23 |
24 | self.include_so = include_so
25 | self.use_model = use_model
26 |
27 | def core_potential(self, ch, r):
28 | """ FROM ARC """
29 | """ core potential felt by valence electron
30 | For more details about derivation of model potential see
31 | Ref. [#marinescu]_.
32 | Args:
33 | l (int): orbital angular momentum
34 | r (float): distance from the nucleus (in a.u.)
35 | Returns:
36 | float: core potential felt by valence electron (in a.u. ???)
37 | References:
38 | .. [#marinescu] M. Marinescu, H. R. Sadeghpour, and A. Dalgarno
39 | PRA **49**, 982 (1994),
40 | https://doi.org/10.1103/PhysRevA.49.982
41 | """
42 | l = ch.l
43 | return -self.effective_charge(ch, r) / r - self.alphaC / (2 * r**4) * \
44 | (1 - np.exp(-(r / self.rc[l])**6))
45 |
46 | def effective_charge(self, ch, r): #l, r):
47 | """ effective charge of the core felt by valence electron
48 | For more details about derivation of model potential see
49 | Ref. [#marinescu]_.
50 | Args:
51 | l (int): orbital angular momentum
52 | r (float): distance from the nucleus (in a.u.)
53 | Returns:
54 | float: effective charge (in a.u.)
55 | """
56 | l = ch.l
57 |
58 | return 1.0 + (self.Z - 1) * np.exp(-self.a1[l] * r) - \
59 | r * (self.a3[l] + self.a4[l] * r) * np.exp(-self.a2[l] * r)
60 |
61 | def potential(self, ch, r):
62 | """ returns total potential that electron feels
63 | Total potential = core potential + Spin-Orbit interaction
64 | Args:
65 | l (int): orbital angular momentum
66 | s (float): spin angular momentum
67 | j (float): total angular momentum
68 | r (float): distance from the nucleus (in a.u.)
69 | Returns:
70 | float: potential (in a.u.)
71 | """
72 |
73 |
74 | l = ch.l
75 | j = ch.j
76 | s = ch.s
77 |
78 | so_factor = 0 if self.include_so else 1
79 |
80 | if l < 4 and self.use_model:
81 | return self.core_potential(ch, r) + so_factor*cs.fine_structure**2 / (2.0 * r**3) * \
82 | (j * (j + 1.0) - l * (l + 1.0) - s * (s + 1)) / 2.0
83 | else:
84 | # act as if it is a Hydrogen atom
85 | return -1. / r + so_factor*cs.fine_structure**2 / (2.0 * r**3) * \
86 | (j * (j + 1.0) - l * (l + 1.0) - s * (s + 1)) / 2.0
87 |
88 | # Simple instance of model_potential for pure 1/r coulomb potential (not clear that this is used)
89 | coulomb_potential = model_potential(0,[0]*4,[0]*4,[0]*4,[0]*4,[1e-3]*4,1,include_so = True)
90 |
91 | class core_state:
92 | """ Class to represent core state used in MQDT formulation.
93 |
94 | The core state represents the quantum numbers of the inner electrons (s,l,j) and nucleus (i), adding up to a total angular momentum f of the core.
95 | There is no mF because a complete state is not formed until this adds with the Rydberg electron.
96 |
97 | There is also an associated ionization energy Ei_Hz, which is used to calculate the outer electron wavefunction associated with each channel.
98 |
99 | Lastly, the dipole and quadrupole polarizabilities alpha_d_a03 and alpha_q_a05 are included to calculate the polarization contribution to the quantum
100 | defect for high-l states.
101 | """
102 |
103 | def __init__(self,qn,Ei_Hz,tt='sljif',config='',potential = None, alpha_d_a03 = 0, alpha_q_a05 = 0):
104 | """ Example:
105 |
106 | 171Yb Fc=0
107 | core_state((1/2,0,1/2,1/2,0), Ei = 50044.123, tt = 'sljif', config = '6s1/2 Fc=0')
108 |
109 | """
110 |
111 | if tt == 'sljif':
112 |
113 | self.s = qn[0]
114 | self.l = qn[1]
115 | self.j = qn[2]
116 | self.i = qn[3]
117 | self.f = qn[4]
118 |
119 | else:
120 | print("tt ", tt, " not currently supported.")
121 |
122 | self.Ei_Hz = Ei_Hz
123 | self.Ei_au = self.Ei_Hz/(2*cs.Rydberg*cs.c)
124 | self.tt = tt
125 | self.config = config
126 |
127 | self.potential = potential
128 |
129 | self.alpha_d_a03 = alpha_d_a03
130 | self.alpha_q_a05 = alpha_q_a05
131 |
132 | def __repr__(self):
133 | return self.config
134 |
135 | def __eq__(self,other):
136 | if self.s == other.s and self.l == other.l and self.j == other.j and self.i == other.i and self.f == other.f and self.Ei_Hz == other.Ei_Hz and self.config == other.config:
137 | return True
138 | else:
139 | return False
140 |
141 |
142 | class channel:
143 |
144 | def __init__(self,core,qn,tt = 'sljf',defect = None, no_me = False):
145 | # FIXME: defect is not used anymore and should be removed\
146 | """
147 | An MQDT channel consists of a core state and a Rydberg electron state. The core state is an instance of core_state,
148 | while the Rydberg electron state is specified by the quantum numbers (s,l,j) of the electron.
149 |
150 | Example:
151 |
152 | 171Yb F=1/2 S channels
153 | coreFc0 = core_state((1/2,0,1/2,1/2,0), Ei = 50044.123, tt = 'sljif', config = '6s1/2 Fc=0')
154 | channel(coreFc2,(1/2,0,1/2), tt = 'slj')
155 |
156 | coreFc1 = core_state((1/2,0,1/2,1/2,1), Ei = 50044.123 + delta, tt = 'sljif', config = '6s1/2 Fc=1')
157 | channel(coreFc1,(1/2,0,1/2), tt = 'slj')
158 |
159 | """
160 |
161 | self.core = core
162 | self.tt = tt
163 | self.qn = qn
164 | self.no_me = no_me
165 |
166 | if defect is None:
167 | self.defects = []
168 | elif type(defect) is list:
169 | self.defects = defect
170 | else:
171 | self.defects = [defect]
172 |
173 | if tt == 'sljf':
174 | # it is not clear why this is here -- it doesn't seem like we should ever be asking about the quantum number f of the Rydberg electron
175 | # because there is explicitly no nuclear contribution to the Rydberg electron (it's in the core). It appears that this is a holdover,
176 | # and all of the actual instances of models use 'slj'.
177 | self.s = qn[0]
178 | self.l = qn[1]
179 | self.j = qn[2]
180 | self.f = qn[3]
181 |
182 | elif tt == 'slj':
183 | self.s = qn[0]
184 | self.l = qn[1]
185 | self.j = qn[2]
186 | self.f = self.j # total angular momentum of Rydberg electron
187 |
188 | else:
189 | print("tt ", tt, " not currently supported in channel.__init__().")
190 | return None
191 |
192 | # def get_defect(self,n):
193 |
194 | # if len(self.defects)==0:
195 | # return 0
196 |
197 | # for d in self.defects:
198 | # if d.is_valid(n):
199 | # return d.get_defect(self,n)
200 |
201 | def __repr__(self):
202 | if len(self.defects) == 0:
203 | defstr="(no defect defined)"
204 | else:
205 | defstr= "(with defect model)"
206 | return "Channel, t=%s, qn=%s, core: %s" % (self.tt,repr(self.qn),self.core.config)
207 |
208 | def __eq__(self,other):
209 |
210 | if self.core == other.core and self.s == other.s and self.l == other.l and self.j == other.j and self.f == other.f:
211 | return True
212 | return False
213 |
214 | class state_mqdt:
215 |
216 | def __init__(self,atom,qn,Ai,Aalpha,channels,energy_Hz,tt='npfm',eq_cutoff_Hz = 20000):
217 | """
218 | Initializes an MQDT state with specified parameters.
219 |
220 | Args:
221 | atom (AlkalineAtom): The atom for which the MQDT state is defined.
222 | qn (tuple): Quantum numbers for the state.
223 | Ai (float): Ionization energy in Hz.
224 | Aalpha (float): Polarizability.
225 | channels (list): List of channels associated with the state.
226 | energy_Hz (float): Energy of the state in Hz.
227 | tt (str, optional): Type tag for the quantum numbers. Defaults to 'npfm'.
228 | eq_cutoff_Hz (float, optional): Energy cutoff in Hz for equality checks. Defaults to 20000 Hz.
229 | """
230 |
231 | self.atom = atom
232 | self.qn = qn
233 | self.tt = tt
234 |
235 | # notes: the quantum number 't' was from a brief attempt to introduce an extra quantum number to distinguish series with the same quantum number (ie, S F=1/2 series in 171Yb).
236 | # however, we realized that ordering was impossible so have given up, and it should not be used but is left here so it doesn't break anything else...
237 | if tt=='npfm':
238 | self.n = qn[0]
239 | self.parity = qn[1]
240 | self.f = qn[2]
241 | self.t = 0
242 | self.m = qn[3]
243 |
244 | elif tt=='npftm':
245 | self.n = qn[0]
246 | self.parity = qn[1]
247 | self.f = qn[2]
248 | self.t = qn[3]
249 | self.m = qn[4]
250 |
251 | elif tt=='vpfm':
252 | self.n = qn[0]
253 | self.v = qn[0]
254 | self.parity = qn[1]
255 | self.f = qn[2]
256 | self.m = qn[3]
257 |
258 | elif tt=='nsljfm':
259 |
260 | self.n = qn[0]
261 | self.s = qn[1]
262 | self.parity = (-1)**qn[2]
263 | self.l = qn[2]
264 | self.j = qn[3]
265 | self.f = qn[4]
266 | self.m = qn[5]
267 |
268 | else:
269 | print("tt=",tt," not supported by state_mqdt.")
270 |
271 | self.channels = channels
272 | self.Ai = Ai
273 | self.Aalpha = Aalpha
274 |
275 | self.energy_Hz = energy_Hz
276 |
277 | self.hashval = id(self)
278 |
279 | self.eq_cutoff_Hz = eq_cutoff_Hz
280 |
281 | def get_channel_idx(self,ch):
282 | for idx,c in enumerate(self.channels):
283 | if c == ch:
284 | return idx
285 | return None
286 |
287 | def get_energy_Hz(self):
288 | return self.energy_Hz
289 |
290 | def get_energy_au(self):
291 | """ Get state energy in atomic units (Hartree) """
292 | return self.energy_Hz/(2*cs.Rydberg*cs.c)
293 |
294 | def get_g(self):
295 | return self.atom.get_g(self)
296 |
297 | def __repr__(self):
298 | # print a nice ket
299 | return self.atom.repr_state(self)
300 |
301 | def __eq__(self,other):
302 | # check for equality
303 |
304 | # it is about 3x faster to do this with the hash, since we've already computed it,
305 | # even though it's not as clear
306 | if self.hashval == other.hashval:
307 | return True
308 | else:
309 |
310 | if self.atom == other.atom and np.round(self.nub*10000)==np.round(other.nub*10000):#np.abs(self.energy_Hz - other.energy_Hz) max_dev:
32 | max_dev = np.abs(me) - 1
33 |
34 | print("Passed wavefunction norm test for Hydrogen (max deviation=%.2e)." % max_dev)
35 |
36 | def test_multipole_circ(self):
37 | """ Two notes about this one:
38 |
39 | 1. It does not pass for n > 75, which is where H.radial_wavefunction switches to computing
40 | log(R). The answers are still very close (1%), but it may be worth looking into this.
41 |
42 | 2. I am also not sure if the reduced mass correction makes sense.
43 | """
44 |
45 | max_dev = 0
46 |
47 | at = self.H
48 |
49 | for k in [1,2,3]:
50 | for n in np.arange(10,80,10):
51 |
52 | st = at.get_state((n,n-1,n-1))
53 | st2 = at.get_state((n-k,n-1-k,n-1-k))
54 |
55 | me = at.get_multipole_me(st,st2,k=k)#*self.H.mu**k
56 | me_th = circ_me(n,k,at.mu)
57 |
58 | #try:
59 | #self.assertAlmostEqual(me,me_th,places=2)
60 | self.assertAlmostEqual(1,me/me_th,places=7,msg="(n,k)=(%d,%d)" % (n,k))
61 | #except:
62 | # print("Failed on n,k = ",n,k)
63 |
64 | if np.abs(me-me_th) > max_dev:
65 | max_dev = np.abs(me - me_th)/me_th
66 |
67 | print("Passed wavefunction matrix element test for Hydrogen (max deviation=%.2e)." % max_dev)
68 |
69 | # def test_multipole_lowl(self):
70 | # """ The wavefunctions for some multipole transitions from nS -> (n+1)k are computed
71 | # and compared to analyitic results from Mathematica.
72 |
73 | # There is not a simple closed-form expression for the general matrix element. """
74 |
75 | # ans = []
76 |
77 | # ans.append({'k': 1, 'n': 1, 'me': 128*np.sqrt(2)/243})
78 | # ans.append({'k': 1, 'n': 5, 'me': 5914803600000*np.sqrt(14)/3138428376721})
79 |
80 | # ans.append({'k': 2, 'n': 1, 'me': 81*np.sqrt(3/2)/128})
81 | # ans.append({'k': 2, 'n': 5, 'me': -1698835053125*np.sqrt(35/3)/52242776064})
82 |
83 | # ans.append({'k': 3, 'n': 1, 'me': 786432/np.sqrt(5)/390625})
84 | # ans.append({'k': 3, 'n': 5, 'me': -48746899046400000000*np.sqrt(330)/665416609183179841})
85 |
86 | # for a in ans:
87 |
88 | # st = self.H.get_state((a['n'],0,0))
89 | # st2 = self.H.get_state((a['n']+a['k'],a['k'],a['k']))
90 |
91 | # me = self.H.get_multipole_me(st,st2,k=a['k'])*self.H.mu**a['k']
92 |
93 | # me_th = a['me']
94 |
95 | # #self.assertAlmostEqual(me,me_th,places=2)
96 | # self.assertAlmostEqual(1,me/me_th,places=7)
97 |
98 | # def test_circ_lifetime(self):
99 |
100 | # """ Test the zero-temperature lifetime of several circular states.
101 |
102 | # Fails above n=75, with log wavefunctions in H.radial_wavefunction() """
103 |
104 | # for n in np.arange(10,80,10):
105 |
106 | # st = self.H.get_state((n,n-1,n-1))
107 |
108 | # env = rydcalc.environment(T_K=0)
109 |
110 | # lifetime = 1/self.H.total_decay(st,env)*self.H.mu
111 |
112 | # #try:#
113 | # self.assertAlmostEqual(1,lifetime/circ_lifetime(n,0),places=6)
114 | # #except:
115 | # # print(n,t)
116 |
117 | # def test_circ_lifetime_finite(self):
118 |
119 | # """ Test the finite-temperature partial decay rate to the next lowest circular state.
120 |
121 | # The agreement here is not as good for reasons that are unclear, so we set places=2 to check for gross errors.
122 | # """
123 |
124 | # for n in np.arange(10,80,10):
125 |
126 | # for t in [4,10,100,300]:
127 |
128 | # st = self.H.get_state((n,n-1,n-1))
129 | # st2 = self.H.get_state((n-1,n-2,n-2))
130 |
131 | # env = rydcalc.environment(T_K=t)
132 |
133 | # lifetime = 1/self.H.partial_decay(st,st2,env)*self.H.mu
134 |
135 | # #print(lifetime,circ_lifetime(n,t))
136 |
137 | # #try:#
138 | # self.assertAlmostEqual(1,lifetime/circ_lifetime(n,t),places=2)
139 | # #except:
140 | # # print(n,t)
141 |
142 |
143 |
144 | def circ_me(n,k,mu=1):
145 | """ Analytic multipole matrix element from |n,n-1,n-1> down to |n-k,n-1-k,n-1-k>.
146 |
147 | Compute the log to ensure we are able to evaluate for large n, k.
148 |
149 | Note that these matrix elements are computed in units of a_0 involving reduced mass, so we have
150 | to scale by that.
151 | """
152 |
153 | gln = scipy.special.gammaln
154 |
155 | log_me = 0
156 |
157 | log_me += (2*n+1)*np.log(2) + np.log((-1)**k*(k-n)**k) + np.log(n) - n*np.log(n*(n-k))
158 | log_me += -2*n*np.log(1/n+1/(n-k))+0.5*gln(k+0.5) - 0.5*gln(k+1) +gln(n)
159 | log_me += -np.log(np.pi**0.25 * (2*n-k)) - gln(n-k)
160 |
161 | return (-1)**k * (1/mu)**k * np.exp(log_me)
162 |
163 |
164 | def circ_lifetime(n,t):
165 | """ Analytic expression for circular state lifetime vs. temp. Adapted from Eq. 5,6 in
166 |
167 | Xia, Zhang and Saffman, PHYSICAL REVIEW A 88, 062337 (2013)
168 |
169 | """
170 |
171 | prefactor = np.pi**5 * cs.c**3 * cs.epsilon_0**5 * cs.hbar**6 / (cs.m_e * cs.e**10)
172 |
173 | n_factor_log = np.log(3) + (5-2*n)*np.log(4) + (4*n-1)*np.log(2*n-1) - (2*n-2)*np.log(n-1) - (2*n - 4)*np.log(n)
174 |
175 | dE = -cs.Rydberg * cs.c * (1/(n**2) - 1/((n-1)**2)) * cs.h
176 |
177 | if t > 0:
178 | temp_factor = (1/(np.exp(dE/(cs.Boltzmann*t)) - 1) +1)
179 | else:
180 | temp_factor = 1
181 |
182 | return prefactor*np.exp(n_factor_log)/temp_factor
183 |
184 | if __name__ == '__main__':
185 | unittest.main(verbosity=2)
186 |
--------------------------------------------------------------------------------