# 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."""fromitertoolsimportproductimportnumpyasnp# 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:frommatplotlib.colorsimportto_rgbexcept(ModuleNotFoundError,ImportError)ase:# pragma: no coverraiseImportError("Module matplotlib is required for visualization in the Fourier module. ""You can install matplolib via \n\n pip install matplotlib")fromefrom.utilsimportformat_nvecdef_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 arrayifnotisinstance(coeffs,list)andnotisinstance(coeffs,np.ndarray):raiseTypeError("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 arrayifisinstance(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 needediflen(coeffs.shape)==n_inputsandcan_be_list:coeffs=np.array([coeffs])# Check now that we have the right number of axes for the type of functionrequired_shape_size=n_inputs+1ifcan_be_listelsen_inputsiflen(coeffs.shape)!=required_shape_size:raiseValueError(f"Plotting function expected a list of {n_inputs}-dimensional inputs. "f"Received coefficients of {len(coeffs.shape)}-dimensional function.")# Size of each sample dimension must be 2d_i + 1 where d_i is the i-th degreedims=coeffs.shape[1:]ifcan_be_listelsecoeffs.shapeifany((dim-1)%2fordimindims):raiseValueError("Shape of input coefficients must be 2d_i + 1, where d_i is the largest frequency "f"in the i-th input. Coefficient array with shape {coeffs.shape} is invalid.")# Return the coefficients; we may have switched to a numpy array or added a needed extra dimensionreturncoeffsdef_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: create generator for indices nvec = (n1, ..., nN),# ranging from (-d1,...,-dN) to (d1,...,dN).nvecs=list(product(*(np.array(range(-(d//2),d//2+1))fordincoeffs[0].shape)))nvecs_formatted=[format_nvec(nvec)fornvecinnvecs]# extract flattened data by real part and imaginary part, and respecting negative indicesdata={}data["real"]=np.array([[c[nvec]fornvecinnvecs]forcincoeffs.real])data["imag"]=np.array([[c[nvec]fornvecinnvecs]forcincoeffs.imag])returnnvecs_formatted,datadef_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]defviolin(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.Z(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 >>> from pennylane.fourier.visualize import violin >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4)) >>> violin(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 shapeifax.size!=2:raiseValueError("Matplotlib axis should consist of two subplots.")ifcolour_dictisNone:colour_dict={"real":"purple","imag":"green"}# Get the labels and datanvecs_formatted,data=_extract_data_and_labels(coeffs)fordata_type,axisinzip(["real","imag"],ax):violinplt=axis.violinplot(data[data_type],showextrema=False)forbdinviolinplt["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 axesax[0].tick_params(axis="x",colors="white")# hack to get rid of ticks but keep gridifshow_freqs:ax[1].tick_params(axis="x",which="both",length=0)# remove ticks without removing labelsax[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 gridreturnax
[docs]defbox(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.Z(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 >>> from pennylane.fourier.visualize import box >>> 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 shapeifax.size!=2:raiseValueError("Matplotlib axis should consist of two subplots.")# The axis received must be a pair of axes in a subplot.ifcolour_dictisNone:colour_dict={"real":"purple","imag":"green"}# Get the labels and datanvecs_formatted,data=_extract_data_and_labels(coeffs)fordata_type,axisinzip(["real","imag"],ax):data_colour=colour_dict[data_type]axis.boxplot(data[data_type],boxprops={"facecolor":to_rgb(data_colour)+(0.4,),"color":data_colour,"edgecolor":data_colour,},medianprops={"color":data_colour,"linewidth":1.5},flierprops={"markeredgecolor":data_colour},whiskerprops={"color":data_colour},capprops={"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 gridifshow_freqs:ax[1].tick_params(axis="x",which="both",length=0)# remove ticks without removing labelsax[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 gridreturnax
[docs]defbar(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.Z(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 >>> from pennylane.fourier.visualize import bar >>> 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 shapeifax.size!=2:raiseValueError("Matplotlib axis should consist of two subplots.")# The axis received must be a pair of axes in a subplot.ifcolour_dictisNone:colour_dict={"real":"purple","imag":"green"}# Get the labels and datanvecs_formatted,data=_extract_data_and_labels(np.array([coeffs]))data_len=len(data["real"][0])fordata_type,axisinzip(["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 gridifshow_freqs:ax[1].tick_params(axis="x",which="both",length=0)# remove ticks without removing labelsax[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 gridreturnax
[docs]defpanel(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.Z(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 >>> from pennylane.fourier.visualize import panel >>> 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); """ifn_inputsin[1,2]:coeffs=_validate_coefficients(coeffs,n_inputs,True)else:raiseValueError("Panel plot function accepts input coefficients for only 1- or 2-dimensional functions.")ifax.shape!=coeffs[0].shape:raiseValueError("Shape of subplot axes must match the shape of the coefficient data.")ifcolourisNone:colour="tab:blue"# This could probably be more efficient.# Plot 1D caseifn_inputs==1:# Range is (0, ..., degree) for rfft, (0, ... degree, -degree, ..., -1) for fftn_freqs=coeffs.shape[1]//2+(coeffs.shape[1]%2)frequency_range=list(range(n_freqs))+list(range(-n_freqs+1,0))forcoeffinrange(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 caseelse:n_freqs=coeffs.shape[1]//2+(coeffs.shape[1]%2)frequency_range=list(range(n_freqs))+list(range(-n_freqs+1,0))forcoeff_1,coeff_2inproduct(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")returnax
[docs]defradial_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={"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.Z(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 from pennylane.fourier.visualize import radial_box fig, ax = plt.subplots( 1, 2, sharex=True, sharey=True, subplot_kw={"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 shapeifax.size!=2:raiseValueError("Matplotlib axis should consist of two subplots.")ifax[0].name!="polar"orax[1].name!="polar":raiseValueError("Matplotlib axes for radial_box must be polar.")ifcolour_dictisNone:colour_dict={"real":"red","imag":"black"}# Number, width, and placement of pie slicesN=coeffs[0].sizeangles=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 datanvecs_formatted,data=_extract_data_and_labels(coeffs)# Set up the violin plotsfordata_type,ainzip(["real","imag"],ax):data_colour=colour_dict[data_type]a.boxplot(data[data_type],positions=angles,widths=width,boxprops={"facecolor":to_rgb(data_colour)+(0.4,),"color":data_colour,"edgecolor":data_colour,},medianprops={"color":data_colour,"linewidth":1.5},flierprops={"markeredgecolor":data_colour},whiskerprops={"color":data_colour},capprops={"color":data_colour},patch_artist=True,showfliers=show_fliers,)# Rotate so that the 0 frequency is at the toa.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-plotforainax:ifshow_freqs:forlabel,angleinzip(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",)ifangle>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([])returnax