添加新运算符

以下步骤将帮助您创建自定义运算符,并有可能将它们添加到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

运算符的基本组成部分如下:

  1. 运算符的名称 (Operator.name),该名称可能具有一个标准的、普遍公认的解释(例如“Hadamard”门),或可能是特定于PennyLane的名称。

    >>> op.name
    Rot
    
  2. 运算符所涉及的子系统 (Operator.wires),从数学上讲,定义了它作用的子空间。

    >>> op.wires
    Wires(['a'])
    
  3. 可训练参数 (Operator.parameters),映射所依赖的,例如旋转角度,可以作为张量对象输入到运算符中。例如,由于我们使用了 jax 数组来指定 op 的三个旋转角度,因此这些参数是 jax Arrays

    >>> 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)]
    
  4. 不可训练的超参数 (Operator.hyperparameters) 影响操作符的行为。 并不是每个操作符都有超参数。

    >>> op.hyperparameters
    {}
    
  5. 运算符的符号或数值表示,可以被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())和对角化门(对于单位 operators Operator.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 类,具有特殊方法或表示的算子实现为子类 OperationObservableChannelCVOperationCVObservable

然而,与许多其他框架不同,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_rotationsself_inverses 这样的属性添加自定义操作非常有用,这些属性用于编译转换。例如,假设您创建了一个新操作 MyGate,您知道它是自己的逆。将其添加到集合中,如下所示

>>> from pennylane.ops.qubits.attributes import self_inverses
>>> self_inverses.add("MyGate")

设备还可以查询属性,以使用特殊技巧实现更高效的实现。新增运算符的贡献者有责任将它们添加到正确的属性中。

注意

当前量子比特门的属性位于 pennylane/ops/qubit/attributes.py

包含的属性列在 Operation 文档中。

将您的新运算符添加到 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的标准。

  • 高效的表示。 尽量尽可能高效地实现表示,因为它们可能会构造多次。

  • 输入检查。 检查操作的输入会引入额外开销,并与即时编译等工具发生冲突。找到添加有意义的有效性检查(例如张量形状)的平衡,同时保持其数量最小。