quadlib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quadlib/__init__.py +39 -0
- quadlib/basis/__init__.py +60 -0
- quadlib/basis/boson.py +61 -0
- quadlib/basis/discrete.py +71 -0
- quadlib/basis/expdvr.py +80 -0
- quadlib/basis/hodvr.py +51 -0
- quadlib/basis/jbasis.py +87 -0
- quadlib/basis/sindvr.py +97 -0
- quadlib/basis/spin.py +64 -0
- quadlib/cli/__init__.py +0 -0
- quadlib/cli/qlb_offset_oper_dof.py +34 -0
- quadlib/experimental/__init__.py +17 -0
- quadlib/experimental/cpd_eom.py +95 -0
- quadlib/experimental/mctdh.py +32 -0
- quadlib/experimental/node.py +259 -0
- quadlib/experimental/pdof.py +9 -0
- quadlib/experimental/tree.py +138 -0
- quadlib/hdmctdh/__init__.py +0 -0
- quadlib/hdmctdh/rundirectory.py +74 -0
- quadlib/legacy/__init__.py +14 -0
- quadlib/legacy/math.py +1337 -0
- quadlib/operator/sop.py +250 -0
- quadlib/propagation/__init__.py +17 -0
- quadlib/propagation/cpd.py +35 -0
- quadlib/propagation/exact.py +139 -0
- quadlib/propagation/integrators.py +108 -0
- quadlib/tools/__init__.py +0 -0
- quadlib/tools/bath.py +84 -0
- quadlib/tools/constants.py +289 -0
- quadlib/tools/iterators.py +276 -0
- quadlib/tools/linalg.py +215 -0
- quadlib/tools/pulses.py +187 -0
- quadlib/tools/spectra.py +179 -0
- quadlib/tools/tensor.py +227 -0
- quadlib/wavefunction/__init__.py +0 -0
- quadlib/wavefunction/wfn_cpd.py +266 -0
- quadlib/wavefunction/wfn_exact.py +147 -0
- quadlib-0.1.0.dist-info/METADATA +135 -0
- quadlib-0.1.0.dist-info/RECORD +43 -0
- quadlib-0.1.0.dist-info/WHEEL +5 -0
- quadlib-0.1.0.dist-info/entry_points.txt +2 -0
- quadlib-0.1.0.dist-info/licenses/LICENSE +19 -0
- quadlib-0.1.0.dist-info/top_level.txt +1 -0
quadlib/tools/linalg.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# QuaDLib: QUAntum Dynamics LIBrary for research and methods development
|
|
2
|
+
# Copyright (C) 2018-2026, The Theoretical Chemistry Group, Heidelberg University
|
|
3
|
+
#
|
|
4
|
+
# This software is licensed under the MIT License (https://spdx.org/licenses/MIT.html)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Linear-algebra helpers used by the library.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from functools import reduce
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sort_eig(evals, evecs, absval=False, order='ascending'):
|
|
16
|
+
"""Sort an (eigvals, eigvecs) pair in place.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
evals : 1D ndarray, modified in place
|
|
21
|
+
evecs : 2D ndarray, eigenvectors stored in columns (axis=1), modified in place
|
|
22
|
+
absval : bool, optional
|
|
23
|
+
If True, sort by ``|evals|`` rather than the signed value.
|
|
24
|
+
order : {'ascending', 'descending'}, optional
|
|
25
|
+
Sort direction. Ignored when ``absval`` is True.
|
|
26
|
+
"""
|
|
27
|
+
if absval:
|
|
28
|
+
s = abs(evals).argsort()
|
|
29
|
+
else:
|
|
30
|
+
if order == 'ascending':
|
|
31
|
+
s = evals.argsort()
|
|
32
|
+
elif order == 'descending':
|
|
33
|
+
s = (-evals).argsort()
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError("order must be 'ascending' or 'descending'")
|
|
36
|
+
evecs[:] = evecs.take(s, axis=1)
|
|
37
|
+
evals[:] = evals.take(s)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def simul_diag(matrices, **kwargs):
|
|
41
|
+
"""Simultaneous diagonalisation of a collection of real symmetric matrices.
|
|
42
|
+
|
|
43
|
+
Finds an orthogonal Us that makes every Us^T A Us as diagonal as
|
|
44
|
+
possible (exactly diagonal if the matrices commute), by Jacobi
|
|
45
|
+
rotations. Based on: "Jacobi angles for simultaneous
|
|
46
|
+
diagonalization", J.-F. Cardoso and A. Souloumiac,
|
|
47
|
+
SIAM J. Matrix Anal. Appl. 17, 161 (1996).
|
|
48
|
+
|
|
49
|
+
Note: kept largely for legacy reasons (not used by the library), but
|
|
50
|
+
it may be useful later, e.g. for DVRs of several coordinates. Only
|
|
51
|
+
real symmetric input is supported (ValueError otherwise): the real
|
|
52
|
+
Jacobi rotations used here do not apply to non-symmetric or complex
|
|
53
|
+
Hermitian matrices; the latter would need complex rotations.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
matrices : sequence of real symmetric (n, n) arrays
|
|
58
|
+
maxiter : int, maximum number of sweeps (default 100)
|
|
59
|
+
tol : float, stop when the change of sum_k |diag(A_k)|^2 between
|
|
60
|
+
sweeps falls below tol (default 1e-4)
|
|
61
|
+
prediag : bool, first diagonalize matrices[matdiag] exactly (default True)
|
|
62
|
+
matdiag : int, index of that matrix (default 0)
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
Us : (n, n) orthogonal matrix; Us^T A Us is (nearly) diagonal
|
|
67
|
+
k : index of the last sweep
|
|
68
|
+
l_eps : array of the convergence measure per sweep
|
|
69
|
+
"""
|
|
70
|
+
for A in matrices:
|
|
71
|
+
A = np.asarray(A)
|
|
72
|
+
if np.iscomplexobj(A):
|
|
73
|
+
raise ValueError('simul_diag: only real symmetric matrices are supported')
|
|
74
|
+
if not np.allclose(A, A.T, rtol=0, atol=1e-12 * max(1.0, np.abs(A).max())):
|
|
75
|
+
raise ValueError('simul_diag: input matrices must be symmetric')
|
|
76
|
+
maxiter = kwargs.get('maxiter', 100)
|
|
77
|
+
tol = kwargs.get('tol', 1.e-4)
|
|
78
|
+
prediag = kwargs.get('prediag', True)
|
|
79
|
+
matdiag = kwargs.get('matdiag', 0) # matrix to fully diagonalise 1st step
|
|
80
|
+
l_eps = np.zeros(maxiter, float)
|
|
81
|
+
rank = len(matrices[0])
|
|
82
|
+
mat0 = []
|
|
83
|
+
sdiag0 = 0.0
|
|
84
|
+
if prediag:
|
|
85
|
+
ev, U = np.linalg.eigh(matrices[matdiag])
|
|
86
|
+
for M in matrices:
|
|
87
|
+
mat0.append(reduce(np.dot, (U.T, M, U)))
|
|
88
|
+
U = U.T
|
|
89
|
+
else:
|
|
90
|
+
U = np.identity(rank, float)
|
|
91
|
+
mat0 = matrices
|
|
92
|
+
for k in range(maxiter):
|
|
93
|
+
mat1, U1, sdiag1 = _sweep(mat0)
|
|
94
|
+
U = np.dot(U1, U)
|
|
95
|
+
mat0 = mat1
|
|
96
|
+
epsilon = abs(sdiag1 - sdiag0)
|
|
97
|
+
sdiag0 = sdiag1
|
|
98
|
+
l_eps[k] = epsilon
|
|
99
|
+
if epsilon < tol:
|
|
100
|
+
break
|
|
101
|
+
return U.T, k, l_eps
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _sweep(matrices):
|
|
105
|
+
"""Single sweep of the simultaneous-diagonalisation algorithm."""
|
|
106
|
+
rank = len(matrices[0])
|
|
107
|
+
dmatr = []
|
|
108
|
+
R = np.identity(rank, float)
|
|
109
|
+
for i in range(rank):
|
|
110
|
+
for j in range(i + 1, rank):
|
|
111
|
+
c, s = _rotation_angles(matrices, i, j)
|
|
112
|
+
Rij = np.identity(rank, float)
|
|
113
|
+
Rij[i, i] = c
|
|
114
|
+
Rij[j, j] = c
|
|
115
|
+
Rij[i, j] = s
|
|
116
|
+
Rij[j, i] = -s
|
|
117
|
+
R = np.dot(Rij, R)
|
|
118
|
+
for A in matrices:
|
|
119
|
+
dA = reduce(np.dot, (R, A, R.T))
|
|
120
|
+
dmatr.append(dA)
|
|
121
|
+
return dmatr, R, _sum_diag2(dmatr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _sum_diag2(matrices):
|
|
125
|
+
"""Return the sum of ``dot(diag(A), diag(A))`` over a list of matrices."""
|
|
126
|
+
d2 = 0.0
|
|
127
|
+
for A in matrices:
|
|
128
|
+
da = np.diag(A)
|
|
129
|
+
d2 = d2 + np.dot(da, da)
|
|
130
|
+
return d2
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _rotation_angles(matrices, i, j):
|
|
134
|
+
if i == j:
|
|
135
|
+
raise ValueError('indices must be different')
|
|
136
|
+
G = np.zeros((2, 2), float)
|
|
137
|
+
for A in matrices:
|
|
138
|
+
h = np.array([A[i, i] - A[j, j], A[i, j] + A[j, i]])
|
|
139
|
+
G += np.outer(h, h)
|
|
140
|
+
E, U = np.linalg.eigh(G) # ascending: column 1 is the top eigenvector
|
|
141
|
+
x = U[0, 1]
|
|
142
|
+
y = U[1, 1]
|
|
143
|
+
if x < 0: # either sign is a valid rotation; x >= 0 avoids x + r ~ 0
|
|
144
|
+
x, y = -x, -y
|
|
145
|
+
r = np.dot(U[:, 1], U[:, 1])**0.5
|
|
146
|
+
c = ((x + r) / (2 * r))**0.5
|
|
147
|
+
s = y / (2 * r * (x + r))**0.5
|
|
148
|
+
return c, s
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _rank2_diagonalization(M):
|
|
152
|
+
"""Closed-form diagonalisation of a 2x2 matrix.
|
|
153
|
+
|
|
154
|
+
Not used; ``_rotation_angles`` uses :func:`numpy.linalg.eigh`
|
|
155
|
+
for the same job.
|
|
156
|
+
"""
|
|
157
|
+
trace = M[0, 0] + M[1, 1]
|
|
158
|
+
detrm = M[0, 0] * M[1, 1] - M[0, 1] * M[1, 0]
|
|
159
|
+
ev0 = trace / 2 + (trace**2 / 4 - detrm)**0.5
|
|
160
|
+
ev1 = trace / 2 - (trace**2 / 4 - detrm)**0.5
|
|
161
|
+
E = np.array([ev0, ev1], float)
|
|
162
|
+
U = np.zeros((2, 2), float)
|
|
163
|
+
U[0, 0] = ev0 - M[1, 1]
|
|
164
|
+
U[0, 1] = ev1 - M[1, 1]
|
|
165
|
+
U[1, 0] = M[1, 0]
|
|
166
|
+
U[1, 1] = M[1, 0]
|
|
167
|
+
U[:, 0] = U[:, 0] / np.dot(U[:, 0], U[:, 0])**0.5
|
|
168
|
+
U[:, 1] = U[:, 1] / np.dot(U[:, 1], U[:, 1])**0.5
|
|
169
|
+
return E, U
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def kron_list(matrices):
|
|
173
|
+
"""Kronecker product of a list of matrices, first factor slowest."""
|
|
174
|
+
return reduce(np.kron, matrices)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def invert_reg(mat, tol=1.0e-8):
|
|
178
|
+
"""
|
|
179
|
+
Invert a Hermitian matrix, flooring its eigenvalues at tol
|
|
180
|
+
|
|
181
|
+
Eigenvalues below tol (including negative ones) are replaced by tol
|
|
182
|
+
before inverting.
|
|
183
|
+
|
|
184
|
+
Returns (inverse, eigenvalues, floored eigenvalues, eigenvectors).
|
|
185
|
+
"""
|
|
186
|
+
evals, evecs = np.linalg.eigh(mat)
|
|
187
|
+
evalsreg = np.fmax(evals, tol)
|
|
188
|
+
inv = evecs @ np.diag(evalsreg**-1) @ evecs.conj().T
|
|
189
|
+
return inv, evals, evalsreg, evecs
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def random_orthogonal_matrix(dim, nvec, istype=float):
|
|
193
|
+
"""(dim, nvec) matrix with orthonormal columns (QR of a random matrix)."""
|
|
194
|
+
T = np.random.random((dim, nvec))
|
|
195
|
+
if istype is complex:
|
|
196
|
+
T = T + 1.0j * np.random.random((dim, nvec))
|
|
197
|
+
elif istype is not float:
|
|
198
|
+
raise ValueError('istype = [float|complex] only')
|
|
199
|
+
Q, R = np.linalg.qr(T)
|
|
200
|
+
return Q
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def random_normal_columns_matrix(ndim, nvec, istype=float):
|
|
204
|
+
"""(ndim, nvec) matrix of random columns, each normalized to 1.
|
|
205
|
+
|
|
206
|
+
The columns are not orthogonal; entries are uniform in [0, 1) before
|
|
207
|
+
normalization ("normal" means normalized, not Gaussian).
|
|
208
|
+
"""
|
|
209
|
+
T = np.random.random((nvec, ndim))
|
|
210
|
+
if istype is complex:
|
|
211
|
+
T = T + 1.0j * np.random.random((nvec, ndim))
|
|
212
|
+
elif istype is not float:
|
|
213
|
+
raise ValueError('istype = [float|complex] only')
|
|
214
|
+
T = T / np.linalg.norm(T, axis=1)[:, None]
|
|
215
|
+
return T.T
|
quadlib/tools/pulses.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# QuaDLib: QUAntum Dynamics LIBrary for research and methods development
|
|
2
|
+
# Copyright (C) 2018-2026, The Theoretical Chemistry Group, Heidelberg University
|
|
3
|
+
#
|
|
4
|
+
# This software is licensed under the MIT License (https://spdx.org/licenses/MIT.html)
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
import quadlib.tools.constants as cv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EField:
|
|
14
|
+
"""
|
|
15
|
+
Represents an oscillating field characterized by
|
|
16
|
+
|
|
17
|
+
E(t) = P A(t)
|
|
18
|
+
|
|
19
|
+
P: unit 3-vector, polarizarion axis of the field
|
|
20
|
+
A(t): function, field amplitude as a function of time
|
|
21
|
+
|
|
22
|
+
P and A must have been defined at execution time.
|
|
23
|
+
|
|
24
|
+
Units for A(t) must be au both for time and E-field.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self,**opts):
|
|
27
|
+
self.P = np.array([0.0,0.0,1.0],float)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def A(self,t):
|
|
31
|
+
return 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def E(self,t):
|
|
35
|
+
return self.A(t) * np.asarray(self.P)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_gaussian_pulse(self, omega, fwhm, amp, t0=0, phase=0):
|
|
39
|
+
"""
|
|
40
|
+
Gaussian shaped pulse with single photon energy
|
|
41
|
+
|
|
42
|
+
A(t) = amp * G(fwhm,t0) * cos( omega t + phase)
|
|
43
|
+
|
|
44
|
+
All input in au
|
|
45
|
+
|
|
46
|
+
omega -- photon energy
|
|
47
|
+
fwhm -- full width at half maximum
|
|
48
|
+
amp -- maximum field amplitude
|
|
49
|
+
t0 -- center of the pulse
|
|
50
|
+
phase -- phase of the cosine carrier, units of Pi
|
|
51
|
+
|
|
52
|
+
No output, modifies self.A
|
|
53
|
+
"""
|
|
54
|
+
g = gaussian_envelope(fwhm,t0)
|
|
55
|
+
p = phase * math.pi
|
|
56
|
+
def a(t):
|
|
57
|
+
return g(t) * amp * np.cos( omega*t + p )
|
|
58
|
+
self.A = a
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def init_from_input_options(self, iopts):
|
|
62
|
+
opt_env = iopts.get('envelope',None)
|
|
63
|
+
opt_amp = iopts.get('amplitude',None)
|
|
64
|
+
opt_omega = iopts.get('omega',None)
|
|
65
|
+
opt_center = iopts.get('center',None)
|
|
66
|
+
opt_fwhm = iopts.get('fwhm',None)
|
|
67
|
+
opt_phase = iopts.get('phase',None)
|
|
68
|
+
opt_pol = iopts.get('polarization',None)
|
|
69
|
+
|
|
70
|
+
if opt_env:
|
|
71
|
+
env = opt_env[0]
|
|
72
|
+
else:
|
|
73
|
+
env = 'gaussian' # Default envelope
|
|
74
|
+
|
|
75
|
+
if opt_amp:
|
|
76
|
+
amp = cv.numval(opt_amp, 'efield')
|
|
77
|
+
else:
|
|
78
|
+
amp = 0.001 # Default peak field amplitude
|
|
79
|
+
|
|
80
|
+
if opt_omega:
|
|
81
|
+
omega = cv.numval(opt_omega, 'energy')
|
|
82
|
+
else:
|
|
83
|
+
omega = 0.0 # Default photon energy
|
|
84
|
+
|
|
85
|
+
if opt_center:
|
|
86
|
+
center = cv.numval(opt_center, 'time')
|
|
87
|
+
else:
|
|
88
|
+
center = 0.0 # Default center
|
|
89
|
+
|
|
90
|
+
if opt_fwhm:
|
|
91
|
+
fwhm = cv.numval(opt_fwhm, 'time')
|
|
92
|
+
else:
|
|
93
|
+
fwhm = 200.0 # Default fwhm in au
|
|
94
|
+
|
|
95
|
+
if opt_phase:
|
|
96
|
+
phase = float( opt_phase[0] )
|
|
97
|
+
else:
|
|
98
|
+
phase = 0.0
|
|
99
|
+
|
|
100
|
+
if opt_pol:
|
|
101
|
+
pol = np.array( list( map(float,opt_pol) ) )
|
|
102
|
+
pol = pol/np.dot(pol,pol)**0.5
|
|
103
|
+
else:
|
|
104
|
+
pol = np.array([0,0,1],float)
|
|
105
|
+
|
|
106
|
+
self.P = pol
|
|
107
|
+
if env == 'gaussian':
|
|
108
|
+
self.set_gaussian_pulse(omega,fwhm,amp,center,phase)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def plot(self, ti=0.0, tf=10.0, **opts):
|
|
113
|
+
"""
|
|
114
|
+
Use matplotlib to plot the time-profile of the pulse
|
|
115
|
+
|
|
116
|
+
ti -- float, intial time
|
|
117
|
+
tf -- float, final time
|
|
118
|
+
"""
|
|
119
|
+
import matplotlib.pyplot as plt
|
|
120
|
+
tunit = opts.get('tunit','au') # time units (field is plotted in au)
|
|
121
|
+
npoints = opts.get('npoints',500) # number of points used for plotting
|
|
122
|
+
color = opts.get('color','r')
|
|
123
|
+
t = np.linspace(ti,tf,npoints)
|
|
124
|
+
if tunit == 'fs':
|
|
125
|
+
au_t = t*cv.fs2au
|
|
126
|
+
else:
|
|
127
|
+
au_t = t
|
|
128
|
+
amp = list( map(self.A,au_t) )
|
|
129
|
+
plt.plot(t,amp,color)
|
|
130
|
+
# plt.show()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__k = 4.0*math.log(2.0)
|
|
136
|
+
__N = __k/math.pi
|
|
137
|
+
def gaussian_envelope(fwhm, t0=0.0, norm=False):
|
|
138
|
+
"""
|
|
139
|
+
Return Gaussian function f(t)
|
|
140
|
+
|
|
141
|
+
fwhm -- float; full width at half maximum
|
|
142
|
+
t0 -- float; center of envelope; Def t0 = 0.0
|
|
143
|
+
norm -- bool; True: integral = 1, False: maximum = 1. Def. norm=False
|
|
144
|
+
"""
|
|
145
|
+
N = __N/fwhm
|
|
146
|
+
if norm:
|
|
147
|
+
def g(t):
|
|
148
|
+
return N*np.exp(-__k*((t-t0)/fwhm)**2)
|
|
149
|
+
else:
|
|
150
|
+
def g(t):
|
|
151
|
+
return np.exp(-__k*((t-t0)/fwhm)**2)
|
|
152
|
+
return g
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def gaussian_pulse(w, F, E0, t0=0.0, phi=0.0):
|
|
156
|
+
"""
|
|
157
|
+
Return a Gaussian pulse f(t)
|
|
158
|
+
|
|
159
|
+
The pulsed is derived from the vector potential
|
|
160
|
+
A[t] = G[t]cos(wt + phi)
|
|
161
|
+
as E[t] = -dA[t]/dt
|
|
162
|
+
thus ensuring that the pulse is fully propagating
|
|
163
|
+
|
|
164
|
+
w - photon energy
|
|
165
|
+
F - full with at half maximum (duration) of the INTENSITY profile
|
|
166
|
+
E0 - maximum field amplitude in atomic units
|
|
167
|
+
t0 - center of the pulse
|
|
168
|
+
phi - additional phase
|
|
169
|
+
"""
|
|
170
|
+
ln5 = np.log(0.5)
|
|
171
|
+
def g(Time):
|
|
172
|
+
ENVg = np.exp(2*ln5*(Time-t0)**2/F**2)
|
|
173
|
+
dtENVg = ENVg*4*ln5*(Time-t0)/F**2
|
|
174
|
+
CARCOS = np.cos(w*Time - phi)
|
|
175
|
+
CARSIN = np.sin(w*Time - phi)
|
|
176
|
+
return -E0*CARSIN*ENVg + E0/w*CARCOS*dtENVg
|
|
177
|
+
return g
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
ef = EField()
|
|
182
|
+
ef.P = np.array([0,0,1],float)
|
|
183
|
+
ef.set_gaussian_pulse(100*cv.ic2au, 200.0*cv.fs2au, 1.0, 250.0*cv.fs2au, 0.5)
|
|
184
|
+
ef.plot(ti=0,tf=1000,tunit='fs')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
quadlib/tools/spectra.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# QuaDLib: QUAntum Dynamics LIBrary for research and methods development
|
|
2
|
+
# Copyright (C) 2018-2026, The Theoretical Chemistry Group, Heidelberg University
|
|
3
|
+
#
|
|
4
|
+
# This software is licensed under the MIT License (https://spdx.org/licenses/MIT.html)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Spectra from time signals: correlation-function spectra and Fourier
|
|
8
|
+
transforms.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from scipy.integrate import simpson
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def corrspec(corr,dt,**opts):
|
|
16
|
+
"""
|
|
17
|
+
Compute spectrum of a correlation function
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
corr : array_like
|
|
22
|
+
1D complex array with correlation function.
|
|
23
|
+
dt : float
|
|
24
|
+
time interval between correlation values; [au]
|
|
25
|
+
|
|
26
|
+
Options
|
|
27
|
+
-------
|
|
28
|
+
eref : float; 0.0
|
|
29
|
+
energy shift, e.g. zero point energy; [au]
|
|
30
|
+
emin : float; 0.0
|
|
31
|
+
start of spectrum counting from eref; [au]
|
|
32
|
+
emax : float; 0.0 (calculated from dt if not given)
|
|
33
|
+
end of spectrum counting from eref; [au]
|
|
34
|
+
npoints : int; 500
|
|
35
|
+
number of discrete points where spectrum is evaluated
|
|
36
|
+
between emin and emax
|
|
37
|
+
EP : bool; False
|
|
38
|
+
if True, multiplication of spectrum with energy factor
|
|
39
|
+
ncos : int; 1
|
|
40
|
+
exponent of the cosine damping function
|
|
41
|
+
"""
|
|
42
|
+
eref = opts.get('eref',0.0)
|
|
43
|
+
emin = opts.get('emin',0.0)
|
|
44
|
+
emax = opts.get('emax',0.0)
|
|
45
|
+
npoints = opts.get('npoints',500)
|
|
46
|
+
EP = opts.get('EP',False)
|
|
47
|
+
ncos = opts.get('ncos',1)
|
|
48
|
+
|
|
49
|
+
a = np.asarray(corr)
|
|
50
|
+
maxwidth = 2.0*np.pi/dt
|
|
51
|
+
eminspec = eref + emin
|
|
52
|
+
emaxspec = eref + emax
|
|
53
|
+
if emax <= emin:
|
|
54
|
+
emaxspec = eminspec + maxwidth
|
|
55
|
+
T = (len(a) - 1)*dt
|
|
56
|
+
tlist = np.linspace(0.0,T,len(a))
|
|
57
|
+
elist = np.linspace(eminspec,emaxspec,npoints)
|
|
58
|
+
spect = np.zeros(len(elist), complex)
|
|
59
|
+
|
|
60
|
+
cos_damp = np.cos(0.5*np.pi*tlist/T)**ncos
|
|
61
|
+
adamp = a*cos_damp
|
|
62
|
+
|
|
63
|
+
for i,energy in enumerate(elist):
|
|
64
|
+
tmp = energy*tlist*1.0j
|
|
65
|
+
tmp = np.exp(tmp)
|
|
66
|
+
tmp = tmp*adamp
|
|
67
|
+
spect[i] = tmp.sum()*dt
|
|
68
|
+
#spect[i] = spect[i].real
|
|
69
|
+
|
|
70
|
+
if EP:
|
|
71
|
+
spect = spect*(elist-eref)
|
|
72
|
+
|
|
73
|
+
return elist-eref,spect
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fourierTransform(y, dt, **option):
|
|
77
|
+
"""
|
|
78
|
+
Compute Fourier transform
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
-----------
|
|
82
|
+
y : 1D array_like
|
|
83
|
+
function to be Fourier transformed.
|
|
84
|
+
dt : float
|
|
85
|
+
interval of the domain function.
|
|
86
|
+
method : string, default: 'simp', optional
|
|
87
|
+
integration method to be used.
|
|
88
|
+
'trapz' for trapezoidal rule,
|
|
89
|
+
'simp' for Simpson rule
|
|
90
|
+
t_init : float, default: 0.0, optional
|
|
91
|
+
initial point of the domain function.
|
|
92
|
+
odamp : int, default: 1, optional
|
|
93
|
+
cosine damping function order (to damp the signal function at its end) [as the cosine power]
|
|
94
|
+
usage : {'phys', 'math'}, default: 'phys', optional
|
|
95
|
+
usage of the Fourier transform:
|
|
96
|
+
'phys': mostly for physics application, it means using angular frequency, (2pi*f),
|
|
97
|
+
thus the inverse is asymmetric.
|
|
98
|
+
'math': mostly for mathematics application, it means using normal frequency,
|
|
99
|
+
thus the inverse is symmetric.
|
|
100
|
+
f_range : tuple of float, default: if usage='phys' (-pi/dt, pi/dt), if usage='math' (-0.5/dt, 0.5/dt), optional
|
|
101
|
+
minimum frequency and maximum frequency.
|
|
102
|
+
npoints : int, default: len(y), optional
|
|
103
|
+
number of points of transformed domain function.
|
|
104
|
+
symmetry : {'gen', 'even', 'odd'}, default: 'gen', optional
|
|
105
|
+
symmetry of the function y to be transformed.
|
|
106
|
+
'gen': general, the exponential function will be used.
|
|
107
|
+
'even': even function, cosine transform will be used.
|
|
108
|
+
'odd': odd function, sine transform will be used.
|
|
109
|
+
if symmetry is 'even' or 'odd', the t_init will be set to 0.0
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
fline, FT_func : (list)
|
|
114
|
+
fline : 1D numpy.ndarray
|
|
115
|
+
Domain of transformed function (frequency domain)
|
|
116
|
+
FT_func : 1D numpy.ndarray
|
|
117
|
+
Fourier transform of y function.
|
|
118
|
+
"""
|
|
119
|
+
func = np.asarray(y, dtype=complex)
|
|
120
|
+
# evaluating options
|
|
121
|
+
method = option.get('method', 'simp')
|
|
122
|
+
t_init = option.get('t_init', 0.0)
|
|
123
|
+
odamp = option.get('odamp', 1)
|
|
124
|
+
usage = option.get('usage', 'phys')
|
|
125
|
+
npoints = option.get('npoints', len(func))
|
|
126
|
+
symmetry = option.get('symmetry', 'gen')
|
|
127
|
+
if usage == 'math':
|
|
128
|
+
f_range = option.get('f_range', (-0.5/dt, 0.5/dt))
|
|
129
|
+
else:
|
|
130
|
+
f_range = option.get('f_range', (-np.pi/dt, np.pi/dt))
|
|
131
|
+
fmin = f_range[0]
|
|
132
|
+
fmax = f_range[1]
|
|
133
|
+
|
|
134
|
+
Nsegments = len(func) - 1
|
|
135
|
+
spanT = dt * (len(func) - 1)
|
|
136
|
+
if symmetry == 'even' or symmetry == 'odd':
|
|
137
|
+
t_init = 0.0
|
|
138
|
+
t_end = t_init + spanT
|
|
139
|
+
tline = np.linspace(t_init, t_end, Nsegments + 1)
|
|
140
|
+
|
|
141
|
+
if usage == 'phys':
|
|
142
|
+
#fline = np.linspace(0.0, 2.0 * np.pi / dt, npoints)
|
|
143
|
+
factor = 1.0
|
|
144
|
+
else:
|
|
145
|
+
#fline = np.linspace(0.0, 1.0 / dt, npoints)
|
|
146
|
+
factor = 2.0 * np.pi
|
|
147
|
+
fline = np.linspace(fmin, fmax, npoints)
|
|
148
|
+
|
|
149
|
+
# integration method
|
|
150
|
+
int_method = {'simp': simpson, 'trapz': np.trapezoid}
|
|
151
|
+
if method != 'simp' and method != 'trapz':
|
|
152
|
+
method = 'simp'
|
|
153
|
+
|
|
154
|
+
FT_func = np.zeros(npoints, dtype=complex)
|
|
155
|
+
|
|
156
|
+
# damped input function
|
|
157
|
+
if odamp == 0:
|
|
158
|
+
damp_func = func
|
|
159
|
+
else:
|
|
160
|
+
fn_damp = np.cos(0.5 * np.pi * (tline - t_init) / spanT)**odamp
|
|
161
|
+
damp_func = func * fn_damp
|
|
162
|
+
|
|
163
|
+
if symmetry == 'even':
|
|
164
|
+
for i, alpha in enumerate(fline):
|
|
165
|
+
integrand = alpha * tline * factor
|
|
166
|
+
integrand = damp_func * np.cos(integrand)
|
|
167
|
+
FT_func[i] = 2.0 * int_method[method](integrand, dx = dt)
|
|
168
|
+
elif symmetry == 'odd':
|
|
169
|
+
for i, alpha in enumerate(fline):
|
|
170
|
+
integrand = alpha * tline * factor
|
|
171
|
+
integrand = damp_func * np.sin(integrand)
|
|
172
|
+
FT_func[i] = 2.0 * 1.0j * int_method[method](integrand, dx = dt)
|
|
173
|
+
else:
|
|
174
|
+
for i, alpha in enumerate(fline):
|
|
175
|
+
integrand = -alpha * tline * factor
|
|
176
|
+
integrand = damp_func * np.exp(1.0j * integrand)
|
|
177
|
+
FT_func[i] = int_method[method](integrand, dx = dt)
|
|
178
|
+
|
|
179
|
+
return fline, FT_func
|