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/tensor.py
ADDED
|
@@ -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)
|