Source code for pennylane.fourier.visualize

# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains visualization functions for Fourier series and coefficients."""
import warnings
from itertools import product
import numpy as np

# pylint:disable=too-many-arguments,blacklisted-name

# Matplotlib is not a hard requirement for PennyLane in general, but it *is*
# a hard requirement for everything in this module.
try:
    from matplotlib.colors import to_rgb
except (ModuleNotFoundError, ImportError) as e:
    raise ImportError(
        "Module matplotlib is required for visualization in the Fourier module. "
        "You can install matplolib via \n\n   pip install matplotlib"
    ) from e

from .utils import to_dict, format_nvec


def _validate_coefficients(coeffs, n_inputs, can_be_list=True):
    """Helper function to validate input coefficients of plotting functions.

    Args:
        coeffs (array[complex]): A set (or list of sets) of Fourier coefficients of a
            n_inputs-dimensional function.
        n_inputs (int): The number of inputs (dimension) of the function the coefficients are for.
        can_be_list (bool): Whether or not the plotting function accepts a list of
            coefficients, or only a single set.

    Raises:
        TypeError: If the coefficients are not a list or array.
        ValueError: if the coefficients are not a suitable type for the plotting function.
    """
    # Make sure we have a list or numpy array
    if not isinstance(coeffs, list) and not isinstance(coeffs, np.ndarray):
        raise TypeError(
            "Input to coefficient plotting functions must be a list of numerical "
            f"Fourier coefficients. Received input of type {type(coeffs)}"
        )

    # In case we have a list, turn it into a numpy array
    if isinstance(coeffs, list):
        coeffs = np.array(coeffs)

    # Check if the user provided a single set of coefficients to a function that is
    # meant to accept multiple samples; add an extra dimension around it if needed
    if len(coeffs.shape) == n_inputs and can_be_list:
        coeffs = np.array([coeffs])

    # Check now that we have the right number of axes for the type of function
    required_shape_size = n_inputs + 1 if can_be_list else n_inputs
    if len(coeffs.shape) != required_shape_size:
        raise ValueError(
            f"Plotting function expected a list of {n_inputs}-dimensional inputs. "
            f"Received coefficients of {len(coeffs.shape)}-dimensional function."
        )

    # Shape in all dimensions of a single set of coefficients must be the same
    shape_set = set(coeffs.shape[1:]) if can_be_list else set(coeffs.shape)
    if len(shape_set) != 1:
        raise ValueError(
            "All dimensions of coefficient array must be the same. "
            f"Received array with dimensions {coeffs.shape}"
        )

    # Size of each sample dimension must be 2d + 1 where d is the degree
    shape_dim = coeffs.shape[1] if can_be_list else coeffs.shape[0]
    if (shape_dim - 1) % 2 != 0:
        raise ValueError(
            "Shape of input coefficients must be 2d + 1, where d is the largest frequency. "
            f"Coefficient array with shape {coeffs.shape} is invalid."
        )

    # Return the coefficients; we may have switched to a numpy array or added a needed extra dimension
    return coeffs


def _extract_data_and_labels(coeffs):
    """Helper function for creating frequency labels and partitioning data.

    Args:
        coeffs (array[complex]): A list of sets of Fourier coefficients.

    Returns:
        (list(str), dict[str, array[complex]): The set of frequency labels, and a data
            dictionary split into real and imaginary parts.
    """
    # extract the x ticks
    nvecs = list(to_dict(coeffs[0]).keys())
    nvecs_formatted = [format_nvec(nvec) for nvec in nvecs]

    # make data
    data = {}
    data["real"] = np.array([[c[nvec].real for nvec in nvecs] for c in coeffs])
    data["imag"] = np.array([[c[nvec].imag for nvec in nvecs] for c in coeffs])

    return nvecs_formatted, data


def _adjust_spine_placement(ax):
    """Helper function to set some common axis properties when plotting."""
    ax.xaxis.grid()
    ax.spines["top"].set_visible(False)
    ax.spines["bottom"].set_position("zero")
    ax.spines["right"].set_visible(False)
    ax.set_axisbelow(True)


[docs]def violin(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True): """Plots a list of sets of Fourier coefficients as a violin plot. Args: coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the coefficient arrays should resemble that of the output of NumPy/SciPy's ``fftn`` function, or :func:`~.pennylane.fourier.coefficients`. n_inputs (int): The number of input variables in the function. ax (array[matplotlib.axes.Axes]): Axis on which to plot. Must be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``. colour_dict (dict[str, str]): A dictionary of the form ``{"real" : colour_string, "imag" : other_colour_string}`` indicating which colours should be used in the plot. show_freqs (bool): Whether or not to print the frequency labels on the plot axis. Returns: array[matplotlib.axes.Axes]: The axes on which the data is plotted. **Example** Suppose we have the following quantum function: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit_with_weights(w, x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) qml.Rot(*w[0], wires=0) qml.Rot(*w[1], wires=1) qml.CNOT(wires=[1, 0]) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) return qml.expval(qml.PauliZ(0)) We would like to compute and plot the distribution of Fourier coefficients for many random values of the weights ``w``. First, we generate all the coefficients: .. code-block:: python from functools import partial coeffs = [] n_inputs = 2 degree = 2 for _ in range(100): weights = np.random.normal(0, 1, size=(2, 3)) c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree) coeffs.append(c) We can now plot by setting up a pair of ``matplotlib`` axes and passing them to the plotting function: >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4)) >>> violinplt(coeffs, n_inputs, ax, show_freqs=True) .. image:: ../../_static/fourier_vis_violin.png :align: center :width: 800px :target: javascript:void(0); """ coeffs = _validate_coefficients(coeffs, n_inputs, True) # Check axis shape if ax.size != 2: raise ValueError("Matplotlib axis should consist of two subplots.") if colour_dict is None: colour_dict = {"real": "purple", "imag": "green"} # Get the labels and data nvecs_formatted, data = _extract_data_and_labels(coeffs) for (data_type, axis) in zip(["real", "imag"], ax): violinplt = axis.violinplot(data[data_type], showextrema=False) for bd in violinplt["bodies"]: bd.set_color(colour_dict[data_type]) bd.set_alpha(0.7) axis.set_ylabel(data_type) axis.xaxis.set_ticks(np.arange(1, len(data[data_type][0]) + 1)) _adjust_spine_placement(axis) # Format axes ax[0].tick_params(axis="x", colors="white") # hack to get rid of ticks but keep grid if show_freqs: ax[1].tick_params(axis="x", which="both", length=0) # remove ticks without removing labels ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey") ax[1].xaxis.set_ticks_position("top") else: ax[1].tick_params(axis="x", colors="white") # hack to get rid of ticks but keep grid return ax
[docs]def box(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True, show_fliers=True): """Plot a list of sets of Fourier coefficients as a box plot. Args: coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the coefficient arrays should resemble that of the output of numpy/scipy's ``fftn`` function, or :func:`~.pennylane.fourier.coefficients`. n_inputs (int): The number of input variables in the function. ax (array[matplotlib.axes.Axes]): Axis on which to plot. Must be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``. colour_dict (dict[str, str]): A dictionary of the form {"real" : colour_string, "imag" : other_colour_string} indicating which colours should be used in the plot. show_freqs (bool): Whether or not to print the frequency labels on the plot axis. show_fliers (bool): Whether to display the box plot outliers. Returns: array[matplotlib.axes.Axes]: The axes after plotting is complete. **Example** Suppose we have the following quantum function: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit_with_weights(w, x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) qml.Rot(*w[0], wires=0) qml.Rot(*w[1], wires=1) qml.CNOT(wires=[1, 0]) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) return qml.expval(qml.PauliZ(0)) We would like to compute and plot the distribution of Fourier coefficients for many random values of the weights ``w``. First, we generate all the coefficients: .. code-block:: python from functools import partial coeffs = [] n_inputs = 2 degree = 2 for _ in range(100): weights = np.random.normal(0, 1, size=(2, 3)) c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree) coeffs.append(c) We can now plot by setting up a pair of ``matplotlib`` axes and passing them to the plotting function: >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4)) >>> box(coeffs, n_inputs, ax, show_freqs=True) .. image:: ../../_static/fourier_vis_box.png :align: center :width: 800px :target: javascript:void(0); """ coeffs = _validate_coefficients(coeffs, n_inputs, True) # Check axis shape if ax.size != 2: raise ValueError("Matplotlib axis should consist of two subplots.") # The axis received must be a pair of axes in a subplot. if colour_dict is None: colour_dict = {"real": "purple", "imag": "green"} # Get the labels and data nvecs_formatted, data = _extract_data_and_labels(coeffs) for (data_type, axis) in zip(["real", "imag"], ax): data_colour = colour_dict[data_type] axis.boxplot( data[data_type], boxprops=dict( facecolor=to_rgb(data_colour) + (0.4,), color=data_colour, edgecolor=data_colour ), medianprops=dict(color=data_colour, linewidth=1.5), flierprops=dict(markeredgecolor=data_colour), whiskerprops=dict(color=data_colour), capprops=dict(color=data_colour), patch_artist=True, showfliers=show_fliers, ) _adjust_spine_placement(axis) axis.set_ylabel(data_type) axis.xaxis.set_ticks(np.arange(1, len(nvecs_formatted) + 1)) ax[0].tick_params(axis="x", colors="white") # hack to get rid of ticks but keep grid if show_freqs: ax[1].tick_params(axis="x", which="both", length=0) # remove ticks without removing labels ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey") ax[1].xaxis.set_ticks_position("top") else: ax[1].tick_params(axis="x", colors="white") # hack to get rid of ticks but keep grid return ax
[docs]def bar(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True): """Plot a set of Fourier coefficients as a bar plot. Args: coeffs (array[complex]): A single set of Fourier coefficients. The dimensions of the coefficient array should be ``(2d + 1, ) * n_inputs`` where ``d`` is the largest frequency. n_inputs (int): The number of input variables in the function. ax (list[matplotlib.axes.Axes]): Axis on which to plot. Must be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``. colour_dict (dict[str, str]): A dictionary of the form ``{"real" : colour_string, "imag" : other_colour_string}`` indicating which colours should be used in the plot. show_freqs (bool): Whether or not to print the frequency labels on the plot axis. Returns: array[matplotlib.axes.Axes]: The axes after plotting is complete. **Example** Suppose we have the following quantum function: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit_with_weights(w, x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) qml.Rot(*w[0], wires=0) qml.Rot(*w[1], wires=1) qml.CNOT(wires=[1, 0]) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) return qml.expval(qml.PauliZ(0)) We would like to compute and plot a single set of Fourier coefficients. We will choose some values for ``w`` at random: .. code-block:: python from functools import partial n_inputs = 2 degree = 2 weights = np.random.normal(0, 1, size=(2, 3)) coeffs = coefficients(partial(circuit_with_weights, weights), n_inputs, degree) We can now plot by setting up a pair of ``matplotlib`` axes and passing them to the plotting function: >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4)) >>> bar(coeffs, n_inputs, ax, colour_dict={"real" : "red", "imag" : "blue"}) .. image:: ../../_static/fourier_vis_bar_plot_2.png :align: center :width: 800px :target: javascript:void(0); """ coeffs = _validate_coefficients(coeffs, n_inputs, False) # Check axis shape if ax.size != 2: raise ValueError("Matplotlib axis should consist of two subplots.") # The axis received must be a pair of axes in a subplot. if colour_dict is None: colour_dict = {"real": "purple", "imag": "green"} # Get the labels and data nvecs_formatted, data = _extract_data_and_labels(np.array([coeffs])) data_len = len(data["real"][0]) for (data_type, axis) in zip(["real", "imag"], ax): axis.bar(np.arange(data_len), data[data_type][0], color=colour_dict[data_type], alpha=0.7) axis.set_ylabel(data_type) axis.xaxis.set_ticks(np.arange(data_len)) _adjust_spine_placement(axis) ax[0].tick_params(axis="x", colors="white") # hack to get rid of ticklabels but keep grid if show_freqs: ax[1].tick_params(axis="x", which="both", length=0) # remove ticks without removing labels ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey") ax[1].xaxis.set_ticks_position("top") else: ax[1].tick_params(axis="x", colors="white") # hack to get rid of ticklabels but keep grid return ax
[docs]def panel(coeffs, n_inputs, ax, colour=None): """Plot a list of sets of coefficients in the complex plane for a 1- or 2-dimensional function. Args: coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the coefficient arrays must all be either 1- or 2-dimensional, i.e., each array should have shape ``(2d + 1,)`` for the 1-dimensional case, or ``(2d + 1, 2d + 1)`` where ``d`` is the degree, i.e., the maximum frequency of present in the coefficients. Such an array may be the output of the numpy/scipy ``fft``/``fft2`` functions, or :func:`~.pennylane.fourier.coefficients`. n_inputs (int): The number of variables in the function. ax (array[matplotlib.axes._subplots.AxesSubplot]): Axis on which to plot. For 1-dimensional data, length must be the number of frequencies. For 2-dimensional data, must be a grid that matches the dimensions of a single set of coefficients. colour (str): The outline colour of the points on the plot. Returns: array[matplotlib.axes.Axes]: The axes after plotting is complete. **Example** Suppose we have the following quantum function: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit_with_weights(w, x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) qml.Rot(*w[0], wires=0) qml.Rot(*w[1], wires=1) qml.CNOT(wires=[1, 0]) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) return qml.expval(qml.PauliZ(0)) We would like to compute and plot the distribution of Fourier coefficients for many random values of the weights ``w``. First, we generate all the coefficients: .. code-block:: python from functools import partial coeffs = [] n_inputs = 2 degree = 2 for _ in range(100): weights = np.random.normal(0, 1, size=(2, 3)) c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree) coeffs.append(c) We can now plot by setting up a pair of ``matplotlib`` axes and passing them to the plotting function. The of axes must be large enough to represent all the available coefficients (in this case, since we have 2 variables and use degree 2, we need a 5x5 grid. >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(5, 5, figsize=(12, 10), sharex=True, sharey=True) >>> panel(coeffs, n_inputs, ax) .. image:: ../../_static/fourier_vis_panel.png :align: center :width: 800px :target: javascript:void(0); """ if n_inputs in [1, 2]: coeffs = _validate_coefficients(coeffs, n_inputs, True) else: raise ValueError( "Panel plot function accepts input coefficients for only 1- or 2-dimensional functions." ) if ax.shape != coeffs[0].shape: raise ValueError("Shape of subplot axes must match the shape of the coefficient data.") if colour is None: colour = "tab:blue" # This could probably be more efficient. # Plot 1D case if n_inputs == 1: # Range is (0, ..., degree) for rfft, (0, ... degree, -degree, ..., -1) for fft n_freqs = coeffs.shape[1] // 2 + (coeffs.shape[1] % 2) frequency_range = list(range(n_freqs)) + list(range(-n_freqs + 1, 0)) for coeff in range(coeffs.shape[1]): ax[coeff].scatter( coeffs[:, coeff].real, coeffs[:, coeff].imag, facecolor="white", edgecolor=colour ) ax[coeff].set_title(f"{frequency_range[coeff]}", fontsize=14) ax[coeff].grid(True) ax[coeff].set_aspect("equal") # Plot 2D case else: n_freqs = coeffs.shape[1] // 2 + (coeffs.shape[1] % 2) frequency_range = list(range(n_freqs)) + list(range(-n_freqs + 1, 0)) for coeff_1, coeff_2 in product(list(range(coeffs.shape[1])), list(range(coeffs.shape[2]))): ax[coeff_1, coeff_2].scatter( coeffs[:, coeff_1, coeff_2].real, coeffs[:, coeff_1, coeff_2].imag, facecolor="white", edgecolor=colour, ) ax[coeff_1, coeff_2].set_title( f"{frequency_range[coeff_1]}, {frequency_range[coeff_2]}", fontsize=14 ) ax[coeff_1, coeff_2].grid(True) ax[coeff_1, coeff_2].set_aspect("equal") return ax
[docs]def radial_box(coeffs, n_inputs, ax, show_freqs=True, colour_dict=None, show_fliers=True): """Plot a list of sets of Fourier coefficients on a radial plot as box plots. Produces a 2-panel plot in which the left panel represents the real parts of Fourier coefficients. This method accepts multiple sets of coefficients, and plots the distribution of each coefficient as a boxplot. Args: coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the coefficient arrays should resemble that of the output of numpy/scipy's ``fftn`` function, or :func:`~.pennylane.fourier.coefficients`. n_inputs (int): Dimension of the transformed function. ax (array[matplotlib.axes.Axes]): Axes to plot on. For this function, subplots must specify ``subplot_kw=dict(polar=True)`` upon construction. show_freqs (bool): Whether or not to label the frequencies on the radial axis. Turn off for large plots. colour_dict (dict[str, str]): Specify a colour mapping for positive and negative real/imaginary components. If none specified, will default to: ``{"real" : "red", "imag" : "black"}`` showfliers (bool): Whether or not to plot outlying "fliers" on the boxplots. merge_plots (bool): Whether to plot real/complex values on the same panel, or on separate panels. Default is to plot real/complex values on separate panels. Returns: array[matplotlib.axes.Axes]: The axes after plotting is complete. **Example** Suppose we have the following quantum function: .. code-block:: python dev = qml.device('default.qubit', wires=2) @qml.qnode(dev) def circuit_with_weights(w, x): qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) qml.Rot(*w[0], wires=0) qml.Rot(*w[1], wires=1) qml.CNOT(wires=[1, 0]) qml.RX(x[0], wires=0) qml.RY(x[1], wires=1) qml.CNOT(wires=[1, 0]) return qml.expval(qml.PauliZ(0)) We would like to compute and plot the distribution of Fourier coefficients for many random values of the weights ``w``. First, we generate all the coefficients: .. code-block:: python from functools import partial coeffs = [] n_inputs = 2 degree = 2 for _ in range(100): weights = np.random.normal(0, 1, size=(2, 3)) c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree) coeffs.append(c) We can now plot by setting up a pair of ``matplotlib`` axes and passing them to the plotting function. Note that the axes passed must use polar coordinates. .. code-block:: python import matplotlib.pyplot as plt fig, ax = plt.subplots( 1, 2, sharex=True, sharey=True, subplot_kw=dict(polar=True), figsize=(15, 8) ) radial_box(coeffs, 2, ax, show_freqs=True, show_fliers=False) .. image:: ../../_static/fourier_vis_radial_box.png :align: center :width: 800px :target: javascript:void(0); """ coeffs = _validate_coefficients(coeffs, n_inputs, True) # Check axis shape if ax.size != 2: raise ValueError("Matplotlib axis should consist of two subplots.") if ax[0].name != "polar" or ax[1].name != "polar": raise ValueError("Matplotlib axes for radial_box must be polar.") if colour_dict is None: colour_dict = {"real": "red", "imag": "black"} # Number, width, and placement of pie slices N = coeffs[0].size angles = np.linspace(0, 2 * np.pi, N, endpoint=False) angles = np.concatenate((angles[-N // 2 + 1 :], angles[: -N // 2 + 1]))[::-1] width = (angles[1] - angles[0]) / 2 # Get the labels and data nvecs_formatted, data = _extract_data_and_labels(coeffs) # Set up the violin plots for data_type, a in zip(["real", "imag"], ax): data_colour = colour_dict[data_type] a.boxplot( data[data_type], positions=angles, widths=width, boxprops=dict( facecolor=to_rgb(data_colour) + (0.4,), color=data_colour, edgecolor=data_colour ), medianprops=dict(color=data_colour, linewidth=1.5), flierprops=dict(markeredgecolor=data_colour), whiskerprops=dict(color=data_colour), capprops=dict(color=data_colour), patch_artist=True, showfliers=show_fliers, ) # Rotate so that the 0 frequency is at the to a.set_thetagrids((180 / np.pi) * angles, labels=nvecs_formatted) a.set_theta_zero_location("N") a.set_rlabel_position(0) # Set and rotate the tickmarks; taken from SO # https://stackoverflow.com/questions/46719340/how-to-rotate-tick-labels-in-polar-matplotlib-plot for a in ax: if show_freqs: for label, angle in zip(a.get_xticklabels(), angles): x, y = label.get_position() lab = a.text( x, y, label.get_text(), transform=label.get_transform(), ha=label.get_ha(), va=label.get_va(), fontsize=14, color="grey", ) if angle > np.pi: lab.set_rotation((180 / np.pi) * angle + 90) else: lab.set_rotation((180 / np.pi) * angle + 270) a.tick_params(pad=7 * n_inputs) a.set_xticklabels([]) return ax