From bec307945b5fc275535884cc5d14a9a63ad6d589 Mon Sep 17 00:00:00 2001 From: Cody Wang Date: Fri, 9 Feb 2024 13:05:07 -0800 Subject: [PATCH] Include all gates in simulator targets (#147) * Simulator targets now include all gates, regardless of qubit count * Major refactor to make code more readable, with functions being extracted as appropriate * Adds the device qubit count to the target. --- qiskit_braket_provider/providers/adapter.py | 322 +++++++----------- .../providers/braket_job.py | 1 + qiskit_braket_provider/version.py | 1 + tests/providers/test_adapter.py | 1 + tests/providers/test_braket_backend.py | 3 +- tests/providers/test_braket_provider.py | 1 + 6 files changed, 138 insertions(+), 191 deletions(-) diff --git a/qiskit_braket_provider/providers/adapter.py b/qiskit_braket_provider/providers/adapter.py index 43b6f187..8f62edd6 100644 --- a/qiskit_braket_provider/providers/adapter.py +++ b/qiskit_braket_provider/providers/adapter.py @@ -1,5 +1,7 @@ """Util function for provider.""" + from collections.abc import Callable, Iterable +from math import pi from typing import Optional, Union import warnings @@ -12,30 +14,20 @@ ) import braket.circuits.gates as braket_gates -from braket.device_schema import ( - DeviceActionType, - GateModelQpuParadigmProperties, - JaqcdDeviceActionProperties, - OpenQASMDeviceActionProperties, -) +from braket.device_schema import DeviceActionType, OpenQASMDeviceActionProperties from braket.device_schema.ionq import IonqDeviceCapabilities from braket.device_schema.oqc import OqcDeviceCapabilities from braket.device_schema.rigetti import RigettiDeviceCapabilities -from braket.device_schema.simulators import ( - GateModelSimulatorDeviceCapabilities, - GateModelSimulatorParadigmProperties, -) +from braket.device_schema.simulators import GateModelSimulatorDeviceCapabilities from braket.devices import LocalSimulator from braket.ir.openqasm.modifiers import Control -from numpy import pi - from qiskit import QuantumCircuit, transpile from qiskit.circuit import Instruction as QiskitInstruction from qiskit.circuit import ControlledGate, Measure, Parameter import qiskit.circuit.library as qiskit_gates -from qiskit.transpiler import InstructionProperties, Target +from qiskit.transpiler import Target from qiskit_ionq import ionq_gates from qiskit_braket_provider.exception import QiskitBraketException @@ -232,210 +224,160 @@ def _get_controlled_gateset(max_qubits: Optional[int] = None) -> set[str]: def local_simulator_to_target(simulator: LocalSimulator) -> Target: - """Converts properties of LocalSimulator into Qiskit Target object. + """Converts properties of a Braket LocalSimulator into a Qiskit Target object. Args: - simulator: AWS LocalSimulator + simulator (LocalSimulator): Amazon Braket LocalSimulator Returns: - target for Qiskit backend + Target: Target for Qiskit backend """ - target = Target() - - instructions = [ - inst for inst in _GATE_NAME_TO_QISKIT_GATE.values() if inst is not None - ] - properties = simulator.properties - paradigm: GateModelSimulatorParadigmProperties = properties.paradigm - - # add measurement instruction - target.add_instruction(Measure(), {(i,): None for i in range(paradigm.qubitCount)}) - - for instruction in instructions: - instruction_props: Optional[ - dict[Union[tuple[int], tuple[int, int]], Optional[InstructionProperties]] - ] = {} - - if instruction.num_qubits == 1: - for i in range(paradigm.qubitCount): - instruction_props[(i,)] = None - target.add_instruction(instruction, instruction_props) - elif instruction.num_qubits == 2: - for src in range(paradigm.qubitCount): - for dst in range(paradigm.qubitCount): - if src != dst: - instruction_props[(src, dst)] = None - instruction_props[(dst, src)] = None - target.add_instruction(instruction, instruction_props) - - return target + return _simulator_target( + Target( + description=f"Target for Amazon Braket local simulator: {simulator.name}" + ), + simulator.properties, + ) def aws_device_to_target(device: AwsDevice) -> Target: - """Converts properties of Braket device into Qiskit Target object. + """Converts properties of Braket AwsDevice into a Qiskit Target object. Args: - device: AWS Braket device + device (AwsDevice): Amazon Braket AwsDevice Returns: - target for Qiskit backend + Target: Target for Qiskit backend """ # building target - target = Target(description=f"Target for AWS Device: {device.name}") - + target = Target(description=f"Target for Amazon Braket device: {device.name}") properties = device.properties - # gate model devices - if isinstance( + + if isinstance(properties, GateModelSimulatorDeviceCapabilities): + return _simulator_target(target, properties) + elif isinstance( properties, (IonqDeviceCapabilities, RigettiDeviceCapabilities, OqcDeviceCapabilities), ): - action_properties: OpenQASMDeviceActionProperties = ( - properties.action.get(DeviceActionType.OPENQASM) - if properties.action.get(DeviceActionType.OPENQASM) - else properties.action.get(DeviceActionType.JAQCD) - ) - paradigm: GateModelQpuParadigmProperties = properties.paradigm - connectivity = paradigm.connectivity - instructions: list[QiskitInstruction] = [] - - for operation in action_properties.supportedOperations: - instruction = _GATE_NAME_TO_QISKIT_GATE.get(operation.lower(), None) - if instruction is not None: - # TODO: remove when target will be supporting > 2 qubit gates # pylint:disable=fixme - if instruction.num_qubits <= 2: - instructions.append(instruction) - - # add measurement instructions - target.add_instruction( - Measure(), {(i,): None for i in range(paradigm.qubitCount)} - ) + return _qpu_target(target, properties) - for instruction in instructions: - instruction_props: Optional[ - dict[ - Union[tuple[int], tuple[int, int]], Optional[InstructionProperties] - ] - ] = {} - # adding 1 qubit instructions - if instruction.num_qubits == 1: - for i in range(paradigm.qubitCount): - instruction_props[(i,)] = None - # adding 2 qubit instructions - elif instruction.num_qubits == 2: - # building coupling map for fully connected device - if connectivity.fullyConnected: - for src in range(paradigm.qubitCount): - for dst in range(paradigm.qubitCount): - if src != dst: - instruction_props[(src, dst)] = None - instruction_props[(dst, src)] = None - # building coupling map for device with connectivity graph - else: - if isinstance(properties, RigettiDeviceCapabilities): - - def convert_continuous_qubit_indices( - connectivity_graph: dict, - ) -> dict: - """Aspen qubit indices are discontinuous (label between x0 and x7, x being - the number of the octagon) while the Qiskit transpiler creates and/or - handles coupling maps with continuous indices. This function converts the - discontinous connectivity graph from Aspen to a continuous one. - - Args: - connectivity_graph (dict): connectivity graph from Aspen. For example - 4 qubit system, the connectivity graph will be: - {"0": ["1", "2", "7"], "1": ["0","2","7"], "2": ["0","1","7"], - "7": ["0","1","2"]} - - Returns: - dict: Connectivity graph with continuous indices. For example for an - input connectivity graph with discontinuous indices (qubit 0, 1, 2 and - then qubit 7) as shown here: - {"0": ["1", "2", "7"], "1": ["0","2","7"], "2": ["0","1","7"], - "7": ["0","1","2"]} - the qubit index 7 will be mapped to qubit index 3 for the qiskit - transpilation step. Thereby the resultant continous qubit indices - output will be: - {"0": ["1", "2", "3"], "1": ["0","2","3"], "2": ["0","1","3"], - "3": ["0","1","2"]} - """ - # Creates list of existing qubit indices which are discontinuous. - indices = [int(key) for key in connectivity_graph.keys()] - indices.sort() - # Creates a list of continuous indices for number of qubits. - map_list = list(range(len(indices))) - # Creates a dictionary to remap the discountinous indices to continuous. - mapper = dict(zip(indices, map_list)) - # Performs the remapping from the discontinous to the continuous indices. - continous_connectivity_graph = { - mapper[int(k)]: [mapper[int(v)] for v in val] - for k, val in connectivity_graph.items() - } - return continous_connectivity_graph - - connectivity.connectivityGraph = ( - convert_continuous_qubit_indices( - connectivity.connectivityGraph - ) - ) - - for src, connections in connectivity.connectivityGraph.items(): - for dst in connections: - instruction_props[(int(src), int(dst))] = None - # for more than 2 qubits - else: - instruction_props = None + raise QiskitBraketException( + f"Cannot convert to target. " + f"{properties.__class__} device capabilities are not supported yet." + ) - target.add_instruction(instruction, instruction_props) - # gate model simulators - elif isinstance(properties, GateModelSimulatorDeviceCapabilities): - simulator_action_properties: JaqcdDeviceActionProperties = ( - properties.action.get(DeviceActionType.JAQCD) - ) - simulator_paradigm: GateModelSimulatorParadigmProperties = properties.paradigm - instructions = [] - - for operation in simulator_action_properties.supportedOperations: - instruction = _GATE_NAME_TO_QISKIT_GATE.get(operation.lower(), None) - if instruction is not None: - # TODO: remove when target will be supporting > 2 qubit gates # pylint:disable=fixme - if instruction.num_qubits <= 2: - instructions.append(instruction) - - # add measurement instructions - target.add_instruction( - Measure(), {(i,): None for i in range(simulator_paradigm.qubitCount)} - ) +def _simulator_target(target: Target, properties: GateModelSimulatorDeviceCapabilities): + target.num_qubits = properties.paradigm.qubitCount + action = ( + properties.action.get(DeviceActionType.OPENQASM) + if properties.action.get(DeviceActionType.OPENQASM) + else properties.action.get(DeviceActionType.JAQCD) + ) + for operation in action.supportedOperations: + instruction = _GATE_NAME_TO_QISKIT_GATE.get(operation.lower(), None) + if instruction: + target.add_instruction(instruction) + target.add_instruction(Measure()) + return target + + +def _qpu_target( + target: Target, + properties: Union[ + IonqDeviceCapabilities, RigettiDeviceCapabilities, OqcDeviceCapabilities + ], +): + action_properties = ( + properties.action.get(DeviceActionType.OPENQASM) + if properties.action.get(DeviceActionType.OPENQASM) + else properties.action.get(DeviceActionType.JAQCD) + ) + qubit_count = properties.paradigm.qubitCount + target.num_qubits = qubit_count + connectivity = properties.paradigm.connectivity + + for operation in action_properties.supportedOperations: + instruction = _GATE_NAME_TO_QISKIT_GATE.get(operation.lower(), None) - for instruction in instructions: - simulator_instruction_props: Optional[ - dict[ - Union[tuple[int], tuple[int, int]], - Optional[InstructionProperties], - ] - ] = {} - # adding 1 qubit instructions + # TODO: Add 3+ qubit gates once Target supports them # pylint:disable=fixme + if instruction and instruction.num_qubits <= 2: if instruction.num_qubits == 1: - for i in range(simulator_paradigm.qubitCount): - simulator_instruction_props[(i,)] = None - # adding 2 qubit instructions + target.add_instruction( + instruction, {(i,): None for i in range(qubit_count)} + ) elif instruction.num_qubits == 2: - # building coupling map for fully connected device - for src in range(simulator_paradigm.qubitCount): - for dst in range(simulator_paradigm.qubitCount): - if src != dst: - simulator_instruction_props[(src, dst)] = None - simulator_instruction_props[(dst, src)] = None - target.add_instruction(instruction, simulator_instruction_props) + target.add_instruction( + instruction, + _2q_instruction_properties(qubit_count, connectivity, properties), + ) + target.add_instruction(Measure(), {(i,): None for i in range(qubit_count)}) + return target + + +def _2q_instruction_properties(qubit_count, connectivity, properties): + instruction_props = {} + + # building coupling map for fully connected device + if connectivity.fullyConnected: + for src in range(qubit_count): + for dst in range(qubit_count): + if src != dst: + instruction_props[(src, dst)] = None + instruction_props[(dst, src)] = None + + # building coupling map for device with connectivity graph else: - raise QiskitBraketException( - f"Cannot convert to target. " - f"{properties.__class__} device capabilities are not supported yet." - ) + if isinstance(properties, RigettiDeviceCapabilities): + connectivity.connectivityGraph = _convert_aspen_qubit_indices( + connectivity.connectivityGraph + ) - return target + for src, connections in connectivity.connectivityGraph.items(): + for dst in connections: + instruction_props[(int(src), int(dst))] = None + + return instruction_props + + +def _convert_aspen_qubit_indices(connectivity_graph: dict) -> dict: + """Aspen qubit indices are discontinuous (label between x0 and x7, x being + the number of the octagon) while the Qiskit transpiler creates and/or + handles coupling maps with continuous indices. This function converts the + discontinuous connectivity graph from Aspen to a continuous one. + + Args: + connectivity_graph (dict): connectivity graph from Aspen. For example + 4 qubit system, the connectivity graph will be: + {"0": ["1", "2", "7"], "1": ["0","2","7"], "2": ["0","1","7"], + "7": ["0","1","2"]} + + Returns: + dict: Connectivity graph with continuous indices. For example for an + input connectivity graph with discontinuous indices (qubit 0, 1, 2 and + then qubit 7) as shown here: + {"0": ["1", "2", "7"], "1": ["0","2","7"], "2": ["0","1","7"], + "7": ["0","1","2"]} + the qubit index 7 will be mapped to qubit index 3 for the qiskit + transpilation step. Thereby the resultant continous qubit indices + output will be: + {"0": ["1", "2", "3"], "1": ["0","2","3"], "2": ["0","1","3"], + "3": ["0","1","2"]} + """ + # Creates list of existing qubit indices which are discontinuous. + indices = [int(key) for key in connectivity_graph.keys()] + indices.sort() + # Creates a list of continuous indices for number of qubits. + map_list = list(range(len(indices))) + # Creates a dictionary to remap the discontinuous indices to continuous. + mapper = dict(zip(indices, map_list)) + # Performs the remapping from the discontinuous to the continuous indices. + continous_connectivity_graph = { + mapper[int(k)]: [mapper[int(v)] for v in val] + for k, val in connectivity_graph.items() + } + return continous_connectivity_graph def to_braket( diff --git a/qiskit_braket_provider/providers/braket_job.py b/qiskit_braket_provider/providers/braket_job.py index 2e754622..7125d33f 100644 --- a/qiskit_braket_provider/providers/braket_job.py +++ b/qiskit_braket_provider/providers/braket_job.py @@ -1,4 +1,5 @@ """AWS Braket job.""" + from datetime import datetime from typing import List, Optional, Union from warnings import warn diff --git a/qiskit_braket_provider/version.py b/qiskit_braket_provider/version.py index 671335ba..c1f97e40 100644 --- a/qiskit_braket_provider/version.py +++ b/qiskit_braket_provider/version.py @@ -1,2 +1,3 @@ """Qiskit-Braket provider version.""" + __version__ = "0.0.5" diff --git a/tests/providers/test_adapter.py b/tests/providers/test_adapter.py index 51c4c4b7..033c1b5a 100644 --- a/tests/providers/test_adapter.py +++ b/tests/providers/test_adapter.py @@ -1,4 +1,5 @@ """Tests for Qiskit to Braket adapter.""" + from unittest import TestCase from unittest.mock import Mock, patch diff --git a/tests/providers/test_braket_backend.py b/tests/providers/test_braket_backend.py index c704d54d..3c79571f 100644 --- a/tests/providers/test_braket_backend.py +++ b/tests/providers/test_braket_backend.py @@ -1,4 +1,5 @@ """Tests for AWS Braket backends.""" + import unittest from typing import Dict, List from unittest import TestCase @@ -318,4 +319,4 @@ def test_target(self): self.assertEqual(target.num_qubits, 30) self.assertEqual(len(target.operations), 2) self.assertEqual(len(target.instructions), 60) - self.assertIn("Target for AWS Device", target.description) + self.assertIn("Target for Amazon Braket device", target.description) diff --git a/tests/providers/test_braket_provider.py b/tests/providers/test_braket_provider.py index b31ad1da..4e5ed678 100644 --- a/tests/providers/test_braket_provider.py +++ b/tests/providers/test_braket_provider.py @@ -1,4 +1,5 @@ """Tests for AWS Braket provider.""" + from unittest import TestCase from unittest.mock import Mock, patch import uuid