├── .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 | --------------------------------------------------------------------------------