qrotor.plot

Description

This module provides straightforward functions to plot QRotor data.

Index

potential() Potential values as a function of the angle
energies() Calculated eigenvalues
reduced_energies() Reduced energies E/B as a function of the reduced potential V/B
wavefunction() Selected wavefunctions or squared wavefunctions of a system
splittings() Tunnel splitting energies of a list of systems
convergence() Energy convergence of a list of systems calculated with different parameters

  1"""
  2# Description
  3
  4This 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 of a list of systems calculated with different parameters |
 17
 18---
 19"""
 20
 21
 22from .system import System
 23from . import systems
 24from . import constants
 25import matplotlib.pyplot as plt
 26import numpy as np
 27from copy import deepcopy
 28import aton.alias as alias
 29
 30
 31def potential(
 32        data:System|list,
 33        title:str=None,
 34        marker:str|list='',
 35        linestyle:str|list='-',
 36        cm:str=None,
 37        cm_range:tuple|list=(0.0,0.8),
 38        color:list|tuple|str=None,
 39        normalize:bool=False,
 40        ylim:tuple=None,
 41        rc:dict={},
 42        ) -> None:
 43    """Plot the potential values of `data` (System object, or list of systems).
 44
 45    Title can be customized with `title`.
 46    If empty, system[0].comment will be used as title if no more comments are present.
 47
 48    `marker` and `linestyle` can be a Matplotlib string or list of strings.
 49    Custom colormaps can be set through `cm`, in a custom `cm_range` from 0 to 1.
 50    Custom colors can also be set through `color`.
 51    Alternatively, custom color lists can be set through the `color` variable.
 52
 53    Set `normalize = True` to normalize by their respective `qrotor.system.System.potential_max`.
 54    This can be useful if you have performed subtractions or similar operations.
 55    In this case, you might also want to play with `ylim` to adjust the y-axis limits.
 56
 57    Additional matplotlib runtime configuration
 58    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
 59    can be set with the `rc` dict.
 60    """
 61    data_copy = deepcopy(data)
 62    system = systems.as_list(data_copy)
 63    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')
 64    # Marker as a list
 65    if isinstance(marker, list):
 66        if len(marker) < len(system):
 67            marker.extend([''] * (len(system) - len(marker)))
 68    else:
 69        marker = [marker] * len(system)
 70    # Linestyle as a list
 71    if isinstance(linestyle, list):
 72        if len(linestyle) < len(system):
 73            linestyle.extend(['-'] * (len(system) - len(linestyle)))
 74    else:
 75        linestyle = [linestyle] * len(system)
 76    # Custom colormap or colors
 77    colors = None
 78    if isinstance(cm, str):
 79        cmap = plt.get_cmap(cm)
 80        colors = cmap(np.linspace(cm_range[0], cm_range[1], len(system)))
 81    elif color:
 82        colors = color
 83        if isinstance(colors, list):
 84            if len(colors) < len(system):
 85                colors.extend(['grey'] * (len(system) - len(colors)))
 86        else:
 87            colors = [colors] * len(system)
 88
 89    with plt.rc_context(rc):
 90        plt.figure(layout='constrained')
 91        plt.title(title_str)
 92        plt.xlabel('Angle / rad')
 93        plt.ylabel('Potential energy / meV')
 94        if normalize:
 95            plt.ylabel('Energy / V$_{3}$')
 96            for s in system:
 97                s.potential_values = s.potential_values / s.potential_max
 98        if ylim:
 99            plt.ylim(ylim)
100
101        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$'])
102
103        if colors is not None:
104            for i, s in enumerate(system):
105                plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment, color=colors[i])
106        else:  # Regular plot
107            for i, s in enumerate(system):
108                plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment)
109
110        if all(s.comment for s in system) and len(system) != 1:
111            leg = plt.legend()
112            leg.set_draggable(True)
113
114        plt.show()
115
116
117def energies(
118        data,
119        title:str=None,
120        rc:dict={},
121        ) -> None:
122    """Plot the eigenvalues of `data` (System or a list of System objects).
123
124    You can use up to 1 tag per system to differentiate between molecular groups.
125
126    Additional matplotlib runtime configuration
127    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
128    can be set with the `rc` dict.
129    """
130    if isinstance(data, System):
131        var = [data]
132    else:  # Should be a list
133        systems.as_list(data)
134        var = data
135
136    V_colors = ['C0', 'C1', 'C2', 'C3', 'C4']
137    E_colors = ['lightblue', 'sandybrown', 'lightgrey', 'lightcoral', 'plum']
138    E_linestyles = ['--', ':', '-.']
139    edgecolors = E_colors
140
141    V_linestyle = '-'
142    title = title if title else (var[0].comment if var[0].comment else 'Energy eigenvalues')
143    ylabel_text = f'Energy / meV'
144    xlabel_text = 'Angle / radians'
145
146    with plt.rc_context(rc):
147        plt.figure(layout='constrained')
148        plt.xlabel(xlabel_text)
149        plt.ylabel(ylabel_text)
150        plt.title(title)
151        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$'])
152
153        unique_potentials = []
154        unique_groups = []
155        for i, system in enumerate(var):
156            V_color = V_colors[i % len(V_colors)]
157            E_color = E_colors[i % len(E_colors)]
158            E_linestyle = E_linestyles[i % len(E_linestyles)]
159            edgecolor = edgecolors[i % len(edgecolors)]
160
161            # Plot potential energy if it is unique
162            if not any(np.array_equal(system.potential_values, value) for value in unique_potentials):
163                unique_potentials.append(system.potential_values)
164                plt.plot(system.grid, system.potential_values, color=V_color, linestyle=V_linestyle)
165
166            # Plot eigenvalues
167            if any(system.eigenvalues):
168                text_offset = 3 * len(unique_groups)
169                if system.tags not in unique_groups:
170                    unique_groups.append(system.tags)
171                for j, energy in enumerate(system.eigenvalues):
172                    plt.axhline(y=energy, color=E_color, linestyle=E_linestyle)
173                    # Textbox positions are a bit weird when plotting more than 2 systems, but whatever...
174                    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))
175                if len(systems.get_tags(var)) > 1:
176                    plt.plot([], [], color=E_color, label=f'{system.tags} Energies')  # Add to legend
177
178        if len(systems.get_tags(var)) > 1:
179            plt.subplots_adjust(right=0.85)
180            leg = plt.legend() #(bbox_to_anchor=(1.1, 0.5), loc='center', fontsize='small')
181            leg.set_draggable(True)
182
183        plt.show()
184
185
186def reduced_energies(
187        data:list,
188        title:str=None,
189        values:list=[],
190        legend:list=[],
191        rc:dict={},
192        ) -> None:
193    """Plots the reduced energy of the system E/B vs the reduced potential energy V/B.
194
195    Takes a `data` list of System objects as input.
196    An optional `title` can be specified.
197
198    Optional maximum reduced potential `values` are plotted
199    as vertical lines (floats or ints) or regions
200    (lists inside the values list, from min to max).
201    A `legend` of the same len as `values` can be included.
202    These values are assumed to be divided by B by the user.
203
204    Additional matplotlib runtime configuration
205    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
206    can be set with the `rc` dict.
207    """
208    if values and (isinstance(values, float) or isinstance(values, int) or isinstance(values, np.float64)):
209        values = [values]
210    if values and len(values) <= len(legend):
211        plot_legend = True
212    else:
213        plot_legend = False
214        legend = [''] * len(values)
215    systems.as_list(data)
216    title = title if title else (data[0].comment if data[0].comment else 'Reduced energies')
217    number_of_levels = data[0].searched_E
218    x = []
219    for system in data:
220        potential_max_B = system.potential_max / system.B
221        x.append(potential_max_B)
222    colors = plt.cm.viridis(np.linspace(0, 1, number_of_levels+1))  # +1 to avoid the lighter tones
223
224    with plt.rc_context(rc):
225        plt.figure(layout='constrained')
226
227        for i in range(number_of_levels):
228            y = []
229            for system in data:
230                eigenvalues_B_i = system.eigenvalues[i] / system.B
231                y.append(eigenvalues_B_i)
232            plt.plot(x, y, marker='', linestyle='-', color=colors[i])
233        # Add vertical lines in the specified values
234        line_colors = plt.cm.tab10(np.linspace(0, 1, len(values)))
235        for i, value in enumerate(values):
236            if isinstance(value, list):
237                min_value = min(value)
238                max_value = max(value)
239                plt.axvspan(min_value, max_value, color=line_colors[i], alpha=0.2, linestyle='', label=legend[i])
240            else:
241                plt.axvline(x=value, color=line_colors[i], linestyle='--', label=legend[i], alpha=0.5)
242        plt.xlabel('V$_{B}$ / B')
243        plt.ylabel('E / B')
244        plt.title(title)
245        if plot_legend:
246            leg = plt.legend()
247            leg.set_draggable(True)
248        plt.show()
249
250
251def wavefunction(
252        system:System,
253        title:str=None,
254        square:bool=True,
255        levels:list|int=[0, 1, 2],
256        overlap:bool|int=False,
257        yticks:bool=False,
258        rc:dict={},
259        ) -> None:
260    """Plot the wavefunction of a `system` for the specified `levels`.
261
262    Wavefunctions are squared by default, showing the probabilities;
263    To show the actual wavefunctions, set `square = False`.
264
265    `levels` can be a list of indexes, or the number of levels to plot.
266
267    Specific wavefunctions can be overlapped with `overlap` as a list with the target indexes.
268    The `overlap` value can also be the max number of wavefunctions to add.
269    All found wavefunctions can be added together with `overlap = True`;
270    but note that this overlap is limited by the number of System.searched_E,
271    that must be specified before solving the system.
272    Setting `overlap` will ignore the `levels` argument.
273
274    Set `yticks = True` to plot the wavefunction yticks.
275
276    Additional matplotlib runtime configuration
277    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
278    can be set with the `rc` dict.
279    """
280    data = deepcopy(system)
281    eigenvectors = data.eigenvectors
282    title = title if title else (data.comment if data.comment else 'System wavefunction')
283    with plt.rc_context(rc):
284        fig, ax1 = plt.subplots(layout='constrained')
285        plt.title(title)
286        ax1.set_xlabel('Angle / radians')
287        ax1.set_ylabel('Potential / meV')
288        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$'])
289        ax1.plot(data.grid, data.potential_values, color='blue', linestyle='-')
290        ax2 = ax1.twinx()
291        if not yticks:
292            ax2.set_yticks([])
293        ax2.set_ylabel('Squared wavefunction' if square else 'Wavefunction')
294        # Set levels list
295        if isinstance(levels, int):
296            levels = [levels]
297        if not isinstance(levels, list):
298            raise ValueError('levels must be an int or a list of ints')
299        # Set overlap if requested
300        if overlap == True and isinstance(overlap, bool):
301            eigenvectors = [np.sum(eigenvectors, axis=0)]
302            levels = [0]
303            show_legend = False
304        elif overlap is not False and (isinstance(overlap, int) or isinstance(overlap, float)):
305            max_int = int(overlap)
306            eigenvectors = [np.sum(eigenvectors[:max_int], axis=0)]
307            levels = [0]
308            show_legend = False
309        elif isinstance(overlap, list):
310            eigenvectors = [np.sum([eigenvectors[i] for i in overlap], axis=0)]
311            levels = [0]
312            show_legend = False
313        if len(levels) > 1:
314            show_legend = True
315        else:
316            show_legend = False
317        # Square values if so
318        if square:
319            eigenvectors = [vec**2 for vec in eigenvectors]
320        # Plot the wavefunction
321        for i in levels:
322            ax2.plot(data.grid, eigenvectors[i], linestyle='--', label=f'{i}')
323        if show_legend:
324            leg = fig.legend()#(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small', title='Index')
325            leg.set_draggable(True)
326        plt.show()
327
328
329def splittings(
330        data:list,
331        title:str=None,
332        units:str='ueV',
333        rc:dict={},
334        ) -> None:
335    """Plot the tunnel splitting energies of a `data` list of systems.
336
337    The different `System.comment` are shown in the horizontal axis.
338    An optional `title` can be specified.
339    Default units shown are $\\mu$eV (`'ueV'`).
340    Available units are: `'ueV'`, `'meV'`, `'Ry'`, or `'B'` (free rotor units).
341
342    Additional matplotlib runtime configuration
343    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
344    can be set with the `rc` dict.
345    """
346    title = title if title != None else 'Tunnel splitting energies'
347    calcs = deepcopy(data)
348    calcs = systems.as_list(calcs)
349
350    with plt.rc_context(rc):
351        fig, ax = plt.subplots(layout='constrained')
352        ax.set_ylabel("Energy / meV")
353
354        y = [c.splittings[0] for c in calcs]
355        x = [c.comment for c in calcs]
356        # What units do we want?
357        if units.lower() in alias.units['ueV']:
358            y = [j * 1000 for j in y]  # Convert meV to micro eV
359            ax.set_ylabel("Energy / $\\mu$eV")
360        elif units.lower() in alias.units['Ry']:
361            y = [j * constants.meV_to_Ry for j in y]
362            ax.set_ylabel("Energy / Ry")
363        elif units.upper() == 'B':
364            y = [j / c.B for j, c in zip(y, calcs)]
365            ax.set_ylabel("Energy / B")
366        #else:  # It's okay let's use meV
367
368        ax.bar(range(len(y)), y)
369        for i, comment in enumerate(x):
370            ax.text(x=i, y=0, s=comment+' ', rotation=45, verticalalignment='top', horizontalalignment='right')
371        ax.set_xlabel("")
372        ax.set_title(title)
373        ax.set_xticks([])
374        fig.tight_layout()
375        plt.show()
376
377
378def convergence(data:list, rc:dict={}) -> None:
379    """Plot the energy convergence of a `data` list of Systems as a function of the gridsize.
380
381    Additional matplotlib runtime configuration
382    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
383    can be set with the `rc` dict.
384    """
385    systems.as_list(data)
386    gridsizes = [system.gridsize for system in data]
387    runtimes = [system.runtime for system in data]
388    deviations = []  # List of lists, containing all eigenvalue deviations for every system
389    searched_E = data[0].searched_E
390    for system in data:
391        deviation_list = []
392        for i, eigenvalue in enumerate(system.eigenvalues):
393            ideal_E = systems.calculate_ideal_E(i)
394            deviation = abs(ideal_E - eigenvalue)
395            deviation_list.append(deviation)
396        deviation_list = deviation_list[1:]  # Remove ground state
397        deviations.append(deviation_list)
398    # Plotting
399    with plt.rc_context(rc):
400        fig, ax1 = plt.subplots(layout='constrained')
401        ax1.set_xlabel('Grid size')
402        ax1.set_ylabel('Error / meV')
403        ax1.set_xscale('log')
404        ax1.set_yscale('log')
405        ax2 = ax1.twinx()
406        ax2.set_ylabel('Runtime / s')
407        ax2.set_yscale('log')
408        ax2.plot(gridsizes, runtimes, color='tab:grey', label='Runtime', linestyle='--')
409        colors = plt.cm.viridis(np.linspace(0, 1, searched_E))  # Should be searched_E-1 but we want to avoid lighter colors
410        for i in range(searched_E-1):
411            if i % 2 == 0:  # Ignore even numbers, since those levels are degenerated.
412                continue
413            ax1.plot(gridsizes, [dev[i] for dev in deviations], label=f'$E_{{{int((i+1)/2)}}}$', color=colors[i])
414        leg = fig.legend()#(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small')
415        leg.set_draggable(True)
416        plt.title(data[0].comment if data[0].comment else 'Energy convergence vs grid size')
417        plt.show()
def potential( data: qrotor.system.System | list, title: str = None, marker: str | list = '', linestyle: str | list = '-', cm: str = None, cm_range: tuple | list = (0.0, 0.8), color: list | tuple | str = None, normalize: bool = False, ylim: tuple = None, rc: dict = {}) -> None:
 32def potential(
 33        data:System|list,
 34        title:str=None,
 35        marker:str|list='',
 36        linestyle:str|list='-',
 37        cm:str=None,
 38        cm_range:tuple|list=(0.0,0.8),
 39        color:list|tuple|str=None,
 40        normalize:bool=False,
 41        ylim:tuple=None,
 42        rc:dict={},
 43        ) -> None:
 44    """Plot the potential values of `data` (System object, or list of systems).
 45
 46    Title can be customized with `title`.
 47    If empty, system[0].comment will be used as title if no more comments are present.
 48
 49    `marker` and `linestyle` can be a Matplotlib string or list of strings.
 50    Custom colormaps can be set through `cm`, in a custom `cm_range` from 0 to 1.
 51    Custom colors can also be set through `color`.
 52    Alternatively, custom color lists can be set through the `color` variable.
 53
 54    Set `normalize = True` to normalize by their respective `qrotor.system.System.potential_max`.
 55    This can be useful if you have performed subtractions or similar operations.
 56    In this case, you might also want to play with `ylim` to adjust the y-axis limits.
 57
 58    Additional matplotlib runtime configuration
 59    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
 60    can be set with the `rc` dict.
 61    """
 62    data_copy = deepcopy(data)
 63    system = systems.as_list(data_copy)
 64    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')
 65    # Marker as a list
 66    if isinstance(marker, list):
 67        if len(marker) < len(system):
 68            marker.extend([''] * (len(system) - len(marker)))
 69    else:
 70        marker = [marker] * len(system)
 71    # Linestyle as a list
 72    if isinstance(linestyle, list):
 73        if len(linestyle) < len(system):
 74            linestyle.extend(['-'] * (len(system) - len(linestyle)))
 75    else:
 76        linestyle = [linestyle] * len(system)
 77    # Custom colormap or colors
 78    colors = None
 79    if isinstance(cm, str):
 80        cmap = plt.get_cmap(cm)
 81        colors = cmap(np.linspace(cm_range[0], cm_range[1], len(system)))
 82    elif color:
 83        colors = color
 84        if isinstance(colors, list):
 85            if len(colors) < len(system):
 86                colors.extend(['grey'] * (len(system) - len(colors)))
 87        else:
 88            colors = [colors] * len(system)
 89
 90    with plt.rc_context(rc):
 91        plt.figure(layout='constrained')
 92        plt.title(title_str)
 93        plt.xlabel('Angle / rad')
 94        plt.ylabel('Potential energy / meV')
 95        if normalize:
 96            plt.ylabel('Energy / V$_{3}$')
 97            for s in system:
 98                s.potential_values = s.potential_values / s.potential_max
 99        if ylim:
100            plt.ylim(ylim)
101
102        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$'])
103
104        if colors is not None:
105            for i, s in enumerate(system):
106                plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment, color=colors[i])
107        else:  # Regular plot
108            for i, s in enumerate(system):
109                plt.plot(s.grid, s.potential_values, marker=marker[i], linestyle=linestyle[i], label=s.comment)
110
111        if all(s.comment for s in system) and len(system) != 1:
112            leg = plt.legend()
113            leg.set_draggable(True)
114
115        plt.show()

Plot the potential values of data (System object, or list of systems).

Title can be customized with title. If empty, system[0].comment will be used as title if no more comments are present.

marker and linestyle can be a Matplotlib string or list of strings. Custom colormaps can be set through cm, in a custom cm_range from 0 to 1. Custom colors can also be set through color. Alternatively, custom color lists can be set through the color variable.

Set normalize = True to normalize by their respective qrotor.system.System.potential_max. This can be useful if you have performed subtractions or similar operations. In this case, you might also want to play with ylim to adjust the y-axis limits.

Additional matplotlib runtime configuration rcParams can be set with the rc dict.

def energies(data, title: str = None, rc: dict = {}) -> None:
118def energies(
119        data,
120        title:str=None,
121        rc:dict={},
122        ) -> None:
123    """Plot the eigenvalues of `data` (System or a list of System objects).
124
125    You can use up to 1 tag per system to differentiate between molecular groups.
126
127    Additional matplotlib runtime configuration
128    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
129    can be set with the `rc` dict.
130    """
131    if isinstance(data, System):
132        var = [data]
133    else:  # Should be a list
134        systems.as_list(data)
135        var = data
136
137    V_colors = ['C0', 'C1', 'C2', 'C3', 'C4']
138    E_colors = ['lightblue', 'sandybrown', 'lightgrey', 'lightcoral', 'plum']
139    E_linestyles = ['--', ':', '-.']
140    edgecolors = E_colors
141
142    V_linestyle = '-'
143    title = title if title else (var[0].comment if var[0].comment else 'Energy eigenvalues')
144    ylabel_text = f'Energy / meV'
145    xlabel_text = 'Angle / radians'
146
147    with plt.rc_context(rc):
148        plt.figure(layout='constrained')
149        plt.xlabel(xlabel_text)
150        plt.ylabel(ylabel_text)
151        plt.title(title)
152        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$'])
153
154        unique_potentials = []
155        unique_groups = []
156        for i, system in enumerate(var):
157            V_color = V_colors[i % len(V_colors)]
158            E_color = E_colors[i % len(E_colors)]
159            E_linestyle = E_linestyles[i % len(E_linestyles)]
160            edgecolor = edgecolors[i % len(edgecolors)]
161
162            # Plot potential energy if it is unique
163            if not any(np.array_equal(system.potential_values, value) for value in unique_potentials):
164                unique_potentials.append(system.potential_values)
165                plt.plot(system.grid, system.potential_values, color=V_color, linestyle=V_linestyle)
166
167            # Plot eigenvalues
168            if any(system.eigenvalues):
169                text_offset = 3 * len(unique_groups)
170                if system.tags not in unique_groups:
171                    unique_groups.append(system.tags)
172                for j, energy in enumerate(system.eigenvalues):
173                    plt.axhline(y=energy, color=E_color, linestyle=E_linestyle)
174                    # Textbox positions are a bit weird when plotting more than 2 systems, but whatever...
175                    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))
176                if len(systems.get_tags(var)) > 1:
177                    plt.plot([], [], color=E_color, label=f'{system.tags} Energies')  # Add to legend
178
179        if len(systems.get_tags(var)) > 1:
180            plt.subplots_adjust(right=0.85)
181            leg = plt.legend() #(bbox_to_anchor=(1.1, 0.5), loc='center', fontsize='small')
182            leg.set_draggable(True)
183
184        plt.show()

Plot the eigenvalues of data (System or a list of System objects).

You can use up to 1 tag per system to differentiate between molecular groups.

Additional matplotlib runtime configuration rcParams can be set with the rc dict.

def reduced_energies( data: list, title: str = None, values: list = [], legend: list = [], rc: dict = {}) -> None:
187def reduced_energies(
188        data:list,
189        title:str=None,
190        values:list=[],
191        legend:list=[],
192        rc:dict={},
193        ) -> None:
194    """Plots the reduced energy of the system E/B vs the reduced potential energy V/B.
195
196    Takes a `data` list of System objects as input.
197    An optional `title` can be specified.
198
199    Optional maximum reduced potential `values` are plotted
200    as vertical lines (floats or ints) or regions
201    (lists inside the values list, from min to max).
202    A `legend` of the same len as `values` can be included.
203    These values are assumed to be divided by B by the user.
204
205    Additional matplotlib runtime configuration
206    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
207    can be set with the `rc` dict.
208    """
209    if values and (isinstance(values, float) or isinstance(values, int) or isinstance(values, np.float64)):
210        values = [values]
211    if values and len(values) <= len(legend):
212        plot_legend = True
213    else:
214        plot_legend = False
215        legend = [''] * len(values)
216    systems.as_list(data)
217    title = title if title else (data[0].comment if data[0].comment else 'Reduced energies')
218    number_of_levels = data[0].searched_E
219    x = []
220    for system in data:
221        potential_max_B = system.potential_max / system.B
222        x.append(potential_max_B)
223    colors = plt.cm.viridis(np.linspace(0, 1, number_of_levels+1))  # +1 to avoid the lighter tones
224
225    with plt.rc_context(rc):
226        plt.figure(layout='constrained')
227
228        for i in range(number_of_levels):
229            y = []
230            for system in data:
231                eigenvalues_B_i = system.eigenvalues[i] / system.B
232                y.append(eigenvalues_B_i)
233            plt.plot(x, y, marker='', linestyle='-', color=colors[i])
234        # Add vertical lines in the specified values
235        line_colors = plt.cm.tab10(np.linspace(0, 1, len(values)))
236        for i, value in enumerate(values):
237            if isinstance(value, list):
238                min_value = min(value)
239                max_value = max(value)
240                plt.axvspan(min_value, max_value, color=line_colors[i], alpha=0.2, linestyle='', label=legend[i])
241            else:
242                plt.axvline(x=value, color=line_colors[i], linestyle='--', label=legend[i], alpha=0.5)
243        plt.xlabel('V$_{B}$ / B')
244        plt.ylabel('E / B')
245        plt.title(title)
246        if plot_legend:
247            leg = plt.legend()
248            leg.set_draggable(True)
249        plt.show()

Plots the reduced energy of the system E/B vs the reduced potential energy V/B.

Takes a data list of System objects as input. An optional title can be specified.

Optional maximum reduced potential values are plotted as vertical lines (floats or ints) or regions (lists inside the values list, from min to max). A legend of the same len as values can be included. These values are assumed to be divided by B by the user.

Additional matplotlib runtime configuration rcParams can be set with the rc dict.

def wavefunction( system: qrotor.system.System, title: str = None, square: bool = True, levels: list | int = [0, 1, 2], overlap: bool | int = False, yticks: bool = False, rc: dict = {}) -> None:
252def wavefunction(
253        system:System,
254        title:str=None,
255        square:bool=True,
256        levels:list|int=[0, 1, 2],
257        overlap:bool|int=False,
258        yticks:bool=False,
259        rc:dict={},
260        ) -> None:
261    """Plot the wavefunction of a `system` for the specified `levels`.
262
263    Wavefunctions are squared by default, showing the probabilities;
264    To show the actual wavefunctions, set `square = False`.
265
266    `levels` can be a list of indexes, or the number of levels to plot.
267
268    Specific wavefunctions can be overlapped with `overlap` as a list with the target indexes.
269    The `overlap` value can also be the max number of wavefunctions to add.
270    All found wavefunctions can be added together with `overlap = True`;
271    but note that this overlap is limited by the number of System.searched_E,
272    that must be specified before solving the system.
273    Setting `overlap` will ignore the `levels` argument.
274
275    Set `yticks = True` to plot the wavefunction yticks.
276
277    Additional matplotlib runtime configuration
278    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
279    can be set with the `rc` dict.
280    """
281    data = deepcopy(system)
282    eigenvectors = data.eigenvectors
283    title = title if title else (data.comment if data.comment else 'System wavefunction')
284    with plt.rc_context(rc):
285        fig, ax1 = plt.subplots(layout='constrained')
286        plt.title(title)
287        ax1.set_xlabel('Angle / radians')
288        ax1.set_ylabel('Potential / meV')
289        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$'])
290        ax1.plot(data.grid, data.potential_values, color='blue', linestyle='-')
291        ax2 = ax1.twinx()
292        if not yticks:
293            ax2.set_yticks([])
294        ax2.set_ylabel('Squared wavefunction' if square else 'Wavefunction')
295        # Set levels list
296        if isinstance(levels, int):
297            levels = [levels]
298        if not isinstance(levels, list):
299            raise ValueError('levels must be an int or a list of ints')
300        # Set overlap if requested
301        if overlap == True and isinstance(overlap, bool):
302            eigenvectors = [np.sum(eigenvectors, axis=0)]
303            levels = [0]
304            show_legend = False
305        elif overlap is not False and (isinstance(overlap, int) or isinstance(overlap, float)):
306            max_int = int(overlap)
307            eigenvectors = [np.sum(eigenvectors[:max_int], axis=0)]
308            levels = [0]
309            show_legend = False
310        elif isinstance(overlap, list):
311            eigenvectors = [np.sum([eigenvectors[i] for i in overlap], axis=0)]
312            levels = [0]
313            show_legend = False
314        if len(levels) > 1:
315            show_legend = True
316        else:
317            show_legend = False
318        # Square values if so
319        if square:
320            eigenvectors = [vec**2 for vec in eigenvectors]
321        # Plot the wavefunction
322        for i in levels:
323            ax2.plot(data.grid, eigenvectors[i], linestyle='--', label=f'{i}')
324        if show_legend:
325            leg = fig.legend()#(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small', title='Index')
326            leg.set_draggable(True)
327        plt.show()

Plot the wavefunction of a system for the specified levels.

Wavefunctions are squared by default, showing the probabilities; To show the actual wavefunctions, set square = False.

levels can be a list of indexes, or the number of levels to plot.

Specific wavefunctions can be overlapped with overlap as a list with the target indexes. The overlap value can also be the max number of wavefunctions to add. All found wavefunctions can be added together with overlap = True; but note that this overlap is limited by the number of System.searched_E, that must be specified before solving the system. Setting overlap will ignore the levels argument.

Set yticks = True to plot the wavefunction yticks.

Additional matplotlib runtime configuration rcParams can be set with the rc dict.

def splittings(data: list, title: str = None, units: str = 'ueV', rc: dict = {}) -> None:
330def splittings(
331        data:list,
332        title:str=None,
333        units:str='ueV',
334        rc:dict={},
335        ) -> None:
336    """Plot the tunnel splitting energies of a `data` list of systems.
337
338    The different `System.comment` are shown in the horizontal axis.
339    An optional `title` can be specified.
340    Default units shown are $\\mu$eV (`'ueV'`).
341    Available units are: `'ueV'`, `'meV'`, `'Ry'`, or `'B'` (free rotor units).
342
343    Additional matplotlib runtime configuration
344    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
345    can be set with the `rc` dict.
346    """
347    title = title if title != None else 'Tunnel splitting energies'
348    calcs = deepcopy(data)
349    calcs = systems.as_list(calcs)
350
351    with plt.rc_context(rc):
352        fig, ax = plt.subplots(layout='constrained')
353        ax.set_ylabel("Energy / meV")
354
355        y = [c.splittings[0] for c in calcs]
356        x = [c.comment for c in calcs]
357        # What units do we want?
358        if units.lower() in alias.units['ueV']:
359            y = [j * 1000 for j in y]  # Convert meV to micro eV
360            ax.set_ylabel("Energy / $\\mu$eV")
361        elif units.lower() in alias.units['Ry']:
362            y = [j * constants.meV_to_Ry for j in y]
363            ax.set_ylabel("Energy / Ry")
364        elif units.upper() == 'B':
365            y = [j / c.B for j, c in zip(y, calcs)]
366            ax.set_ylabel("Energy / B")
367        #else:  # It's okay let's use meV
368
369        ax.bar(range(len(y)), y)
370        for i, comment in enumerate(x):
371            ax.text(x=i, y=0, s=comment+' ', rotation=45, verticalalignment='top', horizontalalignment='right')
372        ax.set_xlabel("")
373        ax.set_title(title)
374        ax.set_xticks([])
375        fig.tight_layout()
376        plt.show()

Plot the tunnel splitting energies of a data list of systems.

The different System.comment are shown in the horizontal axis. An optional title can be specified. Default units shown are $\mu$eV ('ueV'). Available units are: 'ueV', 'meV', 'Ry', or 'B' (free rotor units).

Additional matplotlib runtime configuration rcParams can be set with the rc dict.

def convergence(data: list, rc: dict = {}) -> None:
379def convergence(data:list, rc:dict={}) -> None:
380    """Plot the energy convergence of a `data` list of Systems as a function of the gridsize.
381
382    Additional matplotlib runtime configuration
383    [rcParams](https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)
384    can be set with the `rc` dict.
385    """
386    systems.as_list(data)
387    gridsizes = [system.gridsize for system in data]
388    runtimes = [system.runtime for system in data]
389    deviations = []  # List of lists, containing all eigenvalue deviations for every system
390    searched_E = data[0].searched_E
391    for system in data:
392        deviation_list = []
393        for i, eigenvalue in enumerate(system.eigenvalues):
394            ideal_E = systems.calculate_ideal_E(i)
395            deviation = abs(ideal_E - eigenvalue)
396            deviation_list.append(deviation)
397        deviation_list = deviation_list[1:]  # Remove ground state
398        deviations.append(deviation_list)
399    # Plotting
400    with plt.rc_context(rc):
401        fig, ax1 = plt.subplots(layout='constrained')
402        ax1.set_xlabel('Grid size')
403        ax1.set_ylabel('Error / meV')
404        ax1.set_xscale('log')
405        ax1.set_yscale('log')
406        ax2 = ax1.twinx()
407        ax2.set_ylabel('Runtime / s')
408        ax2.set_yscale('log')
409        ax2.plot(gridsizes, runtimes, color='tab:grey', label='Runtime', linestyle='--')
410        colors = plt.cm.viridis(np.linspace(0, 1, searched_E))  # Should be searched_E-1 but we want to avoid lighter colors
411        for i in range(searched_E-1):
412            if i % 2 == 0:  # Ignore even numbers, since those levels are degenerated.
413                continue
414            ax1.plot(gridsizes, [dev[i] for dev in deviations], label=f'$E_{{{int((i+1)/2)}}}$', color=colors[i])
415        leg = fig.legend()#(loc='upper right', bbox_to_anchor=(0.9, 0.88), fontsize='small')
416        leg.set_draggable(True)
417        plt.title(data[0].comment if data[0].comment else 'Energy convergence vs grid size')
418        plt.show()

Plot the energy convergence of a data list of Systems as a function of the gridsize.

Additional matplotlib runtime configuration rcParams can be set with the rc dict.