-
Notifications
You must be signed in to change notification settings - Fork 118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: Introduce Validating Emulators with Noise Models to the SDK #1017
base: main
Are you sure you want to change the base?
Changes from 94 commits
ea85675
28ec84e
f41c52b
fee7888
6f216ab
4e909e5
e3e4593
eda10b7
096aae7
0483fb9
a801ad4
36eff02
a328916
94b4226
1d37b0a
52e0465
467c27c
5dca87d
4c92658
88c9530
1ff8cfa
3355759
edef55d
14d09e8
c229adc
362ef45
74fecd0
28e79bc
dc4a9cf
56e730e
cf0af85
d802eaf
b14cf12
d648b87
0ecd0fa
bc19df5
7c1ac20
ad202d7
50c53cd
10a1dee
49809ba
a3f8459
4c8039b
d4bd18c
d9c9ede
5fc4437
e2c4b55
695c415
f1de87a
8c1b3ec
d38432b
a4906d9
39bbfaa
dcc061e
4b207ee
80f94d0
7268799
b236055
a55edb2
e39941d
9416ffb
ac49677
7b3ebf3
b5828ad
6b0ad92
ece64a4
7caf75e
19b78ab
51296d9
532451c
e254410
ae3a0e3
8756960
82d24e3
c6e60f1
5bec8d4
40bf179
3e1b47c
3f5094e
66703d7
ac070e2
c37bac4
545c8ce
afbb96c
934b0ae
b787222
8080de7
6cd4ffc
1e16ba1
87d342b
fdfa8c0
20c59a2
073fb4e
3c9fd67
0601c63
c5b9fc3
34b94ae
70881c6
b698648
e1cb307
3b500a3
7d7279e
8c63da6
df11f66
a79e07d
96e917a
f30379c
9bddfbc
4626332
e3ca8a5
baccbb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,13 @@ | |
|
||
from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation | ||
from braket.annealing.problem import Problem | ||
from braket.aws.aws_emulator_helpers import ( | ||
create_connectivity_criterion, | ||
create_gate_connectivity_criterion, | ||
create_gate_criterion, | ||
create_qubit_count_criterion, | ||
) | ||
from braket.aws.aws_noise_models import create_device_noise_model | ||
from braket.aws.aws_quantum_task import AwsQuantumTask | ||
from braket.aws.aws_quantum_task_batch import AwsQuantumTaskBatch | ||
from braket.aws.aws_session import AwsSession | ||
|
@@ -39,14 +46,18 @@ | |
|
||
# TODO: Remove device_action module once this is added to init in the schemas repo | ||
from braket.device_schema.pulse.pulse_device_action_properties_v1 import PulseDeviceActionProperties | ||
from braket.devices import Devices | ||
from braket.devices.device import Device | ||
from braket.emulators import Emulator | ||
from braket.emulators.emulator_passes import ProgramType | ||
from braket.ir.blackbird import Program as BlackbirdProgram | ||
from braket.ir.openqasm import Program as OpenQasmProgram | ||
from braket.parametric.free_parameter import FreeParameter | ||
from braket.parametric.free_parameter_expression import _is_float | ||
from braket.pulse import ArbitraryWaveform, Frame, Port, PulseSequence | ||
from braket.pulse.waveforms import _parse_waveform_from_calibration_schema | ||
from braket.schema_common import BraketSchemaBase | ||
from braket.tasks import QuantumTask | ||
|
||
|
||
class AwsDeviceType(str, Enum): | ||
|
@@ -855,3 +866,116 @@ def _parse_calibration_json( | |
parsed_calibration_data[gate_qubit_key] = gate_qubit_pulse | ||
|
||
return parsed_calibration_data | ||
|
||
@property | ||
def emulator(self) -> Emulator: | ||
""" | ||
A device emulator mimics the restrictions and noise of an AWS QPU by validating and | ||
compiling programs before running them on a simulated backend. An emulator can be used | ||
as a soft check that a program can run on an AwsDevice. | ||
|
||
Examples: | ||
>>> device = AwsDevice(Devices.IQM.Garnet) | ||
>>> circuit = Circuit().cnot(0, 1).h(2).cz(2, 3) | ||
>>> device.validate(circuit) | ||
>>> # validates, compiles and runs on the local simulator. | ||
>>> result = device.emulator(circuit, shots=100) | ||
>>> print(result.result().measurement_counts) | ||
|
||
Returns: | ||
Emulator: An emulator for this device, if this is not a simulator device. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add in "Returns" that an error will be thrown if the device is not a QPU? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed - added! |
||
""" | ||
if self._arn in [simulator_enum.value for simulator_enum in Devices.Amazon]: | ||
raise ValueError( | ||
"Creating an emulator from a Braket managed simulator is not supported." | ||
) | ||
if not hasattr(self, "_emulator"): | ||
self._emulator = self._setup_emulator() | ||
return self._emulator | ||
|
||
def _setup_emulator(self) -> Emulator: | ||
""" | ||
Sets up an Emulator object whose properties mimic that of this AwsDevice, if the device is a | ||
real QPU (not simulated). | ||
Altanali marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Returns: | ||
Emulator: An emulator with a noise model, compilation passes, and validation passes | ||
based on this device's properites. | ||
""" | ||
emulator_noise_model = create_device_noise_model(self.properties, self._arn) | ||
self._emulator = Emulator( | ||
noise_model=emulator_noise_model, backend="braket_dm", name=self._name | ||
) | ||
|
||
self._emulator.add_pass(create_qubit_count_criterion(self.properties)) | ||
self._emulator.add_pass(create_gate_criterion(self.properties)) | ||
self._emulator.add_pass(create_connectivity_criterion(self.properties, self.topology_graph)) | ||
self._emulator.add_pass( | ||
create_gate_connectivity_criterion(self.properties, self.topology_graph) | ||
) | ||
return self._emulator | ||
|
||
def validate( | ||
self, | ||
task_specification: Circuit, | ||
) -> None: | ||
""" | ||
Runs all non-modifying emulator passes on the input program and raises an | ||
error if any device-specific criterion are not met by the program. If the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not super important: can we check the usage of "criterion" vs "criteria" in doc-string and function names? I thought the former is singular and the latter is plural, but I am not the best person to check the grammar.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your call out is correct, this was a mistake on my part! "Criteria" should have been used here (and in line 926) where we are referencing multiple device constraints/criteria. |
||
program meets all criterion, returns. | ||
|
||
Args: | ||
task_specification (Circuit): The quantum program to emulate against | ||
this AwsDevice device properties. | ||
|
||
""" | ||
self.emulator.run_validation_passes(task_specification) | ||
return | ||
Altanali marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def run_emulator_passes( | ||
self, task_specification: ProgramType, apply_noise_model: bool = True | ||
) -> ProgramType: | ||
""" | ||
Runs all emulator passes and returns the modified program, which should be the same | ||
type as the input program. | ||
|
||
Args: | ||
task_specification (ProgramType): The quantum program to emulate against | ||
this AwsDevice device properties. | ||
|
||
apply_noise_model (bool): If true, apply a device specific noise model to the program | ||
before returning. | ||
|
||
Returns: | ||
ProgramType: A validated and compiled program that may be augmented with noise | ||
operations to mimic noise on this device. | ||
""" | ||
task_specification = task_specification.copy() | ||
return self.emulator.run_program_passes(task_specification, apply_noise_model) | ||
|
||
def emulate( | ||
self, | ||
task_specification: Circuit, | ||
shots: Optional[int] = None, | ||
inputs: Optional[dict[str, float]] = None, | ||
) -> QuantumTask: | ||
"""Emulate a quantum task specification on this quantum device emulator. | ||
A quantum task can be a circuit or an annealing problem. Emulation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the task be annealing problem? If yes, why do we want to do annealing, given that Dwave is long gone. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The task cannot be an annealing problem, this was a typo, thank you! |
||
involves running all emulator passes on the input program before running | ||
the program on the emulator's backend. | ||
|
||
Args: | ||
task_specification (Circuit): Specification of a quantum task | ||
to run on device. | ||
|
||
shots (Optional[int]): The number of times to run the quantum task on the device. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the behavior when shots is None, is it just shots=0 and we return the final density matrix? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The arguments and results of Emulator.emulate() follows that of |
||
Default is `None`. | ||
|
||
inputs (Optional[dict[str, float]]): Inputs to be passed along with the | ||
IR. If IR is an OpenQASM Program, the inputs will be updated with this value. | ||
Not all devices and IR formats support inputs. Default: {}. | ||
Returns: | ||
QuantumTask: The QuantumTask tracking task execution on this device emulator. | ||
""" | ||
task_specification = task_specification.copy() | ||
return self.emulator.run(task_specification, shots, inputs) |
speller26 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
from collections.abc import Iterable | ||
from functools import singledispatch | ||
from typing import Union | ||
|
||
from networkx import DiGraph | ||
|
||
from braket.device_schema import DeviceActionType, DeviceCapabilities | ||
from braket.device_schema.ionq import IonqDeviceCapabilities | ||
from braket.device_schema.iqm import IqmDeviceCapabilities | ||
from braket.device_schema.rigetti import RigettiDeviceCapabilities | ||
from braket.emulators.emulator_passes import ( | ||
ConnectivityCriterion, | ||
GateConnectivityCriterion, | ||
GateCriterion, | ||
QubitCountCriterion, | ||
) | ||
|
||
|
||
def create_qubit_count_criterion(properties: DeviceCapabilities) -> QubitCountCriterion: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated! |
||
""" | ||
Create a QubitCountCriterion pass which checks that the number of qubits used in a program does | ||
not exceed the number of qubits allowed by a QPU, as defined in the device properties. | ||
|
||
Args: | ||
properties (DeviceCapabilities): QPU Device Capabilities object with a | ||
QHP-specific schema. | ||
|
||
Returns: | ||
QubitCountCriterion: An eulator pass that checks that the number of qubits used in a program | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: "An emulator pass ..." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed! |
||
does not exceed that of the max qubit count on the device. | ||
""" | ||
qubit_count = properties.paradigm.qubitCount | ||
return QubitCountCriterion(qubit_count) | ||
|
||
|
||
def create_gate_criterion(properties: DeviceCapabilities) -> GateCriterion: | ||
supported_gates = properties.action[DeviceActionType.OPENQASM].supportedOperations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move this line to below the doc string. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed! |
||
""" | ||
Create a GateCriterion pass which defines what supported and native gates are allowed in a | ||
program based on the provided device properties. | ||
|
||
Args: | ||
properties (DeviceCapabilities): QPU Device Capabilities object with a | ||
QHP-specific schema. | ||
|
||
Returns: | ||
GateCriterion: An emulator pass that checks that a circuit only uses supported gates and | ||
verbatim circuits only use native gates. | ||
""" | ||
|
||
if isinstance(properties, IqmDeviceCapabilities): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment on why need to do something specific for IQM, and what is the logic here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment about this! To make note of the logic here: IqmDeviceCapabilities include the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: start/end_verbatim_box have been removed from the IQM Device Capabilities struct so this check can be removed as well. |
||
try: | ||
supported_gates.remove("start_verbatim_box") | ||
supported_gates.remove("end_verbatim_box") | ||
except ValueError: | ||
pass | ||
|
||
native_gates = properties.paradigm.nativeGateSet | ||
|
||
return GateCriterion(supported_gates=supported_gates, native_gates=native_gates) | ||
|
||
|
||
@singledispatch | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
def connectivity_criterion(...):
return _connectivity_criterion(...)
@singledispatch
def _connectivity_criterion(...):
... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated! Thank you for this callout, I wasn't aware of that interaction! |
||
def create_connectivity_criterion( | ||
properties: DeviceCapabilities, connectivity_graph: DiGraph | ||
) -> ConnectivityCriterion: | ||
""" | ||
Creates a ConnectivityCriterion pass which validates that multi-qubit gates are applied to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we are only dealing with 2-qubit gates here right? If yes, then let's be specific and say " .... validates that two-qubit gates ... ". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's correct! Updated the docstring! |
||
connected qubits based on this device's connectivity graph. | ||
|
||
Args: | ||
properties (DeviceCapabilities): QPU Device Capabilities object with a | ||
QHP-specific schema. | ||
|
||
connectivity_graph (DiGraph): Connectivity graph for this device. | ||
|
||
Returns: | ||
ConnectivityCriterion: An emulator pass that checks that a circuit only applies two-qubit | ||
gates to connected qubits on the device. | ||
""" | ||
connectivity_criterion = ConnectivityCriterion(connectivity_graph) | ||
return connectivity_criterion | ||
|
||
|
||
@create_connectivity_criterion.register(IqmDeviceCapabilities) | ||
def _(properties: IqmDeviceCapabilities, connectivity_graph: DiGraph) -> ConnectivityCriterion: | ||
""" | ||
IQM qubit connectivity is undirected but the directed graph that represents qubit connectivity | ||
does not include back-edges. Thus, we must explicitly introduce back edges before creating | ||
the ConnectivityCriterion for an IQM device. | ||
""" | ||
connectivity_graph = connectivity_graph.copy() | ||
for edge in connectivity_graph.edges: | ||
connectivity_graph.add_edge(edge[1], edge[0]) | ||
return ConnectivityCriterion(connectivity_graph) | ||
|
||
|
||
@singledispatch | ||
def create_gate_connectivity_criterion( | ||
properties: DeviceCapabilities, connectivity_graph: DiGraph | ||
) -> GateConnectivityCriterion: | ||
raise NotImplementedError | ||
|
||
|
||
@create_gate_connectivity_criterion.register(IqmDeviceCapabilities) | ||
@create_gate_connectivity_criterion.register(RigettiDeviceCapabilities) | ||
def _( | ||
properties: RigettiDeviceCapabilities, connectivity_graph: DiGraph | ||
) -> GateConnectivityCriterion: | ||
""" | ||
Both IQM and Rigetti have undirected connectivity graphs; Rigetti device capabilities | ||
provide back edges, but the calibration data only provides edges in one direction. | ||
Additionally, IQM does not provide back edges in its connectivity_graph (nor is this | ||
resolved manually by AwsDevice at the moment). | ||
""" | ||
gate_connectivity_graph = connectivity_graph.copy() | ||
edge_properties = properties.standardized.twoQubitProperties | ||
for u, v in gate_connectivity_graph.edges: | ||
edge_key = "-".join([str(qubit) for qubit in (u, v)]) | ||
edge_property = edge_properties.get(edge_key) | ||
if not edge_property: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment explaining when this if statement will be triggered, i.e., when we will not have a supported gate between two qubits despite there is an edge connecting them in the graph? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment regarding this! This check is in case there is an edge between qubits in their connectivity graph, but the QHP has not provided calibration data about the gates that can be applied to the qubit-pair; the per-edge/per-two-qubit-gate calibration data is used to determine what gates can be applied to a qubit-pair. |
||
gate_connectivity_graph[u][v]["supported_gates"] = set() | ||
continue | ||
edge_supported_gates = get_qpu_gate_translation( | ||
properties, [property.gateName for property in edge_property.twoQubitGateFidelity] | ||
) | ||
gate_connectivity_graph[u][v]["supported_gates"] = set(edge_supported_gates) | ||
|
||
for u, v in gate_connectivity_graph.edges: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment explaining the motivation to add the reversed edges into the connectivity graph? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added! To note the reason here: because Rigetti/IQM have undirected topologies but their topology_graph is directed (and does not include reverse edges), we have to manually add reverse edges so that during validation, a circuit is not marked as invalid for using using two-qubit gates in a direction not provided explicitly by the QHP. |
||
if (v, u) not in gate_connectivity_graph.edges or gate_connectivity_graph[v][u].get( | ||
"supported_gates" | ||
) in [None, set()]: | ||
gate_connectivity_graph.add_edge( | ||
v, u, supported_gates=set(gate_connectivity_graph[u][v]["supported_gates"]) | ||
) | ||
|
||
return GateConnectivityCriterion(gate_connectivity_graph) | ||
|
||
|
||
@create_gate_connectivity_criterion.register(IonqDeviceCapabilities) | ||
def _(properties: IonqDeviceCapabilities, connectivity_graph: DiGraph) -> GateConnectivityCriterion: | ||
""" | ||
Qubits in IonQ's trapped ion devices are all fully connected with identical | ||
gate-pair capabilities. IonQ does not expliclty provide a set of edges for | ||
gate connectivity between qubit pairs in their trapped ion QPUs. | ||
We extrapolate gate connectivity across all possible qubit edge pairs. | ||
""" | ||
gate_connectivity_graph = connectivity_graph.copy() | ||
native_gates = get_qpu_gate_translation(properties, properties.paradigm.nativeGateSet) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Argument docstring is updated to more explicitly describe this behavior! |
||
|
||
for edge in gate_connectivity_graph.edges: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to add the reversed edges for Ionq, given that we have done that for the rigetti and iqm devices above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The connectivity_graph for IonQ actually already has reverse edges as it's created using |
||
gate_connectivity_graph[edge[0]][edge[1]]["supported_gates"] = set(native_gates) | ||
|
||
return GateConnectivityCriterion(gate_connectivity_graph) | ||
|
||
|
||
def get_qpu_gate_translation( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the names not already standardized across QPUs? This shouldn't be necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few places where translation to a Braket gate name was necessary, i.e. gate names in Rigetti calibration data ("CPHASE" is used instead of the standard "cphaseshift") or in IonQs native gate set (translating "GPI"/"GPI2" to "GPi"/"GPi2"). I felt this was a little cleaner than doing translations in each place required. |
||
properties: DeviceCapabilities, gate_name: Union[str, Iterable[str]] | ||
) -> Union[str, list[str]]: | ||
"""Returns the translated gate name(s) for a given QPU device capabilities schema type | ||
and gate name(s). | ||
|
||
Args: | ||
properties (DeviceCapabilities): Device capabilities object based on a | ||
device-specific schema. | ||
gate_name (Union[str, Iterable[str]]): The name(s) of the gate(s) | ||
|
||
Returns: | ||
Union[str, list[str]]: The translated gate name(s) | ||
""" | ||
if isinstance(gate_name, str): | ||
return _get_qpu_gate_translation(properties, gate_name) | ||
else: | ||
return [_get_qpu_gate_translation(properties, name) for name in gate_name] | ||
|
||
|
||
@singledispatch | ||
def _get_qpu_gate_translation(properties: DeviceCapabilities, gate_name: str) -> str: | ||
"""Returns the translated gate name for a given QPU ARN and gate name. | ||
|
||
Args: | ||
properties (DeviceCapabilities): QPU Device Capabilities object with a | ||
QHP-specific schema. | ||
gate_name (str): The name of the gate | ||
|
||
Returns: | ||
str: The translated gate name | ||
""" | ||
return gate_name | ||
|
||
|
||
@_get_qpu_gate_translation.register(RigettiDeviceCapabilities) | ||
def _(properties: RigettiDeviceCapabilities, gate_name: str) -> str: | ||
translations = {"CPHASE": "CPhaseShift"} | ||
return translations.get(gate_name, gate_name) | ||
|
||
|
||
@_get_qpu_gate_translation.register(IonqDeviceCapabilities) | ||
def _(properties: IonqDeviceCapabilities, gate_name: str) -> str: | ||
translations = {"GPI": "GPi", "GPI2": "GPi2"} | ||
return translations.get(gate_name, gate_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's emphasize that the emulator is tied to the QPU, or 1-1 mapped to the QPU. So maybe let's say "A device emulator mimics the restrictions and noise of the AWS QPU by validating and compiling programs before running them on a simulated backend. An emulator can be used as a soft check that a program can run on the target AwsDevice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point, I've made the proposed changes to clarify that the emulator's are specific to the exact AwsDevice being used.