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,227 @@
1
+ import numpy as np
2
+
3
+ # Pre-built einsum index strings used by the alternative SOP RHS in
4
+ # propagation/exact.rhs_exact_alt1. Indexed by (ndim, op_type, axis),
5
+ # where op_type is 2 for a matrix operator and 3 for a diagonal operator.
6
+ # Covers ndim = 2..5.
7
+ # fmt: off
8
+ esumstr = {
9
+ # (dimensions, op type, index); op type 2: matrix, 3: diag matrix
10
+ (2, 2, 0): 'hj,ji->hi',
11
+ (2, 2, 1): 'hj,ij->ih',
12
+
13
+ (3, 2, 0): 'hj,jik->hik',
14
+ (3, 2, 1): 'hj,ijk->ihk',
15
+ (3, 2, 2): 'hj,ikj->ikh',
16
+
17
+ (4, 2, 0): 'hj,jikl->hikl',
18
+ (4, 2, 1): 'hj,ijkl->ihkl',
19
+ (4, 2, 2): 'hj,ikjl->ikhl',
20
+ (4, 2, 3): 'hj,iklj->iklh',
21
+
22
+ (5, 2, 0): 'hj,jiklm->hiklm',
23
+ (5, 2, 1): 'hj,ijklm->ihklm',
24
+ (5, 2, 2): 'hj,ikjlm->ikhlm',
25
+ (5, 2, 3): 'hj,ikljm->iklhm',
26
+ (5, 2, 4): 'hj,iklmj->iklmh',
27
+
28
+ (2, 3, 0): 'j,ji->ji',
29
+ (2, 3, 1): 'j,ij->ij',
30
+
31
+ (3, 3, 0): 'j,jik->jik',
32
+ (3, 3, 1): 'j,ijk->ijk',
33
+ (3, 3, 2): 'j,ikj->ikj',
34
+
35
+ (4, 3, 0): 'j,jikl->jikl',
36
+ (4, 3, 1): 'j,ijkl->ijkl',
37
+ (4, 3, 2): 'j,ikjl->ikjl',
38
+ (4, 3, 3): 'j,iklj->iklj',
39
+
40
+ (5, 3, 0): 'j,jiklm->jiklm',
41
+ (5, 3, 1): 'j,ijklm->ijklm',
42
+ (5, 3, 2): 'j,ikjlm->ikjlm',
43
+ (5, 3, 3): 'j,ikljm->ikljm',
44
+ (5, 3, 4): 'j,iklmj->iklmj',
45
+ }
46
+ # fmt: on
47
+
48
+
49
+ def _view(a, shape):
50
+ """Reshape a without copying; the result writes through to a.
51
+
52
+ Replaces the in-place ``a.shape = shape`` idiom (deprecated in NumPy
53
+ 2.5), keeping its guarantee: raise instead of silently copying.
54
+ """
55
+ v = a.reshape(shape)
56
+ if not np.shares_memory(v, a):
57
+ raise ValueError('array cannot be reshaped to ' + str(shape) + ' without a copy')
58
+ return v
59
+
60
+
61
+ def ijk_shape(f, oshape):
62
+ """
63
+ change tuple oshape representing a shape to a 3 index tuple collecting
64
+ indices before and after index f
65
+ """
66
+ nmodes = len(oshape)
67
+ a = np.array(oshape)
68
+ nshape=[1,1,1]
69
+ nshape[1] = oshape[f]
70
+ if f > 0:
71
+ nshape[0] = a[:f].prod()
72
+ if f < nmodes - 1:
73
+ nshape[2] = a[f+1:].prod()
74
+ return tuple(nshape)
75
+
76
+
77
+ def matrix_tensor(M, T):
78
+ for i in range(0, T.shape[0]):
79
+ for k in range(0, T.shape[2]):
80
+ T[i, :, k] = np.dot(M, T[i, :, k])
81
+ return T
82
+
83
+
84
+ def hprod_phi(hlist,nphi,phidim,sdim,phi,hole=None):
85
+ """
86
+ Apply matrices in hlist to phi
87
+ |phi[i]> <= h1.h2.h2....hn |phi[i]>
88
+
89
+ hlist - list (or array) of 2D square matrices
90
+ nphi - number of tensors in phi
91
+ phidim- total length of each tensor phi
92
+ sdim - shape of each tensor phi[i]
93
+ phi - 1D array, a list of tensors phi[i] (input/output)
94
+ hole - integer, hlist[hole] is skipped
95
+
96
+ The operations are accumulated in phi, i.e. phi is modified in place.
97
+ - note: phi must be conformable to (nphi,phidim) or (nphi,prod(sdim))
98
+ """
99
+ phi2 = _view(phi, (nphi, phidim))
100
+ for i in range(nphi):
101
+ for l,hmat in enumerate(hlist):
102
+ if l == hole: continue
103
+ matrix_tensor_alt(l,sdim,hmat,phi2[i])
104
+
105
+
106
+ def hprod_phi2(hlist,nphi,sdim1,sdim2,phi,hole=None):
107
+ """
108
+ Apply matrices in hlist to phi
109
+ |traphi[i]> = h1.h2.h2....hn |phi[i]>
110
+
111
+ hlist - list (or array) of 2D matrices. NEED NOT BE SQUARE.
112
+ nphi - number of tensors in phi
113
+ phidim- total length of each tensor phi
114
+ sdim1 - shape of each tensor on output (first dimension of each matrix)
115
+ sdim2 - shape of each tensor on input (second dimension of each matrix)
116
+ phi - 1D array, a list of tensors phi[i] (input)
117
+ hole - integer, hlist[hole] is skipped
118
+
119
+ traphi - transformed phi (output)
120
+ """
121
+ phic = phi.copy()
122
+ philen1 = 1
123
+ for i in sdim1: philen1 = philen1*i
124
+ philen2 = 1
125
+ for i in sdim2: philen2 = philen2*i
126
+ phic = phic.reshape((nphi, philen2))
127
+ traphi = []
128
+ for i in range(nphi):
129
+ phiold = phic[i]
130
+ sdimold = list(sdim2)
131
+ for l,hmat in enumerate(hlist):
132
+ if l == hole: continue
133
+ phinew = matrix_tensor2(l,sdimold,hmat,phiold)
134
+ phiold = phinew
135
+ sdimold[l] = sdim1[l]
136
+ traphi.append(phiold)
137
+ traphi = np.array(traphi)
138
+ return traphi.flatten()
139
+
140
+
141
+ def matrix_tensor_alt(m,sdim,mat,phi):
142
+ """
143
+ Perform a matrix tensor multiplication on index m of tensor phi
144
+
145
+ m - input, index summed over
146
+ sdim - input, dimensions (shape) of phi when in tesorized form
147
+ mat - input, 2D square array
148
+ phi - input/output, 1D array conformable to the sdim shape
149
+
150
+ phi(I,i,K) <= sum_j mat(i,j)*phi(I,j,K)
151
+ """
152
+ ijk = ijk_shape(m,sdim)
153
+ p = _view(phi, ijk)
154
+ for i in range(ijk[0]):
155
+ for k in range(ijk[2]):
156
+ p[i,:,k] = np.dot(mat,p[i,:,k])
157
+
158
+
159
+ def matrix_tensor2(m,sdim,mat,phi):
160
+ """
161
+ Perform a matrix tensor multiplication on index m of tensor phi
162
+
163
+ m - input, index summed over
164
+ sdim - input, dimensions (shape) of phi when in tesorized form
165
+ mat - input, 2D array, need not be square
166
+ phi - input/output, 1D array conformable to the sdim shape
167
+
168
+ The length of the multiplied index changes if the first dimension
169
+ of mat is not equal to the second dimension.
170
+ phiout(I,i,K) <= sum_j mat(i,j)*phi(I,j,K)
171
+ """
172
+ ijk = ijk_shape(m,sdim)
173
+ ilk = list(ijk)
174
+ ilk[1] = mat.shape[0]
175
+ ilk = tuple(ilk)
176
+ phiout = np.empty(ilk,complex)
177
+ p = phi.reshape(ijk)
178
+ for i in range(ijk[0]):
179
+ for k in range(ijk[2]):
180
+ phiout[i,:,k] = np.dot(mat,p[i,:,k])
181
+ return phiout.flatten()
182
+
183
+
184
+ def vector_tensor(m,sdim,vec,phi):
185
+ """
186
+ Perform a vector tensor multiplication on index m of tensor phi
187
+
188
+ m - input, index summed over
189
+ sdim - input, dimensions (shape) of phi when in tesorized form
190
+ mat - input, 1D
191
+ phi - input/output, 1D array conformable to the sdim shape
192
+
193
+ hphi(I,j,K) <= vec(j)*phi(I,j,K)
194
+ """
195
+ ijk = ijk_shape(m,sdim)
196
+ p = _view(phi, ijk)
197
+ for i in range(ijk[0]):
198
+ for k in range(ijk[2]):
199
+ p[i,:,k] = vec*p[i,:,k]
200
+ return phi
201
+
202
+
203
+ def compute_density(dm,f,sdim,ten1,ten2=None):
204
+ """
205
+ Calculate the density matrix for axis f of tensors ten1 and ten2
206
+
207
+ dm: density matrix, output
208
+ f: integer, axis to be used for the density
209
+ sdim: dimensions of ten1 and ten2
210
+ ten1: array conformable to ten1.shape=sdim
211
+ ten2: if not given ten2=ten1
212
+ """
213
+ if ten2 is None: ten2=ten1
214
+ l = len(sdim)
215
+ ax = list(range(f)) + list(range(f+1,l))
216
+ t1 = ten1.reshape(sdim)
217
+ t2 = ten2.reshape(sdim)
218
+ dm[:] = np.tensordot(t1.conjugate(),t2,axes=(ax,ax))
219
+
220
+
221
+ def overlap(avecs,bvecs):
222
+ """
223
+ Compute the overlap matrix from two two-dimensional np.array
224
+ avecs and bvecs need only to conform in the second index (index 1)
225
+ """
226
+ return np.tensordot(avecs.conjugate(),bvecs,axes=(1,1))
227
+
File without changes
@@ -0,0 +1,266 @@
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
+
8
+ import quadlib.tools.linalg as linalg
9
+ from quadlib.operator.sop import SumOfProducts
10
+
11
+
12
+ class WfnCPD:
13
+ """
14
+ Return a CPD wavefunction
15
+
16
+ - pdim: list/tuple, primitive dimension of each degree of freedom
17
+ - rank: number of product terms
18
+ """
19
+ def __init__(self, pdim: list[int], rank: int, datatype: type = complex):
20
+ self.psi = empty_cpd(pdim, rank, datatype=datatype) # psi[0] -> A-vec, psi[1] -> SPFs_1, etc.
21
+ self.ndof = len(pdim)
22
+ self.pdim = pdim
23
+ self.rank = rank
24
+ self.hamiltonian = None
25
+ self.time = 0.0
26
+ self._overlaps = []
27
+ self._hterms = None
28
+
29
+ def _update_overlaps(self):
30
+ self._overlaps = cpd_overlap_tensor(self.psi, self.psi)
31
+
32
+ def _coeff(self):
33
+ return np.asarray(self.hamiltonian.coeff, dtype=complex)
34
+
35
+ def _update_hterms(self):
36
+ assert isinstance(self.hamiltonian, SumOfProducts)
37
+ self._hterms = cpd_hterms_tensor(self.psi, self.psi, self.hamiltonian)
38
+
39
+ def norm(self):
40
+ self._update_overlaps()
41
+ Nm = cpd_overlap_prod(self._overlaps)
42
+ norm = np.sum(Nm)
43
+ norm = np.real(norm)**0.5
44
+ return norm
45
+
46
+ def normalize(self):
47
+ N = self.norm()
48
+ self.psi[-1] = self.psi[-1]/N
49
+
50
+ def overlap(self, other):
51
+ tensor = cpd_overlap_tensor(self.psi, other.psi)
52
+ overlap_matrix = cpd_overlap_prod(tensor)
53
+ ovl = np.sum(overlap_matrix)
54
+ return ovl
55
+
56
+ def build_hartree(self, spfs, datatype=complex):
57
+ """
58
+ Build a Hartree product state with the list of spfs, one per DOF
59
+
60
+ All other SPFs are set random normalized
61
+ All A-vector entris are set to zero except A[0]=1.0
62
+ """
63
+ for j in range(self.ndof):
64
+ rand_spfs = linalg.random_normal_columns_matrix(self.pdim[j], self.rank, datatype)
65
+ self.psi[j] = rand_spfs.T
66
+ self.psi[j][0] = spfs[j][:] # copy contents to avoid accidental modification
67
+ self.psi[-1][0] = 1.0 # A[0] = 1.0
68
+
69
+ def build_hartree_list(self, spfs_list, datatype=complex):
70
+ """
71
+ Take a list of Hartree products as input and set the corresponding SPFs
72
+
73
+ All other SPFs are set random normalized
74
+ All A-vector entris are set to zero except A[0]=1.0
75
+ """
76
+ nhartree = len(spfs_list)
77
+ for j in range(self.ndof):
78
+ rand_spfs = linalg.random_normal_columns_matrix(self.pdim[j], self.rank, datatype)
79
+ self.psi[j] = rand_spfs.T
80
+ for k in range(nhartree):
81
+ self.psi[j][k] = np.copy(spfs_list[k][j]) # copy contents to avoid accidental modification
82
+ for k in range(nhartree):
83
+ self.psi[-1][k] = 1.0 # A[k] = 1.0
84
+ self.normalize()
85
+
86
+ def density1d(self, dof):
87
+ """
88
+ Return 1d density matrix for dof in the primitive repr.
89
+
90
+ den1d[a, b] = sum Psi(..a..) Psi*(..b..), so that <O> = Tr(den1d O).
91
+ """
92
+ rho = self.rho_matrix(dof) # rho[i, j] = A_i* A_j prod_{d != dof} <phi_i|phi_j>
93
+ spfs = self.psi[dof]
94
+ return spfs.T @ rho.T @ spfs.conj()
95
+
96
+ def expval1d(self, op, dof):
97
+ """
98
+ Return expectation value of operator op for degree of freedom dof
99
+ """
100
+ den1d = self.density1d(dof)
101
+ expval = np.trace(den1d @ op) # <o> = Tr[rho, oper] for 1D operators
102
+ return expval
103
+
104
+ def S_matrix(self): # S -> configuration overlap matrix
105
+ self._update_overlaps()
106
+ S = cpd_overlap_prod(self._overlaps, hole=-1)
107
+ return S
108
+
109
+ def rho_matrix(self, dof): # rho -> density matrix
110
+ self._update_overlaps()
111
+ R = cpd_overlap_prod(self._overlaps, hole=dof)
112
+ return R
113
+
114
+ def H_matrix(self):
115
+ """
116
+ Compute Hamiltonian in the representation of the configurations
117
+ """
118
+ self._update_hterms()
119
+ hmat = np.prod(self._hterms, axis=1)
120
+ hmat = np.einsum('k,kij->kij', self._coeff(), hmat)
121
+ return hmat
122
+
123
+ def A_density(self):
124
+ """
125
+ Density matrix including only the A-vector entries
126
+ """
127
+ return np.outer(self.psi[-1].conj(), self.psi[-1])
128
+
129
+ def rhs_A(self):
130
+ """
131
+ Time derivative of the A-vector with the SPFs frozen
132
+
133
+ S dA/dt = -i H A, with S and H in the (non-orthogonal)
134
+ configuration basis. The coupled equations with moving SPFs are
135
+ in quadlib.experimental.cpd_eom.
136
+ """
137
+ S1 = linalg.invert_reg(self.S_matrix())[0]
138
+ H = self.H_matrix().sum(axis=0)
139
+ return -1.0j * S1 @ (H @ self.psi[-1])
140
+
141
+ def nullspace_dim(self, tol=None):
142
+ """
143
+ Dimension of the null space of the CPD parametrization at the
144
+ current state; see cpd_nullspace_dim.
145
+ """
146
+ return cpd_nullspace_dim(self.psi, tol=tol)
147
+
148
+ def empty_cpd(pdim, rank, datatype=complex):
149
+ cpd = [] # List containing the A-vector and the SPF matrices for each DOF
150
+ for d in pdim:
151
+ spfs = np.zeros((rank, d), datatype) # each SPF stored as a ROW
152
+ cpd.append(spfs)
153
+ cpd.append(np.zeros(rank, datatype)) # A-vector comes last
154
+ return cpd
155
+
156
+ def cpd_overlap_tensor(bra, ket):
157
+ ovl = []
158
+ for nu in range(len(bra) - 1):
159
+ ovl.append(bra[nu].conj() @ ket[nu].T)
160
+ ovl.append(np.outer(bra[-1].conj(), ket[-1]))
161
+ return np.asarray(ovl)
162
+
163
+ def cpd_overlap_prod(tensor, hole=None):
164
+ if hole is None:
165
+ matr = np.prod(tensor, axis=0)
166
+ elif type(hole) is int:
167
+ t = np.ones(tensor.shape, bool)
168
+ t[hole,:] = False
169
+ matr = np.prod(tensor, axis=0, where=t)
170
+ else:
171
+ raise ValueError()
172
+ return matr
173
+
174
+ def cpd_hterms_tensor(bra, ket, oper):
175
+ """
176
+ Representation of the operator in the space of the SPFs (hterms in MCTDH)
177
+
178
+ bra -- psi attribute of WfnCPD
179
+
180
+ Notes:
181
+ The coefficients oper.coeff are NOT included; the caller applies them.
182
+ """
183
+ oprank = len(oper.terms)
184
+ ndofs = oper.ndofs
185
+ hterms = []
186
+ for k in range(oprank):
187
+ hk = []
188
+ for d in range(ndofs):
189
+ hkd = oper.matrix_element_term(k, d, bra[d], ket[d])
190
+ hk.append(hkd)
191
+ hterms.append(hk)
192
+ return np.asarray(hterms)
193
+
194
+
195
+ def cpd_jacobian(psi):
196
+ """
197
+ Jacobian of the full-space vector of a CPD wavefunction with respect
198
+ to all its parameters
199
+
200
+ Psi = sum_j A_j phi_{1,j} (x) phi_{2,j} (x) ... (x) phi_{f,j}
201
+
202
+ psi -- psi attribute of WfnCPD: SPF matrices psi[d] of shape
203
+ (rank, n_d), one SPF per row, and the A-vector psi[-1]
204
+
205
+ Returns J of shape (prod n_d, rank * (1 + sum n_d)), complex. Psi is
206
+ a holomorphic function of the complex parameters, so J is the
207
+ complex-linear derivative dPsi/dp. Column order: the R entries of A,
208
+ then for each DOF d and configuration j the n_d components of
209
+ phi_{d,j}. Rows follow the full product space in C order (first DOF
210
+ slowest), as in SumOfProducts.full_matrix_operator.
211
+
212
+ The full product space is built explicitly: memory scales as
213
+ prod(n_d) * rank * (1 + sum n_d).
214
+ """
215
+ spfs, A = psi[:-1], psi[-1]
216
+ rank = len(A)
217
+ cols = []
218
+ for j in range(rank): # dPsi/dA_j = Phi_j
219
+ cols.append(linalg.kron_list([s[j][:, None] for s in spfs]))
220
+ for d, sd in enumerate(spfs): # dPsi/dphi_{d,j} = A_j Phi_j with phi_{d,j} -> 1
221
+ for j in range(rank):
222
+ factors = [s[j][:, None] for s in spfs]
223
+ factors[d] = np.eye(sd.shape[1])
224
+ cols.append(A[j] * linalg.kron_list(factors))
225
+ return np.hstack(cols)
226
+
227
+
228
+ def cpd_nullspace_dim(psi, tol=None):
229
+ """
230
+ Dimension of the null space of the CPD parametrization at psi
231
+
232
+ The null space of the Jacobian (cpd_jacobian) collects the parameter
233
+ changes that leave Psi unchanged to first order: the redundancy
234
+ (gauge freedom) of the ansatz at this point. It makes the linear
235
+ system for the parameter time derivatives in variational equations
236
+ of motion singular unless a gauge condition removes it.
237
+
238
+ Known counts, for generic (e.g. random) parameters, all complex:
239
+ - per configuration j, rescaling phi_{d,j} -> (1 + e_d) phi_{d,j} and
240
+ A_j -> (1 - sum_d e_d) A_j leaves Psi unchanged: at least f * rank;
241
+ - f >= 3 and parameters that do not saturate the product space:
242
+ exactly f * rank;
243
+ - f = 2 and R <= min(n_1, n_2): Psi is a rank-R matrix whose
244
+ factorization has an extra GL(R) freedom; the null space is
245
+ n_params - R (n_1 + n_2 - R);
246
+ - the Jacobian rank never exceeds prod(n_d): if n_params - f * rank
247
+ exceeds it, the null space is at least n_params - prod(n_d);
248
+ - defective formats fall below min(prod(n_d), n_params - f * rank),
249
+ the naive expectation for the rank. The classical example is
250
+ 3 x 3 x 3 at rank 4 (Strassen): rank 26 instead of 27, null space
251
+ 14 instead of 13.
252
+ Special points (e.g. some A_j = 0) have larger null spaces. For the
253
+ generic count of an ansatz, evaluate at random parameters.
254
+
255
+ psi -- psi attribute of WfnCPD (see cpd_jacobian)
256
+ tol -- singular-value threshold for the rank; default: that of
257
+ numpy.linalg.matrix_rank (relative to the largest singular
258
+ value)
259
+
260
+ Returns (null_dim, n_params, jacobian_rank), counted in complex
261
+ dimensions.
262
+ """
263
+ J = cpd_jacobian(psi)
264
+ jac_rank = int(np.linalg.matrix_rank(J, tol=tol))
265
+ n_params = J.shape[1]
266
+ return n_params - jac_rank, n_params, jac_rank
@@ -0,0 +1,147 @@
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
+ Exact (full-tensor) wavefunction.
8
+
9
+ WfnExact holds psi as an array of shape pdim (one index per degree of
10
+ freedom) and propagates it with quadlib.propagation.propagate_exact.
11
+ The free functions below work on plain arrays and implement the
12
+ corresponding WfnExact methods.
13
+ """
14
+
15
+ import numpy as np
16
+
17
+ import quadlib.tools.constants as cv
18
+ import quadlib.tools.tensor as tens
19
+ from quadlib.propagation.exact import propagate_exact
20
+
21
+
22
+ class WfnExact:
23
+ """
24
+ Full-tensor wavefunction psi[i1, i2, ..., if]
25
+
26
+ pdim -- primitive dimension of each degree of freedom
27
+ """
28
+ def __init__(self, pdim, dtype=complex):
29
+ self.pdim = tuple(int(n) for n in pdim)
30
+ self.psi = np.zeros(self.pdim, dtype)
31
+ self.psi0 = None # reference state for the autocorrelation
32
+ self.t = 0.0
33
+ self.dt = 1.0 # output interval of propagate()
34
+ self.time_list = []
35
+ self.auto_list = []
36
+ self.psi_list = []
37
+
38
+ @classmethod
39
+ def from_product(cls, vecs, dtype=complex):
40
+ """Product state from one 1D vector per degree of freedom."""
41
+ w = cls([len(v) for v in vecs], dtype)
42
+ w.psi[:] = product_state_exact(vecs).reshape(w.pdim)
43
+ return w
44
+
45
+ @property
46
+ def len(self):
47
+ return self.psi.size
48
+
49
+ def reset_initial(self):
50
+ """Take the current psi and t as the reference for auto(t)."""
51
+ self.psi0 = self.psi.copy()
52
+ self.time_list = [self.t]
53
+ self.auto_list = [self.auto()]
54
+ self.psi_list = []
55
+
56
+ def norm(self):
57
+ return np.sqrt(np.real(np.vdot(self.psi, self.psi)))
58
+
59
+ def auto(self):
60
+ """Autocorrelation <psi0|psi(t)>."""
61
+ return np.vdot(self.psi0, self.psi)
62
+
63
+ def density1d(self, dof):
64
+ return density1d(self.psi, dof)
65
+
66
+ def expval1d(self, op, dof):
67
+ return expval1d(self.psi, op, dof)
68
+
69
+ def propagate(self, final_time, operator, *, auto=True, psi=False, **opts):
70
+ """
71
+ Propagate exactly from self.t to final_time
72
+
73
+ Output every self.dt, appended to self.time_list, self.auto_list
74
+ (if auto) and self.psi_list (if psi). reset_initial() is called
75
+ first if no reference state has been set. opts go to
76
+ quadlib.propagation.propagate_exact (solver, and for 'zvode'
77
+ method, atol, rtol, maxstep).
78
+ """
79
+ if self.psi0 is None:
80
+ self.reset_initial()
81
+ observables = {}
82
+ if auto:
83
+ psi0 = self.psi0.ravel()
84
+ observables['auto'] = lambda t, y: np.vdot(psi0, y)
85
+ out = propagate_exact(self.psi, operator, self.dt, final_time,
86
+ tinit=self.t, store_psi=psi,
87
+ observables=observables, **opts)
88
+ self.time_list.extend(out['time'][1:])
89
+ if auto:
90
+ self.auto_list.extend(out['auto'][1:])
91
+ if psi:
92
+ self.psi_list.extend(out['psi'][1:])
93
+ self.psi = out['final']
94
+ self.t = out['time'][-1]
95
+ return out
96
+
97
+ def plauto(self):
98
+ """Plot the autocorrelation function (time in fs)."""
99
+ import matplotlib.pyplot as plt
100
+
101
+ if not self.auto_list:
102
+ raise ValueError('plauto: missing auto in wavefunction\n')
103
+ t = np.asarray(self.time_list)
104
+ a = np.asarray(self.auto_list)
105
+ plt.plot(t*cv.au2fs, np.real(a), label='Re')
106
+ plt.plot(t*cv.au2fs, np.imag(a), label='Im')
107
+ plt.plot(t*cv.au2fs, np.abs(a), label='Abs')
108
+ plt.legend()
109
+ plt.title('auto')
110
+ plt.show()
111
+
112
+
113
+ def product_state_exact(vec_list):
114
+ """
115
+ Take list of 1D state vectors and create the product state
116
+ """
117
+ state_vec = vec_list[0]
118
+ for v in vec_list[1:]:
119
+ state_vec = np.outer(state_vec, v)
120
+ return state_vec.flatten()
121
+
122
+ def density1d(A, dof):
123
+ """
124
+ Return density matrix for degree of freedom dof
125
+
126
+ A -- multidimensional array of shape (n1, n2, ...)
127
+
128
+ Return density matrix d1d[a, b] = sum A(..a..) A*(..b..), so that
129
+ <O> = Tr(d1d O)
130
+ """
131
+ ijk = tens.ijk_shape(dof, A.shape)
132
+ Ar = A.reshape(ijk)
133
+ return np.einsum('ijk,ilk->jl', Ar, Ar.conj())
134
+
135
+ def expval1d(A, op, dof):
136
+ """
137
+ Return expectation value for operator op on dof
138
+
139
+ A -- multidimensional array of shape (n1, n2, ...)
140
+ op -- (nDOF, nDOF) matrix
141
+
142
+ Return expectation value
143
+ """
144
+ ijk = tens.ijk_shape(dof, A.shape)
145
+ Ar = A.reshape(ijk)
146
+ opA = np.einsum('ij,kjm->kim', op, Ar)
147
+ return np.vdot(Ar, opA)