构建一个遗留插件¶
要添加一个继承自新设备接口的插件,请参见 构建插件。
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'
注意
大多数设备继承自一个名为 QubitDevice 的 pennylane.devices.LegacyDevice 子类,它包含了许多特定于基于量子比特计算的功能。我们将在下面更深入地看待这个重要的案例。
警告
PennyLane设备的API目前正在更新,以遵循由pennylane.devices.Device类定义的新接口。 本指南描述了如何使用pennylane.devices.LegacyDevice和pennylane.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.xpennylane_requires = ">=0.1.5,<0.6"以支持一系列PennyLane版本
pennylane.devices.LegacyDevice.version: 设备的版本号pennylane.devices.LegacyDevice.author: 设备的作者
定义上述所有属性是必需的。
设备能力¶
此外,您必须告诉PennyLane您的设备支持的操作,以及潜在的其他功能,通过提供以下类属性/属性:
pennylane.devices.LegacyDevice.stopping_condition:这个BooleanFn应该对于支持的 操作和测量过程返回True,否则返回False。注意,这个函数是在 两个Operator和MeasurementProcess类上调用的。尽管这个函数必须接受Operator和MeasurementProcess类,但它并不影响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分解方法 将 单量子比特旋转门分解为RZ和RY门。注意
如果内置的 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(int 或 Iterable[Number, str]): 设备所表示的子系统数量,或一个可迭代的包含子系统唯一标签的数字(例如,[-1, 0, 2])和/或字符串(['auxiliary', 'q1', 'q2'])。shots=1000(None, int 或 List[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在您的设备上应用操作和测量可观察量。
要对设备执行操作,以下方法必须被定义:
|
应用量子操作,将电路旋转到测量基,并编译和执行量子电路。 |
如果设备是一个状态向量模拟器(它可以在shots=None时执行解析计算)那么它必须覆盖:
|
返回设备最后一次运行中每个计算基态的(边际)概率。 |
这个 QubitDevice 类提供了以下插件可以使用的便利方法:
|
返回由一组算子作用的线路。 |
|
通过对未指定线路上的概率进行求和,返回计算基态的边际概率。 |
此外,如果你的qubit设备在执行后为测量线路生成自己的计算基样本,你需要重写以下方法:
|
返回为所有线路生成的计算基样本。 |
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.LegacyDevice 和 QubitDevice 设备中被调用。除非你正在重写 execute 和 batch_execute 方法,或者想要自定义存储的信息,否则你不需要添加任何新代码。
虽然这是推荐的用法,但可以在设备的任何位置调用 update 和 record 方法。上述示例跟踪执行、拍摄和批次,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中。设备可以利用这一信息来实现更快的模拟。