Skip to content

Commit

Permalink
Merge branch 'parameter-broadcasting-0' into parameter-broadcasting-1
Browse files Browse the repository at this point in the history
  • Loading branch information
dwierichs committed May 23, 2022
2 parents 733a852 + c3c24dd commit 8960bbb
Show file tree
Hide file tree
Showing 30 changed files with 480 additions and 159 deletions.
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve
introduction/operations
introduction/measurements
introduction/templates
introduction/inspecting_circuits
introduction/optimizers
introduction/chemistry
introduction/configuration
Expand Down
282 changes: 282 additions & 0 deletions doc/introduction/inspecting_circuits.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
.. role:: html(raw)
:format: html


.. _intro_inspecting_circuits:

Inspecting circuits
===================

PennyLane offers functionality to inspect, visualize or analyze quantum circuits.

.. _intro_qtransforms:

Most of these tools are implemented as **transforms**. Transforms take a :class:`~pennylane.QNode` instance and return a function:

>>> @qml.qnode(dev, diff_method='parameter-shift')
... def my_qnode(x, a=True):
... # ...
>>> new_func = my_transform(qnode)

This new function accepts the same arguments as the QNode and returns the desired outcome,
such as a dictionary of the QNode's properties, a matplotlib figure drawing the circuit,
or a DAG representing its connectivity structure.

>>> new_func(0.1, a=False)

More information on the concept of transforms can be found in
`Di Matteo et al. (2022) <https://arxiv.org/abs/2202.13414>`_.

Extracting properties of a circuit
----------------------------------

The :func:`~pennylane.specs` transform takes a
QNode and creates a function that returns
details about the QNode, including depth, number of gates, and number of
gradient executions required.

For example:

.. code-block:: python
dev = qml.device('default.qubit', wires=4)
@qml.qnode(dev, diff_method='parameter-shift')
def circuit(x, y):
qml.RX(x[0], wires=0)
qml.Toffoli(wires=(0, 1, 2))
qml.CRY(x[1], wires=(0, 1))
qml.Rot(x[2], x[3], y, wires=0)
return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
We can now use the :func:`~pennylane.specs` transform to generate a function that returns
details and resource information:

>>> x = np.array([0.05, 0.1, 0.2, 0.3], requires_grad=True)
>>> y = np.array(0.4, requires_grad=False)
>>> specs_func = qml.specs(circuit)
>>> specs_func(x, y)
{'gate_sizes': defaultdict(<class 'int'>, {1: 2, 3: 1, 2: 1}),
'gate_types': defaultdict(<class 'int'>, {'RX': 1, 'Toffoli': 1, 'CRY': 1, 'Rot': 1}),
'num_operations': 4,
'num_observables': 2,
'num_diagonalizing_gates': 1,
'num_used_wires': 3, 'depth': 4,
'num_trainable_params': 4,
'num_device_wires': 4,
'device_name': 'default.qubit',
'expansion_strategy': 'gradient',
'gradient_options': {},
'interface': 'autograd',
'diff_method': 'parameter-shift',
'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift',
'num_gradient_executions': 10}

Circuit drawing
---------------

PennyLane has two built-in circuit drawers, :func:`~pennylane.draw` and
:func:`~pennylane.draw_mpl`.

For example:

.. code-block:: python
dev = qml.device('lightning.qubit', wires=(0,1,2,3))
@qml.qnode(dev)
def circuit(x, z):
qml.QFT(wires=(0,1,2,3))
qml.IsingXX(1.234, wires=(0,2))
qml.Toffoli(wires=(0,1,2))
qml.CSWAP(wires=(0,2,3))
qml.RX(x, wires=0)
qml.CRZ(z, wires=(3,0))
return qml.expval(qml.PauliZ(0))
fig, ax = qml.draw_mpl(circuit)(1.2345,1.2345)
fig.show()
.. image:: ../_static/draw_mpl/main_example.png
:align: center
:width: 400px
:target: javascript:void(0);

More information, including various fine-tuning options, can be found in
the :doc:`drawing module <../code/qml_drawer>`.

Debugging with mid-circuit snapshots
------------------------------------

When debugging quantum circuits run on simulators, we may want to inspect the current quantum state between gates.

:class:`~pennylane.Snapshot` is an operator like a gate, but it saves the device state at its location in the circuit instead of manipulating the quantum state.

Currently supported devices include:

* ``default.qubit``: each snapshot saves the quantum state vector
* ``default.mixed``: each snapshot saves the density matrix
* ``default.gaussian``: each snapshot saves the covariance matrix and vector of means

During normal execution, the snapshots are ignored:

.. code-block:: python
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev, interface=None)
def circuit():
qml.Snapshot()
qml.Hadamard(wires=0)
qml.Snapshot("very_important_state")
qml.CNOT(wires=[0, 1])
qml.Snapshot()
return qml.expval(qml.PauliX(0))
However, when using the :func:`~pennylane.snapshots`
transform, intermediate device states will be stored and returned alongside the
results.

>>> qml.snapshots(circuit)()
{0: array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]),
'very_important_state': array([0.707+0.j, 0.+0.j, 0.707+0.j, 0.+0.j]),
2: array([0.707+0.j, 0.+0.j, 0.+0.j, 0.707+0.j]),
'execution_results': array(0.)}

Graph representation
--------------------

PennyLane makes use of several ways to represent a quantum circuit as a Directed Acyclic Graph (DAG).

DAG of causal relations between ops
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A DAG can be used to represent which operator in a circuit is causally related to another. There are two
options to construct such a DAG:

The :class:`~pennylane.CircuitGraph` class takes a list of gates or channels and hermitian observables
as well as a set of wire labels and constructs a DAG in which the :class:`~.Operator`
instances are the nodes, and each directed edge corresponds to a wire
(or a group of wires) on which the "nodes" act subsequently.

For example, thiscan be used to compute the effective depth of a circuit,
or to check whether two gates causally influence each other.

.. code-block:: python
import pennylane as qml
from pennylane import CircuitGraph
dev = qml.device('lightning.qubit', wires=(0,1,2,3))
@qml.qnode(dev)
def circuit():
qml.Hadamard(0)
qml.CNOT([1, 2])
qml.CNOT([2, 3])
qml.CNOT([3, 1])
return qml.expval(qml.PauliZ(0))
circuit()
tape = circuit.qtape
ops = tape.operations
obs = tape.observables
g = CircuitGraph(ops, obs, tape.wires)
Internally, the :class:`~pennylane.CircuitGraph` class constructs a ``retworkx`` graph object.

>>> type(g.graph)
<class 'retworkx.PyDiGraph'>

There is no edge between the ``Hadamard`` and the first ``CNOT``, but between consecutive ``CNOT`` gates:

>>> g.has_path(ops[0], ops[1])
False
>>> g.has_path(ops[1], ops[3])
True

The Hadamard is connected to the observable, while the ``CNOT`` operators are not. The observable
does not follow the Hadamard.

>>> g.has_path(ops[0], obs[0])
True
>>> g.has_path(ops[1], obs[0])
False
>>> g.has_path(obs[0], ops[0])
False


Anther way to construct the "causal" DAG of a circuit is to use the
:func:`~pennylane.transforms.qcut.tape_to_graph` function used by the qcut module. This
function takes a quantum tape and creates a ``MultiDiGraph`` instance from the ``networkx`` python package.

Using the above example, we get:

>>> g2 = qml.transforms.qcut.tape_to_graph(tape)
>>> type(g2)
<class 'networkx.classes.multidigraph.MultiDiGraph'>
>>> for k, v in g2.adjacency():
... print(k, v)
Hadamard(wires=[0]) {expval(PauliZ(wires=[0])): {0: {'wire': 0}}}
CNOT(wires=[1, 2]) {CNOT(wires=[2, 3]): {0: {'wire': 2}}, CNOT(wires=[3, 1]): {0: {'wire': 1}}}
CNOT(wires=[2, 3]) {CNOT(wires=[3, 1]): {0: {'wire': 3}}}
CNOT(wires=[3, 1]) {}
expval(PauliZ(wires=[0])) {}

DAG of non-commuting ops
~~~~~~~~~~~~~~~~~~~~~~~~

The :func:`~pennylane.commutation_dag` transform can be used to produce an instance of the ``CommutationDAG`` class.
In a commutation DAG, each node represents a quantum operation, and edges represent non-commutation
between two operations.

This transform takes into account that not all operations can be moved next to each other by
pairwise commutation:

>>> def circuit(x, y, z):
... qml.RX(x, wires=0)
... qml.RX(y, wires=0)
... qml.CNOT(wires=[1, 2])
... qml.RY(y, wires=1)
... qml.Hadamard(wires=2)
... qml.CRZ(z, wires=[2, 0])
... qml.RY(-y, wires=1)
... return qml.expval(qml.PauliZ(0))
>>> dag_fn = qml.commutation_dag(circuit)
>>> dag = dag_fn(np.pi / 4, np.pi / 3, np.pi / 2)

Nodes in the commutation DAG can be accessed via the ``get_nodes()`` method, returning a list of
the form ``(ID, CommutationDAGNode)``:

>>> nodes = dag.get_nodes()
>>> nodes
NodeDataView({0: <pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x7f461c4bb580>, ...}, data='node')

Specific nodes in the commutation DAG can be accessed via the ``get_node()`` method:

>>> second_node = dag.get_node(2)
>>> second_node
<pennylane.transforms.commutation_dag.CommutationDAGNode object at 0x136f8c4c0>
>>> second_node.op
CNOT(wires=[1, 2])
>>> second_node.successors
[3, 4, 5, 6]
>>> second_node.predecessors
[]

Fourier representation
----------------------

Parametrized quantum circuits often compute functions in the parameters that
can be represented by Fourier series of a low degree.

The :doc:`../code/qml_fourier` module contains functionality to compute and visualize
properties of such Fourier series.

.. image:: ../_static/fourier_vis_radial_box.png
:align: center
:width: 500px
:target: javascript:void(0);
17 changes: 15 additions & 2 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,18 @@
method for a provided device and interface, in human-readable format.
[(#2533)](https://github.com/PennyLaneAI/pennylane/pull/2533)

* Using `Operation.inv()` in a queuing environment no longer updates the queue's metadata, but merely updates
the operation in place.
[(#2596)](https://github.com/PennyLaneAI/pennylane/pull/2596)

* Sparse Hamiltonians representation has changed from COOrdinate (COO) to Compressed Sparse Row (CSR) format. The CSR representation is more performant for arithmetic operations and matrix vector products. This change decreases the `expval()` calculation time, for `qml.SparseHamiltonian`, specially for large workflows. Also, the CRS format consumes less memory for the `qml.SparseHamiltonian` storage.
[(#2561)](https://github.com/PennyLaneAI/pennylane/pull/2561)

<h3>Breaking changes</h3>

* The unused keyword argument `do_queue` for `Operation.adjoint` is now fully removed.
[(#2583)](https://github.com/PennyLaneAI/pennylane/pull/2583)

* The module `qml.gradients.param_shift_hessian` has been renamed to
`qml.gradients.parameter_shift_hessian` in order to distinguish it from the identically named
function. Note that the `param_shift_hessian` function is unaffected by this change and can be
Expand Down Expand Up @@ -117,6 +127,9 @@

<h3>Bug fixes</h3>

* `QNode`'s now can interpret variations on the interface name, like `"tensorflow"` or `"jax-jit"`, when requesting backpropagation.
[(#2591)](https://github.com/PennyLaneAI/pennylane/pull/2591)

* Fixed a bug for `diff_method="adjoint"` where incorrect gradients were
computed for QNodes with parametrized observables (e.g., `qml.Hermitian`).
[(#2543)](https://github.com/PennyLaneAI/pennylane/pull/2543)
Expand Down Expand Up @@ -156,5 +169,5 @@

This release contains contributions from (in alphabetical order):

Guillermo Alonso-Linaje, Mikhail Andrenkov, Juan Miguel Arrazola, Utkarsh Azad, Christian Gogolin,
Soran Jahangiri, Edward Jiang, Christina Lee, Chae-Yeun Park, Maria Schuld, Jay Soni, David Wierichs
Amintor Dusko, Chae-Yeun Park, Christian Gogolin, Christina Lee, David Wierichs, Edward Jiang, Guillermo Alonso-Linaje,
Jay Soni, Juan Miguel Arrazola, Maria Schuld, Mikhail Andrenkov, Soran Jahangiri, Utkarsh Azad
11 changes: 5 additions & 6 deletions pennylane/devices/default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from string import ascii_letters as ABC

import numpy as np
from scipy.sparse import coo_matrix
from scipy.sparse import csr_matrix

import pennylane as qml
from pennylane import QubitDevice, DeviceError, QubitStateVector, BasisState, Snapshot
Expand Down Expand Up @@ -504,7 +504,7 @@ def expval(self, observable, shot_range=None, bin_size=None):
for op, coeff in zip(observable.ops, observable.data):

# extract a scipy.sparse.coo_matrix representation of this Pauli word
coo = qml.operation.Tensor(op).sparse_matrix(wires=self.wires)
coo = qml.operation.Tensor(op).sparse_matrix(wires=self.wires, format="coo")
Hmat = qml.math.cast(qml.math.convert_like(coo.data, self.state), self.C_DTYPE)

product = (
Expand All @@ -522,16 +522,15 @@ def expval(self, observable, shot_range=None, bin_size=None):
else:
# Coefficients and the state are not trainable, we can be more
# efficient in how we compute the Hamiltonian sparse matrix.

if observable.name == "Hamiltonian":
Hmat = qml.utils.sparse_hamiltonian(observable, wires=self.wires)
elif observable.name == "SparseHamiltonian":
Hmat = observable.sparse_matrix()

state = qml.math.toarray(self.state)
res = coo_matrix.dot(
coo_matrix(qml.math.conj(state)),
coo_matrix.dot(Hmat, coo_matrix(state.reshape(len(self.state), 1))),
res = csr_matrix.dot(
csr_matrix(qml.math.conj(state)),
csr_matrix.dot(Hmat, csr_matrix(state.reshape(len(self.state), 1))),
).toarray()[0]

if observable.name == "Hamiltonian":
Expand Down
6 changes: 3 additions & 3 deletions pennylane/devices/tests/test_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# pylint: disable=no-self-use,pointless-statement, no-member
import pytest
from flaky import flaky
from scipy.sparse import coo_matrix
from scipy.sparse import csr_matrix

import pennylane as qml
from pennylane import numpy as np
Expand All @@ -34,7 +34,7 @@
"PauliY": qml.PauliY(wires=[0]),
"PauliZ": qml.PauliZ(wires=[0]),
"Projector": qml.Projector(np.array([1]), wires=[0]),
"SparseHamiltonian": qml.SparseHamiltonian(coo_matrix(np.eye(8)), wires=[0, 1, 2]),
"SparseHamiltonian": qml.SparseHamiltonian(csr_matrix(np.eye(8)), wires=[0, 1, 2]),
"Hamiltonian": qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliX(0)]),
}

Expand Down Expand Up @@ -608,7 +608,7 @@ def test_sparse_hamiltonian_expval(self, device, tol):
h_data = np.array(
[-1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1], dtype=np.complex128
)
h = coo_matrix((h_data, (h_row, h_col)), shape=(16, 16)) # XXYY
h = csr_matrix((h_data, (h_row, h_col)), shape=(16, 16)) # XXYY

@qml.qnode(dev, diff_method="parameter-shift")
def result():
Expand Down
Loading

0 comments on commit 8960bbb

Please sign in to comment.