添加新运算符¶
以下步骤将帮助您创建自定义运算符,并有可能将它们添加到PennyLane中。
请注意,在PennyLane中,由多个门组成的电路ansatz也是一个操作符——其作用是通过指定作为其他操作符组合的表示来定义的。出于历史原因,您可以在 pennylane/template/ 文件夹中找到电路ansätze,而所有其他操作都在 pennylane/ops/ 中找到。
构建新运算符的基础类, Operator 和相应的子类,位于 pennylane/operations.py。
注意
查看 qml.measurements 以获取有关如何创建新测量的文档。
抽象¶
量子力学中的算符是作用于向量空间的映射,在可微分量子计算中,这些映射可以依赖于一组可训练的参数。Operator 类作为这些对象的主要抽象,所有算符(例如门、通道、可观测量)都继承自它。
>>> from jax import numpy as jnp
>>> op = qml.Rot(jnp.array(0.1), jnp.array(0.2), jnp.array(0.3), wires=["a"])
>>> isinstance(op, qml.operation.Operator)
True
运算符的基本组成部分如下:
运算符的名称 (
Operator.name),该名称可能具有一个标准的、普遍公认的解释(例如“Hadamard”门),或可能是特定于PennyLane的名称。>>> op.name Rot
运算符所涉及的子系统 (
Operator.wires),从数学上讲,定义了它作用的子空间。>>> op.wires Wires(['a'])
可训练参数 (
Operator.parameters),映射所依赖的,例如旋转角度,可以作为张量对象输入到运算符中。例如,由于我们使用了 jax 数组来指定op的三个旋转角度,因此这些参数是 jaxArrays。>>> op.parameters [Array(0.1, dtype=float32, weak_type=True), Array(0.2, dtype=float32, weak_type=True), Array(0.3, dtype=float32, weak_type=True)]
不可训练的超参数 (
Operator.hyperparameters) 影响操作符的行为。 并不是每个操作符都有超参数。>>> op.hyperparameters {}
运算符的符号或数值表示,可以被PennyLane的设备用来解释该映射。示例包括:
作为算子乘积的表示 (
Operator.decomposition()):>>> op = qml.Rot(0.1, 0.2, 0.3, wires=["a"]) >>> op.decomposition() [RZ(0.1, wires=['a']), RY(0.2, wires=['a']), RZ(0.3, wires=['a'])]
作为算符的线性组合的表示(
Operator.terms()):>>> op = qml.Hamiltonian([1., 2.], [qml.PauliX(0), qml.PauliZ(0)]) >>> op.terms() ((1.0, 2.0), [PauliX(wires=[0]), PauliZ(wires=[0])])
通过特征值分解表示,由特征值指定(对于对角矩阵,
Operator.eigvals())和对角化门(对于单位 operatorsOperator.diagonalizing_gates()):>>> op = qml.PauliX(0) >>> op.diagonalizing_gates() [H(0)] >>> op.eigvals() [ 1 -1]
作为一个 矩阵 (
Operator.matrix()) 的表示,依据一个全局线序来告诉我们电线在寄存器上的位置:>>> op = qml.PauliRot(0.2, "X", wires=["b"]) >>> op.matrix(wire_order=["a", "b"]) [[9.95e-01-2.26e-18j 2.72e-17-9.98e-02j, 0+0j, 0+0j] [2.72e-17-9.98e-02j 9.95e-01-2.26e-18j, 0+0j, 0+0j] [0+0j, 0+0j, 9.95e-01-2.26e-18j 2.72e-17-9.98e-02j] [0+0j, 0+0j, 2.72e-17-9.98e-02j 9.95e-01-2.26e-18j]]
作为 稀疏矩阵 的表示(
Operator.sparse_matrix()):>>> from scipy.sparse.coo import coo_matrix >>> row = np.array([0, 1]) >>> col = np.array([1, 0]) >>> data = np.array([1, -1]) >>> mat = coo_matrix((data, (row, col)), shape=(4, 4)) >>> op = qml.SparseHamiltonian(mat, wires=["a"]) >>> op.sparse_matrix(wire_order=["a"]) (0, 1) 1 (1, 0) - 1
通过对运算符应用算术函数,可以创建新的运算符,例如加法、标量乘法、乘法、取伴随或控制运算符。目前,这种算术仅针对特定子类实现。
继承自
Observable的算子支持加法和标量乘法:>>> op = qml.PauliX(0) + 0.1 * qml.PauliZ(0) >>> op.name 哈密顿量 >>> op (0.1) [Z0] + (1.0) [X0]
运算符可以定义厄米共轭:
>>> qml.RX(1., wires=0).adjoint() RX(-1.0, wires=[0])
创建自定义运算符¶
可以通过继承 Operator 或其子类之一来创建自定义操作符。
以下是一个自定义门的示例,它可能会翻转一个量子比特,然后旋转另一个量子比特。自定义操作符定义了一个分解,设备可以使用这个分解(因为设备不太可能知道 FlipAndRotate 的本地实现)。它还定义了一个伴随操作符。
import pennylane as qml
class FlipAndRotate(qml.operation.Operation):
# Define how many wires the operator acts on in total.
# In our case this may be one or two, which is why we
# use the AnyWires Enumeration to indicate a variable number.
num_wires = qml.operation.AnyWires
# This attribute tells PennyLane what differentiation method to use. Here
# we request parameter-shift (or "analytic") differentiation.
grad_method = "A"
def __init__(self, angle, wire_rot, wire_flip=None, do_flip=False, id=None):
# checking the inputs --------------
if do_flip and wire_flip is None:
raise ValueError("Expected a wire to flip; got None.")
# note: we use the framework-agnostic math library since
# trainable inputs could be tensors of different types
shape = qml.math.shape(angle)
if len(shape) > 1:
raise ValueError(f"Expected a scalar angle; got angle of shape {shape}.")
#------------------------------------
# do_flip is not trainable but influences the action of the operator,
# which is why we define it to be a hyperparameter
self._hyperparameters = {
"do_flip": do_flip
}
# we extract all wires that the operator acts on,
# relying on the Wire class arithmetic
all_wires = qml.wires.Wires(wire_rot) + qml.wires.Wires(wire_flip)
# The parent class expects all trainable parameters to be fed as positional
# arguments, and all wires acted on fed as a keyword argument.
# The id keyword argument allows users to give their instance a custom name.
super().__init__(angle, wires=all_wires, id=id)
@property
def num_params(self):
# if it is known before creation, define the number of parameters to expect here,
# which makes sure an error is raised if the wrong number was passed
return 1
@staticmethod
def compute_decomposition(angle, wires, do_flip): # pylint: disable=arguments-differ
# Overwriting this method defines the decomposition of the new gate, as it is
# called by Operator.decomposition().
# The general signature of this function is (*parameters, wires, **hyperparameters).
op_list = []
if do_flip:
op_list.append(qml.PauliX(wires=wires[1]))
op_list.append(qml.RX(angle, wires=wires[0]))
return op_list
def adjoint(self):
# the adjoint operator of this gate simply negates the angle
return FlipAndRotate(-self.parameters[0], self.wires[0], self.wires[1], do_flip=self.hyperparameters["do_flip"])
@classmethod
def _unflatten(cls, data, metadata):
# as the class differs from the standard `__init__` call signature of
# (*data, wires=wires, **hyperparameters), the _unflatten method that
# must be defined as well
# _unflatten recreates an operation from the serialized data and metadata of ``Operator._flatten``
# copied_op = type(op)._unflatten(*op._flatten())
wires = metadata[0]
hyperparams = dict(metadata[1])
return cls(data[0], wire_rot=wires[0], wire_flip=wires[1], do_flip=hyperparams['do_flip'])
现在可以按照以下方式创建新的网关:
>>> op = FlipAndRotate(0.1, wire_rot="q3", wire_flip="q1", do_flip=True)
>>> op
FlipAndRotate(0.1, wires=['q3', 'q1'])
>>> op.decomposition()
[PauliX(wires=['q1']), RX(0.1, wires=['q3'])]
>>> op.adjoint()
FlipAndRotate(-0.1, wires=['q3', 'q1'])
一旦类被创建,您可以使用 ops.functions.assert_valid() 运行一系列验证检查。此函数将警告您自定义运算符中的一些常见错误。
>>> qml.ops.functions.assert_valid(op)
如果上述操作符省略了_unflatten自定义定义,将会引发:
TypeError: FlipAndRotate.__init__() got an unexpected keyword argument 'wires'
The above exception was the direct cause of the following exception:
AssertionError: FlipAndRotate._unflatten must be able to reproduce the original operation
from (0.1,) and (Wires(['q3', 'q1']), (('do_flip', True),)). You may need to override
either the _unflatten or _flatten method.
For local testing, try type(op)._unflatten(*op._flatten())
新门可以与PennyLane设备一起使用。可以通过 dev.stopping_condition(op) 检查设备对某个操作的支持。如果 True,则设备支持该操作。
DefaultQubit 首先检查运算符是否具有矩阵,使用 has_matrix 属性。
如果设备注册了对具有相同名称的操作的支持,PennyLane 将门的实现留给设备。该设备可能有一个硬编码的实现,或者它可能引用操作符的一种数值表示(例如
Operator.matrix())。如果设备不支持某个操作,PennyLane 将自动使用
Operator.decomposition()分解该门。
from pennylane import numpy as np
dev = qml.device("default.qubit", wires=["q1", "q2", "q3"])
@qml.qnode(dev)
def circuit(angle):
FlipAndRotate(angle, wire_rot="q1", wire_flip="q1")
return qml.expval(qml.PauliZ("q1"))
>>> a = np.array(3.14)
>>> circuit(a)
-0.9999987318946099
如果所有在分解中使用的门都有定义的梯度公式,我们甚至可以在没有额外努力的情况下计算使用新门的电路的梯度。
>>> qml.grad(circuit)(a)
-0.0015926529164868282
注意
这个FlipAndRotate的例子简单得足以让人编写一个函数
def FlipAndRotate(angle, wire_rot, wire_flip=None, do_flip=False):
if do_flip:
qml.PauliX(wires=wire_flip)
qml.RX(angle, wires=wire_rot)
并在量子函数中调用它 就像它是一个门一样。然而,类允许更多的功能,例如定义上面提到的伴随门,定义可训练参数的期望形状,或指定梯度规则。
定义算子的特殊属性¶
除了主要的 Operator 类,具有特殊方法或表示的算子实现为子类 Operation、Observable、Channel、CVOperation 和 CVObservable。
然而,与许多其他框架不同,PennyLane 不使用类继承来定义算子的细粒度属性,例如它是否是自身的自反,是否是对角的,或者它是否可以分解为 Pauli 旋转。这避免了每次应用需要查询新属性时更改继承结构。
相反,PennyLane 使用“属性”,这是一种记账类,用于列出满足特定属性的运算符。
例如,我们可以创建一个新的属性,pauli_ops,如下所示:
>>> from pennylane.ops.qubits.attributes import Attribute
>>> pauli_ops = Attribute(["PauliX", "PauliY", "PauliZ"])
我们可以检查一个字符串或一个操作是否包含在这个集合中:
>>> qml.PauliX(0) in pauli_ops
True
>>> "Hadamard" in pauli_ops
False
我们还可以在运行时动态地向集合中添加运算符。这对于向诸如 composable_rotations 和 self_inverses 这样的属性添加自定义操作非常有用,这些属性用于编译转换。例如,假设您创建了一个新操作 MyGate,您知道它是自己的逆。将其添加到集合中,如下所示
>>> from pennylane.ops.qubits.attributes import self_inverses
>>> self_inverses.add("MyGate")
设备还可以查询属性,以使用特殊技巧实现更高效的实现。新增运算符的贡献者有责任将它们添加到正确的属性中。
将您的新运算符添加到 PennyLane¶
如果您希望PennyLane原生支持您的新操作符,您必须提交一个拉取请求,将其添加到pennylane/ops/的适当文件夹中。测试被添加到tests/ops/中一个名称和位置相似的文件中。如果您的操作符定义了一个ansatz,请将其添加到pennylane/templates/中的适当子文件夹中。
新操作可能需要在模块的 __init__.py 文件中导入,以便正确导入。
确保所有超参数和错误都经过测试,并且参数可以作为张量从所有支持的自动微分框架中传递。
不要忘记还要在 docs/introduction/operations.rst 文件中将新的操作符添加到文档中,或者如果它是一个 ansatz,则添加到模板库中。这可以通过在 doc/introduction/templates.rst 中正确的部分添加一个 gallery-item 来完成:
.. gallery-item::
:link: ../code/api/pennylane.templates.<templ_type>.MyNewTemplate.html
:description: MyNewTemplate
:figure: ../_static/templates/<templ_type>/my_new_template.png
注意
这加载了添加到 doc/_static/templates/test_ 的模板的图像。确保该图像与文件夹中其他模板图标具有相同的尺寸和样式。
这里有一些添加操作符的额外提示:
仔细选择名称。 好的名称能告诉用户操作符的用途,或它实现的架构。问问自己,是否可能在不同的上下文中很快添加一个类似名称的门。
撰写良好的文档字符串。 在清晰的文档字符串中解释您的操作符的功能,并提供充足的示例。 您可以在文档的指南中了解更多有关Pennylane的标准。
高效的表示。 尽量尽可能高效地实现表示,因为它们可能会构造多次。
输入检查。 检查操作的输入会引入额外开销,并与即时编译等工具发生冲突。找到添加有意义的有效性检查(例如张量形状)的平衡,同时保持其数量最小。