Source code for qumada.utils.plotting

# Copyright (c) 2023 JARA Institute for Quantum Information
#
# This file is part of QuMADA.
#
# QuMADA is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# QuMADA is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# QuMADA. If not, see <https://www.gnu.org/licenses/>.
#
# Contributors:
# - Sionludi Lab
# - Till Huckeman
# %%

import matplotlib
import matplotlib.ticker as ticker

# from qumada.instrument.mapping.base import flatten_list
import numpy as np
from matplotlib import pyplot as plt
from qcodes.dataset.data_export import reshape_2D_data

from qumada.utils.load_from_sqlite_db import (
    get_parameter_data,
    pick_measurements,
    separate_up_down,
)


# %%
def _handle_overload(*args, output_dimension: int = 1, x_name=None, y_name=None, z_name=None, **kwargs):
    """
    Reduces the amount of input parameters to output_dimension according to
    user input.
    """
    all_params = list(args)
    params = [None for i in range(output_dimension)]
    if len(all_params) == output_dimension:
        return all_params
    if x_name:
        for i, param in enumerate(all_params):
            if param[0] == x_name:
                params[0] = all_params.pop(i)
                output_dimension -= 1
    if y_name:
        for i, param in enumerate(all_params):
            if param[0] == y_name:
                params[1] = all_params.pop(i)
                output_dimension -= 1
    if z_name:
        for i, param in enumerate(all_params):
            if param[0] == z_name:
                params[2] = all_params.pop(i)
                output_dimension -= 1

    print(f"To many parameters found. Please choose {output_dimension} parameter(s)")
    for i in range(0, output_dimension):
        for idx, j in enumerate(all_params):
            print(f"{idx} : {j[0]}")
        choice = input("Please enter ID: ")
        for k in range(len(params)):
            if not params[k]:
                params[k] = all_params.pop(int(choice))
    return params


def _get_scaled_unit_and_factor(unit: str, values: list):
    """
    Determines the best scaling factor and prefix for the given values.
    """
    prefixes = {-12: "p", -9: "n", -6: "µ", -3: "m", 0: "", 3: "k", 6: "M", 9: "G"}
    abs_max_value = max(abs(min(values)), abs(max(values)))
    exponent = int(np.floor(np.log10(abs_max_value)) // 3 * 3) if abs_max_value != 0 else 0
    prefix = prefixes.get(exponent, "")
    scaling_factor = 10 ** (-exponent)
    return scaling_factor, f"{prefix}{unit}"


def _rescale_axis(axis, data, unit, axis_type="x"):
    """
    Rescales the axis ticks to avoid scientific notation and sets appropriate labels.
    """
    factor, scaled_unit = _get_scaled_unit_and_factor(unit, data)
    axis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x * factor:.0f}"))
    return factor, scaled_unit


[docs]def get_parameter_name_by_label(dataset, label): """ Returns the parameter name for a given parameter label in a QCoDeS dataset. Parameters ---------- dataset : qcodes.dataset The QCoDeS dataset containing the parameters. label : str The label of the parameter for which to find the name. Returns ------- str The name of the parameter corresponding to the provided label. Raises ------ ValueError If no parameter matches the label or if multiple parameters share the same label. """ matching_parameters = [] # Iterate over all parameters in the dataset and compare labels for parameter in dataset.get_parameters(): if parameter.label == label: matching_parameters.append(parameter.name) # Handle different cases if not matching_parameters: raise ValueError(f"No parameter found with label '{label}'.") elif len(matching_parameters) > 1: raise ValueError(f"Multiple parameters found with label '{label}': {matching_parameters}") else: return matching_parameters[0]
[docs]def plot_2D( x_data, y_data, z_data, fig=None, ax=None, x_label=None, y_label=None, z_label=None, scale_axis=True, *args, **kwargs, ): """ Plots 2D derivatives. Requires tuples of name and 1D arrays corresponding to x, y, and z data as input. Supports axis and colorbar scaling. Parameters ---------- x_data, y_data, z_data : tuple Tuples containing name, values, units, and labels of x, y, and z axes. fig : matplotlib.figure.Figure, optional The figure object. Default is None. ax : matplotlib.axes.Axes, optional The axis object. Default is None. x_label, y_label, z_label : str, optional Custom axis labels. Default is None. scale_axis : bool, optional If True, rescales axes and colorbar to use SI prefixes. Default is True. *args, **kwargs Additional arguments for flexibility. Returns ------- fig, ax : tuple The figure and axis objects for further customization. """ if args: x_data, y_data, z_data = _handle_overload(x_data, y_data, z_data, *args, output_dimension=3) if ax is None or fig is None: fig, ax = plt.subplots() # Skalierung der Achsendaten und Einheiten x_values, y_values, z_values = x_data[1], y_data[1], z_data[1] x_unit, y_unit, z_unit = x_data[2], y_data[2], z_data[2] if scale_axis: x_scale, scaled_x_unit = _get_scaled_unit_and_factor(x_unit, x_values) y_scale, scaled_y_unit = _get_scaled_unit_and_factor(y_unit, y_values) z_scale, scaled_z_unit = _get_scaled_unit_and_factor(z_unit, z_values) # Skaliere die Daten x_values = np.array(x_values) * x_scale y_values = np.array(y_values) * y_scale z_values = np.array(z_values) * z_scale # Aktualisiere die Einheiten x_unit, y_unit, z_unit = scaled_x_unit, scaled_y_unit, scaled_z_unit # Reshape der Daten für 2D-Darstellung x, y, z = reshape_2D_data(x_values, y_values, z_values) # Plotten der 2D-Daten im = ax.pcolormesh(x, y, z, shading="auto") cbar = fig.colorbar(im, ax=ax) cbar.set_label(f"{z_data[3]} ({z_unit})") # Achsentitel aktualisieren x_label = x_label or f"{x_data[3]} ({x_unit})" y_label = y_label or f"{y_data[3]} ({y_unit})" ax.set_xlabel(x_label) ax.set_ylabel(y_label) plt.tight_layout() plt.show() return fig, ax
# %%
[docs]def plot_2D_grad(x_data, y_data, z_data, *args, direction="both"): """ Plots 2D derivatives. Requires tuples of name and 1D arrays corresponding to x, y and z data as input. Works well with QuMADA "get_parameter_data" method found in load_from_sqlite. direction argument can be x, y or z corresponding to the direction of the gradient used. "both" adds the gradients quadratically. TODO: Add get_parameter_data method as default to call when no data is provided TODO: Add further image manipulation and line detection functionality """ if args: x_data, y_data, z_data = _handle_overload(x_data, y_data, z_data, *args, output_dimension=3) fig, ax = plt.subplots() x, y, z = reshape_2D_data(x_data[1], y_data[1], z_data[1]) z_gradient = np.gradient(z, x, y) if direction == "both": grad = np.sqrt(z_gradient[0] ** 2 + z_gradient[1] ** 2) elif direction == "x": grad = z_gradient[0] elif direction == "y": grad = z_gradient[1] im = plt.pcolormesh(x, y, grad) fig.colorbar(im, ax=ax, label=f"Gradient of {z_data[0]} in {z_data[2]}/{x_data[2]}") plt.xlabel(f"{x_data[0]} in {x_data[2]}") plt.ylabel(f"{y_data[0]} in {y_data[2]}") plt.show() return fig, ax
# %%
[docs]def plot_2D_sec_derivative(x_data, y_data, z_data, *args): """ Plots second derivative of data. Requires tuples of name and 1D arrays corresponding to x, y and z data as input. Works well with QuMADA "get_parameter_data" method found in load_from_sqlite. direction argument can be x, y or z corresponding to the direction of the gradient used. "both" adds the gradients quadratically. TODO: Add get_parameter_data method as default to call when no data is provided TODO: Add further image manipulation and line detection functionality """ if args: x_data, y_data, z_data = _handle_overload(x_data, y_data, z_data, *args, output_dimension=3) fig, ax = plt.subplots() x, y, z = reshape_2D_data(x_data[1], y_data[1], z_data[1]) z_gradient = np.gradient(z, x, y) z_g_x = np.gradient(z_gradient[0], x, y) z_g_y = np.gradient(z_gradient[1], x, y) grad2 = np.sqrt(z_g_x[0] ** 2 + z_g_y[1] ** 2 + 2 * z_g_x[1] * z_g_y[0]) im = plt.pcolormesh(x, y, grad2) fig.colorbar(im, ax=ax, label="2nd Derivative of {z[1]}") plt.xlabel(f"{x_data[0]} in {x_data[2]}") plt.ylabel(f"{y_data[0]} in {y_data[2]}") plt.show() return fig, ax
# %%
[docs]def plot_hysteresis(dataset, x_name, y_name): fig, ax = plt.subplots() grad = np.gradient(dataset[x_name]) curr_sign = np.sign(grad[0]) data_list_x = list() data_list_y = list() start_helper = 0 for i in range(0, len(grad)): if np.sign(grad[i]) != curr_sign: data_list_x.append(dataset[x_name][start_helper:i]) data_list_y.append(dataset[y_name][start_helper:i]) start_helper = i + 1 curr_sign = np.sign(grad[i]) data_list_x.append(dataset[x_name][start_helper : len(grad)]) data_list_y.append(dataset[y_name][start_helper : len(grad)]) for i in range(0, len(data_list_x)): plt.plot(data_list_x[i], data_list_y[i]) plt.show() return fig, ax
# %%
[docs]def plot_hysteresis_new(x_data, y_data): fig, ax = plt.subplots() grad = np.gradient(x_data[1]) curr_sign = np.sign(grad[0]) signs = [curr_sign] data_list_x = list() data_list_y = list() start_helper = 0 for i in range(0, len(grad)): if np.sign(grad[i]) != curr_sign: data_list_x.append(x_data[1][start_helper:i]) data_list_y.append(y_data[1][start_helper:i]) start_helper = i + 1 curr_sign = np.sign(grad[i]) signs.append(curr_sign) data_list_x.append(x_data[1][start_helper : len(grad)]) data_list_y.append(y_data[1][start_helper : len(grad)]) for i in range(0, len(data_list_x)): options = {-1: "v", 1: "^"} plt.plot(data_list_x[i], data_list_y[i], options[signs[i]]) plt.show() return fig, ax
# %%
[docs]def plot_multiple_datasets( datasets: list = None, x_axis_parameters_name: str = None, y_axis_parameters_name: str = None, plot_hysteresis: bool = True, ax=None, fig=None, scale_axis=True, legend=True, exclude_string_from_legend: list = [], **kwargs, ): """ Plot multiple datasets from a QCoDeS database into a single figure. This function supports plotting and can handle multiple datasets. It automatically manages axis labels, legends, and optionally rescales the axes to use appropriate SI prefixes (e.g., µA, mV) instead of scientific notation. Parameters ---------- datasets : list, optional List of QCoDeS datasets to plot. If None, the function allows you to pick measurements from the currently loaded QCoDeS database. Default is None. x_axis_parameters_name : str, optional The name of the parameter to use for the x-axis. If None, you will be prompted to select it individually for each dataset if more than one parameter exists. Default is None. y_axis_parameters_name : str, optional The name of the parameter to use for the y-axis. If None, you will be prompted to select it individually for each dataset if more than one parameter exists. Default is None. plot_hysteresis : bool, optional If True, separates datasets with multiple sweeps into different curves based on the monotonicity of the x-axis data. For example, foresweep and backsweep can be plotted with distinct markers. Default is True. ax : matplotlib.axes._axes.Axes, optional Matplotlib axis to plot on. If None, a new figure and axis will be created. Default is None. fig : matplotlib.figure.Figure, optional Matplotlib figure object. Required if `ax` is provided. Default is None. scale_axis : bool, optional If True, rescales the x- and y-axes to use SI prefixes (e.g., µ, m, k) instead of scientific notation for better readability. Default is True. **kwargs : dict Additional keyword arguments for customizing the plot. For example: - font: int, font size for the plot. - marker: str, marker style for the data points. - markersize: int, size of the markers. - legend_fontsize: int, font size for the legend. - legend_markerscale: float, scale factor for legend markers. Returns ------- ax : matplotlib.axes._axes.Axes The axis object containing the plotted data. Notes ----- - This function assumes the input datasets are from QCoDeS and compatible with the `get_parameter_data` function. - Axis scaling is applied only when `scale_axis` is True, and the scaling factor is calculated based on the data range. - Monotonicity of the x-axis is used to detect and separate hysteresis loops. """ if not datasets: datasets = pick_measurements() x_data = list() y_data = list() x_units = list() y_units = list() if kwargs.get("font", None) is not None: matplotlib.rc("font", size=35) if ax is None or fig is None: fig, ax = plt.subplots(figsize=(30, 30)) x_labels = [] y_labels = [] for i in range(len(datasets)): label = datasets[i].name for string in exclude_string_from_legend: label = label.strip(string) x, y = _handle_overload( *get_parameter_data(datasets[i], y_axis_parameters_name), x_name=x_axis_parameters_name, y_name=y_axis_parameters_name, output_dimension=2, ) x_data.append(x[1]) y_data.append(y[1]) x_labels.append(x[3]) y_labels.append(y[3]) x_units.append(x[2]) y_units.append(y[2]) if plot_hysteresis: x_s, y_s, signs = separate_up_down(x_data[i], y_data[i]) for j in range(len(x_s)): if signs[j] == 1: marker = "^" f_label = f"{label} foresweep" else: marker = "v" f_label = f"{label} backsweep" plt.plot( x_s[j], y_s[j], marker, label=f_label, markersize=kwargs.get("markersize", 15), ) else: plt.plot( x_data[i], y_data[i], marker=kwargs.get("marker", "."), label=label, markersize=kwargs.get("markersize", 15), ) # Scale axes and update labels if scale_axis is True: x_scaling_factor, x_units[0] = _rescale_axis(ax.xaxis, np.concatenate(x_data), x_units[0], "x") y_scaling_factor, y_units[0] = _rescale_axis(ax.yaxis, np.concatenate(y_data), y_units[0], "y") plt.xlabel(f"{x_labels[0]} ({x_units[0]})") plt.ylabel(f"{y_labels[0]} ({y_units[0]})") # Update x and y labels if legend is True: plt.legend( loc=kwargs.get("legend_position", "upper left"), fontsize=kwargs.get("legend_fontsize", 15), markerscale=kwargs.get("legend_markerscale", 1), ) plt.tight_layout() return ax
# %%
[docs]def separate_up_down_old(x_data, y_data): grad = np.gradient(x_data) curr_sign = np.sign(grad[0]) data_list_x = list() data_list_y = list() start_helper = 0 for i in range(0, len(grad)): if np.sign(grad[i]) != curr_sign: data_list_x.append(x_data[start_helper:i]) data_list_y.append(y_data[start_helper:i]) start_helper = i + 1 curr_sign = np.sign(grad[i]) data_list_x.append(x_data[start_helper : len(grad)]) data_list_y.append(y_data[start_helper : len(grad)]) return data_list_x, data_list_y
# %%
[docs]def hysteresis(dataset, I_threshold=15e-12, parameter_name="lockin_current"): data = list(get_parameter_data(dataset=dataset, parameter_name=parameter_name)) voltage = data[0][1] current = data[1][1] v, c = separate_up_down_old(voltage, current) for i in range(0, len(v[0])): if c[0][i] >= I_threshold: V_threshold_up = v[0][i] break for i in range(0, len(v[1])): if c[1][i] <= I_threshold: V_threshold_down = v[1][i] break return V_threshold_up, V_threshold_down