The following steps will help you to add your favourite circuit ansatz to
PennyLane internally loosely distinguishes different types of templates, such as
subroutines. Below you need to replace
<templ_type> with the
correct template type.
Templates are just gates¶
Conceptually, there is no difference in PennyLane between a template or ansatz and a operation.
Both inherit from the
Operation class, which has an
that can be used to define a decomposition into other gates. If a device does not recognise the name of the operation,
it calls the
expand() function which returns a
tape instance that
represents the queue of the decomposing gates.
For example, the following shows a simple template for a layer of of Pauli-X rotations:
import pennylane as qml from pennylane.operation import Operation, AnyWires class MyNewTemplate(Operation): num_params = 1 num_wires = AnyWires par_domain = "A" # note: this attribute will be deprecated soon def expand(self): # extract the weights as the first parameter # passed to this operation weights = self.parameters # extract the length of the weights vector using # the interface-agnostic `math` module num_weights = qml.math.shape(weights) # record the ansatz in a tape with qml.tape.QuantumTape() as tape: for i in range(num_weights): qml.RX(weights[i], wires=self.wires[i]) return tape
num_wires class attributes determine that an instance of this template can be created
by passing a single parameter (of arbitrary shape and type), as well as an arbitrary number of wires:
weights = np.array([0.1, 0.2, 0.3]) MyNewTemplate(weights, wires=['a', 'b', 'd'])
Operation, templates can define other methods and attributes, such as a matrix representation,
a generator, or even a gradient rule.
In principle, templates could also inherit from the
class and define a sequence of diagonalising gates as an ansatz.
Templates often perform extensive pre-processing on the arguments they receive.
Any substantial pre-processing should be implemented by overwriting the
__init__ function of the
This also allows us to define templates with more flexible signatures than the
signature expected by the
As an illustration, let us extend
MyNewTemplate and check that the first
parameter it receives is one-dimensional, apply a sine function to each weight,
and invert the wires that the operation acts on.
def MyNewTemplate(Operation): num_params = 1 num_wires = AnyWires par_domain = "A" # note: this attribute will be deprecated soon def __init__(weights, raw_wires) shp = qml.math.shape(weights) if len(shp) != 1: raise ValueError("Expected one-dimensional weights tensor.") # pre-process weights new_weights = qml.math.sin(weights) # pre-process wires inverted_wires = wires[::-1] # initialise operation with pre-processed parameters and wires super().__init__(new_weights, wires=inverted_wires) def expand(self): weights = self.parameters num_weights = qml.math.shape(weights) with qml.tape.QuantumTape() as tape: for i in range(num_weights): qml.RX(weights[i], wires=self.wires[i]) return tape
wires attributes used in the
refer to the
inverted_wires that were used to initialize the parent class.
The template design should make as many arguments differentiable as possible.
Differentiable arguments are always tensors of the allowed interfaces,
This means that we have to process them with interface-agnostic pre-processing methods inside the templates.
A lot of functionality
is provided by the
pennylane.math module - for example, the length of the weights in the code above
was computed with the
qml.math.shape(weights) function, since some tensor types do not support
To retrieve elements from a tensor, keep in mind that not all tensor types support iteration.
Avoid expressions like
for w in weightsand rather iterate over ranges like
for i in range(num_weights).
When indexing into the tensor, use multi-indexing where possible — expressions like
weightsare usually a lot slower than
weights[6, 5, 2].
Adding the template¶
Add the template by adding a new file
my_new_template.py to the correct
subdirectory. The file contains your new template class.
Make sure you consider the following:
Choose the name carefully. Good names tell the user what a template is used for, or what architecture it implements. The class name (i.e.,
MyNewTemplate) is written in camel case.
Explicit decompositions. Try to implement the decomposition in the
expand()function without the use of convenient methods like the
broadcast()function - this avoids unnecessary overhead.
Write an extensive docstring that explains how to use the template. Include a sketch of the template (add the file to the
doc/_static/templates/<templ_type>/directory). You should also display a small usage example at the beginning of the docstring. If you want to explain the behaviour in more detail, add a section starting with the
.. UsageDetails::directive at the end of the docstring. Use the docstring of one of the existing templates for inspiration, such as
Input checks. While checking the inputs of the template for consistency introduces an overhead and should be kept to a minimum, it is still advised to do some basic sanity checks, for example making sure that the shape of the parameters is correct.
Importing the new template¶
Import the new template in
templates/<templ_type>/__init__.py by adding the new line
from .mynewtemplate import MyNewTemplate
Adding your template to the documentation¶
Add your template to the documentation by adding a
customgalleryitem to the correct layer type section in
.. customgalleryitem:: :link: ../code/api/pennylane.templates.<templ_type>.MyNewTemplate.html :description: MyNewTemplate :figure: ../_static/templates/<templ_type>/my_new_template.png
This loads the image of the template added to
doc/_static/templates/test_<templ_type>/. Make sure that
this image has the same dimensions and style as other template icons in the folder.
Don’t forget to add tests for your new template to the test suite. Create a separate file
tests/templates/<templ_type>/test_my_new_template.py with all tests.
You can draw some inspiration from