Skip to content
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

Open
wants to merge 111 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 94 commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
ea85675
feat: Introduce emulator criterion
ltnln Jun 3, 2024
28ec84e
feat: Introduce NativeGateCriterion + Unit Tests
ltnln Jun 3, 2024
f41c52b
feat: Introduce SupportedGateCriterion + Unit Tests
ltnln Jun 3, 2024
fee7888
feat: Introduce Connectivity Criterion + Unit Tests
ltnln Jun 3, 2024
6f216ab
feat: Add __init__.py to emulators/criteria module
ltnln Jun 3, 2024
4e909e5
feat: Add OpenQASM3.0-to-Pytket translations
ltnln Jun 5, 2024
e3e4593
feat: Introduce PytketProgramContext to generate PytketCircuits from …
ltnln Jun 5, 2024
eda10b7
feat: Begin emulator pass implementation
ltnln Jun 6, 2024
096aae7
feat: Change gate translations to use pytket OpType objects instead o…
ltnln Jun 6, 2024
0483fb9
Merge branch 'add_pytket_context' into add_tket_mapping_pass
ltnln Jun 6, 2024
a801ad4
feat: Introduce Pytket-to-Openqasm Translators
ltnln Jun 7, 2024
36eff02
fix: Covert from radians to Tket half-turns in PytketContext
ltnln Jun 7, 2024
a328916
feat: add MEASUREMENT_REGISTER_NAME to translation constants
ltnln Jun 7, 2024
94b4226
feat: Introduce EmulatorPass abstraction and have EmulatorCriterion i…
ltnln Jun 7, 2024
1d37b0a
fix: Update __init__.pys for emulator_pass and criteria directories
ltnln Jun 7, 2024
52e0465
fix: Correct import paths in emulator_passes source and tests
ltnln Jun 7, 2024
467c27c
feat: Introduce Gate Connectivity Criterion
ltnln Jun 10, 2024
5dca87d
fix: Correct qubit order in error print statement
ltnln Jun 10, 2024
4c92658
feat: Add GateConnectivityCriterion tests
ltnln Jun 10, 2024
88c9530
feat: Basic AWS Noise Model
ltnln Jun 12, 2024
1ff8cfa
fix: Discard IonQ Single Qubit Gate Fidelities and use Gate classes i…
ltnln Jun 14, 2024
3355759
Merge branch 'emulation_criteria_basic' into end_to_end_prototype
ltnln Jun 14, 2024
edef55d
fix: Include EmulatorCriterion in __init__.py for emulator_passes module
ltnln Jun 14, 2024
14d09e8
feat: Introduce Emulator Interface and AwsEmulator
ltnln Jun 14, 2024
c229adc
fix: Fix gate device noise model file name typo
ltnln Jun 14, 2024
362ef45
Merge branch 'add_gate_connectivity' into end_to_end_prototype
ltnln Jun 14, 2024
74fecd0
fix: Remove stray print statement
ltnln Jun 18, 2024
28e79bc
feat: Introduce emulators and instantiate emulators within AWS device…
ltnln Jun 18, 2024
dc4a9cf
feat: Introduce Qubit Count Criteria
ltnln Jun 18, 2024
56e730e
feat: Move NoiseModel generation to a set of helper functions and cal…
ltnln Jun 18, 2024
cf0af85
fix: Clean Up Branch
ltnln Jun 18, 2024
d802eaf
Merge branch 'add_pytket_context' into end_to_end_prototype
ltnln Jun 19, 2024
b14cf12
feat: Complete tket_to_qasm3 translations
ltnln Jun 19, 2024
d648b87
fix: Return Qasm3 string instead of program context from tket_to_qasm…
ltnln Jun 19, 2024
0ecd0fa
fix: Add tket_to_qasm3 to pytket_translator module __init__.py
ltnln Jun 19, 2024
bc19df5
feat: Introduce LexiRoutingPass
ltnln Jun 19, 2024
7c1ac20
feat: Split out mapping into a run() dispatch with a pytket circuit a…
ltnln Jun 19, 2024
ad202d7
fix: Add LexiRoutingPass to emulator_passes module __init__.py
ltnln Jun 19, 2024
50c53cd
fix: Implement __eq__ in qubit_count_criterion and fix error print st…
ltnln Jun 19, 2024
10a1dee
feat: AwsDevices add mapping/routing pass to their emulator based on …
ltnln Jun 19, 2024
49809ba
feat: Add validation and compile methods to AwsDevice
ltnln Jun 19, 2024
a3f8459
fix: Update connectivity criterion to validate connectivity of ALL 2-…
ltnln Jun 19, 2024
4c8039b
fix: Remove stray print statement
ltnln Jun 19, 2024
d4bd18c
fix: Add classical indices to add_measure instruction in Tket Context
ltnln Jun 20, 2024
d9c9ede
fix: Add classical targets option to add_measure instruction in Brake…
ltnln Jun 20, 2024
5fc4437
Merge branch 'main' into end_to_end_prototype
ltnln Jun 24, 2024
e2c4b55
fix: Update add_instruction to update circuit _measurement_targets at…
ltnln Jun 24, 2024
695c415
Merge branch 'introduce_classical_measure_targets' into end_to_end_pr…
ltnln Jun 24, 2024
f1de87a
feat: Merge SupportedGateCriterion and NativeGateCriterion classes in…
ltnln Jun 24, 2024
8c1b3ec
fix: Allow GateConnectivityCriterion to be instantiated with a graph …
ltnln Jun 25, 2024
d38432b
feat: Allow ConnectivityGraphs to be instantiated with a graph marked…
ltnln Jun 25, 2024
a4906d9
feat: Add QubitCountCriterion tests and check if qubit_count is negat…
ltnln Jun 25, 2024
39bbfaa
change: remove SpportedGateCriterion and NativeGateCriterion after re…
ltnln Jun 25, 2024
dcc061e
feat: Add tests for tket_to_qasm3 translation, remove extraneous comm…
ltnln Jun 25, 2024
4b207ee
change: Remove references to NativeGateCriterion and SupportedGateCri…
ltnln Jun 25, 2024
80f94d0
Merge branch 'amazon-braket:main' into end_to_end_prototype
Altanali Jun 25, 2024
7268799
fix: Add set apply_noise_model flag to false in Emulator.run to preve…
ltnln Jun 25, 2024
b236055
Merge branch 'main' into introduce_classical_measure_targets
Altanali Jun 25, 2024
a55edb2
change: Target default simulator branch with classical register indic…
ltnln Jun 26, 2024
e39941d
fix: Run linter and fix formatting/complexity
ltnln Jun 26, 2024
9416ffb
Merge branch 'introduce_classical_measure_targets' into end_to_end_pr…
ltnln Jun 26, 2024
ac49677
feat: Run Linter
ltnln Jun 26, 2024
7b3ebf3
fix: Revert default simulator versions and dependent round_trip tests
ltnln Jun 26, 2024
b5828ad
Update src/braket/circuits/braket_program_context.py
speller26 Jun 26, 2024
6b0ad92
Update src/braket/circuits/braket_program_context.py
speller26 Jun 26, 2024
ece64a4
Update src/braket/circuits/braket_program_context.py
speller26 Jun 26, 2024
7caf75e
Merge branch 'main' into introduce_classical_measure_targets
speller26 Jun 26, 2024
19b78ab
Merge branch 'main' of https://github.com/amazon-braket/amazon-braket…
ltnln Jun 26, 2024
51296d9
Merge branch 'introduce_classical_measure_targets'
ltnln Jun 26, 2024
532451c
Merge branch 'introduce_classical_measure_targets'
ltnln Jun 26, 2024
e254410
Merge branch 'main' into main
speller26 Jun 26, 2024
ae3a0e3
fix: Add round_trip test for classical target tracking during measure…
ltnln Jun 26, 2024
8756960
change: Run Linter
ltnln Jun 26, 2024
82d24e3
Merge branch 'main' into prevent_verbatim_circuit_mapping
ltnln Jun 27, 2024
c6e60f1
fix: Remove measurements from Pytket circuit before performing mapping
ltnln Jun 27, 2024
5bec8d4
feat: Add decompose bridge pass after Tket Routing
ltnln Jun 27, 2024
40bf179
feat: Add emulator names to error messages and use AwsDevice names wh…
ltnln Jun 27, 2024
3e1b47c
fix: Fix mistake with applying RB calibration data if sRB data is una…
ltnln Jun 28, 2024
3f5094e
fix: Fix add_pass to correctly check if iterable supplied as argument
ltnln Jul 1, 2024
66703d7
fix: Return nothing in AwsDevice validate if program is valid.
ltnln Jul 1, 2024
ac070e2
change: Remove tket passes and files, fix emulator_interface.py filen…
ltnln Jul 4, 2024
c37bac4
fix: Update EmulatorInterface import in Emulator module
ltnln Jul 4, 2024
545c8ce
fix: Remove references to LexiRouting pass throughout repo.
ltnln Jul 4, 2024
afbb96c
feat!: Run linter, add comments/documentation, add unit tests for emu…
ltnln Jul 9, 2024
934b0ae
test!: Add unit tests and run linters
ltnln Jul 10, 2024
b787222
test: Add AwsNoise model tests
ltnln Jul 10, 2024
8080de7
Merge branch 'main' into only_validation
speller26 Jul 10, 2024
6cd4ffc
test: Reach 100% coverage, run linters
ltnln Jul 15, 2024
1e16ba1
change: Change task_specification type in validate documentation
ltnln Jul 15, 2024
87d342b
Merge branch 'only_validation' of github.com:Altanali/amazon-braket-s…
ltnln Jul 15, 2024
fdfa8c0
fix: Use explicit ProgramType as Python3.9 does not support type para…
ltnln Jul 15, 2024
20c59a2
test: Add test for IonQ create_noise_model dispatch for native gates …
ltnln Jul 15, 2024
073fb4e
fix: Place local_simulator._simulator_device setup in a fixture rathe…
ltnln Jul 15, 2024
3c9fd67
fix: Compare arn strings against explicit enum string values
ltnln Jul 15, 2024
0601c63
change: Update documentation, add check to GateConnectivityCriterion …
ltnln Jul 18, 2024
c5b9fc3
change: Clean up GateConnectivityCriterion instantiation logic.
ltnln Jul 18, 2024
34b94ae
change: Add Raises documentation and make dispatched functions privat…
ltnln Jul 22, 2024
70881c6
Merge branch 'amazon-braket:main' into only_validation
Altanali Jul 29, 2024
b698648
change: Use 'validator' in place of 'criterion', fix usage of generic…
ltnln Jul 29, 2024
e1cb307
change: Reorganize modules to place all gate-device emulator passes i…
ltnln Jul 30, 2024
3b500a3
fix: Correct __init__.pys for emulator_passes module and remove circu…
ltnln Jul 30, 2024
7d7279e
change: Rename emulator_passes directory to emulation_passes
ltnln Jul 30, 2024
8c63da6
change: Remove outdated connectivity_criterion.py file
ltnln Jul 30, 2024
df11f66
Merge branch 'main' into only_validation
Altanali Jul 30, 2024
a79e07d
fix: Use deepcopy before applying emulation passes to a program, fix …
ltnln Jul 30, 2024
96e917a
Merge branch 'only_validation' of github.com:Altanali/amazon-braket-s…
ltnln Jul 30, 2024
f30379c
fix: Fix Emulator raising a new Exception instance from the original …
ltnln Jul 30, 2024
9bddfbc
change: Change error message when trying to create a NoiseModel from …
ltnln Jul 30, 2024
4626332
feat: Replace 'EmulationPass' with more general BasePass class
ltnln Aug 2, 2024
e3ca8a5
change: Remove mentions of 'emulation' from the BasePass module.
ltnln Aug 2, 2024
baccbb1
change: Fix style in BasePass.py
ltnln Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions src/braket/aws/aws_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.


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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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..

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

@Altanali Altanali Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arguments and results of Emulator.emulate() follows that of LocalSimulator.run; if no result types are provided (and if shots=0), emulate() raises an exception (ValueError: No result types specified for circuit and shots=0. See braket.circuits.result_types) just as the LocalSimulator would.

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)
201 changes: 201 additions & 0 deletions src/braket/aws/aws_emulator_helpers.py
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just qubit_count_criterion is fine; same goes for other create methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "An emulator pass ..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this line to below the doc string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 start/end_verbatim_box instructions under its device capabilities, which is a mistake. These are Braket compiler directives and we do not want to consider them when validating a program's gate operations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

singledispatch produces really ugly documentation when used on public functions; only use it on private methods, like so:

def connectivity_criterion(...):
    return _connectivity_criterion(...)

@singledispatch
def _connectivity_criterion(...):
    ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ... ".

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is properties.paradigm.nativeGateSet a string? I was trying to understand where get_qpu_gate_translation takes in the argument gate_name as a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties.paradigm.nativeGateSet is a list of strings - get_qpu_gate_translations can either translate a string gate name or a list of string gate names by checking if the argument gate_name is a string or is Iterable. The function _get_qpu_gate_translations actually performs the translations for individual gate strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 nx.complete_graph which includes edges in both directions!

gate_connectivity_graph[edge[0]][edge[1]]["supported_gates"] = set(native_gates)

return GateConnectivityCriterion(gate_connectivity_graph)


def get_qpu_gate_translation(
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Loading