构建一个遗留插件

要添加一个继承自新设备接口的插件,请参见 构建插件

PennyLane 插件允许外部量子库利用 PennyLane 的自动微分能力。编写您自己的插件是一个简单易行的过程。在本节中,我们将逐步介绍在遗留设备 API 中创建您自己的 PennyLane 插件的步骤。

插件提供了什么

以下是关于PennyLane插件的简要介绍:

  • 插件是一个外部Python包,它为PennyLane提供了额外的量子设备

  • 每个插件可能提供一个或多个可以直接通过PennyLane访问的设备,以及任何其他私有函数或类。

  • 根据插件的范围,您可能希望提供额外的(自定义)量子操作和可观测量,供用户导入。

重要

在您的插件模块中,标准的 NumPy (不是 包装的 Autograd 版本的 NumPy, pennylane.numpy) 应在所有地方导入 (即,import numpy as np).

创建您的设备

创建您的PennyLane插件的第一步是创建您的设备类。 这很简单,只需从PennyLane导入抽象基类 pennylane.devices.LegacyDevice,并对其进行子类化:

from pennylane.devices import LegacyDevice

class MyDevice(LegacyDevice):
    """MyDevice docstring"""
    name = 'My custom device'
    short_name = 'example.mydevice'
    pennylane_requires = '0.1.0'
    version = '0.0.1'
    author = 'Ada Lovelace'

注意

大多数设备继承自一个名为 QubitDevicepennylane.devices.LegacyDevice 子类,它包含了许多特定于基于量子比特计算的功能。我们将在下面更深入地看待这个重要的案例。

警告

PennyLane设备的API目前正在更新,以遵循由pennylane.devices.Device类定义的新接口。 本指南描述了如何使用pennylane.devices.LegacyDevicepennylane.devices.QubitDevice基类创建设备,并将在我们继续切换到新API时进行更新。 同时,如果您需要帮助构建插件,请联系PennyLane团队,您可以通过创建一个issue或在我们的discussion forum发帖来进行联系。

在这里,我们已经开始定义一些重要的类属性,使得PennyLane能够识别和使用设备。这些包括:

  • pennylane.devices.LegacyDevice.name: 一个包含设备官方名称的字符串

  • pennylane.devices.LegacyDevice.short_name: 用于识别和加载PennyLane设备的字符串

  • pennylane.devices.LegacyDevice.pennylane_requires: 该设备支持的PennyLane版本。 注意,此类属性支持pip requirements.txt 风格的版本范围, 例如:

    • pennylane_requires = "2" 以支持 PennyLane 版本 2.x.x

    • pennylane_requires = ">=0.1.5,<0.6" 以支持一系列PennyLane版本

  • pennylane.devices.LegacyDevice.version: 设备的版本号

  • pennylane.devices.LegacyDevice.author: 设备的作者

定义上述所有属性是必需的。

设备能力

此外,您必须告诉PennyLane您的设备支持的操作,以及潜在的其他功能,通过提供以下类属性/属性:

  • pennylane.devices.LegacyDevice.stopping_condition:这个 BooleanFn 应该对于支持的 操作和测量过程返回 True,否则返回 False。注意,这个函数是在 两个 OperatorMeasurementProcess 类上调用的。尽管这个函数必须接受 OperatorMeasurementProcess 类,但它并不影响 MeasurementProcess 是否被支持。

    @property
    def stopping_condition(self):
        def accepts_obj(obj):
            return obj.name in {'CNOT', 'PauliX', 'PauliY', 'PauliZ'}
        return qml.BooleanFn(accepts_obj)
    

    支持的操作也可以通过 pennylane.devices.LegacyDevice.operations 属性来确定。 这个属性是一个包含支持操作字符串名称的列表。

    operations = {"CNOT", "PauliX"}
    

    查看 量子算符 以获取 PennyLane 支持的所有操作的完整列表。

    如果您的设备不原生支持一个定义了 decomposition() 静态方法的操作,PennyLane 将 尝试在调用设备之前对该操作进行分解。例如, Rot 分解方法 将 单量子比特旋转门分解为 RZRY 门。

    注意

    如果内置的 PennyLane 操作与目标框架中的对应操作之间的约定不同,请确保插件设备能够自动进行这两种约定之间的转换。

  • pennylane.devices.LegacyDevice.capabilities(): 一个类方法,用于返回设备能力的字典。新设备应覆盖此方法以检索父类的能力字典,制作副本,并在返回副本之前更新和/或添加能力。

    功能示例包括:

    • 'model' (str): 可以是 'qubit''cv'

    • 'returns_state' (bool): 如果设备通过 dev.state 返回量子态,则返回 True

    • 'supports_inverse_operations' (bool): True 如果设备支持应用操作的逆操作。应该被反转的操作具有属性 operation.inverse == True

    • 'supports_tensor_observables' (bool): True 如果设备支持由张量积组成的可观测量,比如 PauliZ(wires=0) @ PauliZ(wires=1)

    • 'supports_tracker' (bool): True 如果它具有设备跟踪器属性并使用该属性更新信息。

    一些功能由PennyLane核心查询,以决定如何最好地运行计算,而其他功能则被建立在设备生态系统之上的外部应用程序使用。

    要了解哪些功能是(可能自动)为您的设备定义的,dev = qml.device('my.device', *args, **kwargs),请检查dev.capabilities()的输出。

向您的设备添加参数

重要

PennyLane 支持量子比特和连续变量 (CV) 设备。然而,从现在开始,我们将演示插件开发,重点关注继承自 QubitDevice 类的量子比特设备。

定义自定义设备的 __init__ 方法不是必需的;默认情况下,将调用 QubitDevice 的初始化,用户可以传递以下参数:

  • wires (intIterable[Number, str]): 设备所表示的子系统数量,或一个可迭代的包含子系统唯一标签的数字(例如, [-1, 0, 2])和/或字符串(['auxiliary', 'q1', 'q2'])。

  • shots=1000 (None, intList[int]): 用于在非解析模式下估计可观察量的概率、期望值、方差的电路评估/随机样本的数量。如果 shots=None,则设备以解析方式计算概率、期望值和方差。如果 shots 是一个整数,则指定样本数量以估计这些量。如果传入的是整数列表,则电路评估会在样本列表上进行批处理。

要添加您自己的设备参数或覆盖上述任何默认值,只需覆盖 __init__ 方法。例如,这里有一个设备,其中导线数量固定为 24,不能在分析模式下使用,并且可以接受低级硬件控制选项的字典:

class CustomDevice(QubitDevice):
    name = 'My custom device'
    short_name = 'example.mydevice'
    pennylane_requires = '0.1.0'
    version = '0.0.1'
    author = 'Ada Lovelace'

    operations = {"PauliX", "RX", "CNOT"}
    observables = {"PauliZ", "PauliX", "PauliY"}

    def __init__(self, shots=1024, hardware_options=None):
        super().__init__(wires=24, shots=shots)
        self.hardware_options = hardware_options or hardware_defaults

请注意,我们还覆盖了默认的射击数量。

用户现在可以将这些参数传递给PennyLane设备加载器:

>>> dev = qml.device("example.mydevice", hardware_options={"t2": 0.1})
>>> dev.hardware_options
{"t2": 0.1}

设备执行

一旦所有类属性被定义,就需要定义一些必需的类方法,以允许PennyLane在您的设备上应用操作和测量可观察量。

要对设备执行操作,以下方法必须被定义:

apply(操作, **kwargs)

应用量子操作,将电路旋转到测量基,并编译和执行量子电路。

如果设备是一个状态向量模拟器(它可以在shots=None时执行解析计算)那么它必须覆盖:

analytic_probability([wires])

返回设备最后一次运行中每个计算基态的(边际)概率。

这个 QubitDevice 类提供了以下插件可以使用的便利方法:

active_wires(operators)

返回由一组算子作用的线路。

marginal_prob(概率[, 电线])

通过对未指定线路上的概率进行求和,返回计算基态的边际概率。

此外,如果你的qubit设备在执行后为测量线路生成自己的计算基样本,你需要重写以下方法:

generate_samples()

返回为所有线路生成的计算基样本。

generate_samples() 应返回形状为 (dev.shots, dev.num_wires) 的样本。此外,PennyLane 使用约定 \(|q_0,q_1,\dots,q_{N-1}\rangle\),其中 \(q_0\) 是最显著的位。

就这样!该设备继承了 expval()var()sample() 方法,每个方法接受一个可观测量(或可观测量的张量积)并返回相应的测量统计值。

有时需要额外的灵活性,以便与更复杂的框架进行接口交互。

当PennyLane需要评估QNode时,它会访问您的插件的 execute() 方法,该方法默认执行以下过程:

self.check_validity(circuit.operations, circuit.observables)

# apply all circuit operations
self.apply(circuit.operations, rotations=circuit.diagonalizing_gates)

# generate computational basis samples
if self.shots is not None or circuit.is_sampled:
    self._samples = self.generate_samples()

# compute the required statistics
results = self.statistics(circuit)

return self._asarray(results)

在这里,

  • circuit 是一个 CircuitGraph 对象

  • circuit.operations 是用户提供的 要执行的操作

  • circuit.observables 是用户提供的待测观测量

  • circuit.diagonalizing_gates 是在测量之前旋转电路的门,以便在请求的可观测量的特征基上执行计算基的测量

  • statistics() 返回 expval(), var()sample() 的结果,具体取决于可观测量的类型。

在高级情况下,execute() 方法以及 statistics() 可以直接被重写。这为您自己处理设备执行提供了完全的灵活性。然而,这可能会产生意想不到的副作用,并且不推荐这样做。

线路处理

PennyLane 使用 Wires 类来内部表示线路。Wires 继承自 Python 的 Sequence,表示一个有序的唯一线路标签集合。labels 属性存储线路标签的元组。用整数索引 Wires 实例将返回相应的标签。用 slice 索引将返回一个 Wires 实例。

例如:

from pennylane.wires import Wires

wires = Wires(['auxiliary', 0, 1])
print(wires.labels) # ('auxiliary', 0, 1)
print(wires[0]) # 'auxiliary'
print(wires[0:1]) # Wires(['auxiliary'])

量子电路一节所示,可以创建具有自定义线标签的设备:

from pennylane import *

dev = device('my.device', wires=['q11', 'q12', 'q21', 'q22'])

@qnode(dev)
def circuit():
   Gate1(wires='q22')
   Gate2(wires=['q21','q11'])
   Gate1(wires=['q21'])
   return expval(Obs(wires='q11') @ Obs(wires='q12'))

在幕后,当 my.device 被创建时,它将 ['q11', 'q12', 'q21', 'q22'] 转换为一个 Wires 对象并将其存储在设备的 wires 属性中。同样,当门和 可观察量被创建时,它们会将其 wires 参数转换为一个 Wires 对象并将其存储在它们的 wires 属性中。

print(dev.wires) #  Wires(['q11', 'q12', 'q21', 'q22'])

op = Gate2(wires=['q21','q11'])
print(op.wires) # Wires(['q21', 'q11'])

当设备应用操作时,它需要将 op.wires 转换为后端“理解”的线标签。这可以通过 pennylane.devices.LegacyDevice.map_wires() 方法来完成,该方法将 Wires 对象映射到其他 Wires 对象,并根据定义翻译的设备的 wire_map 属性更改标签。

# inside the class defining 'my.device', which inherits from the base Device class
device_wires = self.map_wires(op.wires)
print(device_wires) # Wires([2, 0])

默认情况下,映射将自定义标签 'q11', 'q12', 'q21', 'q22' 转换为连续整数 0, 1, 2, 3。如果设备使用不同的线编号,例如非连续的线路 0, 4, 7, 12,那么pennylane.devices.LegacyDevice.define_wire_map() 方法必须相应地被重写。

然后可以对device_wires进行进一步处理,例如,提取实际标签作为元组、列表或数组,或获取导线的数量:

device_wires.labels # (2, 0)

device_wires.tolist() # [2, 0]

device_wires.toarray() # ndarray([2, 0])

len(device_wires) # 2

Wires类还提供了设置功能,例如识别多个Wires对象之间的唯一或共享电线。

作为一种惯例,设备应该尽可能晚地在函数树中进行翻译和解包,并在可能的情况下传递原始 Wires 对象。

设备追踪器支持

设备跟踪器在跟踪模式开启时存储和记录信息。设备可以存储数据,例如执行次数、射击次数、批次数,或远程模拟器成本,以便用户以可自定义的方式进行交互。

与插件设计者相关的Tracker类的三个方面:

  • 布尔 active 属性表示是否更新和记录

  • update 方法接受关键字-值对并存储信息

  • record 方法,用户可以自定义此方法以记录、打印或以其他方式处理存储的信息

要获得任何设备跟踪器功能,设备应初始化一个占位符 Tracker 实例。用户可以通过用设备作为参数初始化一个新实例来覆盖此属性。

我们建议将以下代码放置在execute方法的末尾,

if self.tracker.active:
  self.tracker.update(executions=1, shots=self._shots)
  self.tracker.record()

以及在 batch_execute 方法中的类似代码:

if self.tracker.active:
  self.tracker.update(batches=1, batch_len=len(circuits))
  self.tracker.record()

这些函数在基类 pennylane.devices.LegacyDeviceQubitDevice 设备中被调用。除非你正在重写 executebatch_execute 方法,或者想要自定义存储的信息,否则你不需要添加任何新代码。

虽然这是推荐的用法,但可以在设备的任何位置调用 updaterecord 方法。上述示例跟踪执行、拍摄和批次,update() 方法可以接受任何组合的关键字-值对。例如,一个设备还可以通过以下方式跟踪成本和作业 ID:

price_for_execution = 0.10
job_id = "abcde"
self.tracker.update(price=price_for_execution, job_id=job_id)

识别和安装您的设备

在使用 PennyLane 执行混合计算时,首要步骤通常是初始化量子设备。PennyLane 通过它们的 short_name 来识别设备,这允许设备以以下方式初始化:

import pennylane as qml
dev1 = qml.device(short_name, wires=2)

其中 short_name 是一个唯一标识设备的字符串。 short_name 应该具有 pluginname.devicename 形式,使用句点作为分隔符。

PennyLane使用setuptools entry_points 方法来发现/集成插件。为了使您的插件的设备可供PennyLane访问,只需在您的 setup.py 文件中的setup() 函数中提供以下关键字参数:

devices_list = [
        'example.mydevice1 = MyModule.MySubModule:MyDevice1'
        'example.mydevice2 = MyModule.MySubModule:MyDevice2'
    ],
setup(entry_points={'pennylane.plugins': devices_list})

在哪里

  • devices_list 是您希望注册的设备列表,

  • example.mydevice1 是设备的简称,和

  • MyModule.MySubModule 是您的设备类 MyDevice1 的路径。

为了确保您的设备正常工作,您可以通过开发者模式安装它,使用pip install -e pluginpath,其中pluginpath是插件的位置。然后它将可以通过PennyLane访问。

测试

所有插件都应附带广泛的单元测试,以确保设备的每个逻辑单元都有正确的执行。

集成测试用于检查各种电路和可观测量的概率、期望值、方差和样本是否正确,作为PennyLane设备测试工具的一部分:

pl-device-test --device device_shortname --shots 10000

一般来说,由于所有支持的操作都已由PennyLane定义并测试了其梯度公式,因此不需要测试您的设备是否计算出正确的梯度。有关PennyLane设备测试工具的更多详细信息,请参见 pennylane.devices.tests

支持自定义操作符

如果您希望支持一个当前不被PennyLane支持的操作符(例如门或可观测量),您可以子类化Operator类。详细信息可以在添加新操作符一节中找到。

用户可以直接从您的插件导入此操作符,并在定义 QNode 时使用它:

import pennylane as qml
from MyModule.MySubModule import CustomGate

@qnode(dev1)
def my_qfunc(phi):
    qml.Hadamard(wires=0)
    CustomGate(phi, theta, wires=0)
    return qml.expval(qml.PauliZ(0))

警告

如果您提供的是PennyLane原生不支持的自定义运算符,建议插件单元测试提供测试,以确保PennyLane返回自定义操作的正确梯度。

如果自定义运算符在计算基中是对角的,它可以被添加到diagonal_in_z_basis属性中,在pennylane.ops.qubit.attributes中。设备可以利用这一信息来实现更快的模拟。