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/operator/sop.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
from enum import IntEnum
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
import quadlib.tools.linalg as linalg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpType(IntEnum):
|
|
14
|
+
"""Per-DOF operator kind in a SumOfProducts term.
|
|
15
|
+
|
|
16
|
+
UNIT: identity (no storage, no multiplication).
|
|
17
|
+
MATRIX: full N x N matrix.
|
|
18
|
+
DIAG: diagonal stored as a length-N vector.
|
|
19
|
+
"""
|
|
20
|
+
UNIT = 1
|
|
21
|
+
MATRIX = 2
|
|
22
|
+
DIAG = 3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_unit_sentinel(op):
|
|
26
|
+
"""A SOP-term slot is interpreted as the identity operator when the
|
|
27
|
+
user passes None, an int, or a str (legacy ``1`` / ``'1'`` sentinels).
|
|
28
|
+
The actual value is ignored.
|
|
29
|
+
"""
|
|
30
|
+
return op is None or isinstance(op, (int, str))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SumOfProducts:
|
|
34
|
+
"""Sum-of-products operator H = sum_k coeff_k prod_d O_{k,d}.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
ndims : sequence of int
|
|
39
|
+
Number of basis coefficients (primitive dimension) for each
|
|
40
|
+
degree of freedom.
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
Operators are stored *as the user provides them*. The compiled
|
|
45
|
+
representation (``cOper``, ``cIndx``, ``cType``, ``cCoef``), built
|
|
46
|
+
on demand by :meth:`compile_operator`, is always complex; this
|
|
47
|
+
avoids silently dropping the imaginary part of operators like
|
|
48
|
+
``PauliY`` or a momentum matrix.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, ndims):
|
|
52
|
+
self.ndims = np.asarray(ndims, int)
|
|
53
|
+
self.ndofs = len(ndims)
|
|
54
|
+
self.rank = 0
|
|
55
|
+
self.terms = []
|
|
56
|
+
self.coeff = []
|
|
57
|
+
self.otype = []
|
|
58
|
+
self.is_compiled = False
|
|
59
|
+
|
|
60
|
+
def add_term(self, coeff=1.0, termlist=None, termindx=None):
|
|
61
|
+
"""Add one product term to the operator.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
coeff : complex or float
|
|
66
|
+
Scalar prefactor for the term.
|
|
67
|
+
termlist : sequence of operators, optional
|
|
68
|
+
Each entry is either an ndarray (1D for diagonal, 2D for
|
|
69
|
+
matrix) or a unit-operator sentinel (``None``, an int such
|
|
70
|
+
as ``1``, or the string ``'1'``).
|
|
71
|
+
termindx : sequence of int, optional
|
|
72
|
+
DOF index for each entry of ``termlist``. If omitted,
|
|
73
|
+
``termlist`` is assumed to cover all DOFs in ascending
|
|
74
|
+
order.
|
|
75
|
+
"""
|
|
76
|
+
self.is_compiled = False
|
|
77
|
+
self.coeff.append(coeff)
|
|
78
|
+
if termlist and termindx:
|
|
79
|
+
assert len(termlist) == len(termindx)
|
|
80
|
+
# Default every slot to the unit operator (stored as None).
|
|
81
|
+
term = [None] * self.ndofs
|
|
82
|
+
otype = [OpType.UNIT] * self.ndofs
|
|
83
|
+
if termlist:
|
|
84
|
+
for j, op in enumerate(termlist):
|
|
85
|
+
k = termindx[j] if termindx else j
|
|
86
|
+
if _is_unit_sentinel(op):
|
|
87
|
+
term[k] = None
|
|
88
|
+
otype[k] = OpType.UNIT
|
|
89
|
+
continue
|
|
90
|
+
if len(op) != self.ndims[k]:
|
|
91
|
+
raise ValueError('Wrong term dimension for PDOF ' + str(k))
|
|
92
|
+
term[k] = op
|
|
93
|
+
if op.ndim == 2:
|
|
94
|
+
otype[k] = OpType.MATRIX
|
|
95
|
+
elif op.ndim == 1:
|
|
96
|
+
otype[k] = OpType.DIAG
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
'SOP operators must be 1D (diagonal) or 2D '
|
|
100
|
+
'(matrix); got ndim=' + str(op.ndim)
|
|
101
|
+
)
|
|
102
|
+
self.terms.append(term)
|
|
103
|
+
self.otype.append(otype)
|
|
104
|
+
self.rank = len(self.terms)
|
|
105
|
+
|
|
106
|
+
def multiply_term(self, k, d, vec):
|
|
107
|
+
"""Apply term[k, d] to ``vec``.
|
|
108
|
+
|
|
109
|
+
``vec`` may be a 1D array or a 2D array whose rows are the
|
|
110
|
+
SPFs; the operator is applied to the second index.
|
|
111
|
+
"""
|
|
112
|
+
t = self.otype[k][d]
|
|
113
|
+
if t == OpType.UNIT:
|
|
114
|
+
return vec.copy() # for consistency
|
|
115
|
+
if t == OpType.MATRIX:
|
|
116
|
+
return (self.terms[k][d] @ vec.T).T
|
|
117
|
+
if t == OpType.DIAG:
|
|
118
|
+
# Elementwise multiplication along the SPF axis.
|
|
119
|
+
return (np.diag(self.terms[k][d]) @ vec.T).T
|
|
120
|
+
raise ValueError('SOP otype must be UNIT, MATRIX, or DIAG')
|
|
121
|
+
|
|
122
|
+
def matrix_element_term(self, k, d, bra, ket):
|
|
123
|
+
"""Compute <bra| term[k,d] |ket>.
|
|
124
|
+
|
|
125
|
+
``bra`` and ``ket`` may be 2D arrays of SPFs stored as rows.
|
|
126
|
+
"""
|
|
127
|
+
tmp = self.multiply_term(k, d, ket)
|
|
128
|
+
return bra.conj() @ tmp.T
|
|
129
|
+
|
|
130
|
+
def full_matrix_operator(self, datatype=complex):
|
|
131
|
+
"""Build the explicit Hilbert-space matrix for the operator.
|
|
132
|
+
|
|
133
|
+
Defaults to ``complex`` so that operators with imaginary parts
|
|
134
|
+
(e.g. PauliY) are represented faithfully. Pass ``datatype=float``
|
|
135
|
+
only if you know every term is real.
|
|
136
|
+
"""
|
|
137
|
+
N = np.prod(self.ndims)
|
|
138
|
+
H = np.zeros((N, N), datatype)
|
|
139
|
+
for k, term in enumerate(self.terms):
|
|
140
|
+
oplist = []
|
|
141
|
+
for j, op in enumerate(term):
|
|
142
|
+
t = self.otype[k][j]
|
|
143
|
+
if t == OpType.UNIT:
|
|
144
|
+
oplist.append(np.diag(np.ones(self.ndims[j], datatype)))
|
|
145
|
+
elif t == OpType.MATRIX:
|
|
146
|
+
oplist.append(op)
|
|
147
|
+
elif t == OpType.DIAG:
|
|
148
|
+
oplist.append(np.diag(op))
|
|
149
|
+
h = self.coeff[k] * linalg.kron_list(oplist)
|
|
150
|
+
H = H + h
|
|
151
|
+
return H
|
|
152
|
+
|
|
153
|
+
def sparse_nnz(self):
|
|
154
|
+
"""Upper bound on the non-zeros of :meth:`sparse_matrix_operator`.
|
|
155
|
+
|
|
156
|
+
Sum over terms of the product of the per-DOF non-zero counts
|
|
157
|
+
(exact per term; terms that overlap make the sum an upper bound).
|
|
158
|
+
Computed without building anything. Measured peak memory while
|
|
159
|
+
building and propagating with the sparse matrix was about 90
|
|
160
|
+
bytes per non-zero (complex128); see docs/refactor/claude_refactor_1_6.md, section 4.
|
|
161
|
+
"""
|
|
162
|
+
total = 0
|
|
163
|
+
for k, term in enumerate(self.terms):
|
|
164
|
+
nnz = 1
|
|
165
|
+
for j, op in enumerate(term):
|
|
166
|
+
if self.otype[k][j] == OpType.UNIT:
|
|
167
|
+
nnz *= int(self.ndims[j])
|
|
168
|
+
else:
|
|
169
|
+
nnz *= int(np.count_nonzero(op))
|
|
170
|
+
total += nnz
|
|
171
|
+
return total
|
|
172
|
+
|
|
173
|
+
def sparse_matrix_operator(self, datatype=complex, format='csr'):
|
|
174
|
+
"""Build the Hilbert-space matrix as a scipy sparse matrix.
|
|
175
|
+
|
|
176
|
+
Same matrix as :meth:`full_matrix_operator` (first DOF slowest,
|
|
177
|
+
C order), built term by term from Kronecker products. Unit and
|
|
178
|
+
diagonal operators stay sparse; matrix operators are stored with
|
|
179
|
+
their explicit zeros dropped, so a dense one-DOF matrix (e.g. a
|
|
180
|
+
DVR kinetic energy) contributes ``N * n_d`` non-zeros per term.
|
|
181
|
+
"""
|
|
182
|
+
import scipy.sparse as sp
|
|
183
|
+
H = None
|
|
184
|
+
for k, term in enumerate(self.terms):
|
|
185
|
+
M = None
|
|
186
|
+
for j, op in enumerate(term):
|
|
187
|
+
t = self.otype[k][j]
|
|
188
|
+
if t == OpType.UNIT:
|
|
189
|
+
m = sp.identity(self.ndims[j], dtype=datatype, format='csr')
|
|
190
|
+
elif t == OpType.MATRIX:
|
|
191
|
+
m = sp.csr_matrix(np.asarray(op, dtype=datatype))
|
|
192
|
+
else:
|
|
193
|
+
m = sp.diags(np.asarray(op, dtype=datatype), format='csr')
|
|
194
|
+
M = m if M is None else sp.kron(M, m, format='csr')
|
|
195
|
+
M = self.coeff[k] * M
|
|
196
|
+
H = M if H is None else H + M
|
|
197
|
+
if H is None:
|
|
198
|
+
N = int(np.prod(self.ndims))
|
|
199
|
+
H = sp.csr_matrix((N, N), dtype=datatype)
|
|
200
|
+
return H.asformat(format)
|
|
201
|
+
|
|
202
|
+
def compile_operator(self):
|
|
203
|
+
"""Build the flat-array representation consumed by `quadlib.propagation.exact`.
|
|
204
|
+
|
|
205
|
+
Produces the attributes ``cCoef``, ``cOper``, ``cIndx``,
|
|
206
|
+
``cType``. ``cOper`` is always complex-typed (see class
|
|
207
|
+
docstring).
|
|
208
|
+
"""
|
|
209
|
+
nterms = len(self.coeff)
|
|
210
|
+
oplen, _ = self.len()
|
|
211
|
+
oper = np.zeros(oplen, complex)
|
|
212
|
+
indx = np.zeros((nterms, self.ndofs, 2), int)
|
|
213
|
+
i = 0
|
|
214
|
+
for k, term in enumerate(self.terms):
|
|
215
|
+
for d, op in enumerate(term):
|
|
216
|
+
nop = 0
|
|
217
|
+
t = self.otype[k][d]
|
|
218
|
+
if t == OpType.MATRIX:
|
|
219
|
+
nop = self.ndims[d] ** 2
|
|
220
|
+
oper[i:i + nop] = np.ravel(op)
|
|
221
|
+
elif t == OpType.DIAG:
|
|
222
|
+
nop = self.ndims[d]
|
|
223
|
+
oper[i:i + nop] = op
|
|
224
|
+
indx[k, d, 0] = i
|
|
225
|
+
indx[k, d, 1] = i + nop
|
|
226
|
+
i = i + nop
|
|
227
|
+
self.cCoef = np.asarray(self.coeff, dtype=complex)
|
|
228
|
+
self.cOper = oper
|
|
229
|
+
self.cIndx = indx
|
|
230
|
+
self.cType = np.asarray(self.otype, dtype=int)
|
|
231
|
+
self.is_compiled = True
|
|
232
|
+
|
|
233
|
+
def len(self):
|
|
234
|
+
"""Return (total storage in flat repr, per-term storage list)."""
|
|
235
|
+
oplen = 0
|
|
236
|
+
termslen = []
|
|
237
|
+
for k, term in enumerate(self.terms):
|
|
238
|
+
tlen = 0
|
|
239
|
+
for d, _op in enumerate(term):
|
|
240
|
+
t = self.otype[k][d]
|
|
241
|
+
if t == OpType.UNIT:
|
|
242
|
+
continue
|
|
243
|
+
if t == OpType.MATRIX:
|
|
244
|
+
oplen += self.ndims[d] ** 2
|
|
245
|
+
tlen += self.ndims[d] ** 2
|
|
246
|
+
elif t == OpType.DIAG:
|
|
247
|
+
oplen += self.ndims[d]
|
|
248
|
+
tlen += self.ndims[d]
|
|
249
|
+
termslen.append(tlen)
|
|
250
|
+
return oplen, termslen
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
Time propagation.
|
|
8
|
+
|
|
9
|
+
integrate - generic ODE driver (scipy zvode) with output every dt
|
|
10
|
+
propagate_exact - numerically exact propagation of a full tensor under
|
|
11
|
+
a SumOfProducts operator
|
|
12
|
+
propagate_cpd_A - A-vector of a WfnCPD with frozen SPFs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from quadlib.propagation.cpd import propagate_cpd_A
|
|
16
|
+
from quadlib.propagation.exact import propagate_exact, rhs_exact
|
|
17
|
+
from quadlib.propagation.integrators import integrate
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
Propagation of CPD wavefunctions (quadlib.WfnCPD).
|
|
8
|
+
|
|
9
|
+
Only the A-vector with frozen SPFs is supported; the coupled A/SPF
|
|
10
|
+
equations are in quadlib.experimental.cpd_eom.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from quadlib.propagation.integrators import integrate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def propagate_cpd_A(wfn, dt, tfinal=None, **opts):
|
|
17
|
+
"""
|
|
18
|
+
Propagate the A-vector of a WfnCPD with the SPFs frozen
|
|
19
|
+
|
|
20
|
+
S dA/dt = -i H A (WfnCPD.rhs_A), from wfn.time to tfinal with output
|
|
21
|
+
every dt (one step if tfinal is None or not beyond wfn.time). Updates
|
|
22
|
+
wfn.psi[-1] and wfn.time. opts go to quadlib.propagation.integrate.
|
|
23
|
+
Returns the integrate() output dict.
|
|
24
|
+
"""
|
|
25
|
+
if tfinal is None or tfinal < wfn.time:
|
|
26
|
+
tfinal = wfn.time + dt
|
|
27
|
+
|
|
28
|
+
def funcA(t, A):
|
|
29
|
+
wfn.psi[-1] = A
|
|
30
|
+
return wfn.rhs_A()
|
|
31
|
+
|
|
32
|
+
out = integrate(funcA, wfn.psi[-1], dt, tfinal, tinit=wfn.time, **opts)
|
|
33
|
+
wfn.time = out['time'][-1]
|
|
34
|
+
wfn.psi[-1] = out['final']
|
|
35
|
+
return out
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
Numerically exact ("standard method") propagation of a full tensor
|
|
8
|
+
under a sum-of-products operator.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
import quadlib.tools.tensor as tens
|
|
14
|
+
from quadlib.propagation.integrators import OutputRecorder, integrate, n_output_steps
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def propagate_exact(psi0, op, dt, tfinal, *, tinit=0.0, solver='zvode', **opts):
|
|
18
|
+
"""
|
|
19
|
+
Propagate the TDSE i dpsi/dt = H psi exactly for a full tensor psi
|
|
20
|
+
|
|
21
|
+
psi0 - array of shape op.ndims (primitive dimension per DOF)
|
|
22
|
+
op - SumOfProducts operator H (time independent)
|
|
23
|
+
dt, tfinal, tinit - output interval, final and initial time
|
|
24
|
+
solver - 'zvode': integrate the TDSE with the SOP right-hand side
|
|
25
|
+
(rhs_exact); memory ~ size of psi plus the SOP
|
|
26
|
+
'expm': build H as a sparse matrix (op.sparse_matrix_operator)
|
|
27
|
+
and apply exp(-i H dt) at every output step with
|
|
28
|
+
scipy.sparse.linalg.expm_multiply; accurate to
|
|
29
|
+
machine precision, but H must fit in memory
|
|
30
|
+
opts - store_psi, observables (both solvers; observables receive psi
|
|
31
|
+
flattened to 1D); for 'zvode' also method, atol, rtol,
|
|
32
|
+
maxstep (see quadlib.propagation.integrate)
|
|
33
|
+
|
|
34
|
+
Returns the integrate()-style output dict; 'psi' entries and 'final'
|
|
35
|
+
have the shape of psi0.
|
|
36
|
+
"""
|
|
37
|
+
shape = np.asarray(psi0.shape, int)
|
|
38
|
+
if tuple(shape) != tuple(op.ndims):
|
|
39
|
+
raise ValueError('propagate_exact: psi0 shape ' + str(psi0.shape)
|
|
40
|
+
+ ' does not match operator dimensions ' + str(tuple(op.ndims)))
|
|
41
|
+
y0 = np.asarray(psi0, complex).reshape(-1)
|
|
42
|
+
if solver == 'zvode':
|
|
43
|
+
if not op.is_compiled:
|
|
44
|
+
op.compile_operator()
|
|
45
|
+
coef, oper, indx, otyp = op.cCoef, op.cOper, op.cIndx, op.cType
|
|
46
|
+
|
|
47
|
+
def dtpsi(t, A):
|
|
48
|
+
return rhs_exact(A, shape, coef, oper, indx, otyp)
|
|
49
|
+
|
|
50
|
+
out = integrate(dtpsi, y0, dt, tfinal, tinit=tinit, **opts)
|
|
51
|
+
elif solver == 'expm':
|
|
52
|
+
out = _propagate_expm(y0, op, dt, tfinal, tinit, **opts)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("propagate_exact: solver must be 'zvode' or 'expm', got "
|
|
55
|
+
+ str(solver))
|
|
56
|
+
if 'psi' in out:
|
|
57
|
+
out['psi'] = [p.reshape(psi0.shape) for p in out['psi']]
|
|
58
|
+
out['final'] = out['final'].reshape(psi0.shape)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _propagate_expm(y0, op, dt, tfinal, tinit, store_psi=True, observables=None):
|
|
63
|
+
from scipy.sparse.linalg import expm_multiply
|
|
64
|
+
H = op.sparse_matrix_operator()
|
|
65
|
+
A = -1.0j * dt * H
|
|
66
|
+
traceA = A.diagonal().sum()
|
|
67
|
+
rec = OutputRecorder(lambda t, y: np.real(np.vdot(y, H @ y)), store_psi, observables)
|
|
68
|
+
y = y0
|
|
69
|
+
rec.record(tinit, y)
|
|
70
|
+
for i in range(1, n_output_steps(tinit, tfinal, dt) + 1):
|
|
71
|
+
y = expm_multiply(A, y, traceA=traceA)
|
|
72
|
+
rec.record(tinit + i * dt, y)
|
|
73
|
+
out = rec.finish(y)
|
|
74
|
+
out['integrator'] = None
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def rhs_exact(avector, shape, coef, oper, indx, otyp):
|
|
79
|
+
"""
|
|
80
|
+
Perform the exact RHS of the TDSE
|
|
81
|
+
"""
|
|
82
|
+
Nd = np.prod(shape) # number of coefficients in tensor
|
|
83
|
+
Nf = len(shape) # number of degrees of freedom (indices)
|
|
84
|
+
Nk = len(coef) # number of product terms in the operator
|
|
85
|
+
ijk = np.zeros(3, int)
|
|
86
|
+
dt_a = np.zeros_like(avector)
|
|
87
|
+
for k in range(Nk): # loop operator terms
|
|
88
|
+
tmp = np.copy(avector)
|
|
89
|
+
for f in range(Nf):
|
|
90
|
+
ijk[0] = np.prod(shape[0:f])
|
|
91
|
+
ijk[1] = shape[f]
|
|
92
|
+
ijk[2] = np.prod(shape[f+1:])
|
|
93
|
+
tmp = tmp.reshape(ijk)
|
|
94
|
+
i0 = indx[k, f, 0]
|
|
95
|
+
i1 = indx[k, f, 1]
|
|
96
|
+
if otyp[k, f] == 1: # unit operator
|
|
97
|
+
continue
|
|
98
|
+
elif otyp[k, f] == 2: # matrix operator
|
|
99
|
+
op = oper[i0:i1].reshape(ijk[1], ijk[1])
|
|
100
|
+
tmp = np.einsum('hj,ijk->ihk', op, tmp)
|
|
101
|
+
elif otyp[k, f] == 3: # diagonal operator
|
|
102
|
+
op = oper[i0:i1]
|
|
103
|
+
tmp = np.einsum('j,ijk->ijk', op, tmp)
|
|
104
|
+
tmp = tmp.reshape(Nd)
|
|
105
|
+
dt_a = dt_a + tmp*coef[k]
|
|
106
|
+
dt_a = -1.0j*dt_a
|
|
107
|
+
return dt_a
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def rhs_exact_alt1(avector, shape, coef, oper, indx, otyp):
|
|
111
|
+
"""
|
|
112
|
+
Perform the exact RHS of the TDSE
|
|
113
|
+
|
|
114
|
+
Alternative to rhs_exact using the precomputed einsum strings in
|
|
115
|
+
quadlib.tools.tensor.esumstr; same result, not used by default.
|
|
116
|
+
"""
|
|
117
|
+
Nd = np.prod(shape) # number of coefficients in tensor
|
|
118
|
+
Nf = len(shape) # number of degrees of freedom (indices)
|
|
119
|
+
Nk = len(coef) # number of product terms in the operator
|
|
120
|
+
dt_a = np.zeros_like(avector)
|
|
121
|
+
for k in range(Nk): # loop operator terms
|
|
122
|
+
tmp = np.copy(avector)
|
|
123
|
+
tmp = tmp.reshape(shape)
|
|
124
|
+
for f in range(Nf):
|
|
125
|
+
i0 = indx[k, f, 0]
|
|
126
|
+
i1 = indx[k, f, 1]
|
|
127
|
+
nf = shape[f]
|
|
128
|
+
if otyp[k, f] == 1: # unit operator
|
|
129
|
+
continue
|
|
130
|
+
elif otyp[k, f] == 2: # matrix operator
|
|
131
|
+
op = oper[i0:i1].reshape(nf, nf)
|
|
132
|
+
tmp = np.einsum(tens.esumstr[(Nf, 2, f)], op, tmp)
|
|
133
|
+
elif otyp[k, f] == 3: # diagonal operator
|
|
134
|
+
op = oper[i0:i1]
|
|
135
|
+
tmp = np.einsum(tens.esumstr[(Nf, 3, f)], op, tmp)
|
|
136
|
+
tmp = tmp.reshape(Nd)
|
|
137
|
+
dt_a = dt_a + tmp*coef[k]
|
|
138
|
+
dt_a = -1.0j*dt_a
|
|
139
|
+
return dt_a
|
|
@@ -0,0 +1,108 @@
|
|
|
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 numpy as np
|
|
7
|
+
from scipy.integrate import ode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def n_output_steps(tinit, tfinal, dt):
|
|
11
|
+
"""Number of output steps of size dt from tinit up to tfinal.
|
|
12
|
+
|
|
13
|
+
The tolerance absorbs float round-off in (tfinal - tinit)/dt so that
|
|
14
|
+
tfinal itself is reached when it is a multiple of dt.
|
|
15
|
+
"""
|
|
16
|
+
return int(np.floor((tfinal - tinit) / dt + 1e-8))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OutputRecorder:
|
|
20
|
+
"""Collects the output dict shared by the propagators.
|
|
21
|
+
|
|
22
|
+
energy - callable(t, psi) returning the energy expectation value
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, energy, store_psi=True, observables=None):
|
|
25
|
+
self.energy = energy
|
|
26
|
+
self.store_psi = store_psi
|
|
27
|
+
self.observables = observables or {}
|
|
28
|
+
self.out = {'time': [], 'psi': [], 'norm': [], 'energy': []}
|
|
29
|
+
for name in self.observables:
|
|
30
|
+
self.out[name] = []
|
|
31
|
+
|
|
32
|
+
def record(self, t, y):
|
|
33
|
+
out = self.out
|
|
34
|
+
out['time'].append(t)
|
|
35
|
+
if self.store_psi:
|
|
36
|
+
out['psi'].append(y.copy())
|
|
37
|
+
out['norm'].append(np.real(np.vdot(y, y)))
|
|
38
|
+
out['energy'].append(self.energy(t, y))
|
|
39
|
+
for name, obs in self.observables.items():
|
|
40
|
+
out[name].append(obs(t, y))
|
|
41
|
+
|
|
42
|
+
def finish(self, y):
|
|
43
|
+
if not self.store_psi:
|
|
44
|
+
del self.out['psi']
|
|
45
|
+
self.out['final'] = y.copy()
|
|
46
|
+
return self.out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def integrate(func, psi0, dt, tfinal=None, *,
|
|
50
|
+
tinit=0.0,
|
|
51
|
+
method='adams',
|
|
52
|
+
atol=1.e-10,
|
|
53
|
+
rtol=1.e-10,
|
|
54
|
+
maxstep=10000,
|
|
55
|
+
store_psi=True,
|
|
56
|
+
observables=None):
|
|
57
|
+
"""
|
|
58
|
+
Integrate dpsi/dt = func(t, psi) with scipy's zvode, reporting every dt
|
|
59
|
+
|
|
60
|
+
One integrator instance is used for the whole interval, so its step
|
|
61
|
+
size and history carry over between output times.
|
|
62
|
+
|
|
63
|
+
func - callable dtpsi = func(t, psi), psi a 1D complex array
|
|
64
|
+
psi0 - initial state (1D)
|
|
65
|
+
dt - output interval; psi is reported at tinit, tinit + dt, ...,
|
|
66
|
+
up to and including tfinal (tfinal defaults to tinit + dt)
|
|
67
|
+
method - zvode method, 'adams' (non-stiff) or 'bdf' (stiff)
|
|
68
|
+
atol, rtol - zvode absolute and relative tolerances. The default 1e-10
|
|
69
|
+
is deliberate: at 1e-8, the 4-mode spin-boson example
|
|
70
|
+
(20000 states, t = 25) developed a 2.5e-3 error in the
|
|
71
|
+
highest boson states (tests/test_spin_boson.py)
|
|
72
|
+
maxstep - maximum number of internal steps per output interval
|
|
73
|
+
store_psi - keep psi at every output time (False keeps only the last)
|
|
74
|
+
observables - optional dict {name: callable(t, psi)} evaluated at
|
|
75
|
+
every output time
|
|
76
|
+
|
|
77
|
+
Returns a dict with keys
|
|
78
|
+
time - output times
|
|
79
|
+
psi - list of psi at each output time (only if store_psi)
|
|
80
|
+
final - psi at the last output time
|
|
81
|
+
norm - <psi|psi> at each output time
|
|
82
|
+
energy - <psi|i func(t, psi)> at each output time; the energy only
|
|
83
|
+
if func is -iH psi with H Hermitian in an orthonormal basis
|
|
84
|
+
<name> - one list per entry of observables
|
|
85
|
+
integrator - the scipy ode instance
|
|
86
|
+
"""
|
|
87
|
+
if method not in ('adams', 'bdf'):
|
|
88
|
+
raise ValueError('integrate: unknown zvode method ' + str(method))
|
|
89
|
+
if tfinal is None:
|
|
90
|
+
tfinal = tinit + dt
|
|
91
|
+
nsteps = n_output_steps(tinit, tfinal, dt)
|
|
92
|
+
r = ode(func)
|
|
93
|
+
r.set_integrator('zvode', method=method, nsteps=maxstep, atol=atol, rtol=rtol)
|
|
94
|
+
r.set_initial_value(psi0, tinit)
|
|
95
|
+
rec = OutputRecorder(lambda t, y: np.real(np.vdot(y, func(t, y)) / (-1.0j)),
|
|
96
|
+
store_psi, observables)
|
|
97
|
+
y = np.asarray(psi0)
|
|
98
|
+
rec.record(tinit, y)
|
|
99
|
+
for i in range(1, nsteps + 1):
|
|
100
|
+
# integrate to tinit + i*dt rather than accumulating r.t + dt
|
|
101
|
+
r.integrate(tinit + i * dt)
|
|
102
|
+
if not r.successful():
|
|
103
|
+
raise RuntimeError('integrate: zvode failed at t = ' + str(r.t))
|
|
104
|
+
y = r.y
|
|
105
|
+
rec.record(r.t, y)
|
|
106
|
+
out = rec.finish(y)
|
|
107
|
+
out['integrator'] = r
|
|
108
|
+
return out
|
|
File without changes
|
quadlib/tools/bath.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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 numpy as np
|
|
7
|
+
import scipy.integrate as spi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def spectral_density_ohmic_bath(alpha, wcutoff):
|
|
11
|
+
"""Return the spectral density function for an ohmic bath
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
nu = np.pi*alpha/2.0
|
|
15
|
+
def J(w):
|
|
16
|
+
return nu*w*np.exp(-w/wcutoff)
|
|
17
|
+
return J
|
|
18
|
+
|
|
19
|
+
def bath_omegas_couplings(Jspectral, rho, N, wmax):
|
|
20
|
+
"""Return the angular frequencies and mode couplings for a linear bath
|
|
21
|
+
|
|
22
|
+
This routine implements the equations in Section III of
|
|
23
|
+
Wang et al., J. Chem. Phys. 115, 2979 (2001)
|
|
24
|
+
|
|
25
|
+
Jspectral -- function, spectral density of the bath
|
|
26
|
+
rho -- function, mode density; int_0^{w_j} rho(w) dw = j
|
|
27
|
+
N -- number of discrete bath modes
|
|
28
|
+
wmax -- largest frequency of the bath
|
|
29
|
+
|
|
30
|
+
Returns two lists
|
|
31
|
+
[w1, w2, ...], [c1, c2, ...] as arrays
|
|
32
|
+
"""
|
|
33
|
+
# Find out the discrete frequencies
|
|
34
|
+
w, r = _find_omegas(rho, N, wmax)
|
|
35
|
+
c = []
|
|
36
|
+
for j, wj in enumerate(w):
|
|
37
|
+
cj = np.sqrt(Jspectral(wj)/r[j]*2.0*wj/np.pi)
|
|
38
|
+
c.append(cj)
|
|
39
|
+
return np.asarray(w), np.asarray(c)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _normalize_rho(rho, N, wcut):
|
|
43
|
+
R, err = spi.quad(rho, 0, wcut)
|
|
44
|
+
fnorm = N/R
|
|
45
|
+
def rho_norm(w):
|
|
46
|
+
return rho(w)*fnorm
|
|
47
|
+
return rho_norm
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_omegas(rho, N, wcut, discr=5000):
|
|
51
|
+
"""
|
|
52
|
+
find the omegas for a mode density normalized to int_0^{w_j} rho(w) dw = j
|
|
53
|
+
|
|
54
|
+
rho -- function, normalized mode density
|
|
55
|
+
N -- number of modes
|
|
56
|
+
wcut -- largest omega
|
|
57
|
+
discr -- discretization for the internal numerical integration
|
|
58
|
+
"""
|
|
59
|
+
# Normalize rho, just in case
|
|
60
|
+
rho_norm = _normalize_rho(rho, N, wcut)
|
|
61
|
+
# The rho_norm function is now integrated along the w-axis, yielding I(w).
|
|
62
|
+
w = [] # omegas
|
|
63
|
+
r = [] # rho[omega] (at the w points)
|
|
64
|
+
rhos = np.zeros(discr, float)
|
|
65
|
+
ws = np.linspace(0, wcut, discr)
|
|
66
|
+
dw = ws[1] - ws[0]
|
|
67
|
+
for d in range(discr):
|
|
68
|
+
rhos[d] = rho_norm(ws[d])
|
|
69
|
+
I = np.zeros(discr, float)
|
|
70
|
+
I[0] = rhos[0]*dw
|
|
71
|
+
for j in range(1, discr):
|
|
72
|
+
I[j] = rhos[j]*dw + I[j - 1]
|
|
73
|
+
# Search for the minima of (I(w) - j)**2 to locate the omegas
|
|
74
|
+
for j in range(N):
|
|
75
|
+
tmp = (I - j - 1)**2
|
|
76
|
+
k = np.argmin(tmp)
|
|
77
|
+
wj = ws[k]
|
|
78
|
+
rj = rhos[k]
|
|
79
|
+
w.append(wj)
|
|
80
|
+
r.append(rj)
|
|
81
|
+
return np.asarray(w), np.asarray(r)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|