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 ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ .. include:: ../_README_temp.md
3
+ """
4
+
5
+
6
+ from ._version import __version__ as version
7
+ from .system import System
8
+ from .constants import *
9
+ from . import systems
10
+ from . import rotate
11
+ from . import potential
12
+ from . import solve
13
+ from . import plot
14
+
qrotor/_version.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ # Description
3
+
4
+ Package version is defined here. Follows semantic versioning, as in:
5
+
6
+ `vMAJOR.MINOR.PATCH.`
7
+
8
+ More about semantic versioning:
9
+ https://semver.org/
10
+
11
+ """
12
+
13
+ __version__ = 'v4.0.0a1'
14
+
qrotor/constants.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ # Description
3
+
4
+ Common constants and default inertia values used in the QRotor subpackage.
5
+
6
+ Bond lengths and angles were obtained from MAPbI3, see
7
+ [K. Drużbicki *et al*., Crystal Growth & Design 24, 391–404 (2024)](https://doi.org/10.1021/acs.cgd.3c01112).
8
+
9
+ ---
10
+ """
11
+
12
+
13
+ import numpy as np
14
+ import aton.phys as phys
15
+
16
+
17
+ # Distance between Carbon and Hydrogen atoms (measured from MAPbI3)
18
+ distance_CH = 1.09285 # Angstroms
19
+ """Distance of the C-H bond, in Angstroms."""
20
+ distance_NH = 1.040263 # Angstroms
21
+ """Distance of the N-H bond, in Angstroms."""
22
+
23
+ # Angles between atoms: C-C-H or N-C-H etc (from MAPbI3)
24
+ angle_CH_external = 108.7223
25
+ """External angle of the X-C-H bond, in degrees."""
26
+ angle_NH_external = 111.29016
27
+ """External angle of the X-N-H bond, in degrees."""
28
+ angle_CH = 180 - angle_CH_external
29
+ """Internal angle of the X-C-H bond, in degrees."""
30
+ angle_NH = 180 - angle_NH_external
31
+ """Internal angle of the X-N-H bond, in degrees."""
32
+
33
+ # Rotation radius (calculated from distance and angle)
34
+ r_CH = distance_CH * np.sin(np.deg2rad(angle_CH)) * phys.AA_to_m
35
+ """Rotation radius of the methyl group, in meters."""
36
+ r_NH = distance_NH * np.sin(np.deg2rad(angle_NH)) * phys.AA_to_m
37
+ """Rotation radius of the amine group, in meters."""
38
+
39
+ # Inertia, SI units
40
+ I_CH3 = 3 * (phys.atoms['H'].mass * phys.amu_to_kg * r_CH**2)
41
+ """Inertia of CH3, in kg·m^2."""
42
+ I_CD3 = 3 * (phys.atoms['H'].isotope[2].mass * phys.amu_to_kg * r_CH**2)
43
+ """Inertia of CD3, in kg·m^2."""
44
+ I_NH3 = 3 * (phys.atoms['H'].mass * phys.amu_to_kg * r_NH**2)
45
+ """Inertia of NH3, in kg·m^2."""
46
+ I_ND3 = 3 * (phys.atoms['H'].isotope[2].mass * phys.amu_to_kg * r_NH**2)
47
+ """Inertia of ND3, in kg·m^2."""
48
+
49
+ # Rotational energy.
50
+ B_CH3 = ((phys.hbar**2) / (2 * I_CH3)) * phys.J_to_meV
51
+ """Rotational energy of CH3, in meV·s/kg·m^2."""
52
+ B_CD3 = ((phys.hbar**2) / (2 * I_CD3)) * phys.J_to_meV
53
+ """Rotational energy of CD3, in meV·s/kg·m^2."""
54
+ B_NH3 = ((phys.hbar**2) / (2 * I_NH3)) * phys.J_to_meV
55
+ """Rotational energy of NH3, in meV·s/kg·m^2."""
56
+ B_ND3 = ((phys.hbar**2) / (2 * I_ND3)) * phys.J_to_meV
57
+ """Rotational energy of ND3, in meV·s/kg·m^2."""
58
+
59
+ # Potential constants from titov2023 [C1, C2, C3, C4, C5]
60
+ constants_titov2023 = [
61
+ [2.7860, 0.0130,-1.5284,-0.0037,-1.2791], # ZIF-8
62
+ [2.6507, 0.0158,-1.4111,-0.0007,-1.2547], # ZIF-8 + Ar-1
63
+ [2.1852, 0.0164,-1.0017, 0.0003,-1.2061], # ZIF-8 + Ar-{1,2}
64
+ [5.9109, 0.0258,-7.0152,-0.0168, 1.0213], # ZIF-8 + Ar-{1,2,3}
65
+ [1.4526, 0.0134,-0.3196, 0.0005,-1.1461], # ZIF-8 + Ar-{1,2,4}
66
+ ]
67
+ """Potential constants from
68
+ [K. Titov et al., Phys. Rev. Mater. 7, 073402 (2023)](https://link.aps.org/doi/10.1103/PhysRevMaterials.7.073402)
69
+ for the `qrotor.potential.titov2023` potential.
70
+ In meV units.
71
+ """
72
+
qrotor/plot.py ADDED
@@ -0,0 +1,337 @@
1
+ """
2
+ # Description
3
+
4
+ This module provides straightforward functions to plot QRotor data.
5
+
6
+
7
+ # Index
8
+
9
+ | | |
10
+ | --- | --- |
11
+ | `potential()` | Potential values as a function of the angle |
12
+ | `energies()` | Calculated eigenvalues |
13
+ | `reduced_energies()` | Reduced energies E/B as a function of the reduced potential V/B |
14
+ | `wavefunction()` | Selected wavefunctions or squared wavefunctions of a system |
15
+ | `splittings()` | Tunnel splitting energies of a list of systems |
16
+ | `convergence()` | Energy convergence |
17
+
18
+ ---
19
+ """
20
+
21
+
22
+ from .system import System
23
+ from . import systems
24
+ import matplotlib.pyplot as plt
25
+ import numpy as np
26
+ from copy import deepcopy
27
+ import aton.alias as alias
28
+ import aton.phys as phys
29
+
30
+
31
+ def potential(
32
+ data,
33
+ title:str=None,
34
+ marker='',
35
+ linestyle='-',
36
+ cm:bool=False,
37
+ ) -> None:
38
+ """Plot the potential values of `data` (System object, or list of systems).
39
+
40
+ Title can be customized with `title`.
41
+ If empty, system[0].comment will be used as title if no more comments are present.
42
+
43
+ `marker` and `linestyle` can be a Matplotlib string or list of strings.
44
+ Optionally, the Viridis colormap can be used with `cm = True`.
45
+ """
46
+ system = systems.as_list(data)
47
+ title_str = title if title else (system[0].comment if (system[0].comment and (len(system) == 1 or not system[-1].comment)) else 'Rotational potential energy')
48
+ # Marker as a list
49
+ if isinstance(marker, list):
50
+ if len(marker) < len(system):
51
+ marker.extend([''] * (len(system) - len(marker)))
52
+ else:
53
+ marker = [marker] * len(system)
54
+ # Linestyle as a list
55
+ if isinstance(linestyle, list):
56
+ if len(linestyle) < len(system):
57
+ linestyle.extend(['-'] * (len(system) - len(linestyle)))
58
+ else:
59
+ linestyle = [linestyle] * len(system)
60
+
61
+ plt.figure()
62
+ plt.title(title_str)
63
+ plt.xlabel('Angle / rad')
64
+ plt.ylabel('Potential energy / meV')
65
+ plt.xticks([-2*np.pi, -3*np.pi/2, -np.pi, -np.pi/2, 0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi], [r'$-2\pi$', r'$-\frac{3\pi}{2}$', r'$-\pi$', r'$-\frac{\pi}{2}$', '0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$'])
66
+
67
+ if cm: # Plot using a colormap
68
+ colors = plt.cm.viridis(np.linspace(0, 1, len(system)+1)) # +1 to avoid the lighter tones
69
+ for i, s in enumerate(system):
70
+ plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment, color=colors[i])
71
+ else: # Regular plot
72
+ for i, s in enumerate(system):
73
+ plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment)
74
+
75
+ if all(s.comment for s in system) and len(system) != 1:
76
+ plt.legend(fontsize='small')
77
+
78
+ plt.show()
79
+
80
+
81
+ def energies(
82
+ data,
83
+ title:str=None,
84
+ ) -> None:
85
+ """Plot the eigenvalues of `data` (System or a list of System objects)."""
86
+ if isinstance(data, System):
87
+ var = [data]
88
+ else: # Should be a list
89
+ systems.as_list(data)
90
+ var = data
91
+
92
+ V_colors = ['C0', 'C1', 'C2', 'C3', 'C4']
93
+ E_colors = ['lightblue', 'sandybrown', 'lightgrey', 'lightcoral', 'plum']
94
+ E_linestyles = ['--', ':', '-.']
95
+ edgecolors = E_colors
96
+
97
+ V_linestyle = '-'
98
+ title = title if title else (var[0].comment if var[0].comment else 'Energy eigenvalues')
99
+ ylabel_text = f'Energy / meV'
100
+ xlabel_text = 'Angle / radians'
101
+
102
+ plt.figure(figsize=(10, 6))
103
+ plt.xlabel(xlabel_text)
104
+ plt.ylabel(ylabel_text)
105
+ plt.title(title)
106
+ plt.xticks([-2*np.pi, -3*np.pi/2, -np.pi, -np.pi/2, 0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi], [r'$-2\pi$', r'$-\frac{3\pi}{2}$', r'$-\pi$', r'$-\frac{\pi}{2}$', '0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$'])
107
+
108
+ unique_potentials = []
109
+ unique_groups = []
110
+ for i, system in enumerate(var):
111
+ V_color = V_colors[i % len(V_colors)]
112
+ E_color = E_colors[i % len(E_colors)]
113
+ E_linestyle = E_linestyles[i % len(E_linestyles)]
114
+ edgecolor = edgecolors[i % len(edgecolors)]
115
+
116
+ # Plot potential energy if it is unique
117
+ if not any(np.array_equal(system.potential_values, value) for value in unique_potentials):
118
+ unique_potentials.append(system.potential_values)
119
+ plt.plot(system.grid, system.potential_values, color=V_color, linestyle=V_linestyle)
120
+
121
+ # Plot eigenvalues
122
+ if any(system.eigenvalues):
123
+ text_offset = 3 * len(unique_groups)
124
+ if system.group not in unique_groups:
125
+ unique_groups.append(system.group)
126
+ for j, energy in enumerate(system.eigenvalues):
127
+ plt.axhline(y=energy, color=E_color, linestyle=E_linestyle)
128
+ # Textbox positions are a bit weird when plotting more than 2 systems, but whatever...
129
+ plt.text(j%3*1.0 + text_offset, energy, f'$E_{{{j}}}$ = {round(energy,4):.04f}', va='top', bbox=dict(edgecolor=edgecolor, boxstyle='round,pad=0.2', facecolor='white', alpha=0.8))
130
+ if len(systems.get_groups(var)) > 1:
131
+ plt.plot([], [], color=E_color, label=f'{system.group} Energies') # Add to legend
132
+
133
+ if len(systems.get_groups(var)) > 1:
134
+ plt.subplots_adjust(right=0.85)
135
+ plt.legend(bbox_to_anchor=(1.1, 0.5), loc='center', fontsize='small')
136
+
137
+ plt.show()
138
+
139
+
140
+ def reduced_energies(
141
+ data:list,
142
+ title:str=None,
143
+ values:list=[],
144
+ legend:list=[],
145
+ ) -> None:
146
+ """Plots the reduced energy of the system E/B vs the reduced potential energy V/B.
147
+
148
+ Takes a `data` list of System objects as input.
149
+ An optional `title` can be specified.
150
+
151
+ Optional maximum reduced potential `values` are plotted
152
+ as vertical lines (floats or ints) or regions
153
+ (lists inside the values list, from min to max).
154
+ A `legend` of the same len as `values` can be included.
155
+ These values are assumed to be divided by B by the user.
156
+ """
157
+ if values and (isinstance(values, float) or isinstance(values, int) or isinstance(values, np.float64)):
158
+ values = [values]
159
+ if values and len(values) <= len(legend):
160
+ plot_legend = True
161
+ else:
162
+ plot_legend = False
163
+ legend = [''] * len(values)
164
+ systems.as_list(data)
165
+ title = title if title else (data[0].comment if data[0].comment else 'Reduced energies')
166
+ number_of_levels = data[0].searched_E
167
+ x = []
168
+ for system in data:
169
+ potential_max_B = system.potential_max / system.B
170
+ x.append(potential_max_B)
171
+ colors = plt.cm.viridis(np.linspace(0, 1, number_of_levels+1)) # +1 to avoid the lighter tones
172
+ for i in range(number_of_levels):
173
+ y = []
174
+ for system in data:
175
+ eigenvalues_B_i = system.eigenvalues[i] / system.B
176
+ y.append(eigenvalues_B_i)
177
+ plt.plot(x, y, marker='', linestyle='-', color=colors[i])
178
+ # Add vertical lines in the specified values
179
+ line_colors = plt.cm.tab10(np.linspace(0, 1, len(values)))
180
+ for i, value in enumerate(values):
181
+ if isinstance(value, list):
182
+ min_value = min(value)
183
+ max_value = max(value)
184
+ plt.axvspan(min_value, max_value, color=line_colors[i], alpha=0.2, linestyle='', label=legend[i])
185
+ else:
186
+ plt.axvline(x=value, color=line_colors[i], linestyle='--', label=legend[i], alpha=0.5)
187
+ plt.xlabel('V$_{B}$ / B')
188
+ plt.ylabel('E / B')
189
+ plt.title(title)
190
+ if plot_legend:
191
+ plt.legend()
192
+ plt.show()
193
+
194
+
195
+ def wavefunction(
196
+ system:System,
197
+ title:str=None,
198
+ square:bool=True,
199
+ levels=[0, 1, 2],
200
+ overlap=False,
201
+ yticks:bool=False,
202
+ ) -> None:
203
+ """Plot the wavefunction of a `system` for the specified `levels`.
204
+
205
+ Wavefunctions are squared by default, showing the probabilities;
206
+ To show the actual wavefunctions, set `square = False`.
207
+
208
+ `levels` can be a list of indexes, or the number of levels to plot.
209
+
210
+ Specific wavefunctions can be overlapped with `overlap` as a list with the target indexes.
211
+ The `overlap` value can also be the max number of wavefunctions to add.
212
+ All found wavefunctions can be added together with `overlap = True`;
213
+ but note that this overlap is limited by the number of System.searched_E,
214
+ that must be specified before solving the system.
215
+ Setting `overlap` will ignore the `levels` argument.
216
+
217
+ Set `yticks = True` to plot the wavefunction yticks.
218
+ """
219
+ data = deepcopy(system)
220
+ eigenvectors = data.eigenvectors
221
+ title = title if title else (data.comment if data.comment else 'System wavefunction')
222
+ fig, ax1 = plt.subplots()
223
+ plt.title(title)
224
+ ax1.set_xlabel('Angle / radians')
225
+ ax1.set_ylabel('Potential / meV')
226
+ ax1.set_xticks([-2*np.pi, -3*np.pi/2, -np.pi, -np.pi/2, 0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi], [r'$-2\pi$', r'$-\frac{3\pi}{2}$', r'$-\pi$', r'$-\frac{\pi}{2}$', '0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$'])
227
+ ax1.plot(data.grid, data.potential_values, color='blue', linestyle='-')
228
+ ax2 = ax1.twinx()
229
+ if not yticks:
230
+ ax2.set_yticks([])
231
+ ax2.set_ylabel('Squared wavefunction' if square else 'Wavefunction')
232
+ # Set levels list
233
+ if isinstance(levels, int) or isinstance(levels, float):
234
+ levels = [x for x in range(int(levels))]
235
+ if not isinstance(levels, list):
236
+ raise ValueError('levels must be an int or a list of ints')
237
+ # Set overlap if requested
238
+ if overlap == True and isinstance(overlap, bool):
239
+ eigenvectors = [np.sum(eigenvectors, axis=0)]
240
+ levels = [0]
241
+ show_legend = False
242
+ elif overlap is not False and (isinstance(overlap, int) or isinstance(overlap, float)):
243
+ max_int = int(overlap)
244
+ eigenvectors = [np.sum(eigenvectors[:max_int], axis=0)]
245
+ levels = [0]
246
+ show_legend = False
247
+ elif isinstance(overlap, list):
248
+ eigenvectors = [np.sum([eigenvectors[i] for i in overlap], axis=0)]
249
+ levels = [0]
250
+ show_legend = False
251
+ else:
252
+ show_legend = True
253
+ # Square values if so
254
+ if square:
255
+ eigenvectors = [vec**2 for vec in eigenvectors]
256
+ # Plot the wavefunction
257
+ for i in levels:
258
+ ax2.plot(data.grid, eigenvectors[i], linestyle='--', label=f'{i}')
259
+ if show_legend:
260
+ fig.legend(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small', title='Index')
261
+ plt.show()
262
+
263
+
264
+ def splittings(
265
+ data:list,
266
+ title:str=None,
267
+ units:str='ueV'
268
+ ) -> None:
269
+ """Plot the tunnel splitting energies of a `data` list of systems.
270
+
271
+ The different `System.comment` are shown in the horizontal axis.
272
+ An optional `title` can be specified.
273
+ Default units shown are $\\mu$eV (`'ueV'`).
274
+ Available units are: `'ueV'`, `'meV'`, `'Ry'`.
275
+ """
276
+ title = title if title != None else 'Tunnel splitting energies'
277
+ calcs = deepcopy(data)
278
+ calcs = systems.as_list(calcs)
279
+
280
+ fig, ax = plt.subplots()
281
+ ax.set_ylabel("Energy / meV")
282
+
283
+ y = [c.splittings[0] for c in calcs]
284
+ x = [c.comment for c in calcs]
285
+ # What units do we want?
286
+ if units.lower() in alias.units['ueV']:
287
+ y = [j * phys.meV_to_ueV for j in y]
288
+ ax.set_ylabel("Energy / $\\mu$eV")
289
+ elif units.lower() in alias.units['Ry']:
290
+ y = [j * phys.meV_to_Ry for j in y]
291
+ ax.set_ylabel("Energy / Ry")
292
+ #else: # It's okay let's use meV
293
+
294
+ ax.bar(range(len(y)), y)
295
+ for i, comment in enumerate(x):
296
+ ax.text(x=i, y=0, s=comment+' ', rotation=45, verticalalignment='top', horizontalalignment='right')
297
+ ax.set_xlabel("")
298
+ ax.set_title(title)
299
+ ax.set_xticks([])
300
+ fig.tight_layout()
301
+ plt.show()
302
+
303
+
304
+ def convergence(data:list) -> None:
305
+ """Plot the energy convergence of a `data` list of Systems as a function of the gridsize."""
306
+ systems.as_list(data)
307
+ gridsizes = [system.gridsize for system in data]
308
+ runtimes = [system.runtime for system in data]
309
+ deviations = [] # List of lists, containing all eigenvalue deviations for every system
310
+ searched_E = data[0].searched_E
311
+ for system in data:
312
+ deviation_list = []
313
+ for i, eigenvalue in enumerate(system.eigenvalues):
314
+ ideal_E = systems.get_ideal_E(i)
315
+ deviation = abs(ideal_E - eigenvalue)
316
+ deviation_list.append(deviation)
317
+ deviation_list = deviation_list[1:] # Remove ground state
318
+ deviations.append(deviation_list)
319
+ # Plotting
320
+ fig, ax1 = plt.subplots()
321
+ ax1.set_xlabel('Grid size')
322
+ ax1.set_ylabel('Error / meV')
323
+ ax1.set_xscale('log')
324
+ ax1.set_yscale('log')
325
+ ax2 = ax1.twinx()
326
+ ax2.set_ylabel('Runtime / s')
327
+ ax2.set_yscale('log')
328
+ ax2.plot(gridsizes, runtimes, color='tab:grey', label='Runtime', linestyle='--')
329
+ colors = plt.cm.viridis(np.linspace(0, 1, searched_E)) # Should be searched_E-1 but we want to avoid lighter colors
330
+ for i in range(searched_E-1):
331
+ if i % 2 == 0: # Ignore even numbers, since those levels are degenerated.
332
+ continue
333
+ ax1.plot(gridsizes, [dev[i] for dev in deviations], label=f'$E_{{{int((i+1)/2)}}}$', color=colors[i])
334
+ fig.legend(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small')
335
+ plt.title(data[0].comment if data[0].comment else 'Energy convergence vs grid size')
336
+ plt.show()
337
+