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
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
Coupled equations of motion for the CPD wavefunction (A-vector and SPFs).
|
|
8
|
+
|
|
9
|
+
KNOWN INCORRECT, kept as found for later work. The Dirac-Frenkel
|
|
10
|
+
equations for Psi = sum_j A_j prod_d phi_{d,j}, with unconstrained,
|
|
11
|
+
non-orthogonal SPFs, read
|
|
12
|
+
|
|
13
|
+
S dA/dt = -i H A - T A, T_ij = <Phi_i|dPhi_j/dt>
|
|
14
|
+
rho^(d) dphi_d/dt = -i sum_k MF_k^(d) h_k^(d) phi_d - tau^(d) phi_d
|
|
15
|
+
|
|
16
|
+
where T and tau^(d) are sums of terms with one derivative overlap
|
|
17
|
+
each. Compared with them:
|
|
18
|
+
|
|
19
|
+
- T_matrix and tau_matrix take the product of all derivative overlaps
|
|
20
|
+
instead of a sum of terms with one derivative factor each;
|
|
21
|
+
- rhs_A_coupled and rhs_spfs are consistent with those equations only
|
|
22
|
+
if T and tau carry a factor i, which they do not;
|
|
23
|
+
- no gauge condition is imposed and the equations are implicit
|
|
24
|
+
(T and tau need the derivatives being solved for).
|
|
25
|
+
|
|
26
|
+
The mean-field term (mfield_hole and its use in rhs_spfs) does agree
|
|
27
|
+
with the derivation.
|
|
28
|
+
|
|
29
|
+
All functions take the wavefunction ``wfn`` (a ``quadlib.WfnCPD`` with
|
|
30
|
+
its ``hamiltonian`` set) and, where needed, ``dtpsi``: a list with the
|
|
31
|
+
same layout as ``wfn.psi`` holding the time derivatives (SPF matrices
|
|
32
|
+
first, one SPF per row, A-vector last). ``empty_dtpsi`` builds one.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
import quadlib.tools.linalg as linalg
|
|
38
|
+
from quadlib.wavefunction.wfn_cpd import cpd_overlap_prod, cpd_overlap_tensor, empty_cpd
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def empty_dtpsi(wfn):
|
|
42
|
+
return empty_cpd(wfn.pdim, wfn.rank, datatype=complex)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def T_matrix(wfn, dtpsi):
|
|
46
|
+
"""T -> configuration overlap matrix, rhs time derivative (see module notes)"""
|
|
47
|
+
ovl_dt = cpd_overlap_tensor(wfn.psi, dtpsi)
|
|
48
|
+
return cpd_overlap_prod(ovl_dt, hole=-1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tau_matrix(wfn, dof, dtpsi):
|
|
52
|
+
"""tau -> density matrix, rhs time derivative (see module notes)"""
|
|
53
|
+
ovl_dt = cpd_overlap_tensor(wfn.psi, dtpsi)
|
|
54
|
+
return cpd_overlap_prod(ovl_dt, hole=dof)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def mfield_hole(wfn, dof):
|
|
58
|
+
"""
|
|
59
|
+
Compute the meanfield-only (the meanfield hole) of the mean field operator
|
|
60
|
+
|
|
61
|
+
dof -- degree of freedom, counting from 0
|
|
62
|
+
|
|
63
|
+
The coefficient of each operator term is included, so that the
|
|
64
|
+
mean field is complete whichever DOF is left out.
|
|
65
|
+
"""
|
|
66
|
+
wfn._update_hterms()
|
|
67
|
+
t = np.ones(wfn._hterms.shape, bool)
|
|
68
|
+
t[:, dof, :] = False
|
|
69
|
+
mf = np.prod(wfn._hterms, axis=1, where=t)
|
|
70
|
+
mf = np.einsum('k,kij->kij', wfn._coeff(), mf)
|
|
71
|
+
mf = np.einsum('ij,kij->kij', wfn.A_density(), mf)
|
|
72
|
+
return mf
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def rhs_A_coupled(wfn, dtpsi):
|
|
76
|
+
"""A-vector RHS including the T term (see module notes)"""
|
|
77
|
+
S1 = linalg.invert_reg(wfn.S_matrix())[0]
|
|
78
|
+
T = T_matrix(wfn, dtpsi)
|
|
79
|
+
H = wfn.H_matrix()
|
|
80
|
+
dtA = H.sum(axis=0) @ wfn.psi[-1]
|
|
81
|
+
dtA = dtA - T @ wfn.psi[-1]
|
|
82
|
+
return -1.0j * S1 @ dtA
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rhs_spfs(wfn, dof, dtpsi):
|
|
86
|
+
"""SPF RHS for degree of freedom dof (see module notes)"""
|
|
87
|
+
rho1 = linalg.invert_reg(wfn.rho_matrix(dof))[0]
|
|
88
|
+
tau = tau_matrix(wfn, dof, dtpsi)
|
|
89
|
+
mfhole = mfield_hole(wfn, dof)
|
|
90
|
+
dtspf = np.zeros_like(wfn.psi[dof])
|
|
91
|
+
for k in range(wfn.hamiltonian.rank): # operator terms loop
|
|
92
|
+
tmp = wfn.hamiltonian.multiply_term(k, dof, wfn.psi[dof])
|
|
93
|
+
dtspf = dtspf + mfhole[k] @ tmp
|
|
94
|
+
dtspf = dtspf - tau @ wfn.psi[dof]
|
|
95
|
+
return -1.0j * (rho1 @ dtspf)
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
Placeholders for the (ML-)MCTDH equations of motion on a Tree.
|
|
8
|
+
|
|
9
|
+
Not implemented. They mark where the tree EOMs will go; once they work
|
|
10
|
+
they move out of experimental into an actual WfnMCTDH.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def rhsEOM(wfn, oper, **opts):
|
|
15
|
+
"""Right-hand side of the MCTDH equations of motion."""
|
|
16
|
+
raise NotImplementedError('MCTDH equations of motion are not implemented yet')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def stepEOM(wfn, oper, **opts):
|
|
20
|
+
raise NotImplementedError('MCTDH equations of motion are not implemented yet')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def htermsEOM(wfn, oper, **opts):
|
|
24
|
+
raise NotImplementedError('MCTDH equations of motion are not implemented yet')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def mfieldsEOM(wfn, oper, **opts):
|
|
28
|
+
raise NotImplementedError('MCTDH equations of motion are not implemented yet')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dmatEOM(wfn, oper, **opts):
|
|
32
|
+
raise NotImplementedError('MCTDH equations of motion are not implemented yet')
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
import quadlib.tools.linalg as linalg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Node:
|
|
9
|
+
def __init__(self, **opts):
|
|
10
|
+
self.uid = uuid.uuid4().hex # unique ID string for the node
|
|
11
|
+
self.tag = opts.get('tag', self.uid) # human readable tagname for the node
|
|
12
|
+
self.links = []
|
|
13
|
+
self.index_at_link = [] # index of this bond in the corresponding linked node
|
|
14
|
+
#self.shape = np.array([], int)
|
|
15
|
+
self.shape = []
|
|
16
|
+
self.parent_index = -1
|
|
17
|
+
self.tensor = None
|
|
18
|
+
self.is_pdof = False
|
|
19
|
+
self.is_visited = False # needed for some traversing algorithms
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def dim(self):
|
|
24
|
+
"""
|
|
25
|
+
Return node dimensionality (number of bonds)
|
|
26
|
+
"""
|
|
27
|
+
return len(self.shape)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def len(self, **opts):
|
|
32
|
+
"""
|
|
33
|
+
Return node length, i.e. product over dimensions = number of coefficients
|
|
34
|
+
"""
|
|
35
|
+
return np.prod(self.shape)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def allocate(self, **opts):
|
|
39
|
+
dtype = opts.get('dtype', complex)
|
|
40
|
+
order = opts.get('order', 'C')
|
|
41
|
+
self.tensor = np.zeros(self.shape, dtype, order=order)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def init_tensor_orthonormal_rand(self, ortho_index = 'last', **opts):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the tensor as a random orthogonal matrix w.r.t. index ortho_index
|
|
47
|
+
"""
|
|
48
|
+
if ortho_index == 'last':
|
|
49
|
+
dim1 = self.len//self.shape[-1]
|
|
50
|
+
dim2 = self.shape[-1]
|
|
51
|
+
if dim2 > dim1: raise ValueError('init_tensor_orthonormal_rand: error dim2 > dim1')
|
|
52
|
+
Q = linalg.random_orthogonal_matrix(dim1, dim2)
|
|
53
|
+
self.tensor[:] = Q.reshape(self.tensor.shape)
|
|
54
|
+
elif ortho_index == 'first':
|
|
55
|
+
dim1 = self.len//self.shape[0]
|
|
56
|
+
dim2 = self.shape[0]
|
|
57
|
+
if dim2 > dim1: raise ValueError('init_tensor_orthonormal_rand: error dim2 > dim1')
|
|
58
|
+
Q = linalg.random_orthogonal_matrix(dim1, dim2)
|
|
59
|
+
Q = Q.T
|
|
60
|
+
self.tensor[:] = Q.reshape(self.tensor.shape)
|
|
61
|
+
|
|
62
|
+
def init_tensor_normal_rand(self):
|
|
63
|
+
"""
|
|
64
|
+
Initialize the tensor with random numbers and normalize
|
|
65
|
+
|
|
66
|
+
(useful e.g. for top A-vector)
|
|
67
|
+
"""
|
|
68
|
+
t = np.random.random_sample(self.shape)
|
|
69
|
+
tf = t.flatten()
|
|
70
|
+
N = np.sqrt(np.conj(tf) @ tf)
|
|
71
|
+
self.tensor = t / N
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reset_initial(self, **opts):
|
|
75
|
+
"""
|
|
76
|
+
(Re)set the tensor used for getting the autocorrelation
|
|
77
|
+
"""
|
|
78
|
+
self.tensor0 = self.tensor.copy()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def link_node(self, nodeB, dim=0):
|
|
82
|
+
"""
|
|
83
|
+
Link two nodes.
|
|
84
|
+
"""
|
|
85
|
+
nodeA = self
|
|
86
|
+
if nodeA.uid == nodeB.uid: # avoid 1-node loops
|
|
87
|
+
raise ValueError('Not possible to link node with itself\n')
|
|
88
|
+
if nodeA.is_linked_to(nodeB): # avoid double linking
|
|
89
|
+
raise ValueError('Not possible to link two nodes twice\n')
|
|
90
|
+
nodeA.links.append(nodeB)
|
|
91
|
+
if not nodeB.is_pdof:
|
|
92
|
+
nodeB.links.append(nodeA)
|
|
93
|
+
nodeA.shape.append(dim)
|
|
94
|
+
nodeB.shape.append(dim)
|
|
95
|
+
nodeA.index_at_link.append(nodeB.links.index(nodeA))
|
|
96
|
+
nodeB.index_at_link.append(nodeA.links.index(nodeB))
|
|
97
|
+
else:
|
|
98
|
+
nodeA.shape.append(nodeB.dim)
|
|
99
|
+
nodeA.index_at_link.append(0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def transpose_index(self, iold: int, inew: int):
|
|
103
|
+
"""
|
|
104
|
+
Transpose index at position iold to position inew
|
|
105
|
+
|
|
106
|
+
If the node tensor is present the tensor indices are transposed accordingly
|
|
107
|
+
"""
|
|
108
|
+
# Transpose pointers to nodes in the self.links list
|
|
109
|
+
trans_node = self.links.pop(iold)
|
|
110
|
+
self.links.insert(inew, trans_node)
|
|
111
|
+
# Transpose entries with bond dimensions in the self.shape list
|
|
112
|
+
trans_shape = self.shape.pop(iold)
|
|
113
|
+
self.shape.insert(inew, trans_shape)
|
|
114
|
+
# Transpose tensor indices
|
|
115
|
+
if self.tensor is not None:
|
|
116
|
+
# Create list with index positions [0, 1, 2,... ,self.dim()]
|
|
117
|
+
indices = list(range(self.dim))
|
|
118
|
+
trans_indx = indices.pop(iold)
|
|
119
|
+
indices.insert(inew, trans_indx)
|
|
120
|
+
self.tensor = self.tensor.transpose(indices)
|
|
121
|
+
# Update index of this node at linked nodes
|
|
122
|
+
for lb, nodeB in enumerate(self.links):
|
|
123
|
+
if not nodeB.is_pdof:
|
|
124
|
+
la = nodeB.links.index(self)
|
|
125
|
+
nodeB.index_at_link[la] = lb
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def has_loops(self):
|
|
130
|
+
"""
|
|
131
|
+
Return True if the graph under this node structure contains loops
|
|
132
|
+
"""
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def is_linked_to(self, nodeB):
|
|
137
|
+
for lnode in self.links:
|
|
138
|
+
if lnode.uid == nodeB.uid:
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def list_links(self, tags = False, exclude_pdof = False):
|
|
144
|
+
"""
|
|
145
|
+
Lists all connections linked to nodeA, both nodes and primitives
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
list: The list is empty if the node has no links
|
|
149
|
+
"""
|
|
150
|
+
lconnections = self.links[:]
|
|
151
|
+
if exclude_pdof:
|
|
152
|
+
lnodes = []
|
|
153
|
+
for l in lconnections:
|
|
154
|
+
if not l.is_pdof:
|
|
155
|
+
lnodes.append(l)
|
|
156
|
+
lconnections = lnodes
|
|
157
|
+
if tags:
|
|
158
|
+
return _list_tags(lconnections)
|
|
159
|
+
else:
|
|
160
|
+
return lconnections
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def return_link_index(self, other):
|
|
164
|
+
"""
|
|
165
|
+
Return the index of node other @ node self
|
|
166
|
+
"""
|
|
167
|
+
return self.links.index(other)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_primitive(self):
|
|
171
|
+
"""
|
|
172
|
+
Return True if a node is primitive
|
|
173
|
+
|
|
174
|
+
Primitive nodes have ONE link to a non-primitive node
|
|
175
|
+
"""
|
|
176
|
+
p = self.count_pdof()
|
|
177
|
+
n = len(self.shape) - p
|
|
178
|
+
if p >= 1 and n == 1:
|
|
179
|
+
return True
|
|
180
|
+
else:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_mixed(self):
|
|
185
|
+
"""
|
|
186
|
+
Return True if a node is of mixed type
|
|
187
|
+
|
|
188
|
+
A mixed node has primitive DOFs and more than one link to non-primitive nodes
|
|
189
|
+
"""
|
|
190
|
+
p = self.count_pdof()
|
|
191
|
+
n = len(self.shape) - p
|
|
192
|
+
if p >= 1 and n > 1:
|
|
193
|
+
return True
|
|
194
|
+
else:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def is_pure(self):
|
|
199
|
+
"""
|
|
200
|
+
Return True if a node is of pure type
|
|
201
|
+
|
|
202
|
+
A pure node no primitive DOFs
|
|
203
|
+
"""
|
|
204
|
+
p = self.count_pdof()
|
|
205
|
+
n = len(self.shape) - p
|
|
206
|
+
if p == 0 and n >= 1:
|
|
207
|
+
return True
|
|
208
|
+
else:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def count_pdof(self):
|
|
213
|
+
count = 0
|
|
214
|
+
for child in self.list_links():
|
|
215
|
+
if child.is_pdof:
|
|
216
|
+
count += 1
|
|
217
|
+
return count
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sanity_check(self):
|
|
221
|
+
print('------------- Check Node ' + self.tag + ' ------------')
|
|
222
|
+
i = 0
|
|
223
|
+
for nodeB in self.links:
|
|
224
|
+
if nodeB.is_pdof:
|
|
225
|
+
continue
|
|
226
|
+
if nodeB.shape[nodeB.links.index(self)] == self.shape[self.links.index(nodeB)]:
|
|
227
|
+
tagA = self.tag
|
|
228
|
+
tagB = nodeB.tag
|
|
229
|
+
iB_at_A = str(self.links.index(nodeB))
|
|
230
|
+
iA_at_B = str(nodeB.links.index(self))
|
|
231
|
+
iA_at_B_2 = str(self.index_at_link[i])
|
|
232
|
+
m = str(nodeB.shape[nodeB.links.index(self)])
|
|
233
|
+
print(' ... ' + tagA + ' ' + iB_at_A + ' <-> ' + tagB + ' ' + iA_at_B + ' ' + iA_at_B_2 + ' => ' + m)
|
|
234
|
+
else:
|
|
235
|
+
raise ValueError(nodeB.tag + ' <-> ' + self.tag + ' broken')
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def apply_matrix_to_index(self, matrix, indx):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def apply_unitary_to_link(self, U, indx):
|
|
242
|
+
"""
|
|
243
|
+
[NodeA ------ U] ------ [U^-1 --------- NodeB]
|
|
244
|
+
|
|
245
|
+
The convention is to apply U to node A (self) and U^-1 to node B
|
|
246
|
+
"""
|
|
247
|
+
self.apply_matrix_to_index(U, indx)
|
|
248
|
+
UT = np.conj(U.T)
|
|
249
|
+
nodeB = self.links[indx]
|
|
250
|
+
iA_at_B = nodeB.links.index(self)
|
|
251
|
+
nodeB.apply_matrix_to_index(UT, iA_at_B)
|
|
252
|
+
|
|
253
|
+
def _list_tags(nodeList):
|
|
254
|
+
lid = []
|
|
255
|
+
for node in nodeList:
|
|
256
|
+
lid.append(node.tag)
|
|
257
|
+
return lid
|
|
258
|
+
|
|
259
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Tree:
|
|
5
|
+
def __init__(self, root = None, **opts):
|
|
6
|
+
self.uid = uuid.uuid4().hex
|
|
7
|
+
self.root = root
|
|
8
|
+
self.set_root(root)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def len(self, **opts):
|
|
13
|
+
nCoeff = 0
|
|
14
|
+
lnodes = self.traverse_preorder(self.root)
|
|
15
|
+
for n in lnodes:
|
|
16
|
+
nNodeCoeff = n.len
|
|
17
|
+
nCoeff += nNodeCoeff
|
|
18
|
+
return nCoeff
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_root(self, nroot, **opts):
|
|
22
|
+
self.root = nroot
|
|
23
|
+
self.root.is_visited = True
|
|
24
|
+
self.root.parent_index = -1
|
|
25
|
+
L = self.traverse_preorder() # pre-order traversal starting from the new root
|
|
26
|
+
for node in L[1:]:
|
|
27
|
+
node.is_visited = True
|
|
28
|
+
for j, lnode in enumerate(node.links):
|
|
29
|
+
if lnode.is_pdof is True:
|
|
30
|
+
continue
|
|
31
|
+
if lnode.is_visited is True:
|
|
32
|
+
node.parent_index = j
|
|
33
|
+
break
|
|
34
|
+
# clean-up
|
|
35
|
+
for node in L:
|
|
36
|
+
node.is_visited = False
|
|
37
|
+
# Bring node links to canonical order (within each node parent is last self.links entry)
|
|
38
|
+
self.transpose_parent_index(**opts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transpose_parent_index(self, **opts):
|
|
42
|
+
"""
|
|
43
|
+
Transpose parent index to last (first) position
|
|
44
|
+
|
|
45
|
+
For each node, the link to the parent is moved to either the first or the last (default) position.
|
|
46
|
+
The local tensor with the node's data is transposed accordingly.
|
|
47
|
+
"""
|
|
48
|
+
index_order = opts.get('index_order', 'plast')
|
|
49
|
+
nodelist = self.traverse_preorder()
|
|
50
|
+
for n in nodelist[1:]:
|
|
51
|
+
iparent = n.parent_index
|
|
52
|
+
ndim = n.dim
|
|
53
|
+
if index_order == 'plast':
|
|
54
|
+
n.transpose_index(iparent, ndim - 1)
|
|
55
|
+
elif index_order == 'pfirst':
|
|
56
|
+
n.transpose_index(iparent, 0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def traverse_preorder(self,
|
|
60
|
+
next_node = None,
|
|
61
|
+
tags = False,
|
|
62
|
+
lnodes = None,
|
|
63
|
+
discovered = None,
|
|
64
|
+
isFirstCall = True):
|
|
65
|
+
"""
|
|
66
|
+
Traverse the tree in depth-first-search, preorder
|
|
67
|
+
|
|
68
|
+
- rootNode: node where the tree traversal is started
|
|
69
|
+
"""
|
|
70
|
+
if isFirstCall:
|
|
71
|
+
lnodes = []
|
|
72
|
+
discovered = []
|
|
73
|
+
next_node = self.root
|
|
74
|
+
lnodes.append(next_node)
|
|
75
|
+
discovered.append(next_node)
|
|
76
|
+
for node in next_node.list_links(exclude_pdof = True):
|
|
77
|
+
if discovered.count(node) == 0:
|
|
78
|
+
self.traverse_preorder(node,
|
|
79
|
+
tags = False,
|
|
80
|
+
lnodes = lnodes,
|
|
81
|
+
discovered = discovered,
|
|
82
|
+
isFirstCall = False)
|
|
83
|
+
if tags:
|
|
84
|
+
return _list_tags(lnodes)
|
|
85
|
+
else:
|
|
86
|
+
return lnodes
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def traverse_postorder(self,
|
|
90
|
+
next_node = None,
|
|
91
|
+
tags = False,
|
|
92
|
+
lnodes = None,
|
|
93
|
+
discovered = None,
|
|
94
|
+
isFirstCall = True):
|
|
95
|
+
"""
|
|
96
|
+
Traverse the tree in depth-first-search, postorder
|
|
97
|
+
"""
|
|
98
|
+
if isFirstCall:
|
|
99
|
+
lnodes = []
|
|
100
|
+
discovered = []
|
|
101
|
+
next_node = self.root
|
|
102
|
+
discovered.append(next_node)
|
|
103
|
+
for node in next_node.list_links(exclude_pdof = True):
|
|
104
|
+
if discovered.count(node) == 0:
|
|
105
|
+
self.traverse_postorder(node,
|
|
106
|
+
tags = False,
|
|
107
|
+
lnodes = lnodes,
|
|
108
|
+
discovered = discovered,
|
|
109
|
+
isFirstCall = False)
|
|
110
|
+
lnodes.append(next_node)
|
|
111
|
+
if tags:
|
|
112
|
+
return _list_tags(lnodes)
|
|
113
|
+
else:
|
|
114
|
+
return lnodes
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_tree_list(self):
|
|
118
|
+
for n in self.traverse_preorder():
|
|
119
|
+
t = n.tag
|
|
120
|
+
l = n.list_links(tags = True)
|
|
121
|
+
if n.parent_index > -1:
|
|
122
|
+
l.pop(n.parent_index)
|
|
123
|
+
s = t + ' -> ' + str(l)
|
|
124
|
+
print(s)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sanity_check(self):
|
|
128
|
+
lnodes = self.traverse_preorder()
|
|
129
|
+
for node in lnodes:
|
|
130
|
+
node.sanity_check()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _list_tags(nodeList):
|
|
134
|
+
lid = []
|
|
135
|
+
for node in nodeList:
|
|
136
|
+
lid.append(node.tag)
|
|
137
|
+
return lid
|
|
138
|
+
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
# QuaDLib: QUAntum Dynamics LIBrary for research and methods development
|
|
3
|
+
# Copyright (C) 2018-2026, The Theoretical Chemistry Group, Heidelberg University
|
|
4
|
+
#
|
|
5
|
+
# This software is licensed under the MIT License (https://spdx.org/licenses/MIT.html)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# This module defines classes and functions to open and interact with
|
|
9
|
+
# an MCTDH run directory
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
STANDARDFILES = ['input', 'output', 'check', 'log', 'oper', 'dvr']
|
|
14
|
+
|
|
15
|
+
class Rundirectory:
|
|
16
|
+
"""
|
|
17
|
+
Open an MCTDH Run directory for analysis etc.
|
|
18
|
+
|
|
19
|
+
path: str, path (absolute or relative) to run directory
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path):
|
|
23
|
+
rundir = Path(path)
|
|
24
|
+
self._messages = []
|
|
25
|
+
if not rundir.is_dir():
|
|
26
|
+
raise ValueError(path + ' is not a directory')
|
|
27
|
+
# We look for standard files to determine that this is an MCTDH run
|
|
28
|
+
for f in STANDARDFILES:
|
|
29
|
+
rf = rundir / f
|
|
30
|
+
if not rf.is_file():
|
|
31
|
+
msg = ' * Warning: ' + str(rf) + ' file not found'
|
|
32
|
+
self._messages.append(msg)
|
|
33
|
+
self.rundir = rundir
|
|
34
|
+
try:
|
|
35
|
+
self.auto = self.load_auto()
|
|
36
|
+
except Exception:
|
|
37
|
+
msg = ' * Error: auto file could not be loaded'
|
|
38
|
+
self._messages.append(msg)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def messages(self):
|
|
42
|
+
return self._messages
|
|
43
|
+
|
|
44
|
+
def load_auto(self):
|
|
45
|
+
"""
|
|
46
|
+
Load auto file as numpy array and return
|
|
47
|
+
|
|
48
|
+
Returns numpy array:
|
|
49
|
+
auto[0]: time steps
|
|
50
|
+
auto[1]: real part
|
|
51
|
+
auto[2]: imag part
|
|
52
|
+
auto[3]: absolute
|
|
53
|
+
"""
|
|
54
|
+
import numpy as np
|
|
55
|
+
path_auto = self.rundir / 'auto'
|
|
56
|
+
if not path_auto.is_file():
|
|
57
|
+
msg = 'auto file not found in ' + str(self.rundir)
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
self._messages.append(msg)
|
|
60
|
+
auto = np.loadtxt(path_auto).T
|
|
61
|
+
return auto
|
|
62
|
+
|
|
63
|
+
def plauto(self, mode = 'survival'):
|
|
64
|
+
import matplotlib.pyplot as plt
|
|
65
|
+
a = self.auto
|
|
66
|
+
if mode == 'abs':
|
|
67
|
+
plt.plot(a[0], a[3])
|
|
68
|
+
elif mode == 'real':
|
|
69
|
+
plt.plot(a[0], a[1])
|
|
70
|
+
elif mode == 'imag':
|
|
71
|
+
plt.plot(a[0], a[2])
|
|
72
|
+
elif mode == 'survival':
|
|
73
|
+
plt.plot(a[0], a[3]**2)
|
|
74
|
+
plt.show()
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
Older utilities kept for reference and for existing scripts.
|
|
8
|
+
|
|
9
|
+
Not imported by ``quadlib/__init__.py``, not used by the library, and
|
|
10
|
+
not tested; import explicitly, e.g. ``import quadlib.legacy.math``.
|
|
11
|
+
``quadlib.legacy.math`` is the former ``quadlib.tools.math`` minus the
|
|
12
|
+
functions the library uses, which now live in ``quadlib.tools.linalg``
|
|
13
|
+
and ``quadlib.tools.spectra``.
|
|
14
|
+
"""
|