wavepacket 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.
- wavepacket/__init__.py +25 -0
- wavepacket/builder/__init__.py +8 -0
- wavepacket/builder/density.py +73 -0
- wavepacket/builder/wave_function.py +63 -0
- wavepacket/exceptions.py +48 -0
- wavepacket/expression/__init__.py +10 -0
- wavepacket/expression/expressionbase.py +44 -0
- wavepacket/expression/liouvillian.py +64 -0
- wavepacket/expression/schroedingerequation.py +65 -0
- wavepacket/generators.py +88 -0
- wavepacket/grid/__init__.py +12 -0
- wavepacket/grid/dofbase.py +191 -0
- wavepacket/grid/grid.py +145 -0
- wavepacket/grid/planewavedof.py +102 -0
- wavepacket/grid/state.py +93 -0
- wavepacket/grid/state_utilities.py +89 -0
- wavepacket/logging.py +34 -0
- wavepacket/operator/__init__.py +12 -0
- wavepacket/operator/fbroperators.py +94 -0
- wavepacket/operator/operatorbase.py +204 -0
- wavepacket/operator/operatorutils.py +30 -0
- wavepacket/operator/potentials.py +43 -0
- wavepacket/solver/__init__.py +8 -0
- wavepacket/solver/odesolver.py +70 -0
- wavepacket/solver/solverbase.py +119 -0
- wavepacket/testing/__init__.py +11 -0
- wavepacket/testing/assertions.py +20 -0
- wavepacket/testing/dummydof.py +22 -0
- wavepacket/testing/dummyoperator.py +26 -0
- wavepacket/testing/random.py +16 -0
- wavepacket/typing/__init__.py +7 -0
- wavepacket/typing/data_types.py +32 -0
- wavepacket-0.1.0.dist-info/METADATA +111 -0
- wavepacket-0.1.0.dist-info/RECORD +36 -0
- wavepacket-0.1.0.dist-info/WHEEL +4 -0
- wavepacket-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
import wavepacket as wp
|
|
4
|
+
import wavepacket.typing as wpt
|
|
5
|
+
from .operatorbase import OperatorBase
|
|
6
|
+
from ..grid import Grid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlaneWaveFbrOperator(OperatorBase):
|
|
10
|
+
"""
|
|
11
|
+
Base class for operators that are diagonal in a plane wave basis.
|
|
12
|
+
|
|
13
|
+
The :py:class:`wavepacket.grid.PlaneWaveDof` describes an expansion of
|
|
14
|
+
the wave function in plane waves. In the corresponding FBR, derivatives
|
|
15
|
+
are diagonal and essentially transform into a multiplication with the
|
|
16
|
+
wave vector / FBR grid.
|
|
17
|
+
|
|
18
|
+
A key difference to a generic FBR operator is that this operator
|
|
19
|
+
does not apply the shift after the FFT, which increases performance.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
grid : wp.grid.Grid
|
|
24
|
+
The grid on which the operator is defined.
|
|
25
|
+
dof_index : int
|
|
26
|
+
The degree of freedom along which the operator is defined.
|
|
27
|
+
generator : wpt.Generator
|
|
28
|
+
A callable that gives the operator value for each FBR point.
|
|
29
|
+
|
|
30
|
+
Raises
|
|
31
|
+
------
|
|
32
|
+
wp.InvalidValueError
|
|
33
|
+
If the supplied degree of freedom is not a plane wave expansion.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, grid: Grid, dof_index: int, generator: wpt.Generator):
|
|
37
|
+
if not isinstance(grid.dofs[dof_index], wp.grid.PlaneWaveDof):
|
|
38
|
+
raise wp.InvalidValueError(
|
|
39
|
+
f"PlaneWaveFbrOperator requires a PlaneWaveDof, but got {grid.dofs[dof_index].__class__}")
|
|
40
|
+
|
|
41
|
+
self._wf_index = dof_index
|
|
42
|
+
self._ket_index = grid.normalize_index(dof_index)
|
|
43
|
+
self._bra_index = self._ket_index + len(grid.dofs)
|
|
44
|
+
|
|
45
|
+
# shifting the data here allows us to skip the fftshift() on the input data in apply*()
|
|
46
|
+
data = generator(grid.dofs[dof_index].fbr_points)
|
|
47
|
+
shifted_data = np.fft.ifftshift(data)
|
|
48
|
+
self._wf_data = grid.broadcast(shifted_data, dof_index)
|
|
49
|
+
self._ket_data = grid.operator_broadcast(shifted_data, dof_index)
|
|
50
|
+
self._bra_data = grid.operator_broadcast(shifted_data, dof_index, is_ket=False)
|
|
51
|
+
|
|
52
|
+
super().__init__(grid)
|
|
53
|
+
|
|
54
|
+
def apply_to_wave_function(self, psi: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
55
|
+
psi_fft = np.fft.fft(psi, axis=self._wf_index)
|
|
56
|
+
return np.fft.ifft(psi_fft * self._wf_data, axis=self._wf_index)
|
|
57
|
+
|
|
58
|
+
def apply_from_left(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
59
|
+
rho_fft = np.fft.fft(rho, axis=self._ket_index)
|
|
60
|
+
return np.fft.ifft(rho_fft * self._ket_data, axis=self._ket_index)
|
|
61
|
+
|
|
62
|
+
def apply_from_right(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
63
|
+
rho_fft = np.fft.ifft(rho, axis=self._bra_index)
|
|
64
|
+
return np.fft.fft(rho_fft * self._bra_data, axis=self._bra_index)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CartesianKineticEnergy(PlaneWaveFbrOperator):
|
|
68
|
+
"""
|
|
69
|
+
Convenience class that implements the common Cartesian kinetic energy operator.
|
|
70
|
+
|
|
71
|
+
The form of the operator is :math:`-\\frac{1}{2m} \ \\frac{\partial^2}{\partial x^2}`.
|
|
72
|
+
It requires the degree of freedom to be a :py:class:`wavepacket.grid.PlaneWaveDof`.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
grid : wp.grid.Grid
|
|
77
|
+
The grid on which the operator is defined.
|
|
78
|
+
dof_index : inst
|
|
79
|
+
Degree of freedom along which the operator acts
|
|
80
|
+
mass : float
|
|
81
|
+
The mass of the particle.
|
|
82
|
+
|
|
83
|
+
Raises
|
|
84
|
+
------
|
|
85
|
+
wp.InvalidValueError
|
|
86
|
+
If the mass is not positive, or if the degree of freedom does not describe a
|
|
87
|
+
plane wave expansion.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, grid: Grid, dof_index: int, mass: float):
|
|
91
|
+
if mass <= 0:
|
|
92
|
+
raise wp.InvalidValueError(f"Particle mass must be positive, but is {mass}")
|
|
93
|
+
|
|
94
|
+
super().__init__(grid, dof_index, lambda fbr_points: fbr_points ** 2 / (2 * mass))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import wavepacket as wp
|
|
8
|
+
import wavepacket.typing as wpt
|
|
9
|
+
from ..grid import Grid, State
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OperatorBase(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Base class of an operator.
|
|
15
|
+
|
|
16
|
+
An operator can be applied to a wave function or from the left or right to a
|
|
17
|
+
density operator. It is defined on a grid, and can only operate on states on that
|
|
18
|
+
grid.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
grid : wp.grid.Grid
|
|
23
|
+
The grid on which the operator is defined.
|
|
24
|
+
Particular operators may require additional parameters.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
grid
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, grid: Grid):
|
|
32
|
+
self._grid = grid
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def grid(self):
|
|
36
|
+
"""
|
|
37
|
+
Returns the grid on which the operator is defined.
|
|
38
|
+
"""
|
|
39
|
+
return self._grid
|
|
40
|
+
|
|
41
|
+
def apply(self, state: State, t: typing.Optional[float] = None) -> State:
|
|
42
|
+
"""
|
|
43
|
+
Applies the operator onto a wave function or a density operator from the left.
|
|
44
|
+
|
|
45
|
+
This is a convenience function if you just want to apply the operator without
|
|
46
|
+
detailed knowledge of the state.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
state : wp.grid.State
|
|
51
|
+
The state that the operator is applied on.
|
|
52
|
+
t : float, optional
|
|
53
|
+
The time at which the operator is applied.
|
|
54
|
+
The default value `None` raises an exception for time-dependent operators.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
wp.grid.State
|
|
59
|
+
The result of applying the operator on the state.
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
wp.BadGridError
|
|
64
|
+
If the state's grid does not match the grid of the operator.
|
|
65
|
+
wp.BadStateError
|
|
66
|
+
If the state is neither a wave function nor a density operator.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
if state.grid != self._grid:
|
|
70
|
+
raise wp.BadGridError("Grid of state does not match grid of operator.")
|
|
71
|
+
|
|
72
|
+
if state.is_wave_function():
|
|
73
|
+
return State(state.grid, self.apply_to_wave_function(state.data, t))
|
|
74
|
+
elif state.is_density_operator():
|
|
75
|
+
return State(state.grid, self.apply_from_left(state.data, t))
|
|
76
|
+
else:
|
|
77
|
+
raise wp.BadStateError("Cannot apply the operator to an invalid state.")
|
|
78
|
+
|
|
79
|
+
def __add__(self, other: typing.Self) -> typing.Self:
|
|
80
|
+
"""
|
|
81
|
+
Adds two operators and returns the result as a :py:class:`wavepacket.operator.OperatorSum`.
|
|
82
|
+
"""
|
|
83
|
+
return OperatorSum([self, other])
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def apply_to_wave_function(self, psi: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
87
|
+
"""
|
|
88
|
+
Applies the operator on a wave function.
|
|
89
|
+
|
|
90
|
+
This function is mainly for Wavepacket-internal use. It ignores most error
|
|
91
|
+
handling (usually done by the wrapping :py:class:`wavepacket.expression.ExpressionBase`),
|
|
92
|
+
and operates directly on coefficients to avoid the creation of temporary states.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
psi : wpt.ComplexData
|
|
97
|
+
The coefficients describing the wave function on which the operator acts.
|
|
98
|
+
t : float
|
|
99
|
+
The time at which the operator should be evaluated.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
wpt.ComplexData
|
|
104
|
+
The coefficients of the resulting wave function.
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def apply_from_left(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
110
|
+
"""
|
|
111
|
+
Applies the operator on a density operator from the left.
|
|
112
|
+
|
|
113
|
+
This function is mainly for Wavepacket-internal use. It ignores most error
|
|
114
|
+
handling (usually done by the wrapping :py:class:`wavepacket.expression.ExpressionBase`),
|
|
115
|
+
and operates directly on coefficients to avoid the creation of temporary states.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
rho : wpt.ComplexData
|
|
120
|
+
The coefficients describing the density operator on which the operator acts.
|
|
121
|
+
t : float
|
|
122
|
+
The time at which the operator should be evaluated.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
wpt.ComplexData
|
|
127
|
+
The coefficients of the resulting density operator.
|
|
128
|
+
"""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def apply_from_right(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
133
|
+
"""
|
|
134
|
+
Applies the operator on a density operator from the right.
|
|
135
|
+
|
|
136
|
+
This function is mainly for Wavepacket-internal use. It ignores most error
|
|
137
|
+
handling (usually done by the wrapping :py:class:`wavepacket.expression.ExpressionBase`),
|
|
138
|
+
and operates directly on coefficients to avoid the creation of temporary states.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
rho : wpt.ComplexData
|
|
143
|
+
The coefficients describing the density operator on which the operator acts.
|
|
144
|
+
t : float
|
|
145
|
+
The time at which the operator should be evaluated.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
wpt.ComplexData
|
|
150
|
+
The coefficients of the resulting density operator.
|
|
151
|
+
"""
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class OperatorSum(OperatorBase):
|
|
156
|
+
"""
|
|
157
|
+
An operator that represents the sum of multiple other operators.
|
|
158
|
+
|
|
159
|
+
You do not normally construct this operator directly. It is the result of
|
|
160
|
+
adding two or more operators together. All functionality is simply forwarded
|
|
161
|
+
to the individual operators.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
ops : Sequence[wp.operator.OperatorBase]
|
|
166
|
+
The operators that should be summed up.
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
wp.BadGridError
|
|
171
|
+
If the operators are defined on different grids.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, ops: Sequence[OperatorBase]):
|
|
175
|
+
if not ops:
|
|
176
|
+
raise wp.InvalidValueError("OperatorSum needs at least one operator to sum.")
|
|
177
|
+
for op in ops:
|
|
178
|
+
if op.grid != ops[0].grid:
|
|
179
|
+
raise wp.BadGridError("All grids in a sum operator must be equal.")
|
|
180
|
+
|
|
181
|
+
self._ops = ops
|
|
182
|
+
grid = ops[0].grid
|
|
183
|
+
super().__init__(grid)
|
|
184
|
+
|
|
185
|
+
def apply_to_wave_function(self, psi: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
186
|
+
result = np.zeros(self.grid.shape, dtype=np.complex128)
|
|
187
|
+
for op in self._ops:
|
|
188
|
+
result += op.apply_to_wave_function(psi, t)
|
|
189
|
+
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
def apply_from_left(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
193
|
+
result = np.zeros(self.grid.operator_shape, dtype=np.complex128)
|
|
194
|
+
for op in self._ops:
|
|
195
|
+
result += op.apply_from_left(rho, t)
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
def apply_from_right(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
200
|
+
result = np.zeros(self.grid.operator_shape, dtype=np.complex128)
|
|
201
|
+
for op in self._ops:
|
|
202
|
+
result += op.apply_from_right(rho, t)
|
|
203
|
+
|
|
204
|
+
return result
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
import wavepacket as wp
|
|
6
|
+
from .operatorbase import OperatorBase
|
|
7
|
+
from ..grid import State
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def expectation_value(op: OperatorBase, state: State, t: Optional[float] = None) -> complex:
|
|
11
|
+
"""
|
|
12
|
+
Calculates the expectation value of an operator for a given state.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
op : wp.operator.OperatorBase
|
|
17
|
+
The operator whose expectation value is calculated.
|
|
18
|
+
state : wp.grid.State
|
|
19
|
+
The wave function or density operator that is used for the calculation.
|
|
20
|
+
t : float, optional
|
|
21
|
+
The time at which the operator should be evaluated.
|
|
22
|
+
Only required for time-dependent operators, where the default value `None` raises an exception.
|
|
23
|
+
"""
|
|
24
|
+
new = op.apply(state, t)
|
|
25
|
+
|
|
26
|
+
if state.is_wave_function():
|
|
27
|
+
return np.vdot(state.data, new.data)
|
|
28
|
+
else:
|
|
29
|
+
matrix_data = np.reshape(new.data, [new.grid.size, new.grid.size])
|
|
30
|
+
return np.trace(matrix_data)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import wavepacket.typing as wpt
|
|
2
|
+
from .operatorbase import OperatorBase
|
|
3
|
+
from ..grid import Grid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Potential1D(OperatorBase):
|
|
7
|
+
"""
|
|
8
|
+
Operator that represents a real-valued one-dimensional potential.
|
|
9
|
+
|
|
10
|
+
Within the DVR approximation [1]_, potential energy operators are
|
|
11
|
+
diagonal in the DVR. That is, they are completely described by a value
|
|
12
|
+
for each grid point.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
grid : wp.grid.Grid
|
|
17
|
+
The grid on which the operator is defined
|
|
18
|
+
dof_index : int
|
|
19
|
+
the index of the degree of freedom along which the potential is defined.
|
|
20
|
+
generator : wpt.RealGenerator
|
|
21
|
+
A callable that generates a potential energy value for each grid point of the respective DOF.
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
.. [1] https://sourceforge.net/p/wavepacket/wiki/Numerics.DVR>
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, grid: Grid, dof_index: int, generator: wpt.RealGenerator):
|
|
29
|
+
data = generator(grid.dofs[dof_index].dvr_points)
|
|
30
|
+
self._wf_data = grid.broadcast(data, dof_index)
|
|
31
|
+
self._ket_data = grid.operator_broadcast(data, dof_index)
|
|
32
|
+
self._bra_data = grid.operator_broadcast(data, dof_index, False)
|
|
33
|
+
|
|
34
|
+
super().__init__(grid)
|
|
35
|
+
|
|
36
|
+
def apply_to_wave_function(self, psi: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
37
|
+
return self._wf_data * psi
|
|
38
|
+
|
|
39
|
+
def apply_from_left(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
40
|
+
return self._ket_data * rho
|
|
41
|
+
|
|
42
|
+
def apply_from_right(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
43
|
+
return self._bra_data * rho
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.integrate import solve_ivp
|
|
3
|
+
|
|
4
|
+
import wavepacket as wp
|
|
5
|
+
import wavepacket.typing as wpt
|
|
6
|
+
from .solverbase import SolverBase
|
|
7
|
+
from ..grid import Grid, State
|
|
8
|
+
from ..expression import ExpressionBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _inner_solver(t: float, y: wpt.ComplexData,
|
|
12
|
+
eq: ExpressionBase, grid: Grid) -> wpt.ComplexData:
|
|
13
|
+
# 1. Reconstruct a state from the data array y
|
|
14
|
+
if len(y) == grid.size:
|
|
15
|
+
state = State(grid, np.reshape(y, grid.shape))
|
|
16
|
+
else:
|
|
17
|
+
state = State(grid, np.reshape(y, grid.operator_shape))
|
|
18
|
+
|
|
19
|
+
# 2. Insert into the equation expression
|
|
20
|
+
dot_state = eq.apply(state, t)
|
|
21
|
+
|
|
22
|
+
# 3. Extract the result as a vector again and return
|
|
23
|
+
return dot_state.data.ravel()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OdeSolver(SolverBase):
|
|
27
|
+
"""
|
|
28
|
+
A solver that uses scipy's ODE integrators as backend.
|
|
29
|
+
|
|
30
|
+
This solver merely wraps `scipy.integrate.solve_ivp()` in a Wavepacket
|
|
31
|
+
solver. ODESolvers tend to be workhorses that can be applied to most
|
|
32
|
+
systems without a second thought at the expense of performance.
|
|
33
|
+
See [1]_ for more details on the convergence of solvers.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
expr : wp.expression.ExpressionBase
|
|
38
|
+
The right-hand side of the differential equation that encapsulates the quantum system.
|
|
39
|
+
dt : float
|
|
40
|
+
The elementary time step for the time evolution.
|
|
41
|
+
**kwargs
|
|
42
|
+
Additional keyword parameters can be supplied that are directly forwarded to scipy.
|
|
43
|
+
The most important parameters are "method" (defaults to "RK45") and "rtol", "atol"
|
|
44
|
+
for the relative and absolute error tolerances (booth default to 1e-6).
|
|
45
|
+
|
|
46
|
+
References
|
|
47
|
+
----------
|
|
48
|
+
.. [1] https://sourceforge.net/p/wavepacket/cpp/blog/2021/04/convergence-2
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, expr: ExpressionBase, dt: float, **kwargs):
|
|
52
|
+
super().__init__(dt)
|
|
53
|
+
|
|
54
|
+
self._expression = expr
|
|
55
|
+
self._kwargs = kwargs
|
|
56
|
+
|
|
57
|
+
# The default error tolerance is typically too generous for our use cases. Override
|
|
58
|
+
self._kwargs.setdefault('rtol', 1e-6)
|
|
59
|
+
|
|
60
|
+
def step(self, state: State, t: float) -> State:
|
|
61
|
+
t_span = (t, t + self.dt)
|
|
62
|
+
y0 = state.data.ravel()
|
|
63
|
+
args = (self._expression, state.grid)
|
|
64
|
+
|
|
65
|
+
solution = solve_ivp(_inner_solver, t_span, y0, t_eval=[t + self._dt], args=args, **self._kwargs)
|
|
66
|
+
if solution.status != 0:
|
|
67
|
+
raise wp.ExecutionError("Bad return value from integrating"
|
|
68
|
+
f"Status: {solution.status} != 0; Message: {solution.msg}")
|
|
69
|
+
|
|
70
|
+
return State(state.grid, np.reshape(solution.y, state.data.shape))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import wavepacket as wp
|
|
4
|
+
from ..grid import State
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SolverBase(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Abstract base class for all differential equation solvers.
|
|
10
|
+
|
|
11
|
+
Each solver must take in the constructor the size of an
|
|
12
|
+
elementary time step, and it must implement a method :py:meth:`step`
|
|
13
|
+
to evolve a state by one elementary time step forward in time.
|
|
14
|
+
|
|
15
|
+
Usually, a solver does not need to know what step it evolves or
|
|
16
|
+
how a differential equation looks in detail. The form of the
|
|
17
|
+
differential equation is encapsulated in a
|
|
18
|
+
:py:class:`wavepacket.expression.ExpressionBase` instance that
|
|
19
|
+
is usually supplied to the specific implementations.
|
|
20
|
+
|
|
21
|
+
A solver may, however, implicitly assume properties of the
|
|
22
|
+
differential equation. It may also take additional arguments,
|
|
23
|
+
these details are documented in the specific implementations.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
dt : float
|
|
28
|
+
The size of an elementary time step.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
dt
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
wp.InvalidValueError
|
|
38
|
+
If the timestep is not positive.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, dt: float):
|
|
42
|
+
if dt <= 0:
|
|
43
|
+
raise wp.InvalidValueError(f"Require positive timestep, got {dt}")
|
|
44
|
+
|
|
45
|
+
self._dt = dt
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def dt(self) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Returns the size of the elementary time step.
|
|
51
|
+
"""
|
|
52
|
+
return self._dt
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def step(self, state: State, t: float) -> State:
|
|
56
|
+
"""
|
|
57
|
+
Evolves the given state for one elementary time step.
|
|
58
|
+
|
|
59
|
+
Given a wave function or density operator at time t,
|
|
60
|
+
this function should return the propagated wave function
|
|
61
|
+
or density operator at the new time t+dt, where dt is
|
|
62
|
+
the elementary time step.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
state : wp.grid.State
|
|
67
|
+
The state to be evolved in time
|
|
68
|
+
t : float
|
|
69
|
+
The time at which the time evolution starts
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
wp.grid.State
|
|
74
|
+
The state at the new time t+dt.
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def propagate(self, state0: State, t0: float,
|
|
79
|
+
num_steps: int, include_first: bool = True):
|
|
80
|
+
"""
|
|
81
|
+
Generator function that yields the propagated wave function at multiple time steps.
|
|
82
|
+
|
|
83
|
+
This function allows you to propagate in one go with a for loop.
|
|
84
|
+
It repeatedly calls :py:meth:`step` and returns the wave function and current time.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
state0 : wp.grid.State
|
|
89
|
+
The initial state to be propagated in time.
|
|
90
|
+
t0 : float
|
|
91
|
+
The initial time at which the state is given
|
|
92
|
+
num_steps : int
|
|
93
|
+
For how many elementary time steps the state should be propagated.
|
|
94
|
+
include_first : bool
|
|
95
|
+
If true, the function will start by yielding the initial state.
|
|
96
|
+
|
|
97
|
+
Yields
|
|
98
|
+
------
|
|
99
|
+
float
|
|
100
|
+
The time at which the state is yielded. Starts optionally with t0 and progresses in units of dt.
|
|
101
|
+
wp.grid.State
|
|
102
|
+
The propagated state at the given time.
|
|
103
|
+
|
|
104
|
+
Raises
|
|
105
|
+
------
|
|
106
|
+
wp.InvalidValueError
|
|
107
|
+
If num_steps is negative.
|
|
108
|
+
"""
|
|
109
|
+
if num_steps < 0:
|
|
110
|
+
raise wp.InvalidValueError("Cannot propagate for negative number of steps.")
|
|
111
|
+
|
|
112
|
+
if include_first:
|
|
113
|
+
yield t0, state0
|
|
114
|
+
|
|
115
|
+
state = state0
|
|
116
|
+
for step in range(num_steps):
|
|
117
|
+
t = t0 + step * self._dt
|
|
118
|
+
state = self.step(state, t)
|
|
119
|
+
yield t + self._dt, state
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility classes and functions for writing Wavepacket tests.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ['DummyDof', 'DummyOperator',
|
|
6
|
+
'assert_close', 'random_state']
|
|
7
|
+
|
|
8
|
+
from .assertions import assert_close
|
|
9
|
+
from .dummydof import DummyDof
|
|
10
|
+
from .dummyoperator import DummyOperator
|
|
11
|
+
from .random import random_state
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import numpy.testing
|
|
2
|
+
|
|
3
|
+
import wavepacket as wp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def assert_close(actual: wp.grid.State, expected: wp.grid.State, diff: float = 0) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Assertion helper: Verify that two states are on the same grid and similar to each other.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
actual : wp.grid.State
|
|
13
|
+
The expected state
|
|
14
|
+
expected : wp.grid.State
|
|
15
|
+
The state to be tested.
|
|
16
|
+
diff : float
|
|
17
|
+
The maximum absolute difference between any coefficients of the two states.
|
|
18
|
+
"""
|
|
19
|
+
assert actual.grid == expected.grid
|
|
20
|
+
numpy.testing.assert_allclose(actual.data, expected.data, rtol=0, atol=diff)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import wavepacket.typing as wpt
|
|
2
|
+
from ..grid import DofBase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DummyDof(DofBase):
|
|
6
|
+
"""
|
|
7
|
+
Empty DOF without transformations.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, dvr_array: wpt.RealData, fbr_array: wpt.RealData):
|
|
10
|
+
super().__init__(dvr_array, fbr_array)
|
|
11
|
+
|
|
12
|
+
def from_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
13
|
+
return data
|
|
14
|
+
|
|
15
|
+
def to_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
16
|
+
return data
|
|
17
|
+
|
|
18
|
+
def from_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
19
|
+
return data
|
|
20
|
+
|
|
21
|
+
def to_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
22
|
+
return data
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import wavepacket as wp
|
|
2
|
+
import wavepacket.typing as wpt
|
|
3
|
+
from ..grid import Grid
|
|
4
|
+
from ..operator import OperatorBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DummyOperator(OperatorBase):
|
|
8
|
+
"""
|
|
9
|
+
Empty operator that throws when it is applied.
|
|
10
|
+
|
|
11
|
+
Used for testing where we need an operator, but do not get
|
|
12
|
+
as far as actually doing something.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, grid: Grid):
|
|
15
|
+
super().__init__(grid)
|
|
16
|
+
|
|
17
|
+
def apply_to_wave_function(self, psi: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
18
|
+
raise wp.BadFunctionCall("Should be patched.")
|
|
19
|
+
|
|
20
|
+
def apply_from_left(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
21
|
+
raise wp.BadFunctionCall("Should be patched.")
|
|
22
|
+
|
|
23
|
+
def apply_from_right(self, rho: wpt.ComplexData, t: float) -> wpt.ComplexData:
|
|
24
|
+
raise wp.BadFunctionCall("Should be patched.")
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
import wavepacket as wp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def random_state(grid, seed) -> wp.grid.State:
|
|
7
|
+
"""
|
|
8
|
+
Creates a random wave function.
|
|
9
|
+
|
|
10
|
+
Note that this function does not employ high-quality randomization,
|
|
11
|
+
it is only meant to create states without accidental symmetries or lots of code.
|
|
12
|
+
"""
|
|
13
|
+
rng = np.random.default_rng(seed)
|
|
14
|
+
data = rng.random(grid.shape) + 1j * rng.random(grid.shape)
|
|
15
|
+
|
|
16
|
+
return wp.grid.State(grid, data)
|