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,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Definition of an abstract Degree of Freedom (DOF).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod, ABC
|
|
6
|
+
|
|
7
|
+
import wavepacket as wp
|
|
8
|
+
import wavepacket.typing as wpt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DofBase(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract base class of a one-dimensional basis expansion.
|
|
14
|
+
|
|
15
|
+
We assemble a multidimensional grid as direct product of one-dimensional grids.
|
|
16
|
+
To distinguish these two types of grids, we call the one-dimensional grids DOF
|
|
17
|
+
(Degree of Freedom). They are defined by a basis expansion (the FBR), its corresponding
|
|
18
|
+
grid definition in real space (the DVR, see :doc:`/representations`),
|
|
19
|
+
the transformation between the two representations, and
|
|
20
|
+
some descriptive quantum numbers for the FBR.
|
|
21
|
+
|
|
22
|
+
Note that the transformation functions are highly flexible, but awkward and error-prone
|
|
23
|
+
to use, so they should generally be avoided outside of Wavepacket-internal code.
|
|
24
|
+
In general, you should use convenience functions instead that perform the required
|
|
25
|
+
transformation behind the scenes, such as :py:func:`wavepacket.grid.dvr_density`.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
dvr_points : real-valued array_like
|
|
30
|
+
The grid points in real space.
|
|
31
|
+
fbr_points : real-valued array_like
|
|
32
|
+
This is typically used to assign useful numbers to the underlying basis.
|
|
33
|
+
For example, in a plane-wave expansion, we would use the wave vectors as fbr_grid.
|
|
34
|
+
The size must match that of the `dvr_points`.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
dvr_points
|
|
39
|
+
fbr_points
|
|
40
|
+
size
|
|
41
|
+
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
wp.InvalidValueError
|
|
45
|
+
If the input grids are empty, multidimensional, or if the FBR and DVR grid do not match in size.
|
|
46
|
+
|
|
47
|
+
See Also
|
|
48
|
+
--------
|
|
49
|
+
wavepacket.grid.Grid : The multidimensional grid formed from one or more degrees of freedom.
|
|
50
|
+
wavepacket.grid.PlaneWaveDof: An implementation of a degree of freedom
|
|
51
|
+
based on a plane wave expansion.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, dvr_points: wpt.RealData, fbr_points: wpt.RealData):
|
|
55
|
+
if len(dvr_points) == 0 or len(fbr_points) == 0:
|
|
56
|
+
raise wp.InvalidValueError("Degrees of freedom may not be empty.")
|
|
57
|
+
|
|
58
|
+
if dvr_points.ndim != 1 or fbr_points.ndim != 1:
|
|
59
|
+
raise wp.InvalidValueError("A degree of freedom represents only one-dimensional data.")
|
|
60
|
+
|
|
61
|
+
if dvr_points.size != fbr_points.size:
|
|
62
|
+
raise wp.InvalidValueError("The DVR and FBR grids must have the same size.")
|
|
63
|
+
|
|
64
|
+
self._dvr_points = dvr_points
|
|
65
|
+
self._fbr_points = fbr_points
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def dvr_points(self) -> wpt.RealData:
|
|
69
|
+
"""
|
|
70
|
+
Numpy array that gives the grid points in real space as supplied in the constructor.
|
|
71
|
+
"""
|
|
72
|
+
return self._dvr_points
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def fbr_points(self) -> wpt.RealData:
|
|
76
|
+
"""
|
|
77
|
+
Numpy array that gives the grid points of the underlying basis as supplied in the constructor.
|
|
78
|
+
"""
|
|
79
|
+
return self._fbr_points
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def size(self) -> int:
|
|
83
|
+
"""
|
|
84
|
+
The size of the grid (number of elements of `dvr_points`/`fbr_points`)
|
|
85
|
+
"""
|
|
86
|
+
return self._dvr_points.size
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def to_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
90
|
+
"""
|
|
91
|
+
Translates a dimension of the input coefficients from the Wavepacket-default "weighted DVR"
|
|
92
|
+
into the FBR.
|
|
93
|
+
|
|
94
|
+
This function is not meant for public use. It does not handle errors explicitly,
|
|
95
|
+
and is just awkward to use; you need to reach through the state abstraction and transform
|
|
96
|
+
each index correctly.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
data : wp.typing.ComplexData
|
|
101
|
+
The input coefficients of the state to transform.
|
|
102
|
+
index : int
|
|
103
|
+
The index of the coefficient array that should be transformed.
|
|
104
|
+
is_ket : bool, default=True
|
|
105
|
+
If the index is the coefficient for a ket state (True) or a bra state.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
wpt.ComplexData
|
|
110
|
+
The appropriately transformed coefficients. You will generally not wrap the results
|
|
111
|
+
into a :py:class:`wavepacket.grid.State`, because that class implicitly assumes a
|
|
112
|
+
weighted DVR transformation.
|
|
113
|
+
"""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def from_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
118
|
+
"""
|
|
119
|
+
Translates a dimension of the input coefficients from the FBR into the Wavepacket-default
|
|
120
|
+
"weighted DVR"
|
|
121
|
+
|
|
122
|
+
This function is not meant for public use. It does not handle errors explicitly,
|
|
123
|
+
and is just awkward to use; you need to reach through the state abstraction and transform
|
|
124
|
+
each index correctly.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
data : wp.typing.ComplexData
|
|
129
|
+
The input coefficients of the state to transform.
|
|
130
|
+
index : int
|
|
131
|
+
The index of the coefficient array that should be transformed.
|
|
132
|
+
is_ket : bool, default=True
|
|
133
|
+
If the index is the coefficient for a ket state (True) or a bra state.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
wpt.ComplexData
|
|
138
|
+
The appropriately transformed coefficients. You generally need to wrap the result in
|
|
139
|
+
a :py:class:`wavepacket.grid.State` before further use.
|
|
140
|
+
"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
def to_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
145
|
+
"""
|
|
146
|
+
Translates a dimension of the input coefficients from the Wavepacket-default "weighted DVR"
|
|
147
|
+
into the DVR.
|
|
148
|
+
|
|
149
|
+
This function is not meant for public use. It does not handle errors explicitly,
|
|
150
|
+
and is just awkward to use; you need to reach through the state abstraction and transform
|
|
151
|
+
each index correctly.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
data : wp.typing.ComplexData
|
|
156
|
+
The input coefficients of the state to transform.
|
|
157
|
+
index : int
|
|
158
|
+
The index of the coefficient array that should be transformed.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
wpt.ComplexData
|
|
163
|
+
The appropriately transformed coefficients. You will generally not wrap the results
|
|
164
|
+
into a :py:class:`wavepacket.grid.State`, because that class implicitly assumes a
|
|
165
|
+
weighted DVR transformation.
|
|
166
|
+
"""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def from_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
171
|
+
"""
|
|
172
|
+
Translates a dimension of the input coefficients from the DVR into the Wavepacket-default "weighted DVR".
|
|
173
|
+
|
|
174
|
+
This function is not meant for public use. It does not handle errors explicitly,
|
|
175
|
+
and is just awkward to use; you need to reach through the state abstraction and transform
|
|
176
|
+
each index correctly.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
data : wp.typing.ComplexData
|
|
181
|
+
The input coefficients of the state to transform.
|
|
182
|
+
index : int
|
|
183
|
+
The index of the coefficient array that should be transformed.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
wpt.ComplexData
|
|
188
|
+
The appropriately transformed coefficients. They need to be wrapped in a
|
|
189
|
+
:py:class:`wavepacket.grid.State` before further use.
|
|
190
|
+
"""
|
|
191
|
+
pass
|
wavepacket/grid/grid.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
import wavepacket as wp
|
|
7
|
+
import wavepacket.typing as wpt
|
|
8
|
+
from .dofbase import DofBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Grid:
|
|
12
|
+
"""
|
|
13
|
+
Definition of a one- or multidimensional grid.
|
|
14
|
+
|
|
15
|
+
This class collects multiple :py:class:`wavepacket.grid.DofBase`-derived objects,
|
|
16
|
+
each corresponding to a one-dimensional basis expansion, and represents the resulting
|
|
17
|
+
one- or multidimensional grid that you can operate with.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
dofs : Sequence[wp.grid.DofBase] | DofBase
|
|
22
|
+
The degree(s) of freedom that make up the grid. The order determines the order of
|
|
23
|
+
the coefficient array indices.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
shape
|
|
28
|
+
operator_shape
|
|
29
|
+
dofs
|
|
30
|
+
size
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
wp.InvalidValueError
|
|
35
|
+
If no degrees of freedom are supplied.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, dofs: Sequence[DofBase] | DofBase):
|
|
39
|
+
if dofs is None:
|
|
40
|
+
raise wp.InvalidValueError("A grid needs at least one Degree of freedom defined.")
|
|
41
|
+
|
|
42
|
+
if isinstance(dofs, DofBase):
|
|
43
|
+
self._dofs = [dofs]
|
|
44
|
+
else:
|
|
45
|
+
self._dofs = list(dofs)
|
|
46
|
+
|
|
47
|
+
self._shape = tuple(dof.size for dof in self._dofs)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def shape(self) -> tuple[int, ...]:
|
|
51
|
+
"""
|
|
52
|
+
The dimensions of the grid, for use in e.g. array creation.
|
|
53
|
+
"""
|
|
54
|
+
# note: dvr shape
|
|
55
|
+
return self._shape
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def operator_shape(self) -> tuple[int, ...]:
|
|
59
|
+
"""
|
|
60
|
+
The dimensions of an operator matrix on this grid.
|
|
61
|
+
|
|
62
|
+
An example of such a matrix would be the coefficient array for a density operator.
|
|
63
|
+
The operator dimensions are the dimensions of the grid concatenated with themselves.
|
|
64
|
+
For example, a grid with dimensions (5, 4) has operator dimensions (5, 4, 5, 4).
|
|
65
|
+
"""
|
|
66
|
+
return self._shape + self._shape
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def dofs(self) -> Sequence[DofBase]:
|
|
70
|
+
"""
|
|
71
|
+
The vector of degrees of freedom that make up the grid.
|
|
72
|
+
"""
|
|
73
|
+
return self._dofs
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def size(self) -> int:
|
|
77
|
+
"""
|
|
78
|
+
The total number of grid points.
|
|
79
|
+
"""
|
|
80
|
+
return math.prod(dof.size for dof in self._dofs)
|
|
81
|
+
|
|
82
|
+
def normalize_index(self, index: int) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Maps indices on the interval [0, rank_of_grid].
|
|
85
|
+
|
|
86
|
+
Meant for Wavepacket-internal use.
|
|
87
|
+
|
|
88
|
+
For operator matrices, we want to address bra and ket indices separately,
|
|
89
|
+
and with normal Python convention, i.e., -n meaning the n-th entry from the end.
|
|
90
|
+
However, an operator matrix has 2N dimensions, with the first N denoting bra indices,
|
|
91
|
+
and the last N denoting ket indices. Then, the arithmetic becomes cumbersome unless we
|
|
92
|
+
first map the input index onto the range [0,N].
|
|
93
|
+
"""
|
|
94
|
+
if index < -len(self._dofs) or index >= len(self._dofs):
|
|
95
|
+
raise IndexError("Index of degree of freedom out of bounds.")
|
|
96
|
+
|
|
97
|
+
if index < 0:
|
|
98
|
+
return index + len(self._dofs)
|
|
99
|
+
else:
|
|
100
|
+
return index
|
|
101
|
+
|
|
102
|
+
def broadcast(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
103
|
+
"""
|
|
104
|
+
Transforms a 1D array into a more suitable form for scaling.
|
|
105
|
+
|
|
106
|
+
Meant for Wavepacket-internal use. Note that this function is rather slow,
|
|
107
|
+
and should not be used in tight loops.
|
|
108
|
+
|
|
109
|
+
This function has a very specific purpose. Imagine, you have a grid
|
|
110
|
+
with shape (5, 4, 3). On this grid, you have a wave function specified by a coefficient
|
|
111
|
+
array "a" of the same shape. Now you define a potential along the second degree of freedom only.
|
|
112
|
+
The potential is given by a one-dimensional array "V" of size 4.
|
|
113
|
+
If you want to apply this potential to the wave function within the DVR approximation,
|
|
114
|
+
the new coefficients are given as :math:`b_{ijk} = V_j a_{ijk}`.
|
|
115
|
+
|
|
116
|
+
Unfortunately, Numpy does not offer a function for this scaling operation.
|
|
117
|
+
What we can do instead is to blow up the array V into a 3D array
|
|
118
|
+
of shape (1, 4, 1). then you can map the above multiplication onto Numpy's
|
|
119
|
+
broadcasting rules. This reshaping is done by this function.
|
|
120
|
+
"""
|
|
121
|
+
# Note: rather slow, only use for precomputation
|
|
122
|
+
new_shape = len(self._dofs) * [1]
|
|
123
|
+
new_shape[index] = self._dofs[index].size
|
|
124
|
+
return np.reshape(data, new_shape)
|
|
125
|
+
|
|
126
|
+
def operator_broadcast(self, data: wpt.ComplexData, dof_index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
127
|
+
"""
|
|
128
|
+
Similar to broadcast, but blows up the array into a form suitable for multiplication with operators.
|
|
129
|
+
|
|
130
|
+
Meant for Wavepacket-internal use. Note that this function is rather slow,
|
|
131
|
+
and should not be used in tight loops.
|
|
132
|
+
|
|
133
|
+
See :py:meth:`broadcast` for a description of the problem. For the (5, 4, 3)
|
|
134
|
+
grid shape, this function would blow up the potential array into a shape
|
|
135
|
+
(1, 4, 1, 1, 1, 1) or (1, 1, 1, 1, 4, 1). The is_ket parameter switches between the
|
|
136
|
+
two variants, we call the first three indices "ket" and the latter three "bra" indices.
|
|
137
|
+
"""
|
|
138
|
+
new_shape = (2 * len(self._dofs)) * [1]
|
|
139
|
+
|
|
140
|
+
shape_index = self.normalize_index(dof_index)
|
|
141
|
+
if not is_ket:
|
|
142
|
+
shape_index += len(self._dofs)
|
|
143
|
+
|
|
144
|
+
new_shape[shape_index] = self._dofs[dof_index].size
|
|
145
|
+
return np.reshape(data, new_shape)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
import wavepacket as wp
|
|
6
|
+
import wavepacket.typing as wpt
|
|
7
|
+
from .dofbase import DofBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _broadcast(data: wpt.ComplexData, ndim, index: int) -> wpt.ComplexData:
|
|
11
|
+
shape = np.ones(ndim, dtype=int)
|
|
12
|
+
shape[index] = len(data)
|
|
13
|
+
|
|
14
|
+
return np.reshape(data, shape)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlaneWaveDof(DofBase):
|
|
18
|
+
"""
|
|
19
|
+
One-dimensional plane wave basis expansion.
|
|
20
|
+
|
|
21
|
+
This expansion is a good base choice for non-rotational degrees of freedom.
|
|
22
|
+
The DVR grid consists of equally-spaced grid points from xmin to (xmax - dx),
|
|
23
|
+
the FBR grid are the wave vectors of the plane waves centered around 0.
|
|
24
|
+
In the FBR, derivatives become simple multiplications wih the wave vectors, so
|
|
25
|
+
this grid allows a simple representation of kinetic energy operators.
|
|
26
|
+
|
|
27
|
+
The transformation between DVR and FBR uses a FFT, so the performance is ok, even
|
|
28
|
+
though you might need more grid points than with more suitable expansions.
|
|
29
|
+
|
|
30
|
+
Be aware that this degree of freedom implicitly uses periodic boundary conditions
|
|
31
|
+
in real space. That is, if the wave function leaves the grid on one side, it reenters
|
|
32
|
+
the grid on the other side. This problem can only be mitigated with negative imaginary
|
|
33
|
+
potentials. Periodic boundary conditions also hold in the FBR, causing aliasing.
|
|
34
|
+
Nevertheless, the monitoring of convergence is rather simple, see [1]_.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
xmin : float
|
|
39
|
+
The start of the grid.
|
|
40
|
+
xmax : float
|
|
41
|
+
The end of the grid. Note that the last grid point is at xmax - dx.
|
|
42
|
+
n : int
|
|
43
|
+
The number of grid points.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
wp.InvalidValueError
|
|
48
|
+
If the length of the grid is negative or the number of grid points is non-positive.
|
|
49
|
+
|
|
50
|
+
References
|
|
51
|
+
----------
|
|
52
|
+
.. [1] https://sourceforge.net/p/wavepacket/cpp/blog/2020/11/convergence-1-equally-space-grids
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, xmin: float, xmax: float, n: int):
|
|
56
|
+
if xmin > xmax:
|
|
57
|
+
raise wp.InvalidValueError("Range should be positive")
|
|
58
|
+
|
|
59
|
+
if n <= 0:
|
|
60
|
+
raise wp.InvalidValueError(f"Number of grid points must be positive, but is {n}")
|
|
61
|
+
|
|
62
|
+
dx = (xmax - xmin) / n
|
|
63
|
+
|
|
64
|
+
dvr = np.linspace(xmin, xmax, n, endpoint=False)
|
|
65
|
+
if n % 2 == 0:
|
|
66
|
+
fbr = np.linspace(-math.pi / dx, math.pi / dx, n, endpoint=False)
|
|
67
|
+
else:
|
|
68
|
+
fbr = np.linspace(-math.pi / dx * (n - 1) / n, math.pi / dx * (n - 1) / n, n)
|
|
69
|
+
|
|
70
|
+
super().__init__(dvr, fbr)
|
|
71
|
+
|
|
72
|
+
self._sqrt_weights: wpt.RealData = math.sqrt(dx) * np.ones(n)
|
|
73
|
+
self._phase: wpt.ComplexData = np.exp(-1j * fbr * xmin) / np.sqrt(n)
|
|
74
|
+
self._conj_phase: wpt.ComplexData = n * np.conj(self._phase)
|
|
75
|
+
|
|
76
|
+
def to_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
77
|
+
if is_ket:
|
|
78
|
+
phase = _broadcast(self._phase, data.ndim, index)
|
|
79
|
+
transformed = np.fft.fftshift(np.fft.fft(data, axis=index), axes=index)
|
|
80
|
+
else:
|
|
81
|
+
phase = _broadcast(self._conj_phase, data.ndim, index)
|
|
82
|
+
transformed = np.fft.fftshift(np.fft.ifft(data, axis=index), axes=index)
|
|
83
|
+
|
|
84
|
+
return phase * transformed
|
|
85
|
+
|
|
86
|
+
def from_fbr(self, data: wpt.ComplexData, index: int, is_ket: bool = True) -> wpt.ComplexData:
|
|
87
|
+
if is_ket:
|
|
88
|
+
phase = _broadcast(self._conj_phase, data.ndim, index)
|
|
89
|
+
untransformed = phase * data
|
|
90
|
+
return np.fft.ifft(np.fft.ifftshift(untransformed, axes=index), axis=index)
|
|
91
|
+
else:
|
|
92
|
+
phase = _broadcast(self._phase, data.ndim, index)
|
|
93
|
+
untransformed = phase * data
|
|
94
|
+
return np.fft.fft(np.fft.ifftshift(untransformed, axes=index), axis=index)
|
|
95
|
+
|
|
96
|
+
def to_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
97
|
+
conversion_factor = _broadcast(self._sqrt_weights, data.ndim, index)
|
|
98
|
+
return data / conversion_factor
|
|
99
|
+
|
|
100
|
+
def from_dvr(self, data: wpt.ComplexData, index: int) -> wpt.ComplexData:
|
|
101
|
+
conversion_factor = _broadcast(self._sqrt_weights, data.ndim, index)
|
|
102
|
+
return data * conversion_factor
|
wavepacket/grid/state.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import wavepacket as wp
|
|
6
|
+
import wavepacket.typing as wpt
|
|
7
|
+
from .grid import Grid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class State:
|
|
12
|
+
"""
|
|
13
|
+
This class holds the definition of a specific quantum state.
|
|
14
|
+
|
|
15
|
+
A state can be a wave function or a density operator.
|
|
16
|
+
While invalid states can be constructed, they have no use.
|
|
17
|
+
A state is comprised of three parts:
|
|
18
|
+
|
|
19
|
+
1. The grid on which the state is defined.
|
|
20
|
+
2. The expansion coefficients.
|
|
21
|
+
3. The representation / basis for the expansion.
|
|
22
|
+
This is always weighted DVR, see :doc:`/representations`.
|
|
23
|
+
|
|
24
|
+
Once constructed, a State is meant to be immutable. It supports elementary
|
|
25
|
+
arithmetic operations, however, to allow easy composition.
|
|
26
|
+
|
|
27
|
+
The technical reason for a state class is to store grids together with the coefficients,
|
|
28
|
+
which greatly simplifies the Wavepacket API.
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
grid
|
|
33
|
+
The grid on which the state is defined.
|
|
34
|
+
data
|
|
35
|
+
The coefficients of the state.
|
|
36
|
+
"""
|
|
37
|
+
grid: Grid
|
|
38
|
+
data: wpt.ComplexData
|
|
39
|
+
|
|
40
|
+
def is_wave_function(self) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Returns whether the state represents a wave function.
|
|
43
|
+
"""
|
|
44
|
+
return self.data.shape == self.grid.shape
|
|
45
|
+
|
|
46
|
+
def is_density_operator(self) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Returns whether the state represents a density operator.
|
|
49
|
+
"""
|
|
50
|
+
return self.data.shape == self.grid.operator_shape
|
|
51
|
+
|
|
52
|
+
def __add__(self, other: typing.Self | numbers.Number) -> typing.Self:
|
|
53
|
+
if isinstance(other, State):
|
|
54
|
+
self._check_states(other)
|
|
55
|
+
return State(self.grid, self.data + other.data)
|
|
56
|
+
else:
|
|
57
|
+
return State(self.grid, self.data + other)
|
|
58
|
+
|
|
59
|
+
def __radd__(self, other: numbers.Number) -> typing.Self:
|
|
60
|
+
return self + other
|
|
61
|
+
|
|
62
|
+
def __sub__(self, other: typing.Self | numbers.Number) -> typing.Self:
|
|
63
|
+
if isinstance(other, State):
|
|
64
|
+
self._check_states(other)
|
|
65
|
+
return State(self.grid, self.data - other.data)
|
|
66
|
+
else:
|
|
67
|
+
return State(self.grid, self.data - other)
|
|
68
|
+
|
|
69
|
+
def __rsub__(self, other: numbers.Number) -> typing.Self:
|
|
70
|
+
return State(self.grid, other - self.data)
|
|
71
|
+
|
|
72
|
+
def __mul__(self, other: numbers.Number) -> typing.Self:
|
|
73
|
+
return State(self.grid, self.data * other)
|
|
74
|
+
|
|
75
|
+
def __rmul__(self, other: numbers.Number) -> typing.Self:
|
|
76
|
+
return self * other
|
|
77
|
+
|
|
78
|
+
def __truediv__(self, other: numbers.Number) -> typing.Self:
|
|
79
|
+
if other == 0.0:
|
|
80
|
+
raise ZeroDivisionError("State cannot be divided by zero.")
|
|
81
|
+
|
|
82
|
+
return State(self.grid, self.data / other)
|
|
83
|
+
|
|
84
|
+
def __neg__(self) -> typing.Self:
|
|
85
|
+
return State(self.grid, -self.data)
|
|
86
|
+
|
|
87
|
+
def _check_states(self, other: typing.Self) -> None:
|
|
88
|
+
if self.grid != other.grid:
|
|
89
|
+
raise wp.BadGridError("Binary operations with states on different grids are not supported.")
|
|
90
|
+
|
|
91
|
+
if self.data.shape != other.data.shape:
|
|
92
|
+
raise wp.BadStateError(
|
|
93
|
+
"Binary operations can only be performed for two wave_functions or density operators.")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
import wavepacket as wp
|
|
4
|
+
import wavepacket.typing as wpt
|
|
5
|
+
from .grid import Grid
|
|
6
|
+
from .state import State
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _take_diagonal(data: wpt.ComplexData, grid: Grid) -> wpt.RealData:
|
|
10
|
+
# note: the diagonal should be real and positive but for numerical issues
|
|
11
|
+
matrix_form = np.reshape(data, (grid.size, grid.size))
|
|
12
|
+
diagonal = np.abs(np.diag(matrix_form))
|
|
13
|
+
return np.reshape(diagonal, grid.shape)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def dvr_density(state: State) -> wpt.RealData:
|
|
17
|
+
"""
|
|
18
|
+
Returns the density of the input state at the DVR grid points.
|
|
19
|
+
|
|
20
|
+
The density is returned as a real-valued coefficient array
|
|
21
|
+
with the same shape as the underlying grid. The density is mostly a
|
|
22
|
+
dead end: useful for plotting and inspection, but not for
|
|
23
|
+
further computations.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
state : wp.grid.State
|
|
28
|
+
The state (wave function or density operator) whose density should be computed.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
wpt.RealData
|
|
33
|
+
The coefficient array of the density values at the DVR grid points.
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
wp.BadStateError
|
|
38
|
+
If the supplied state is neither a wave function nor a density operator.
|
|
39
|
+
"""
|
|
40
|
+
if state.is_wave_function():
|
|
41
|
+
data = state.data
|
|
42
|
+
for index, dof in enumerate(state.grid.dofs):
|
|
43
|
+
data = dof.to_dvr(data, index)
|
|
44
|
+
|
|
45
|
+
return np.abs(data * data)
|
|
46
|
+
elif state.is_density_operator():
|
|
47
|
+
data = state.data
|
|
48
|
+
grid = state.grid
|
|
49
|
+
|
|
50
|
+
# both, the bra and the ket indices are converted
|
|
51
|
+
for index, dof in enumerate(grid.dofs):
|
|
52
|
+
data = dof.to_dvr(data, index)
|
|
53
|
+
data = dof.to_dvr(data, index + len(grid.dofs))
|
|
54
|
+
|
|
55
|
+
return _take_diagonal(data, grid)
|
|
56
|
+
else:
|
|
57
|
+
raise wp.BadStateError("Input is not a valid state.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def trace(state: State) -> float:
|
|
61
|
+
"""
|
|
62
|
+
Returns the trace of the supplied input state.
|
|
63
|
+
|
|
64
|
+
For density operators, this is the usual trace norm, i.e., sum over the
|
|
65
|
+
diagonal elements. For wave functions, it is the square of the usual
|
|
66
|
+
L2 norm.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
state : wp.grid.State
|
|
71
|
+
The state (wave function or density operator) for which to calculate the trace.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
float
|
|
76
|
+
The trace of the input state.
|
|
77
|
+
|
|
78
|
+
Raises
|
|
79
|
+
------
|
|
80
|
+
wp.BadStateError
|
|
81
|
+
If the supplied state is neither a wave function nor a density operator.
|
|
82
|
+
"""
|
|
83
|
+
if state.is_wave_function():
|
|
84
|
+
return np.abs(state.data ** 2).sum()
|
|
85
|
+
elif state.is_density_operator():
|
|
86
|
+
diagonal = _take_diagonal(state.data, state.grid)
|
|
87
|
+
return diagonal.sum()
|
|
88
|
+
else:
|
|
89
|
+
raise wp.BadStateError("Input is not a valid state.")
|
wavepacket/logging.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numbers
|
|
3
|
+
|
|
4
|
+
from .grid import State, trace
|
|
5
|
+
from .operator import Potential1D, expectation_value
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def log(t: numbers.Real, state: State, precision: int = 6) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Prints some data about the state for inspection.
|
|
11
|
+
|
|
12
|
+
The idea is that you call this function during every solver step and get
|
|
13
|
+
a log with the most important values about the propagation, for example
|
|
14
|
+
the state trace (if it deviates from one, this may be caused by poor convergence).
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
t : float
|
|
19
|
+
The time at which you log.
|
|
20
|
+
state : wp.grid.State
|
|
21
|
+
The state to log.
|
|
22
|
+
precision : int, default=6
|
|
23
|
+
How many decimal places should be printed.
|
|
24
|
+
"""
|
|
25
|
+
print(f"\n\nt = {float(t):.{precision}}, trace = {trace(state):.{precision}}\n")
|
|
26
|
+
|
|
27
|
+
for index, dof in enumerate(state.grid.dofs):
|
|
28
|
+
x = Potential1D(state.grid, 0, lambda dvr_grid: dvr_grid)
|
|
29
|
+
x2 = Potential1D(state.grid, 0, lambda dvr_grid: dvr_grid ** 2)
|
|
30
|
+
|
|
31
|
+
x_expect = expectation_value(x, state).real
|
|
32
|
+
x_expect2 = expectation_value(x2, state).real
|
|
33
|
+
|
|
34
|
+
print(f"<x_{index}> = {x_expect:.{precision}} =/- {math.sqrt(x_expect2 - x_expect ** 2):.{precision}}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains classes that define operators on a given grid.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ['CartesianKineticEnergy', 'OperatorBase',
|
|
6
|
+
'PlaneWaveFbrOperator', 'Potential1D',
|
|
7
|
+
'expectation_value']
|
|
8
|
+
|
|
9
|
+
from .operatorbase import OperatorBase
|
|
10
|
+
from .operatorutils import expectation_value
|
|
11
|
+
from .fbroperators import CartesianKineticEnergy, PlaneWaveFbrOperator
|
|
12
|
+
from .potentials import Potential1D
|