构建插件¶
要添加一个继承自遗留接口的插件,请参见 构建遗留插件。
重要
在你的插件模块中,标准的 NumPy (不是 包裹的 Autograd 版本的 NumPy) 应该在所有地方被导入(即,import numpy as np)。
PennyLane 允许外部量子库通过插件利用 PennyLane 的自动微分能力。编写自己的插件是一个简单易行的过程。在本节中,我们讨论设备接口中涉及的方法和概念。要查看最小设备的实现,我们建议查看 pennylane/devices/reference_qubit.py 中的实现。
创建您的设备¶
为了定义一个自定义设备,您只需重写 execute() 方法。
from pennylane.devices import Device, DefaultExecutionConfig
from pennylane.tape import QuantumScript, QuantumScriptOrBatch
class MyDevice(Device):
"""My Documentation."""
def execute(
self,
circuits: QuantumScriptOrBatch,
execution_config: "ExecutionConfig" = DefaultExecutionConfig
):
# your implementation here.
例如:
class MyDevice(Device):
"""My Documentation."""
def execute(
self,
circuits: QuantumScriptOrBatch,
execution_config: "ExecutionConfig" = DefaultExecutionConfig
)
return 0.0 if isinstance(circuits, qml.tape.QuantumScript) else tuple(0.0 for c in circuits)
dev = MyDevice()
@qml.qnode(dev)
def circuit():
return qml.state()
circuit()
这个执行方法与可选的 Device.preprocess_transforms 和 Device.setup_execution_config() 一起工作,下面会详细描述。预处理变换将通用电路转换为设备支持的电路,或者如果电路无效则引发错误。执行会从这些支持的电路中产生数值结果。
在一个更简单的示例中,对于任何初始的量子胶卷批次和一个配置对象,我们希望能够做到:
execution_config = dev.setup_execution_config(initial_config)
transform_program = dev.preprocess_transforms(execution_config)
circuit_batch, postprocessing = transform_program(initial_circuit_batch)
results = dev.execute(circuit_batch, execution_config)
final_results = postprocessing(results)
镜头¶
虽然镜头的工作流默认值由 Device.shots 指定,但设备本身应使用在每个量子带上指定的 shots 属性中的镜头数量。通过为每个电路动态拉取镜头,用户可以有效地在电路批次之间分配镜头预算。
>>> tape0 = qml.tape.QuantumScript([], [qml.sample(wires=0)], shots=5)
>>> tape1 = qml.tape.QuantumScript([], [qml.sample(wires=0)], shots=10)
>>> dev = qml.device('default.qubit')
>>> dev.execute((tape0, tape1))
(array([0, 0, 0, 0, 0]), array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
Shots 类描述了 shots。用户可以选择性地指定一个 shots 向量,或在计算最终期望值时使用不同数量的 shots。
>>> tape0 = qml.tape.QuantumScript([], [qml.expval(qml.PauliX(0))], shots=(5, 500, 1000))
>>> tape0.shots.shot_vector
(ShotCopies(5 shots x 1),
ShotCopies(500 shots x 1),
ShotCopies(1000 shots x 1))
>>> list(tape0.shots)
[5, 500, 1000]
>>> list(tape0.shots.bins())
[(0, 5), (5, 505), (505, 1505)]
>>> dev.execute(tape0)
(0.2, -0.052, -0.014)
第一个数字 0.2 是通过 5 次测量计算得出的,第二个 -0.052 是通过 500 次测量计算得出的,-0.014 是通过 1000 次测量计算得出的。所有 1,505 次测量可以在一个批次中请求,但后处理成期望值是分别通过测量 0:5、5:505 和 505:1505 完成的。
shots.total_shots 是 None 表示分析执行(无限次试验)。 bool(shots) 也可以用于检测有限次试验和分析执行之间的区别。如果 shots 为真,则存在有限次试验。如果 shots 为假,则应该进行分析执行。
预处理¶
在设备执行的电路预处理过程中,有两个组成部分:
创建一个
TransformProgram,能够将任意批量的QuantumScript转换为一批新的由execute方法支持的磁带。通过填充设备选项并做出关于微分的决策来设置
ExecutionConfig数据类。
这两个任务分别由 setup_execution_config() 和 preprocess_transforms() 执行。一旦变换程序应用于一批电路,程序产生的结果电路批次应该通过 Device.execute 无错误地运行:
execution_config = dev.setup_execution_config(initial_config)
transform_program = dev.preprocess_transforms(execution_config)
batch, fn = transform_program(initial_batch)
fn(dev.execute(batch, execution_config))
本节将重点介绍 preprocess_transforms(),有关 setup_execution_config() 的更多信息,请参见下面的 **执行配置** 部分。
PennyLane 可以通过 preprocess_transforms() 提供变换程序的默认实现,这对于大多数插件设备来说应该是足够的。这要求为您的设备定义一个 TOML 格式的配置文件。该配置文件的详细信息在 下一节 中描述。如果提供,该默认预处理程序将基于此文件中声明的内容进行构建。
您可以使用完全自定义的实现来覆盖 preprocess_transforms() 方法,或者通过添加新的转换来扩展默认行为。
该 preprocess_transforms() 方法应该首先创建一个变换程序:
program = qml.transforms.core.TransformProgram()
一旦程序创建完成,个别变换可以通过 add_transform() 方法添加到程序中。
from pennylane.devices.preprocess import validate_device_wires, validate_measurements, decompose
program.add_transform(validate_device_wires, wires=qml.wires.Wires((0,1,2)), name="my_device")
program.add_transform(validate_measurements, name="my_device")
program.add_transform(qml.defer_measurements)
program.add_transform(qml.transforms.split_non_commuting)
def supports_operation(op):
return getattr(op, "name", None) in operation_names
program.add_transform(decompose, stopping_condition=supports_operation, name="my_device")
program.add_transform(qml.transforms.broadcast_expand)
预处理和验证也可以存在于execute() 方法中,但将它们放在预处理程序中有几个好处。验证可以更早发生,从而减少在错误被引发之前所消耗的资源。用户可以在预处理的不同阶段检查、绘制和指定磁带。这使用户更好地意识到设备实际执行的内容。当设备梯度被使用时,机器学习接口会跟踪预处理的转换。通过机器学习框架跟踪预处理的经典部分,设备无需手动跟踪任何分解或编译的经典部分。例如,
>>> @qml.qnode(qml.device('reference.qubit', wires=2))
... def circuit(x):
... qml.IsingXX(x, wires=(0,1))
... qml.CH((0,1))
... return qml.expval(qml.X(0))
>>> print(qml.draw(circuit, level="device")(0.5))
0: ─╭●──RX(0.50)─╭●────────────╭●──RY(-1.57)─┤ <Z>
1: ─╰X───────────╰X──RY(-0.79)─╰Z──RY(0.79)──┤
允许用户看到 IsingXX 和 CH 都被设备分解,并且对 qml.expval(qml.X(0)) 应用了对角门。
即使有这些好处,设备仍然可以选择将一些变换放置在 execute 方法内部。例如, default.qubit 在 execute 内部将导线映射到仿真索引,而不是在 preprocess_transforms 中。
该 execute() 方法可以假设设备预处理已经在输入带上执行,并且没有义务重新验证输入或提供合理的错误信息。在下面的例子中,我们看到当存在不支持的操作和不支持的测量时,default.qubit 会出错。
>>> op = qml.Permute([2,1,0], wires=(0,1,2))
>>> tape = qml.tape.QuantumScript([op], [qml.probs(wires=(0,1))])
>>> qml.device('default.qubit').execute(tape)
MatrixUndefinedError:
>>> tape = qml.tape.QuantumScript([], [qml.density_matrix(wires=0)], shots=50)
>>> qml.device('default.qubit').execute(tape)
AttributeError: 'DensityMatrixMP' object has no attribute 'process_samples'
设备可以按照自定义转换模块中的描述定义自己的转换,或者可以包含内置的转换,例如:
设备能力¶
可选地,您可以添加一个 config_filepath 类变量,指向您的配置文件。 该文件应是一个 toml 文件,描述您的设备支持哪些门和特性,即 execute() 方法接受什么。
from os import path
from pennylane.devices import Device
class MyDevice(Device):
"""My Documentation."""
config_filepath = path.join(path.dirname(__file__), "relative/path/to/config.toml")
该配置文件将被加载到另一个类变量 capabilities 中,该变量在默认实现的 preprocess_transforms() 中使用,如果您选择不按照上面的描述自行覆盖它。请注意,此文件必须按照这一部分末尾的说明声明为数据包。
以下是一个示例配置文件,定义了所有接受的字段,并提供了如何填写这些字段的内联描述。除非另有说明,否则所有标题和字段都是一般必需的。
schema = 3
# The set of all gate types supported at the runtime execution interface of the
# device, i.e., what is supported by the `execute` method. The gate definitions
# should have the following format:
#
# GATE = { properties = [ PROPS ], conditions = [ CONDS ] }
#
# where PROPS and CONS are zero or more comma separated quoted strings.
#
# PROPS: additional support provided for each gate.
# - "controllable": if a controlled version of this gate is supported.
# - "invertible": if the adjoint of this operation is supported.
# - "differentiable": if device gradient is supported for this gate.
# CONDS: constraints on the support for each gate.
# - "analytic" or "finiteshots": if this operation is only supported in
# either analytic execution or with shots, respectively.
#
[operators.gates]
PauliX = { properties = ["controllable", "invertible"] }
PauliY = { properties = ["controllable", "invertible"] }
PauliZ = { properties = ["controllable", "invertible"] }
RY = { properties = ["controllable", "invertible", "differentiable"] }
RZ = { properties = ["controllable", "invertible", "differentiable"] }
CRY = { properties = ["invertible", "differentiable"] }
CRZ = { properties = ["invertible", "differentiable"] }
CNOT = { properties = ["invertible"] }
# Observables supported by the device for measurements. The observables defined
# in this section should have the following format:
#
# OBSERVABLE = { conditions = [ CONDS ] }
#
# where CONDS is zero or more comma separated quoted strings, same as above.
#
# CONDS: constraints on the support for each observable.
# - "analytic" or "finiteshots": if this observable is only supported in
# either analytic execution or with shots, respectively.
# - "terms-commute": if a composite operator is only supported under the
# condition that its terms commute.
#
[operators.observables]
PauliX = { }
PauliY = { }
PauliZ = { }
Hamiltonian = { conditions = [ "terms-commute" ] }
Sum = { conditions = [ "terms-commute" ] }
SProd = { }
Prod = { }
# Types of measurement processes supported on the device. The measurements in
# this section should have the following format:
#
# MEASUREMENT_PROCESS = { conditions = [ CONDS ] }
#
# where CONDS is zero or more comma separated quoted strings, same as above.
#
# CONDS: constraints on the support for each measurement process.
# - "analytic" or "finiteshots": if this measurement is only supported
# in either analytic execution or with shots, respectively.
#
[measurement_processes]
ExpectationMP = { }
SampleMP = { }
CountsMP = { conditions = ["finiteshots"] }
StateMP = { conditions = ["analytic"] }
# Additional support that the device may provide that informs the compilation
# process. All accepted fields and their default values are listed below.
[compilation]
# Whether the device is compatible with qjit.
qjit_compatible = false
# Whether the device requires run time generation of the quantum circuit.
runtime_code_generation = false
# Whether the device supports allocating and releasing qubits during execution.
dynamic_qubit_management = false
# Whether simultaneous measurements on overlapping wires is supported.
overlapping_observables = true
# Whether simultaneous measurements of non-commuting observables is supported.
# If false, a circuit with multiple non-commuting measurements will have to be
# split into multiple executions for each subset of commuting measurements.
non_commuting_observables = false
# Whether the device supports initial state preparation.
initial_state_prep = false
# The methods of handling mid-circuit measurements that the device supports,
# e.g., "one-shot", "tree-traversal", "device", etc. An empty list indicates
# that the device does not support mid-circuit measurements.
supported_mcm_methods = [ ]
这个 TOML 配置文件对于 PennyLane 是可选的,但对于 Catalyst 集成是必需的, 即与 qml.qjit 的兼容性。更多细节,请参见 自定义设备。
中途电路测量¶
PennyLane 支持 中间电路测量,即在量子电路中间进行的测量,用于动态地塑造电路的结构,并在电路执行期间收集有关量子态的信息。这可能并不是所有设备原生支持的。
如果您的设备不支持中间电路测量,将应用 延迟测量 方法。另一方面,如果您的设备能够通过一次执行来评估动态电路,并为每次执行采样动态执行路径,您应该将 "one-shot" 包含为配置文件中的 supported_mcm_methods 之一。当在 QNode 上请求 "one-shot" 方法时,将应用 动态一次性 方法。
上述提到的两种方法涉及到转换程序以便在电路上应用,这些程序为设备执行做好准备,并进行后处理功能以汇总结果。或者,如果您的设备本身支持PennyLane提供的所有中间电路测量功能,您应该将 "device" 包含为 supported_mcm_methods 之一。
电缆¶
设备现在可以:
在初始化时严格使用用户提供的线路:
device(name, wires=wires)推断提交电路提供的线的数量和顺序
严格要求特定的线标签
选项 2 允许工作流随时间改变线的数量和标签,但有时用户希望强制执行线的约定和标签。如果用户提供线,preprocess_transforms() 应该验证提交的电路仅在请求的范围内具有线。
>>> dev = qml.device('default.qubit', wires=1)
>>> circuit = qml.tape.QuantumScript([qml.CNOT((0,1))], [qml.state()])
>>> dev.preprocess_transforms()((circuit,))
WireError: Cannot run circuit(s) of default.qubit as they contain wires not found on the device.
PennyLane 的导线可以是任何可哈希对象,导线标签通过它们的相等性和哈希值来区分。
如果更倾向于内部使用连续整数(0、1、2、……),可以在
execute() 方法中使用 map_to_standard_wires() 方法。
map_wires 转换也可以将提交电路的导线映射到内部标签。
有时候,硬件量子比特标签无法在不改变行为的情况下任意映射。连接性、噪声、性能和其他限制可能使得对量子比特1的操作无法与对量子比特2的相同操作任意交换。在这种情况下,设备可以硬编码唯一可接受的接线标签列表。在这种情况下,用户需要故意映射接线,如果他们希望发生这样的事情。
>>> qml.device('my_hardware').wires
<Wires = [0, 1, 2, 3]>
>>> qml.device('my_hardware', wires=(10, 11, 12, 13))
TypeError: MyHardware.__init__() got an unexpected keyword argument 'wires'
要实现这样的验证,设备开发者可以简单地在初始化调用签名中留出 wires,并硬编码 wires 属性。他们还应该确保在变换程序中包含 validate_device_wires。
class MyDevice(qml.devices.Device):
def __init__(self, shots=None):
super().__init__(shots=shots)
@property
def wires(self):
return qml.wires.Wires((0,1,2,3))
执行配置¶
执行配置存储两种信息:
关于设备如何执行操作的信息。示例包括
device_options和gradient_method。有关PennyLane如何与设备交互的信息。示例包括
use_device_gradient和grad_on_execution。
设备选项:
设备选项是用于配置执行行为的任何特定于设备的选项。例如, default.qubit 有 max_workers、rng 和 prng_key。 default.tensor 有 contract、contraction_optimizer、cutoff、c_dtype、local_simplify、method 和 max_bond_dim。这些选项通常在初始化时设置为默认值。这些值应放入 ExecutionConfig.device_options 字典中,在 setup_execution_config() 中。请注意,我们确实提供了该方法的默认实现,但您很可能需要自己覆盖它。
>>> dev = qml.device('default.tensor', wires=2, max_bond_dim=4, contract="nonlocal", c_dtype=np.complex64)
>>> dev.setup_execution_config().device_options
{'contract': 'nonlocal',
'contraction_optimizer': 'auto-hq',
'cutoff': None,
'c_dtype': numpy.complex64,
'local_simplify': 'ADCRS',
'max_bond_dim': 4,
'method': 'mps'}
即使属性作为设备上的一个属性存储,执行也应该从配置中获取这些属性的值,而不是从设备实例中获取。虽然尚未在用户顶层集成,我们的目标是允许设备的动态配置。
>>> dev = qml.device('default.qubit')
>>> config = qml.devices.ExecutionConfig(device_options={"rng": 42})
>>> tape = qml.tape.QuantumTape([qml.Hadamard(0)], [qml.sample(wires=0)], shots=10)
>>> dev.execute(tape, config)
array([1, 0, 1, 1, 0, 1, 1, 1, 0, 0])
>>> dev.execute(tape, config)
array([1, 0, 1, 1, 0, 1, 1, 1, 0, 0])
通过从这个字典中提取选项,而不是从设备属性中提取,我们解锁了两个关键的功能:
通过仅检查
ExecutionConfig对象来跟踪和指定执行的确切配置在工作流过程中动态配置设备。
工作流配置:
请注意,这些属性仅适用于提供导数或VJP的设备。如果您的设备不提供导数,您可以安全地忽略这些属性。
工作流选项包括 use_device_gradient、 use_device_jacobian_product 和 grad_on_execution。use_device_gradient=True 表示工作流应从设备请求导数。grad_on_execution=True 表示更倾向于使用 execute_and_compute_derivatives 而不是先执行 execute 然后再执行 compute_derivatives。最后,use_device_jacobian_product 表示请求调用 compute_vjp 而不是 compute_derivatives。请注意,如果 use_device_jacobian_product 为 True,则优先于计算完整的雅可比矩阵。
>>> config = qml.devices.ExecutionConfig(gradient_method="adjoint")
>>> processed_config = qml.device('default.qubit').setup_execution_config(config)
>>> processed_config.use_device_jacobian_product
True
>>> processed_config.use_device_gradient
True
>>> processed_config.grad_on_execution
True
执行¶
有关预期结果类型输出的文档,请参阅 返回类型规范。
设备API允许各个设备以对其本身有意义的方式计算结果。尽管在实现上有这种自由,但也伴随着更大的责任,以处理过程中的每个阶段。
PennyLane 确实提供了一些辅助函数来帮助执行电路。任何 StateMeasurement 都有 process_state 和 process_density_matrix 方法,用于经典后处理状态向量或密度矩阵,而 SampleMeasurement 实现了 process_samples 和 process_counts。pennylane.devices.qubit 模块还包含实现基于 Python 的状态向量模拟的函数。
假设您正在访问只能返回原始样本的硬件。在这里,我们使用mp.process_samples方法将子样本处理为请求的最终结果对象。请注意,当我们没有镜头向量或单个测量时,我们需要挤出单例维度。
def single_tape_execution(tape) -> qml.typing.Result:
samples = get_samples(tape)
results = []
for lower, upper in tape.shots.bins():
sub_samples = samples[lower:upper]
results.append(
tuple(mp.process_samples(sub_samples, tape.wires) for mp in tape.measurements)
)
if len(tape.measurements) == 1:
results = tuple(res[0] for res in results)
if tape.shots.has_partitioned_shots:
results = results[0]
return results
设备修改器¶
PennyLane 目前提供两个设备修改器。
例如,使用自定义设备,我们可以添加模拟器风格的跟踪和处理单个电路的能力。有关每个修饰符的更多详细信息,请参阅文档。
@simulator_tracking
@single_tape_support
class MyDevice(qml.devices.Device):
def execute(self, circuits, execution_config = qml.devices.DefaultExecutionConfig):
return tuple(0.0 for _ in circuits)
>>> dev = MyDevice()
>>> tape = qml.tape.QuantumTape([qml.S(0)], [qml.expval(qml.X(0))])
>>> with dev.tracker:
... out = dev.execute(tape)
>>> out
0.0
>>> dev.tracker.history
{'batches': [1],
'simulations': [1],
'executions': [1],
'results': [0.0],
'resources': [Resources(num_wires=1, num_gates=1,
gate_types=defaultdict(<class 'int'>, {'S': 1}),
gate_sizes=defaultdict(<class 'int'>, {1: 1}), depth=1,
shots=Shots(total_shots=None, shot_vector=()))]}
设备追踪器支持¶
设备跟踪器在跟踪模式开启时存储和记录信息。设备可以存储数据,例如执行次数、射击次数、批次数,或远程模拟器成本,以便用户以可自定义的方式进行交互。
与插件设计者相关的Tracker类的三个方面:
布尔
active属性表示是否更新和记录update方法接受关键字-值对并存储信息record方法,用户可以自定义此方法以记录、打印或以其他方式处理存储的信息
为了获得类似于仿真的跟踪行为,可以将 simulator_tracking() 装饰器添加到设备中:
@qml.devices.modifiers.simulator_tracking
class MyDevice(Device):
...
simulator_tracking 在设备可以同时测量不对易的测量值或处理参数广播时是非常有用的,因为它既跟踪模拟又跟踪相应数量的类QPU电路。
为了实现你自己的跟踪,我们建议将以下代码放在 execute 方法中:
if self.tracker.active:
self.tracker.update(batches=1, executions=len(circuits))
for c in circuits:
self.tracker.update(shots=c.shots)
self.tracker.record()
如果设备提供微分逻辑,我们还建议跟踪衍生批次的数量、执行和衍生批次的数量,以及衍生物的数量。
虽然这是推荐的用法,但可以在设备的任何位置调用 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通过它们的 name 来识别设备,这样可以以以下方式初始化设备:
import pennylane as qml
dev1 = qml.device(name)
其中 name 是一个唯一标识设备的字符串。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访问。
如果为您的设备定义了一个 配置文件,您需要在 setup.py 中将其声明为包数据:
from setuptools import setup, find_packages
setup(
...
include_package_data=True,
package_data={
'package_name' : ['path/to/config/device_name.toml'],
},
...
)
或者,使用 include_package_data=True,您还可以在 MANIFEST.in 中声明该文件:
include path/to/config/device_name.toml
请参阅 打包数据文件 以获取详细说明。这将确保 PennyLane 可以正确加载设备及其相关功能。