diff --git a/qiskit_ionq/exceptions.py b/qiskit_ionq/exceptions.py index 466c5e35..3dd1bd3f 100644 --- a/qiskit_ionq/exceptions.py +++ b/qiskit_ionq/exceptions.py @@ -179,6 +179,10 @@ class IonQBackendError(IonQError): """Errors generated from improper usage of IonQBackend objects.""" +class IonQBackendNotSupportedError(IonQError): + """The requested backend is not supported.""" + + class IonQJobError(IonQError, JobError): """Errors generated from improper usage of IonQJob objects.""" @@ -248,6 +252,7 @@ class IonQPauliExponentialError(IonQError): "IonQClientError", "IonQAPIError", "IonQBackendError", + "IonQBackendNotSupportedError", "IonQJobError", "IonQGateError", "IonQMidCircuitMeasurementError", diff --git a/qiskit_ionq/ionq_backend.py b/qiskit_ionq/ionq_backend.py index 83b3c320..430a1b87 100644 --- a/qiskit_ionq/ionq_backend.py +++ b/qiskit_ionq/ionq_backend.py @@ -39,7 +39,7 @@ from qiskit.providers.models.backendstatus import BackendStatus from qiskit.providers import Options -from . import exceptions, ionq_client, ionq_job, ionq_equivalence_library +from . import exceptions, ionq_client, ionq_job from .helpers import GATESET_MAP, get_n_qubits if TYPE_CHECKING: @@ -140,8 +140,6 @@ class IonQBackend(Backend): _client = None def __init__(self, *args, **kwargs) -> None: - # Add IonQ equivalences - ionq_equivalence_library.add_equivalences() super().__init__(*args, **kwargs) @classmethod @@ -342,19 +340,6 @@ def __ne__(self, other) -> bool: class IonQSimulatorBackend(IonQBackend): """ IonQ Backend for running simulated jobs. - - - .. ATTENTION:: - - When noise_model ideal is specified, the maximum shot-count for a state vector sim is - always ``1``. - - .. ATTENTION:: - - When noise_model ideal is specified, calling - :meth:`get_counts ` - on a job processed by this backend will return counts expressed as - probabilites, rather than a multiple of shots. """ @classmethod @@ -372,11 +357,6 @@ def _default_options(cls) -> Options: def run(self, circuit: QuantumCircuit, **kwargs) -> ionq_job.IonQJob: """Create and run a job on IonQ's Simulator Backend. - .. WARNING: - - The maximum shot-count for a state vector sim is always ``1``. - As a result, the ``shots`` keyword argument in this method is ignored. - Args: circuit (:class:`QuantumCircuit `): A Qiskit QuantumCircuit object. @@ -384,6 +364,8 @@ def run(self, circuit: QuantumCircuit, **kwargs) -> ionq_job.IonQJob: Returns: IonQJob: A reference to the job that was submitted. """ + if "noise_model" not in kwargs: + kwargs["noise_model"] = self.options.noise_model return super().run(circuit, **kwargs) def calibration(self) -> None: @@ -402,6 +384,7 @@ def __init__( provider, name: str = "simulator", gateset: Literal["qis", "native"] = "qis", + noise_model="ideal", ): """Base class for interfacing with an IonQ backend""" self._gateset = gateset @@ -466,6 +449,10 @@ def __init__( } ) super().__init__(configuration=config, provider=provider) + # TODO: passing 'noise_model' to super().__init__ method is the + # proper method to handle this but it fails because Options has + # no field named data, perhaps this will be fixed in BackendV2 + self._options.update_options(noise_model=noise_model) def with_name(self, name, **kwargs) -> IonQSimulatorBackend: """Helper method that returns this backend with a more specific target system.""" diff --git a/qiskit_ionq/ionq_equivalence_library.py b/qiskit_ionq/ionq_equivalence_library.py index ec65c710..c6c1cb8e 100644 --- a/qiskit_ionq/ionq_equivalence_library.py +++ b/qiskit_ionq/ionq_equivalence_library.py @@ -31,7 +31,9 @@ from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary from qiskit.circuit import QuantumRegister, QuantumCircuit, Parameter from qiskit.circuit.library import CXGate, RXGate, RZGate, UGate, XGate, CU3Gate -from .ionq_gates import GPIGate, GPI2Gate, MSGate + +from qiskit_ionq.exceptions import IonQBackendNotSupportedError +from .ionq_gates import GPIGate, GPI2Gate, MSGate, ZZGate def u_gate_equivalence() -> None: @@ -57,8 +59,8 @@ def u_gate_equivalence() -> None: ) -def cx_gate_equivalence() -> None: - """Add CX gate equivalence to the SessionEquivalenceLibrary.""" +def cx_gate_equivalence_ms() -> None: + """Add MS gate based CX gate equivalence to the SessionEquivalenceLibrary.""" q = QuantumRegister(2, "q") cx_gate = QuantumCircuit(q) cx_gate.append(GPI2Gate(1 / 4), [0]) @@ -66,7 +68,38 @@ def cx_gate_equivalence() -> None: cx_gate.append(GPI2Gate(1 / 2), [0]) cx_gate.append(GPI2Gate(1 / 2), [1]) cx_gate.append(GPI2Gate(-1 / 4), [0]) - SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate) + if SessionEquivalenceLibrary.has_entry(CXGate()): + SessionEquivalenceLibrary.set_entry(CXGate(), [cx_gate]) + else: + SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate) + + +def cx_gate_equivalence_zz() -> None: + """Add ZZ gate based CX gate equivalence to the SessionEquivalenceLibrary. + q_0: ────■────Sdag────── + │ZZ + q_1: H───■────Sdag────H─ + """ + q = QuantumRegister(2, "q") + cx_gate = QuantumCircuit(q) + # H + cx_gate.append(GPI2Gate(0), [1]) + cx_gate.append(GPIGate(-0.125), [1]) + cx_gate.append(GPI2Gate(0.5), [1]) + # ZZ + cx_gate.append(ZZGate(), [0, 1]) + # Sdag + cx_gate.append(GPI2Gate(0.75), [0]) + cx_gate.append(GPIGate(0.125), [0]) + cx_gate.append(GPI2Gate(0.5), [0]) + # H * Sdag + cx_gate.append(GPI2Gate(1.25), [1]) + cx_gate.append(GPIGate(0.5), [1]) + cx_gate.append(GPI2Gate(0.5), [1]) + if SessionEquivalenceLibrary.has_entry(CXGate()): + SessionEquivalenceLibrary.set_entry(CXGate(), [cx_gate]) + else: + SessionEquivalenceLibrary.add_equivalence(CXGate(), cx_gate) # Below are the rules needed for Aer simulator to simulate circuits containing IonQ native gates @@ -127,10 +160,60 @@ def ms_gate_equivalence() -> None: ) -def add_equivalences() -> None: +def zz_gate_equivalence() -> None: + """Add ZZ gate equivalence to the SessionEquivalenceLibrary.""" + q = QuantumRegister(2, "q") + zz_gate = QuantumCircuit(q) + zz_gate.h(1) + zz_gate.append(CXGate(), [0, 1]) + zz_gate.s(0) + zz_gate.h(1) + zz_gate.s(1) + SessionEquivalenceLibrary.add_equivalence(ZZGate(), zz_gate) + + +def add_equivalences(backend_name, noise_model=None) -> None: """Add IonQ gate equivalences to the SessionEquivalenceLibrary.""" u_gate_equivalence() - cx_gate_equivalence() + if backend_name in ( + "ionq_mock_backend", + "ionq_qpu", + "ionq_qpu.harmony", + "ionq_qpu.aria-1", + "ionq_qpu.aria-2", + ): + cx_gate_equivalence_ms() + elif backend_name in ( + "ionq_qpu.forte-1", + "ionq_qpu.forte-enterprise-1", + "ionq_qpu.forte-enterprise-2", + ): + cx_gate_equivalence_zz() + elif backend_name == "ionq_simulator": + if noise_model is None or noise_model in [ + "harmony", + "harmony-1", + "harmony-2", + "aria-1", + "aria-2", + "ideal", + "ideal-sampled", + ]: + cx_gate_equivalence_ms() + elif noise_model in ["forte-1", "forte-enterprise-1", "forte-enterprise-2"]: + cx_gate_equivalence_zz() + else: + raise IonQBackendNotSupportedError( + f"The backend with name {backend_name} is not supported. " + "The following backends names are supported: simulator or ionq_simulator " + "(with noise models: ideal as default, ideal-sampled, aria-1, aria-2, forte-1, " + "forte-enterprise-1, forte-enterprise-2, and legacy harmony, harmony-1, harmony-2) " + "qpu.aria-1 or ionq_qpu.aria-1, qpu.aria-2 or ionq_qpu.aria-2, " + "qpu.forte-1 or ionq_qpu.forte-1, " + "qpu.forte-enterprise-1 or ionq_qpu.forte-enterprise-1, " + "qpu.forte-enterprise-2 or ionq_qpu.forte-enterprise-2." + ) gpi_gate_equivalence() gpi2_gate_equivalence() ms_gate_equivalence() + zz_gate_equivalence() diff --git a/qiskit_ionq/ionq_gates.py b/qiskit_ionq/ionq_gates.py index 369848f0..75316331 100644 --- a/qiskit_ionq/ionq_gates.py +++ b/qiskit_ionq/ionq_gates.py @@ -168,19 +168,21 @@ class ZZGate(Gate): \end{pmatrix} """ - def __init__(self, theta: ParameterValueType, label: Optional[str] = None): + def __init__( + self, theta: Optional[ParameterValueType] = 0.25, label: Optional[str] = None + ): """Create new ZZ gate.""" super().__init__("zz", 2, [theta], label=label) def __array__(self, dtype=None) -> np.ndarray: """Return a numpy array for the ZZ gate.""" - itheta2 = 1j * float(self.params[0]) * math.pi + i_theta_over_2 = 1j * float(self.params[0]) * math.pi return np.array( [ - [np.exp(-itheta2), 0, 0, 0], - [0, np.exp(itheta2), 0, 0], - [0, 0, np.exp(itheta2), 0], - [0, 0, 0, np.exp(-itheta2)], + [np.exp(-i_theta_over_2), 0, 0, 0], + [0, np.exp(i_theta_over_2), 0, 0], + [0, 0, np.exp(i_theta_over_2), 0], + [0, 0, 0, np.exp(-i_theta_over_2)], ], dtype=dtype, ) diff --git a/qiskit_ionq/ionq_optimizer_plugin.py b/qiskit_ionq/ionq_optimizer_plugin.py index 22deb3b3..4c65c346 100644 --- a/qiskit_ionq/ionq_optimizer_plugin.py +++ b/qiskit_ionq/ionq_optimizer_plugin.py @@ -41,6 +41,7 @@ GPI2TwiceIsGPI, CompactMoreThanThreeSingleQubitGates, CommuteGPIsThroughMS, + CommuteGPITimesGPIThroughZZ, ) @@ -163,5 +164,6 @@ def pass_manager( custom_pass_manager.append(CancelGPIAdjoint()) custom_pass_manager.append(GPI2TwiceIsGPI()) custom_pass_manager.append(CommuteGPIsThroughMS()) + custom_pass_manager.append(CommuteGPITimesGPIThroughZZ()) custom_pass_manager.append(CompactMoreThanThreeSingleQubitGates()) return custom_pass_manager diff --git a/qiskit_ionq/ionq_provider.py b/qiskit_ionq/ionq_provider.py index 0971bb09..70123680 100644 --- a/qiskit_ionq/ionq_provider.py +++ b/qiskit_ionq/ionq_provider.py @@ -34,6 +34,8 @@ from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.providerutils import filter_backends + +from qiskit_ionq import ionq_equivalence_library from .helpers import resolve_credentials from . import ionq_backend @@ -87,13 +89,29 @@ def get_backend( more than one backend matches the filtering criteria. """ name = "ionq_" + name if not name.startswith("ionq_") else name + + noise_model = None + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model", None) + backends = self.backends(name, **kwargs) if len(backends) > 1: raise QiskitBackendNotFoundError("More than one backend matches criteria.") if not backends: raise QiskitBackendNotFoundError("No backend matches criteria.") - return backends[0].with_name(name, gateset=gateset) + if noise_model: + ionq_equivalence_library.add_equivalences(name, noise_model=noise_model) + else: + ionq_equivalence_library.add_equivalences(name) + + if noise_model: + backend = backends[0].with_name( + name, gateset=gateset, noise_model=noise_model + ) + else: + backend = backends[0].with_name(name, gateset=gateset) + return backend class BackendService: diff --git a/qiskit_ionq/rewrite_rules.py b/qiskit_ionq/rewrite_rules.py index a488217e..0dacef52 100644 --- a/qiskit_ionq/rewrite_rules.py +++ b/qiskit_ionq/rewrite_rules.py @@ -259,15 +259,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: for qreg in dag.qregs.values(): sub_dag.add_qreg(qreg) - # map the ops to the qubits in the sub-DAG - ms_qubits = [next_node.qargs[0], next_node.qargs[1]] - gpis_qubit = [node.qargs[0]] + sub_dag.apply_operation_back(next_node.op, next_node.qargs) + sub_dag.apply_operation_back(node.op, node.qargs) - sub_dag.apply_operation_back(next_node.op, ms_qubits) - sub_dag.apply_operation_back(node.op, gpis_qubit) - - wire_mapping = {qubit: qubit for qubit in ms_qubits} - wire_mapping[node.qargs[0]] = node.qargs[0] + wire_mapping = {qubit: qubit for qubit in next_node.qargs} dag.substitute_node_with_dag( next_node, sub_dag, wires=wire_mapping @@ -279,3 +274,62 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: dag.remove_op_node(node) return dag + + +class CommuteGPITimesGPIThroughZZ(TransformationPass): + """GPI(theta) * GPI(theta') on either qubit commutes with ZZ""" + + def run(self, dag: DAGCircuit) -> DAGCircuit: + nodes_to_remove = set() + + for node in dag.topological_op_nodes(): + if node in nodes_to_remove or node.op.name != "gpi": + continue + successors = [ + succ for succ in dag.successors(node) if isinstance(succ, DAGOpNode) + ] + + gate_pattern_found = False + for next_node in successors: + if gate_pattern_found: + break + if next_node in nodes_to_remove or next_node.op.name != "gpi": + continue + + if node.qargs[0] == next_node.qargs[0]: + next_node_successors = [ + succ + for succ in dag.successors(next_node) + if isinstance(succ, DAGOpNode) + ] + for next_next_node in next_node_successors: + if ( + next_next_node.op.name == "zz" + and next_node.qargs[0] in next_next_node.qargs + ): + sub_dag = DAGCircuit() + for qreg in dag.qregs.values(): + sub_dag.add_qreg(qreg) + + sub_dag.apply_operation_back( + next_next_node.op, next_next_node.qargs + ) + sub_dag.apply_operation_back(node.op, node.qargs) + sub_dag.apply_operation_back(next_node.op, next_node.qargs) + + wire_mapping = { + qubit: qubit for qubit in next_next_node.qargs + } + + dag.substitute_node_with_dag( + next_next_node, sub_dag, wires=wire_mapping + ) + nodes_to_remove.add(node) + nodes_to_remove.add(next_node) + gate_pattern_found = True + break + + for node in nodes_to_remove: + dag.remove_op_node(node) + + return dag diff --git a/test/helpers/test_qiskit_to_ionq.py b/test/helpers/test_qiskit_to_ionq.py index 96f524f5..04f96f84 100644 --- a/test/helpers/test_qiskit_to_ionq.py +++ b/test/helpers/test_qiskit_to_ionq.py @@ -30,8 +30,6 @@ import pytest from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.compiler import transpile -from qiskit.transpiler.exceptions import TranspilerError from qiskit_ionq.exceptions import IonQGateError from qiskit_ionq.helpers import qiskit_to_ionq, decompress_metadata_string @@ -300,23 +298,6 @@ def test_native_circuit_incorrect(simulator_backend): assert exc_info.value.gate_name == "gpi" -def test_native_circuit_transpile(simulator_backend): - """Test a full native circuit on a QIS backend via transpilation - - Args: - simulator_backend (IonQSimulatorBackend): A simulator backend fixture. - """ - circ = QuantumCircuit(3, name="blame_test") - circ.append(GPIGate(0.1), [0]) - circ.append(GPI2Gate(0.2), [1]) - circ.append(MSGate(0.2, 0.3, 0.25), [1, 2]) - circ.append(ZZGate(0.4), [0, 2]) - - with pytest.raises(TranspilerError) as exc_info: - transpile(circ, backend=simulator_backend) - assert "unable to synthesize" in exc_info.value.message - - def test_full_native_circuit(simulator_backend): """Test a full native circuit diff --git a/test/ionq_optimizer_plugin/test_ionq_optimizer_plugin.py b/test/ionq_optimizer_plugin/test_ionq_optimizer_plugin.py index 84274689..bd602e2e 100644 --- a/test/ionq_optimizer_plugin/test_ionq_optimizer_plugin.py +++ b/test/ionq_optimizer_plugin/test_ionq_optimizer_plugin.py @@ -81,7 +81,7 @@ CYGate, CZGate, ) -from qiskit_ionq import GPIGate, GPI2Gate, MSGate +from qiskit_ionq import GPIGate, GPI2Gate, MSGate, ZZGate from qiskit_ionq import ( IonQProvider, TrappedIonOptimizerPlugin, @@ -96,6 +96,7 @@ "GPIGate": GPIGate, "GPI2Gate": GPI2Gate, "MSGate": MSGate, + "ZZGate": ZZGate, # single-qubit gates "HGate": HGate, "IGate": IGate, @@ -452,7 +453,7 @@ def test_ionq_optimizer_plugin_simple_one_qubit_rules(gates, optimized_depth): append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -499,7 +500,7 @@ def test_ionq_optimizer_plugin_simple_one_qubit_rules(gates, optimized_depth): append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -730,7 +731,7 @@ def test_ionq_optimizer_plugin_compact_more_than_three_gates(gates, optimized_de append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -777,7 +778,7 @@ def test_ionq_optimizer_plugin_compact_more_than_three_gates(gates, optimized_de append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -1027,7 +1028,7 @@ def test_commute_gpis_through_ms(gates, optimized_depth): append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -1075,7 +1076,7 @@ def test_commute_gpis_through_ms(gates, optimized_depth): append_gate(qc, gate_name, param, qubits) provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=3 ) @@ -1105,6 +1106,119 @@ def test_commute_gpis_through_ms(gates, optimized_depth): ) +@pytest.mark.parametrize( + "gates, should_commute", + [ + ( + [ + ("GPIGate", [0.5], [0]), + ("GPIGate", [0.7], [0]), + ("ZZGate", [0.25], [0, 1]), + ], + True, + ), + ( + [ + ("GPIGate", [0.5], [1]), + ("GPIGate", [0.7], [1]), + ("ZZGate", [0.25], [0, 1]), + ], + True, + ), + ( + [ + ("GPIGate", [0.5], [0]), + ("GPIGate", [0.7], [1]), + ("ZZGate", [0.25], [0, 1]), + ], + False, + ), + ( + [ + ("GPIGate", [0.5], [1]), + ("GPIGate", [0.7], [0]), + ("ZZGate", [0.25], [0, 1]), + ], + False, + ), + ( + [ + ("GPIGate", [0.5], [0]), + ("GPI2Gate", [0.7], [0]), + ("ZZGate", [0.25], [0, 1]), + ], + False, + ), + ( + [ + ("GPI2Gate", [0.5], [0]), + ("GPIGate", [0.7], [0]), + ("ZZGate", [0.25], [0, 1]), + ], + False, + ), + ( + [ + ("GPI2Gate", [0.5], [0]), + ("GPI2Gate", [0.7], [0]), + ("ZZGate", [0.25], [0, 1]), + ], + False, + ), + ], + ids=lambda val: f"{val}", +) +def test_commute_two_gpi_through_zz(gates, should_commute): + """Test TrappedIonOptimizerPluginCommuteGpi2ThroughMs.""" + + custom_pass_manager_plugin = TrappedIonOptimizerPlugin() + custom_pass_manager = custom_pass_manager_plugin.pass_manager( + optimization_level=3, + ) + + # create a quantum circuit + qc = QuantumCircuit(2) + for gate_name, param, qubits in gates: + append_gate(qc, gate_name, param, qubits) + + provider = IonQProvider() + backend = provider.get_backend( + "ionq_simulator", gateset="native", noise_model="forte-1" + ) + transpiled_circuit_unoptimized = transpile( + qc, backend=backend, optimization_level=3 + ) + + # simulate the unoptimized circuit + statevector_unoptimized = Statevector.from_instruction( + transpiled_circuit_unoptimized + ) + probabilities_unoptimized = np.abs(statevector_unoptimized.data) ** 2 + + # optimized transpilation of circuit to native gates + optimized_circuit = custom_pass_manager.run(transpiled_circuit_unoptimized) + + # simulate the optimized circuit + statevector_optimized = Statevector.from_instruction(optimized_circuit) + probabilities_optimized = np.abs(statevector_optimized.data) ** 2 + + np.testing.assert_allclose( + probabilities_unoptimized, + probabilities_optimized, + atol=1e-5, + err_msg=( + f"Unoptmized: {np.round(probabilities_unoptimized, 3)},\n" + f"Optimized: {np.round(probabilities_optimized, 3)},\n" + f"Circuit: {qc}" + ), + ) + + if should_commute: + assert optimized_circuit != transpiled_circuit_unoptimized + else: + assert optimized_circuit == transpiled_circuit_unoptimized + + @pytest.mark.parametrize( "gates", [ @@ -1209,8 +1323,52 @@ def test_all_rewrite_rules(gates): for gate_name, param, qubits in gates: append_gate(qc, gate_name, param, qubits) + ###################################### + # Testing Aria type devices which + # transpile native gates using MS gate + ###################################### + + provider = IonQProvider() + backend = provider.get_backend("ionq_simulator", gateset="native") + transpiled_circuit_unoptimized = transpile( + qc, backend=backend, optimization_level=1 + ) + + # simulate the unoptimized circuit + statevector_unoptimized = Statevector.from_instruction( + transpiled_circuit_unoptimized + ) + probabilities_unoptimized = np.abs(statevector_unoptimized.data) ** 2 + + # optimized transpilation of circuit to native gates + optimized_circuit = custom_pass_manager.run(transpiled_circuit_unoptimized) + + # simulate the optimized circuit + statevector_optimized = Statevector.from_instruction(optimized_circuit) + probabilities_optimized = np.abs(statevector_optimized.data) ** 2 + + np.testing.assert_allclose( + probabilities_unoptimized, + probabilities_optimized, + atol=1e-5, + err_msg=( + f"Unoptmized: {np.round(probabilities_unoptimized, 3)},\n" + f"Optimized: {np.round(probabilities_optimized, 3)},\n" + f"Circuit: {qc}" + ), + ) + + assert optimized_circuit != transpiled_circuit_unoptimized + + ###################################### + # Testing Forte type devices which + # transpile native gates using ZZ gate + ###################################### + provider = IonQProvider() - backend = provider.get_backend("simulator", gateset="native") + backend = provider.get_backend( + "ionq_simulator", gateset="native", noise_model="forte-1" + ) transpiled_circuit_unoptimized = transpile( qc, backend=backend, optimization_level=1 ) diff --git a/test/ionq_provider/test_backend_service.py b/test/ionq_provider/test_backend_service.py index 78cb5d78..86f60f73 100644 --- a/test/ionq_provider/test_backend_service.py +++ b/test/ionq_provider/test_backend_service.py @@ -51,9 +51,9 @@ def test_backend_eq(): """Verifies equality works for various backends""" pro = IonQProvider("123456") - sub1 = pro.get_backend("ionq_qpu.sub-1") - sub2 = pro.get_backend("ionq_qpu.sub-2") - also_sub1 = pro.get_backend("ionq_qpu.sub-1") + sub1 = pro.get_backend("ionq_qpu.aria-1") + sub2 = pro.get_backend("ionq_qpu.aria-2") + also_sub1 = pro.get_backend("ionq_qpu.aria-1") simulator = pro.get_backend("ionq_simulator") assert sub1 == also_sub1 diff --git a/test/transpile_ionq_gates/test_transpile_ionq_gates.py b/test/transpile_ionq_gates/test_transpile_ionq_gates.py index d9f44b32..aa86b18a 100644 --- a/test/transpile_ionq_gates/test_transpile_ionq_gates.py +++ b/test/transpile_ionq_gates/test_transpile_ionq_gates.py @@ -206,7 +206,7 @@ def test_single_qubit_transpilation(ideal_results, gates): for gate_name, param in gates: append_gate(circuit, gate_name, param, [0]) - # transpile circuit to native gates + # Transpile circuit to native gates using default simulator provider = ionq_provider.IonQProvider() backend = provider.get_backend("ionq_simulator", gateset="native") transpiled_circuit = transpile(circuit, backend) @@ -225,6 +225,28 @@ def test_single_qubit_transpilation(ideal_results, gates): ), ) + # Transpile circuit to native gates. Transpiling to one qubit gates using forte should + # make no difference w.r.t using default simulator, we test this scenario nevertheless. + provider = ionq_provider.IonQProvider() + backend = provider.get_backend( + "ionq_simulator", gateset="native", noise_model="forte-1" + ) + transpiled_circuit = transpile(circuit, backend) + + # simulate the circuit + statevector = Statevector(transpiled_circuit) + probabilities = np.abs(statevector) ** 2 + np.testing.assert_allclose( + probabilities, + ideal_results, + atol=1e-3, + err_msg=( + f"Ideal: {np.round(ideal_results, 3)},\n" + f"Actual: {np.round(probabilities, 3)},\n" + f"Circuit: {circuit}" + ), + ) + @pytest.mark.parametrize( "ideal_results, gates", @@ -361,20 +383,43 @@ def test_single_qubit_transpilation(ideal_results, gates): ], ids=lambda val: f"{val}", ) -def test_multi_qubit_transpilation(ideal_results, gates): - """Test transpiling multi-qubit circuits to native gates.""" +def test_two_qubit_transpilation(ideal_results, gates): + """Test transpiling two-qubit circuits to native gates.""" # create a quantum circuit qr = QuantumRegister(2) circuit = QuantumCircuit(qr) for gate_name, param, qubits in gates: append_gate(circuit, gate_name, param, qubits) - # transpile circuit to native gates + # Transpile circuit to native gates using default simulator provider = ionq_provider.IonQProvider() backend = provider.get_backend("ionq_simulator", gateset="native") # Using optmization level 0 below is important here because ElidePermutations transpiler pass # in Qiskit will remove swap gates and instead premute qubits if optimization level is 2 or 3. - # In the future this feature could be extended to optmization level 1 as well. + # In the future this feature could be extended to optimization level 1 as well. + transpiled_circuit = transpile(circuit, backend, optimization_level=0) + + # simulate the circuit + statevector = Statevector(transpiled_circuit) + probabilities = np.abs(statevector) ** 2 + np.testing.assert_allclose( + probabilities, + ideal_results, + atol=1e-3, + err_msg=( + f"Ideal: {np.round(ideal_results, 3)},\n" + f"Actual: {np.round(probabilities, 3)},\n" + f"Circuit: {circuit}" + ), + ) + # Transpile circuit to native gates using forte + provider = ionq_provider.IonQProvider() + backend = provider.get_backend( + "ionq_simulator", gateset="native", noise_model="forte-1" + ) + # Using optmization level 0 below is important here because ElidePermutations transpiler pass + # in Qiskit will remove swap gates and instead premute qubits if optimization level is 2 or 3. + # In the future this feature could be extended to optimization level 1 as well. transpiled_circuit = transpile(circuit, backend, optimization_level=0) # simulate the circuit