From 0601c63dfc91f268b64a0d42dfeecb3ddeb70e21 Mon Sep 17 00:00:00 2001 From: Nagji Date: Thu, 18 Jul 2024 09:40:51 -0700 Subject: [PATCH] change: Update documentation, add check to GateConnectivityCriterion to ensure user-provided undirected graphs have symmetric forwards/backwards edges --- src/braket/aws/aws_device.py | 15 ++++---- src/braket/aws/aws_emulator_helpers.py | 23 ++++++----- src/braket/emulators/emulator.py | 2 +- .../criteria/connectivity_criterion.py | 38 ++++++++++--------- .../criteria/emulator_criterion.py | 14 +++++-- .../criteria/gate_connectivity_criterion.py | 16 +++++--- .../criteria/gate_criterion.py | 12 ++++-- .../braket/aws/test_aws_emulation.py | 2 +- .../test_gate_connectivity_criterion.py | 6 +-- .../braket/emulation/test_gate_criterion.py | 18 +++++++-- 10 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/braket/aws/aws_device.py b/src/braket/aws/aws_device.py index e7deee418..dd5706f68 100644 --- a/src/braket/aws/aws_device.py +++ b/src/braket/aws/aws_device.py @@ -870,9 +870,9 @@ def _parse_calibration_json( @property def emulator(self) -> Emulator: """ - A device emulator mimics the restrictions and noise of an AWS QPU by validating and + 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 an AwsDevice. + as a soft check that a program can run the target AwsDevice. Examples: >>> device = AwsDevice(Devices.IQM.Garnet) @@ -883,7 +883,8 @@ def emulator(self) -> Emulator: >>> print(result.result().measurement_counts) Returns: - Emulator: An emulator for this device, if this is not a simulator device. + Emulator: An emulator for this device, if this is not a simulator device. Raises an + exception if an emulator is requested for al simulator device. """ if self._arn in [simulator_enum.value for simulator_enum in Devices.Amazon]: raise ValueError( @@ -896,7 +897,7 @@ def emulator(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). + real QPU (not a simulator). Returns: Emulator: An emulator with a noise model, compilation passes, and validation passes @@ -921,8 +922,8 @@ def validate( ) -> 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 - program meets all criterion, returns. + error if any device-specific criteria are not met by the program. If the + program meets all criteria, returns. Args: task_specification (Circuit): The quantum program to emulate against @@ -960,7 +961,7 @@ def emulate( 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 + A quantum task can be a circuit. Emulation involves running all emulator passes on the input program before running the program on the emulator's backend. diff --git a/src/braket/aws/aws_emulator_helpers.py b/src/braket/aws/aws_emulator_helpers.py index 000509730..b703f77e2 100644 --- a/src/braket/aws/aws_emulator_helpers.py +++ b/src/braket/aws/aws_emulator_helpers.py @@ -26,15 +26,14 @@ def create_qubit_count_criterion(properties: DeviceCapabilities) -> QubitCountCr QHP-specific schema. Returns: - QubitCountCriterion: An eulator pass that checks that the number of qubits used in a program - does not exceed that of the max qubit count on the device. + QubitCountCriterion: An emulator pass that checks that the number of qubits used in a + program 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 """ Create a GateCriterion pass which defines what supported and native gates are allowed in a program based on the provided device properties. @@ -48,13 +47,7 @@ def create_gate_criterion(properties: DeviceCapabilities) -> GateCriterion: verbatim circuits only use native gates. """ - if isinstance(properties, IqmDeviceCapabilities): - try: - supported_gates.remove("start_verbatim_box") - supported_gates.remove("end_verbatim_box") - except ValueError: - pass - + supported_gates = properties.action[DeviceActionType.OPENQASM].supportedOperations native_gates = properties.paradigm.nativeGateSet return GateCriterion(supported_gates=supported_gates, native_gates=native_gates) @@ -65,7 +58,7 @@ def create_connectivity_criterion( properties: DeviceCapabilities, connectivity_graph: DiGraph ) -> ConnectivityCriterion: """ - Creates a ConnectivityCriterion pass which validates that multi-qubit gates are applied to + Creates a ConnectivityCriterion pass which validates that two-qubit gates are applied to connected qubits based on this device's connectivity graph. Args: @@ -118,6 +111,8 @@ def _( 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) + + # Check that the QHP provided calibration data for this edge. if not edge_property: gate_connectivity_graph[u][v]["supported_gates"] = set() continue @@ -126,6 +121,8 @@ def _( ) gate_connectivity_graph[u][v]["supported_gates"] = set(edge_supported_gates) + # Add the reversed edge to ensure gates can be applied + # in both directions for a given qubit pair. for u, v in gate_connectivity_graph.edges: if (v, u) not in gate_connectivity_graph.edges or gate_connectivity_graph[v][u].get( "supported_gates" @@ -163,7 +160,9 @@ def get_qpu_gate_translation( 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) + gate_name (Union[str, Iterable[str]]): The name(s) of the gate(s). If gate_name is a list + of string gate names, this function attempts to retrieve translations of all the gate + names. Returns: Union[str, list[str]]: The translated gate name(s) diff --git a/src/braket/emulators/emulator.py b/src/braket/emulators/emulator.py index 7352260e8..b321d1969 100644 --- a/src/braket/emulators/emulator.py +++ b/src/braket/emulators/emulator.py @@ -144,7 +144,7 @@ def run_program_passes( """ Passes the input program through all EmulatorPass objects contained in this emulator and applies the emulator's noise model, if it exists, before - retruning the compiled program. + returning the compiled program. Args: task_specification (ProgramType): The input program to validate and diff --git a/src/braket/emulators/emulator_passes/criteria/connectivity_criterion.py b/src/braket/emulators/emulator_passes/criteria/connectivity_criterion.py index 7385587f2..1c47b2616 100644 --- a/src/braket/emulators/emulator_passes/criteria/connectivity_criterion.py +++ b/src/braket/emulators/emulator_passes/criteria/connectivity_criterion.py @@ -12,23 +12,6 @@ class ConnectivityCriterion(EmulatorCriterion): - """ - args: - connectivity_graph (Union[Dict[int, List[int]], DiGraph]): Either a sparse matrix or DiGraph - representation of the device connectivity. Can be None if fully_connected is true. - - fully_connected (bool): If true, the all qubits in the device are connected. - - num_qubits (int): The number of qubits in the device; if fully_connected is True, - this is used to create a complete graph with num_qubits nodes; ignored if - connectivity_graph is provided and fully_connected if False. - - qubit_labels (Iterable[int]): A set of qubit labels; if fully_connected is True, - the qubits_labels are used as nodes of a fully connected topology; ignored if - connectivity_graph is provided and fully_connected if False. - - """ - def __init__( self, connectivity_graph: Union[Dict[int, Iterable[int]], DiGraph] = None, @@ -37,6 +20,27 @@ def __init__( qubit_labels: Union[Iterable[int], QubitSet] = None, directed: bool = True, ): + """ + args: + connectivity_graph (Union[Dict[int, List[int]], DiGraph]): Either a sparse matrix or + DiGraph representation of the device connectivity. Can be None if fully_connected is + true. + + fully_connected (bool): If true, the all qubits in the device are connected. + + num_qubits (int): The number of qubits in the device; if fully_connected is True, + this is used to create a complete graph with num_qubits nodes; ignored if + connectivity_graph is provided and fully_connected if False. + + qubit_labels (Iterable[int]): A set of qubit labels; if fully_connected is True, + the qubits_labels are used as nodes of a fully connected topology; ignored if + connectivity_graph is provided and fully_connected if False. + + directed (bool): Denotes if the connectivity graph is directed or undirected. If + the connectivity graph is undirected, this constructor attempts to fill in any + missing back edges. + """ + if not (connectivity_graph or fully_connected): raise ValueError( "Either the connectivity_graph must be provided or fully_connected must be True." diff --git a/src/braket/emulators/emulator_passes/criteria/emulator_criterion.py b/src/braket/emulators/emulator_passes/criteria/emulator_criterion.py index 23cad5ff0..a55d6c1e5 100644 --- a/src/braket/emulators/emulator_passes/criteria/emulator_criterion.py +++ b/src/braket/emulators/emulator_passes/criteria/emulator_criterion.py @@ -2,24 +2,32 @@ from abc import abstractmethod -from braket.circuits import Circuit from braket.emulators.emulator_passes.emulator_pass import EmulatorPass, ProgramType class EmulatorCriterion(EmulatorPass): @abstractmethod - def validate(self, circuit: Circuit) -> None: + def validate(self, program: ProgramType) -> None: """ An emulator criterion is used to perform some non-modifying validation pass on an input program. Implementations of validate should return nothing if the input program passes validation and raise an error otherwise. Args: - circuit (Circuit): circuit to be evaluated against this criteria. + program (ProgramType): The program to be evaluated against this criteria. """ raise NotImplementedError def run(self, program: ProgramType) -> ProgramType: + """ + Validate the input program and return the program, unmodified. + + Args: + program (ProgramType): The program to validate. + + Returns: + ProgramType: The unmodified progam passed in as input. + """ self.validate(program) return program diff --git a/src/braket/emulators/emulator_passes/criteria/gate_connectivity_criterion.py b/src/braket/emulators/emulator_passes/criteria/gate_connectivity_criterion.py index 449af1431..1035a5333 100644 --- a/src/braket/emulators/emulator_passes/criteria/gate_connectivity_criterion.py +++ b/src/braket/emulators/emulator_passes/criteria/gate_connectivity_criterion.py @@ -27,6 +27,17 @@ def __init__( self._gate_connectivity_graph.add_edge( *back_edge, supported_gates=supported_gates ) + else: + # check that the supported gate sets are identical + if ( + self._gate_connectivity_graph[u][v]["supported_gates"] + != self._gate_connectivity_graph[v][u]["supported_gates"] + ): + raise ValueError( + f"Connectivity Graph marked as undirected\ + but edges ({u}, {v}) and ({v}, {u}) have different supported\ + gate sets." + ) elif isinstance(gate_connectivity_graph, dict): self._gate_connectivity_graph = DiGraph() @@ -79,11 +90,6 @@ def validate(self, circuit: Circuit) -> None: f"Qubit {target_qubit} does not exist in the device topology." ) idx += 1 - - if idx == len(circuit.instructions) or not isinstance( - circuit.instructions[idx].operator, EndVerbatimBox - ): - raise ValueError(f"No end verbatim box found at index {idx} in the circuit.") idx += 1 def validate_instruction_connectivity( diff --git a/src/braket/emulators/emulator_passes/criteria/gate_criterion.py b/src/braket/emulators/emulator_passes/criteria/gate_criterion.py index 54eb46f7a..a332c5c14 100644 --- a/src/braket/emulators/emulator_passes/criteria/gate_criterion.py +++ b/src/braket/emulators/emulator_passes/criteria/gate_criterion.py @@ -11,19 +11,23 @@ class GateCriterion(EmulatorCriterion): def __init__(self, supported_gates: Iterator[str] = [], native_gates: Iterator[str] = []): """ args: - native_gates (Iterator[str]): A list of gates supported inside of verbatim mode by - the emulator. supported_gates (Iterator[str]): A list of gates supported outside of verbatim mode - by the emulator. A gate is a Braket gate name. + by the emulator. A gate is a Braket gate name. + native_gates (Iterator[str]): A list of gates supported inside of verbatim mode by + the emulator. """ if len(supported_gates) == 0 and len(native_gates) == 0: raise ValueError("Supported gate set or native gate set must be provided.") try: self._supported_gates = set(BRAKET_GATES[gate.lower()] for gate in supported_gates) + except KeyError as e: + raise ValueError(f"Input {str(e)} in supported_gates is not a valid Braket gate name.") + + try: self._native_gates = set(BRAKET_GATES[gate.lower()] for gate in native_gates) except KeyError as e: - raise ValueError(f"Input {str(e)} is not a valid Braket gate name.") + raise ValueError(f"Input {str(e)} in native_gates is not a valid Braket gate name.") def validate(self, circuit: Circuit) -> None: """ diff --git a/test/unit_tests/braket/aws/test_aws_emulation.py b/test/unit_tests/braket/aws/test_aws_emulation.py index e0e18af98..4a72dcef8 100644 --- a/test/unit_tests/braket/aws/test_aws_emulation.py +++ b/test/unit_tests/braket/aws/test_aws_emulation.py @@ -235,7 +235,7 @@ def basic_device_capabilities(): "braket.ir.openqasm.program": { "actionType": "braket.ir.openqasm.program", "version": ["1"], - "supportedOperations": ["H", "CNot", "Ry", "XX", "YY", "start_verbatim_box"], + "supportedOperations": ["H", "CNot", "Ry", "XX", "YY"], } }, "paradigm": { diff --git a/test/unit_tests/braket/emulation/test_gate_connectivity_criterion.py b/test/unit_tests/braket/emulation/test_gate_connectivity_criterion.py index db509e5a1..c01d10b03 100644 --- a/test/unit_tests/braket/emulation/test_gate_connectivity_criterion.py +++ b/test/unit_tests/braket/emulation/test_gate_connectivity_criterion.py @@ -3,8 +3,7 @@ import pytest from networkx.utils import graphs_equal -from braket.circuits import Circuit, Gate, Instruction -from braket.circuits.compiler_directives import StartVerbatimBox +from braket.circuits import Circuit, Gate from braket.circuits.noises import BitFlip from braket.emulators.emulator_passes.criteria import GateConnectivityCriterion @@ -185,7 +184,7 @@ def test_undirected_graph_construction_from_dict(): [(0, 1, {"supported_gates": ["CNot", "CZ"]}), (1, 0, {"supported_gates": ["CNot", "CZ"]})], [ (0, 1, {"supported_gates": ["CNot", "CZ"]}), - (1, 2, {"supported_gates": ["CNot"]}), + (1, 2, {"supported_gates": ["CNot", "CZ", "XX"]}), (2, 3, {"supported_gates": ["CZ"]}), (2, 1, {"supported_gates": ["CNot", "CZ", "XX"]}), ], @@ -258,7 +257,6 @@ def create_undirected_graph_with_exisiting_back_edges(representation): Circuit().add_verbatim_box(Circuit().h(4)), Circuit().add_verbatim_box(Circuit().swap(1, 2).xx(0, 3, np.pi / 2).iswap(0, 1)), Circuit().add_verbatim_box(Circuit().cnot(0, 3)), - Circuit().add_instruction(Instruction(StartVerbatimBox())), ], ) def test_invalid_circuits(basic_4_node_graph, circuit): diff --git a/test/unit_tests/braket/emulation/test_gate_criterion.py b/test/unit_tests/braket/emulation/test_gate_criterion.py index b7ad2d2ba..9f85969d6 100644 --- a/test/unit_tests/braket/emulation/test_gate_criterion.py +++ b/test/unit_tests/braket/emulation/test_gate_criterion.py @@ -113,9 +113,21 @@ def test_non_verbatim_circuit_only_native_gates(): criterion.validate(circuit) -@pytest.mark.parametrize("supported_gates,native_gates", [([], []), (["CX"], [])]) -def test_invalid_instantiation(supported_gates, native_gates): - with pytest.raises(ValueError): +@pytest.mark.parametrize( + "supported_gates,native_gates,error_message", + [ + ([], [], "Supported gate set or native gate set must be provided."), + (["CX"], [], "Input 'cx' in supported_gates is not a valid Braket gate name."), + ([], ["CX"], "Input 'cx' in native_gates is not a valid Braket gate name."), + ( + ["Toffoli"], + ["CX"], + "Input 'toffoli' in supported_gates is not a valid Braket gate name.", + ), + ], +) +def test_invalid_instantiation(supported_gates, native_gates, error_message): + with pytest.raises(ValueError, match=error_message): GateCriterion(supported_gates, native_gates)