qrotor 4.0.0a1__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.
Potentially problematic release.
This version of qrotor might be problematic. Click here for more details.
- qrotor/__init__.py +14 -0
- qrotor/_version.py +14 -0
- qrotor/constants.py +72 -0
- qrotor/plot.py +337 -0
- qrotor/potential.py +473 -0
- qrotor/rotate.py +202 -0
- qrotor/solve.py +271 -0
- qrotor/system.py +275 -0
- qrotor/systems.py +245 -0
- qrotor-4.0.0a1.dist-info/METADATA +167 -0
- qrotor-4.0.0a1.dist-info/RECORD +16 -0
- qrotor-4.0.0a1.dist-info/WHEEL +5 -0
- qrotor-4.0.0a1.dist-info/licenses/LICENSE +661 -0
- qrotor-4.0.0a1.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_qrotor.py +101 -0
qrotor/solve.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Description
|
|
3
|
+
|
|
4
|
+
This module is used to solve any given quantum system.
|
|
5
|
+
|
|
6
|
+
Although the functions of this module can be used independently,
|
|
7
|
+
it is highly recommended to use the methods `System.solve()` or
|
|
8
|
+
`System.solve_potential()` instead to solve the whole quantum system
|
|
9
|
+
or just the potential values.
|
|
10
|
+
These user methods perform all calculations automatically,
|
|
11
|
+
see `qrotor.system.System.solve()` and
|
|
12
|
+
`qrotor.system.System.solve_potential()` respectively for more details.
|
|
13
|
+
|
|
14
|
+
This documentation page is left for reference and advanced users only.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Index
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `energies()` | Solve the quantum system, including eigenvalues and eigenvectors |
|
|
22
|
+
| `potential()` | Solve the potential values of the system |
|
|
23
|
+
| `schrodinger()` | Solve the Schrödiger equation for the system |
|
|
24
|
+
| `hamiltonian_matrix()` | Calculate the hamiltonian matrix of the system |
|
|
25
|
+
| `laplacian_matrix()` | Calculate the second derivative matrix for a given grid |
|
|
26
|
+
| `excitations()` | Get excitation levels and tunnel splitting energies |
|
|
27
|
+
| `E_levels` | Group a list of degenerated eigenvalues by energy levels |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
from .system import System
|
|
34
|
+
from .potential import solve as solve_potential
|
|
35
|
+
from .potential import interpolate
|
|
36
|
+
import time
|
|
37
|
+
import numpy as np
|
|
38
|
+
from scipy import sparse
|
|
39
|
+
import aton
|
|
40
|
+
from ._version import __version__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def energies(system:System, filename:str=None) -> System:
|
|
44
|
+
"""Solves the quantum `system`.
|
|
45
|
+
|
|
46
|
+
This includes solving the potential, the eigenvalues and the eigenvectors.
|
|
47
|
+
|
|
48
|
+
The resulting System object is saved with pickle to `filename` if specified.
|
|
49
|
+
"""
|
|
50
|
+
system = potential(system)
|
|
51
|
+
system = schrodinger(system)
|
|
52
|
+
if filename:
|
|
53
|
+
aton.st.file.save(system, filename)
|
|
54
|
+
return system
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def potential(system:System, gridsize:int=None) -> System:
|
|
58
|
+
"""Solves the potential values of the `system`.
|
|
59
|
+
|
|
60
|
+
Creates a grid if not yet present.
|
|
61
|
+
It also interpolates the potential if `system.gridsize` is larger than the current grid;
|
|
62
|
+
optionally, an alternative `gridsize` can be specified.
|
|
63
|
+
|
|
64
|
+
It then solves the potential according to the potential name.
|
|
65
|
+
Then it applies extra operations, such as removing the potential offset
|
|
66
|
+
if `system.correct_potential_offset = True`.
|
|
67
|
+
"""
|
|
68
|
+
if gridsize:
|
|
69
|
+
system.gridsize = gridsize
|
|
70
|
+
if not any(system.grid):
|
|
71
|
+
system.set_grid()
|
|
72
|
+
if system.gridsize and any(system.grid):
|
|
73
|
+
if system.gridsize > len(system.grid):
|
|
74
|
+
system = interpolate(system)
|
|
75
|
+
V = solve_potential(system)
|
|
76
|
+
if system.correct_potential_offset is True:
|
|
77
|
+
offset = min(V)
|
|
78
|
+
V = V - offset
|
|
79
|
+
system.potential_offset = offset
|
|
80
|
+
system.potential_max = max(V)
|
|
81
|
+
system.potential_min = min(V)
|
|
82
|
+
system.potential_values = V
|
|
83
|
+
return system
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def schrodinger(system:System) -> System:
|
|
87
|
+
"""Solves the Schrödinger equation for a given `system`.
|
|
88
|
+
|
|
89
|
+
Uses ARPACK in shift-inverse mode to solve the hamiltonian sparse matrix.
|
|
90
|
+
"""
|
|
91
|
+
time_start = time.time()
|
|
92
|
+
V = system.potential_values
|
|
93
|
+
H = hamiltonian_matrix(system)
|
|
94
|
+
print('Solving Schrodinger equation...')
|
|
95
|
+
# Solve eigenvalues with ARPACK in shift-inverse mode, with a sparse matrix
|
|
96
|
+
eigenvalues, eigenvectors = sparse.linalg.eigsh(H, system.searched_E, which='LM', sigma=0, maxiter=10000)
|
|
97
|
+
if any(eigenvalues) is None:
|
|
98
|
+
print('WARNING: Not all eigenvalues were found.\n')
|
|
99
|
+
else: print('Done.')
|
|
100
|
+
system.version = __version__
|
|
101
|
+
system.runtime = time.time() - time_start
|
|
102
|
+
system.eigenvalues = eigenvalues
|
|
103
|
+
system.energy_barrier = max(V) - min(eigenvalues)
|
|
104
|
+
# Solve excitations and tunnel splittings, assuming triplet degeneracy
|
|
105
|
+
system = excitations(system)
|
|
106
|
+
# Do we really need to save eigenvectors?
|
|
107
|
+
if system.save_eigenvectors == True:
|
|
108
|
+
system.eigenvectors = np.transpose(eigenvectors)
|
|
109
|
+
# Save potential max and min, in case these are not already saved
|
|
110
|
+
system.potential_max = max(V)
|
|
111
|
+
system.potential_min = min(V)
|
|
112
|
+
return system
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def hamiltonian_matrix(system:System):
|
|
116
|
+
"""Calculates the Hamiltonian sparse matrix for a given `system`."""
|
|
117
|
+
print(f'Creating Hamiltonian sparse matrix of size {system.gridsize}...')
|
|
118
|
+
V = system.potential_values.tolist()
|
|
119
|
+
potential = sparse.diags(V, format='lil')
|
|
120
|
+
B = system.B
|
|
121
|
+
x = system.grid
|
|
122
|
+
H = -B * laplacian_matrix(x) + potential
|
|
123
|
+
return H
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def laplacian_matrix(grid):
|
|
127
|
+
"""Calculates the Laplacian (second derivative) matrix for a given `grid`."""
|
|
128
|
+
x = grid
|
|
129
|
+
n = len(x)
|
|
130
|
+
diagonals = [-2*np.ones(n), np.ones(n), np.ones(n)]
|
|
131
|
+
laplacian_matrix = sparse.spdiags(diagonals, [0, -1, 1], m=n, n=n, format='lil')
|
|
132
|
+
# Periodic boundary conditions
|
|
133
|
+
laplacian_matrix[0, -1] = 1
|
|
134
|
+
laplacian_matrix[-1, 0] = 1
|
|
135
|
+
dx = x[1] - x[0]
|
|
136
|
+
laplacian_matrix /= dx**2
|
|
137
|
+
return laplacian_matrix
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def excitations(system: System) -> System:
|
|
141
|
+
"""Calculate the excitation levels and the tunnel splitting energies of a system.
|
|
142
|
+
|
|
143
|
+
Automatically detects degenerated energy levels by looking at significant jumps
|
|
144
|
+
between consecutive eigenvalues. Within each level, finds two subgroups
|
|
145
|
+
to calculate tunnel splittings. Stops when energies reach the maximum potential.
|
|
146
|
+
|
|
147
|
+
Excitations are calculated as the energy difference between the mean energy of the
|
|
148
|
+
ground state level and the mean energy of each excited level.
|
|
149
|
+
|
|
150
|
+
Tunnel splittings are calculated as the difference between the mean values of
|
|
151
|
+
the two subgroups within each degenerate level.
|
|
152
|
+
"""
|
|
153
|
+
# Get eigenvalues, stop before any possible None value
|
|
154
|
+
eigenvalues = system.eigenvalues
|
|
155
|
+
if not isinstance(eigenvalues, (list, np.ndarray)) or len(eigenvalues) == 0:
|
|
156
|
+
return system
|
|
157
|
+
if None in eigenvalues:
|
|
158
|
+
none_index = eigenvalues.tolist().index(None)
|
|
159
|
+
eigenvalues = eigenvalues[:none_index]
|
|
160
|
+
if len(eigenvalues) < 3:
|
|
161
|
+
return system
|
|
162
|
+
# Group degenerated eigenvalues into energy levels
|
|
163
|
+
levels, degeneracy = E_levels(eigenvalues, system.potential_max)
|
|
164
|
+
system.E_levels = levels
|
|
165
|
+
system.deg = degeneracy
|
|
166
|
+
# Calculate excitations and splittings
|
|
167
|
+
ground_energy = np.mean(levels[0]) # Mean of ground state level
|
|
168
|
+
excitations = []
|
|
169
|
+
tunnel_splittings = []
|
|
170
|
+
for level in levels:
|
|
171
|
+
level_mean = np.mean(level)
|
|
172
|
+
excitations.append(level_mean - ground_energy)
|
|
173
|
+
# Get the tunnel splitting within the level
|
|
174
|
+
if len(level) > 1:
|
|
175
|
+
# Find the largest gap within the level to split into two subgroups
|
|
176
|
+
internal_gaps = np.diff(level)
|
|
177
|
+
split_idx = np.argmax(internal_gaps) + 1
|
|
178
|
+
# Split into two subgroups
|
|
179
|
+
subgroup1 = level[:split_idx]
|
|
180
|
+
subgroup2 = level[split_idx:]
|
|
181
|
+
# Medians of subgroups
|
|
182
|
+
median1 = np.median(subgroup1)
|
|
183
|
+
median2 = np.median(subgroup2)
|
|
184
|
+
# Tunnel splitting is the difference between medians
|
|
185
|
+
tunnel_splittings.append(abs(median2 - median1))
|
|
186
|
+
else:
|
|
187
|
+
tunnel_splittings.append(0)
|
|
188
|
+
system.excitations = excitations[1:] # Exclude ground state
|
|
189
|
+
system.splittings = tunnel_splittings
|
|
190
|
+
return system
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def E_levels(eigenvalues, vmax:float=None) -> list:
|
|
194
|
+
"""Group a list of degenerated eigenvalues by energy levels.
|
|
195
|
+
|
|
196
|
+
Automatically detects degenerated energy levels by
|
|
197
|
+
looking at significant jumps between consecutive eigenvalues.
|
|
198
|
+
|
|
199
|
+
An optional `vmax` can be specified,
|
|
200
|
+
to avoid including too many eigenvalues
|
|
201
|
+
above a certain potential maximum.
|
|
202
|
+
Only two more eigenvalues are considered after `vmax`,
|
|
203
|
+
to properly detect energy levels around the maximum.
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
```python
|
|
207
|
+
levels, deg = qr.solve.E_levels(array([1.1, 1.2, 1.3, 5.4, 5.5, 5.6]))
|
|
208
|
+
levels # [array([1.1, 1.2, 1.3]), array([5.4, 5.5, 5.6])]
|
|
209
|
+
deg # 3
|
|
210
|
+
```
|
|
211
|
+
"""
|
|
212
|
+
if vmax: # Include all eigenvalues below Vmax plus 3 more eigenvalues
|
|
213
|
+
# Check if any values are above vmax
|
|
214
|
+
eigenvalues_above_vmax = eigenvalues > vmax
|
|
215
|
+
if np.any(eigenvalues_above_vmax):
|
|
216
|
+
index_first_above_vmax = np.where(eigenvalues_above_vmax)[0][0]
|
|
217
|
+
eigenvalues = eigenvalues[:(index_first_above_vmax + 2)]
|
|
218
|
+
# Group degenerated eigenvalues into energy levels
|
|
219
|
+
for scale in np.arange(2, 4, 0.25): # First search going to bigger scales
|
|
220
|
+
levels, degeneracy = _get_E_levels_by_gap(eigenvalues, scale)
|
|
221
|
+
if (degeneracy > 1) and (degeneracy % 1 == 0):
|
|
222
|
+
break
|
|
223
|
+
else:
|
|
224
|
+
levels, degeneracy = None, None
|
|
225
|
+
if not degeneracy: # If it didn't work, search with tighter values
|
|
226
|
+
for scale in np.arange(0.75, 2, 0.25):
|
|
227
|
+
levels, degeneracy = _get_E_levels_by_gap(eigenvalues, scale)
|
|
228
|
+
if (degeneracy > 1) and (degeneracy % 1 == 0):
|
|
229
|
+
break
|
|
230
|
+
if not (degeneracy > 1) and not (degeneracy % 1) == 0:
|
|
231
|
+
return levels, degeneracy # I give up
|
|
232
|
+
# Correct the last two levels
|
|
233
|
+
if len(levels) >= 2 and len(levels[-2]) != degeneracy:
|
|
234
|
+
levels[-2] = np.concatenate((levels[-2], levels[-1]))
|
|
235
|
+
levels.pop(-1)
|
|
236
|
+
# Split last level into groups of size = degeneracy
|
|
237
|
+
last_level = levels[-1]
|
|
238
|
+
additional_levels = len(last_level) // degeneracy
|
|
239
|
+
if additional_levels > 0:
|
|
240
|
+
# Replace last level with list of complete degeneracy groups
|
|
241
|
+
complete_groups = [last_level[i:i+degeneracy] for i in range(0, additional_levels*degeneracy, degeneracy)]
|
|
242
|
+
levels.pop(-1) # Remove original last level
|
|
243
|
+
levels.extend(complete_groups) # Add all complete groups
|
|
244
|
+
else:
|
|
245
|
+
levels.pop(-1) # Remove incomplete last level
|
|
246
|
+
return levels, degeneracy
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_E_levels_by_gap(eigenvalues, scale:float=2) -> tuple:
|
|
250
|
+
"""Split a list of eigenvalues into energy levels by looking at gaps.
|
|
251
|
+
|
|
252
|
+
If the gap is bigger than the average gap times `scale`, it is considered a new level.
|
|
253
|
+
|
|
254
|
+
Returns a tuple with the estimated levels and the average degeneracy.
|
|
255
|
+
The last two levels are not taken into account to estimate the degeneracy.
|
|
256
|
+
"""
|
|
257
|
+
# Find gaps between consecutive eigenvalues
|
|
258
|
+
gaps = np.diff(eigenvalues)
|
|
259
|
+
# Use mean gap times scale as threshold to distinguish energy levels
|
|
260
|
+
med_gap = np.mean(gaps)
|
|
261
|
+
level_breaks = np.where(gaps > scale * med_gap)[0] + 1
|
|
262
|
+
levels = np.split(eigenvalues, level_breaks)
|
|
263
|
+
# Calculate average degeneracy excluding last two levels if possible
|
|
264
|
+
if len(levels) > 2:
|
|
265
|
+
avg_degeneracy = float(np.mean([len(level) for level in levels[:-2]]))
|
|
266
|
+
else:
|
|
267
|
+
avg_degeneracy = float(len(levels[0]))
|
|
268
|
+
if avg_degeneracy % 1 == 0:
|
|
269
|
+
avg_degeneracy = int(avg_degeneracy)
|
|
270
|
+
return levels, avg_degeneracy
|
|
271
|
+
|
qrotor/system.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Description
|
|
3
|
+
|
|
4
|
+
The `System` object contains all the information needed for a single QRotor calculation.
|
|
5
|
+
This class can be loaded directly as `qrotor.System()`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from .constants import *
|
|
13
|
+
from aton import alias
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class System:
|
|
18
|
+
"""Quantum system.
|
|
19
|
+
|
|
20
|
+
Contains all the data for a single QRotor calculation, with both inputs and outputs.
|
|
21
|
+
|
|
22
|
+
Energy units are in meV and angles are in radians, unless stated otherwise.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
comment: str = None,
|
|
27
|
+
searched_E: int = 21,
|
|
28
|
+
correct_potential_offset: bool = True,
|
|
29
|
+
save_eigenvectors: bool = True,
|
|
30
|
+
group: str = '',
|
|
31
|
+
B: float = B_CH3,
|
|
32
|
+
gridsize: int = 200000,
|
|
33
|
+
grid = [],
|
|
34
|
+
potential_name: str = '',
|
|
35
|
+
potential_constants: list = None,
|
|
36
|
+
potential_values = [],
|
|
37
|
+
):
|
|
38
|
+
"""A new quantum system can be instantiated as `system = qrotor.System()`.
|
|
39
|
+
This new system will contain the default values listed above.
|
|
40
|
+
"""
|
|
41
|
+
## Technical
|
|
42
|
+
self.version = __version__
|
|
43
|
+
"""Version of the package used to generate the data."""
|
|
44
|
+
self.comment: str = comment
|
|
45
|
+
"""Custom comment for the dataset."""
|
|
46
|
+
self.searched_E: int = searched_E
|
|
47
|
+
"""Number of energy eigenvalues to be searched."""
|
|
48
|
+
self.correct_potential_offset: bool = correct_potential_offset
|
|
49
|
+
"""Correct the potential offset as `V - min(V)` or not."""
|
|
50
|
+
self.save_eigenvectors: bool = save_eigenvectors
|
|
51
|
+
"""Save or not the eigenvectors. Final file size will be bigger."""
|
|
52
|
+
self.group: str = group
|
|
53
|
+
"""Chemical group, methyl or amine: `'CH3'`, `'CD3'`, `'NH3'`, `'ND3'`.
|
|
54
|
+
|
|
55
|
+
Can be used to set the value of `B` automatically at startup.
|
|
56
|
+
It can also be configured afterwards with `System.set_group()`.
|
|
57
|
+
This group can be used as metadata to analyse different datasets.
|
|
58
|
+
"""
|
|
59
|
+
self.set_group(group) # Normalise the group name, and set the value of B
|
|
60
|
+
## Potential
|
|
61
|
+
if not B:
|
|
62
|
+
B = self.B
|
|
63
|
+
self.B: float = B
|
|
64
|
+
"""Rotational inertia, as in $B=\\frac{\\hbar^2}{2I}$.
|
|
65
|
+
|
|
66
|
+
Defaults to the value for a methyl group.
|
|
67
|
+
"""
|
|
68
|
+
self.gridsize: int = gridsize
|
|
69
|
+
"""Number of points in the grid."""
|
|
70
|
+
self.grid = grid
|
|
71
|
+
"""The grid with the points to be used in the calculation.
|
|
72
|
+
|
|
73
|
+
Can be set automatically over $2 \\pi$ with `System.set_grid()`.
|
|
74
|
+
Units must be in radians.
|
|
75
|
+
"""
|
|
76
|
+
self.potential_name: str = potential_name
|
|
77
|
+
"""Name of the desired potential: `'zero'`, `'titov2023'`, `'test'`...
|
|
78
|
+
|
|
79
|
+
If empty or unrecognised, the custom potential values inside `System.potential_values` will be used.
|
|
80
|
+
"""
|
|
81
|
+
self.potential_constants: list = potential_constants
|
|
82
|
+
"""List of constants to be used in the calculation of the potential energy, in the `qrotor.potential` module."""
|
|
83
|
+
self.potential_values = potential_values
|
|
84
|
+
"""Numpy [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) with the potential values for each point in the grid.
|
|
85
|
+
|
|
86
|
+
Can be calculated with a function available in the `qrotor.potential` module,
|
|
87
|
+
or loaded externally with the `qrotor.potential.load()` function.
|
|
88
|
+
Potential energy units must be in meV.
|
|
89
|
+
"""
|
|
90
|
+
# Potential values determined upon solving
|
|
91
|
+
self.potential_offset: float = None
|
|
92
|
+
"""`min(V)` before offset correction when `correct_potential_offset = True`"""
|
|
93
|
+
self.potential_min: float = None
|
|
94
|
+
"""`min(V)`"""
|
|
95
|
+
self.potential_max: float = None
|
|
96
|
+
"""`max(V)`"""
|
|
97
|
+
# Energies determined upon solving
|
|
98
|
+
self.eigenvectors = []
|
|
99
|
+
"""Eigenvectors, if `save_eigenvectors` is True. Beware of the file size."""
|
|
100
|
+
self.eigenvalues = []
|
|
101
|
+
"""Calculated eigenvalues of the system. In meV."""
|
|
102
|
+
self.E_levels: list = []
|
|
103
|
+
"""List of `eigenvalues` grouped by energy levels, found below `potential_max`."""
|
|
104
|
+
self.deg: float = None
|
|
105
|
+
"""Estimated degeneracy of the `E_levels` found below `potential_max`."""
|
|
106
|
+
self.excitations: list = []
|
|
107
|
+
"""Torsional excitations, as the difference between each energy level with respect to the ground state.
|
|
108
|
+
|
|
109
|
+
Considers the means between degenerated eigenvalues for all energy levels below `potential_max`.
|
|
110
|
+
"""
|
|
111
|
+
self.splittings: list = []
|
|
112
|
+
"""Tunnel splitting energies, for every degenerated energy level.
|
|
113
|
+
|
|
114
|
+
Calculated for all `E_levels` as the difference between
|
|
115
|
+
the mean of the eigenvalues from A and the mean of the eigenvalues from E,
|
|
116
|
+
see [R. M. Dimeo, American Journal of Physics 71, 885–893 (2003)](https://doi.org/10.1119/1.1538575).
|
|
117
|
+
"""
|
|
118
|
+
self.energy_barrier: float = None
|
|
119
|
+
"""Activation energy or energy barrier, from the ground torsional state to the top of the potential barrier, `max(V) - min(eigenvalues)`"""
|
|
120
|
+
self.runtime: float = None
|
|
121
|
+
"""Time taken to solve the eigenvalues."""
|
|
122
|
+
|
|
123
|
+
def solve(self, new_gridsize:int=None):
|
|
124
|
+
"""Default user method to solve the quantum system.
|
|
125
|
+
|
|
126
|
+
The potential can be interpolated to a `new_gridsize`.
|
|
127
|
+
|
|
128
|
+
Same as running `qrotor.solve.energies(System)`
|
|
129
|
+
with an optional new gridsize.
|
|
130
|
+
"""
|
|
131
|
+
from .solve import energies
|
|
132
|
+
if new_gridsize:
|
|
133
|
+
self.gridsize = new_gridsize
|
|
134
|
+
return energies(self)
|
|
135
|
+
|
|
136
|
+
def solve_potential(self, new_gridsize:int=None):
|
|
137
|
+
"""Default user method to quickly solve the potential of the quantum system.
|
|
138
|
+
|
|
139
|
+
This method does not solve the energies of the system,
|
|
140
|
+
it just computes the potential and sets `System.potential_max`,
|
|
141
|
+
`System.potential_min` and `System.potential_offset` accordingly.
|
|
142
|
+
To solve the potential AND the energies, check `System.solve()`.
|
|
143
|
+
|
|
144
|
+
The potential can be interpolated to a `new_gridsize`.
|
|
145
|
+
|
|
146
|
+
Same as running `qrotor.solve.potential(System)`
|
|
147
|
+
with an optional new gridsize.
|
|
148
|
+
"""
|
|
149
|
+
from .solve import potential
|
|
150
|
+
if new_gridsize:
|
|
151
|
+
self.gridsize = new_gridsize
|
|
152
|
+
return potential(self)
|
|
153
|
+
|
|
154
|
+
def change_phase(self, phase:float, calculate:bool=True):
|
|
155
|
+
"""Apply a phase shift to the grid and potential values.
|
|
156
|
+
|
|
157
|
+
The `phase` should be a multiple of $\\pi$ (e.g., 3/2 for $3\\pi/2$).
|
|
158
|
+
The resulting grid will be expressed between $-2\\pi$ and $2\\pi$.
|
|
159
|
+
|
|
160
|
+
The System is solved immediately after the phase change.
|
|
161
|
+
This last step ensures that all eigenvalues and wavefunctions are correct.
|
|
162
|
+
You can override this step with `calculate = False`,
|
|
163
|
+
but remember to solve the System later!
|
|
164
|
+
"""
|
|
165
|
+
if not any(self.potential_values) or not any(self.grid):
|
|
166
|
+
raise ValueError("System.potential_values and System.grid must be set before applying a phase shift.")
|
|
167
|
+
# Normalise the phase between 0 and 2
|
|
168
|
+
if abs(phase) >= 2:
|
|
169
|
+
phase = phase % 2
|
|
170
|
+
while phase < 0:
|
|
171
|
+
phase = phase + 2
|
|
172
|
+
# Shift the grid, between -2pi and 2pi
|
|
173
|
+
self.grid = (self.grid + (phase * np.pi))
|
|
174
|
+
# Apply the phase shift to potential values
|
|
175
|
+
phase_points = int((phase / 2) * self.gridsize)
|
|
176
|
+
self.potential_values = np.roll(self.potential_values, phase_points)
|
|
177
|
+
# Check that the grid is still within -2pi and 2pi, otherwise normalise it for a final time
|
|
178
|
+
while self.grid[0] <= (-2 * np.pi + 0.1): # With a small tolerance
|
|
179
|
+
self.grid = self.grid + 2 * np.pi
|
|
180
|
+
while self.grid[-1] >= 2.5 * np.pi: # It was not a problem until reaching 5/2 pi
|
|
181
|
+
self.grid = self.grid -2 * np.pi
|
|
182
|
+
print(f'Potential shifted by {phase}π')
|
|
183
|
+
if calculate:
|
|
184
|
+
self.solve()
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def set_grid(self, gridsize:int=None):
|
|
188
|
+
"""Sets the `System.grid` to the specified `gridsize` from 0 to $2\\pi$.
|
|
189
|
+
|
|
190
|
+
If the system had a previous grid and potential values,
|
|
191
|
+
it will interpolate those values to the new gridsize,
|
|
192
|
+
using `qrotor.potential.interpolate()`.
|
|
193
|
+
"""
|
|
194
|
+
if gridsize == self.gridsize:
|
|
195
|
+
return self # Nothing to do here
|
|
196
|
+
if gridsize:
|
|
197
|
+
self.gridsize = gridsize
|
|
198
|
+
# Should we interpolate?
|
|
199
|
+
if any(self.potential_values) and any(self.grid) and self.gridsize:
|
|
200
|
+
from .potential import interpolate
|
|
201
|
+
self = interpolate(self)
|
|
202
|
+
# Should we create the values from zero?
|
|
203
|
+
elif self.gridsize:
|
|
204
|
+
self.grid = np.linspace(0, 2*np.pi, self.gridsize)
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError('gridsize must be provided if there is no System.gridsize')
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def set_group(self, group:str=None, B:float=None):
|
|
210
|
+
"""Normalise `System.group` name, and set `System.B` based on it."""
|
|
211
|
+
for name in alias.chemical['CH3']:
|
|
212
|
+
if group.lower() == name:
|
|
213
|
+
self.group = 'CH3'
|
|
214
|
+
if not B:
|
|
215
|
+
B = B_CH3
|
|
216
|
+
self.B = B
|
|
217
|
+
return self
|
|
218
|
+
for name in alias.chemical['CD3']:
|
|
219
|
+
if group.lower() == name:
|
|
220
|
+
self.group = 'CD3'
|
|
221
|
+
if not B:
|
|
222
|
+
B = B_CD3
|
|
223
|
+
self.B = B
|
|
224
|
+
return self
|
|
225
|
+
for name in alias.chemical['NH3']:
|
|
226
|
+
if group.lower() == name:
|
|
227
|
+
self.group = 'NH3'
|
|
228
|
+
if not B:
|
|
229
|
+
B = B_NH3
|
|
230
|
+
self.B = B
|
|
231
|
+
return self
|
|
232
|
+
for name in alias.chemical['ND3']:
|
|
233
|
+
if group.lower() == name:
|
|
234
|
+
self.group = 'ND3'
|
|
235
|
+
if not B:
|
|
236
|
+
B = B_ND3
|
|
237
|
+
self.B = B
|
|
238
|
+
return self
|
|
239
|
+
self.group = group # No match was found
|
|
240
|
+
self.B = None
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def reduce_size(self):
|
|
244
|
+
"""Discard data that takes too much space,
|
|
245
|
+
like eigenvectors, potential values and grids."""
|
|
246
|
+
self.eigenvectors = []
|
|
247
|
+
self.potential_values = []
|
|
248
|
+
self.grid = []
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def summary(self):
|
|
252
|
+
"""Returns a dict with a summary of the System data."""
|
|
253
|
+
return {
|
|
254
|
+
'version': self.version,
|
|
255
|
+
'comment': self.comment,
|
|
256
|
+
'searched_E': self.searched_E,
|
|
257
|
+
'correct_potential_offset': self.correct_potential_offset,
|
|
258
|
+
'save_eigenvectors': self.save_eigenvectors,
|
|
259
|
+
'group': self.group,
|
|
260
|
+
'B': self.B,
|
|
261
|
+
'gridsize': self.gridsize,
|
|
262
|
+
'potential_name': self.potential_name,
|
|
263
|
+
'potential_constants': self.potential_constants.tolist() if isinstance(self.potential_constants, np.ndarray) else self.potential_constants,
|
|
264
|
+
'potential_offset': self.potential_offset,
|
|
265
|
+
'potential_min': self.potential_min,
|
|
266
|
+
'potential_max': self.potential_max,
|
|
267
|
+
'eigenvalues': self.eigenvalues.tolist() if isinstance(self.eigenvalues, np.ndarray) else self.eigenvalues,
|
|
268
|
+
'E_levels': self.E_levels,
|
|
269
|
+
'deg': self.deg,
|
|
270
|
+
'excitations': self.excitations,
|
|
271
|
+
'splittings': self.splittings,
|
|
272
|
+
'energy_barrier': self.energy_barrier,
|
|
273
|
+
'runtime': self.runtime,
|
|
274
|
+
}
|
|
275
|
+
|