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.
Files changed (43) hide show
  1. quadlib/__init__.py +39 -0
  2. quadlib/basis/__init__.py +60 -0
  3. quadlib/basis/boson.py +61 -0
  4. quadlib/basis/discrete.py +71 -0
  5. quadlib/basis/expdvr.py +80 -0
  6. quadlib/basis/hodvr.py +51 -0
  7. quadlib/basis/jbasis.py +87 -0
  8. quadlib/basis/sindvr.py +97 -0
  9. quadlib/basis/spin.py +64 -0
  10. quadlib/cli/__init__.py +0 -0
  11. quadlib/cli/qlb_offset_oper_dof.py +34 -0
  12. quadlib/experimental/__init__.py +17 -0
  13. quadlib/experimental/cpd_eom.py +95 -0
  14. quadlib/experimental/mctdh.py +32 -0
  15. quadlib/experimental/node.py +259 -0
  16. quadlib/experimental/pdof.py +9 -0
  17. quadlib/experimental/tree.py +138 -0
  18. quadlib/hdmctdh/__init__.py +0 -0
  19. quadlib/hdmctdh/rundirectory.py +74 -0
  20. quadlib/legacy/__init__.py +14 -0
  21. quadlib/legacy/math.py +1337 -0
  22. quadlib/operator/sop.py +250 -0
  23. quadlib/propagation/__init__.py +17 -0
  24. quadlib/propagation/cpd.py +35 -0
  25. quadlib/propagation/exact.py +139 -0
  26. quadlib/propagation/integrators.py +108 -0
  27. quadlib/tools/__init__.py +0 -0
  28. quadlib/tools/bath.py +84 -0
  29. quadlib/tools/constants.py +289 -0
  30. quadlib/tools/iterators.py +276 -0
  31. quadlib/tools/linalg.py +215 -0
  32. quadlib/tools/pulses.py +187 -0
  33. quadlib/tools/spectra.py +179 -0
  34. quadlib/tools/tensor.py +227 -0
  35. quadlib/wavefunction/__init__.py +0 -0
  36. quadlib/wavefunction/wfn_cpd.py +266 -0
  37. quadlib/wavefunction/wfn_exact.py +147 -0
  38. quadlib-0.1.0.dist-info/METADATA +135 -0
  39. quadlib-0.1.0.dist-info/RECORD +43 -0
  40. quadlib-0.1.0.dist-info/WHEEL +5 -0
  41. quadlib-0.1.0.dist-info/entry_points.txt +2 -0
  42. quadlib-0.1.0.dist-info/licenses/LICENSE +19 -0
  43. quadlib-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
+