From ee086b0bbefe1fde4e1c4139ac2f9c10ff4789ae Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Mon, 4 Mar 2024 13:09:10 -0500 Subject: [PATCH] Raise if abstract circuits are used (#1437) * support dynamic circuits * raise for abstract circuits * lint * fix test * remove hard coded instructions * check target is not none * lint * fix test * check for error not warning * bypass simulator --- qiskit_ibm_runtime/base_primitive.py | 6 +- qiskit_ibm_runtime/utils/utils.py | 12 +-- ...isa-circuit-required-ed361bd65cef5ed8.yaml | 10 +++ test/unit/test_ibm_primitives.py | 90 ++++++++++++++++--- test/unit/test_sampler.py | 24 +++-- test/utils.py | 25 ++++-- 6 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 releasenotes/notes/isa-circuit-required-ed361bd65cef5ed8.yaml diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 2b5090b2f..0c91c0c70 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -146,10 +146,12 @@ def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJo Submitted job. """ if ( - self._backend + self._backend # pylint: disable=too-many-boolean-expressions and isinstance(self._backend, IBMBackend) and isinstance(self._backend.service, QiskitRuntimeService) - and hasattr(self._backend, "target") + and not self._backend.simulator + and getattr(self._backend, "target", None) + and self._service._channel_strategy != "q-ctrl" ): validate_isa_circuits(primitive_inputs["circuits"], self._backend.target) diff --git a/qiskit_ibm_runtime/utils/utils.py b/qiskit_ibm_runtime/utils/utils.py index 1edf11bfd..7f60c6970 100644 --- a/qiskit_ibm_runtime/utils/utils.py +++ b/qiskit_ibm_runtime/utils/utils.py @@ -17,7 +17,6 @@ import os import re import hashlib -import warnings from queue import Queue from threading import Condition from typing import List, Optional, Any, Dict, Union, Tuple, Sequence @@ -74,17 +73,14 @@ def validate_isa_circuits(circuits: Sequence[QuantumCircuit], target: Target) -> for circuit in circuits: message = is_isa_circuit(circuit, target) if message: - warnings.warn( + raise IBMInputValueError( message - + " Circuits that do not match the target hardware definition will no longer be " - "supported after March 1, 2024. See the transpilation documentation " + + " Circuits that do not match the target hardware definition are no longer " + "supported after March 4, 2024. See the transpilation documentation " "(https://docs.quantum.ibm.com/transpile) for instructions to transform circuits and " "the primitive examples (https://docs.quantum.ibm.com/run/primitives-examples) to see " - "this coupled with operator transformations.", - DeprecationWarning, - stacklevel=6, + "this coupled with operator transformations." ) - break def validate_job_tags(job_tags: Optional[List[str]]) -> None: diff --git a/releasenotes/notes/isa-circuit-required-ed361bd65cef5ed8.yaml b/releasenotes/notes/isa-circuit-required-ed361bd65cef5ed8.yaml new file mode 100644 index 000000000..64301c557 --- /dev/null +++ b/releasenotes/notes/isa-circuit-required-ed361bd65cef5ed8.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + Circuits that do not match the target hardware definition are no longer + supported by Qiskit Runtime primitives, unless ``channel_strategy="q-ctrl"`` + is used. See the transpilation documentation + (https://docs.quantum.ibm.com/transpile) for instructions to transform + circuits and the primitive examples + (https://docs.quantum.ibm.com/run/primitives-examples) to see + this coupled with operator transformations. diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py index d6530c268..66cef03b6 100644 --- a/test/unit/test_ibm_primitives.py +++ b/test/unit/test_ibm_primitives.py @@ -18,16 +18,14 @@ from unittest.mock import MagicMock, patch from dataclasses import asdict from typing import Dict -import warnings from ddt import data, ddt from qiskit import transpile, pulse -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.pulse.library import Gaussian from qiskit.quantum_info import SparsePauliOp -from qiskit.providers.models.backendconfiguration import QasmBackendConfiguration -from qiskit_ibm_runtime.fake_provider import FakeManila +from qiskit_ibm_runtime.fake_provider import FakeManila, FakeSherbrooke from qiskit_ibm_runtime import ( Sampler, Estimator, @@ -36,6 +34,7 @@ ) from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION +from qiskit_ibm_runtime.exceptions import IBMInputValueError from ..ibm_test_case import IBMTestCase from ..utils import ( @@ -866,7 +865,7 @@ def test_abstract_circuits(self, primitive): else: circ.measure_all() - with self.assertWarnsRegex(DeprecationWarning, "target hardware"): + with self.assertRaisesRegex(IBMInputValueError, "target hardware"): inst.run(**run_input) @data(Sampler, Estimator) @@ -876,7 +875,6 @@ def test_abstract_circuits_backend_no_coupling_map(self, primitive): config = FakeManila().configuration().to_dict() for gate in config["gates"]: gate.pop("coupling_map", None) - config = QasmBackendConfiguration.from_dict(config) backend = get_mocked_backend(configuration=config) inst = primitive(backend=backend) @@ -889,10 +887,7 @@ def test_abstract_circuits_backend_no_coupling_map(self, primitive): else: transpiled.measure_all() - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") - inst.run(**run_input) - self.assertFalse(warns) + inst.run(**run_input) @data(Sampler, Estimator) def test_pulse_gates_is_isa(self, primitive): @@ -912,10 +907,60 @@ def test_pulse_gates_is_isa(self, primitive): else: circuit.measure_all() - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") - inst.run(**run_input) - self.assertFalse(warns) + inst.run(**run_input) + + @data(Sampler, Estimator) + def test_dynamic_circuit_is_isa(self, primitive): + """Test passing dynmaic circuits is considered ISA.""" + # pylint: disable=not-context-manager + # pylint: disable=invalid-name + sherbrooke = FakeSherbrooke() + config = sherbrooke._get_conf_dict_from_json() + config["supported_instructions"] += ["for_loop", "switch_case", "while_loop"] + + backend = get_mocked_backend( + configuration=config, + properties=sherbrooke._set_props_dict_from_json(), + defaults=sherbrooke._set_defs_dict_from_json(), + ) + + inst = primitive(backend=backend) + + qubits = QuantumRegister(3) + clbits = ClassicalRegister(3) + circuit = QuantumCircuit(qubits, clbits) + (q0, q1, q2) = qubits + (c0, c1, c2) = clbits + + circuit.x(q0) + circuit.measure(q0, c0) + with circuit.if_test((c0, 1)): + circuit.x(q0) + + circuit.measure(q1, c1) + with circuit.switch(c1) as case: + with case(0): + circuit.x(q0) + with case(1): + circuit.x(q1) + + circuit.measure(q1, c1) + circuit.measure(q2, c2) + with circuit.while_loop((clbits, 0b111)): + circuit.rz(1.5, q1) + circuit.rz(1.5, q2) + circuit.measure(q1, c1) + circuit.measure(q2, c2) + + with circuit.for_loop(range(2)) as _: + circuit.x(q0) + + circuit = transpile(circuit, backend=backend) + run_input = {"circuits": circuit} + if isinstance(inst, Estimator): + run_input["observables"] = SparsePauliOp("ZZZ").apply_layout(circuit.layout) + + inst.run(**run_input) def _update_dict(self, dict1, dict2): for key, val in dict1.items(): @@ -995,3 +1040,20 @@ def test_qctrl_unsupported_values_for_options(self): _ = inst.run(self.qx, observables=self.obs, **bad_opt) self.assertIn(expected_message, str(exc.exception)) + + @data(Sampler, Estimator) + def test_qctrl_abstract_circuit(self, primitive): + """Test q-ctrl can still accept abstract circuits.""" + backend = get_mocked_backend() + backend._service._channel_strategy = "q-ctrl" + inst = primitive(backend=backend) + + circ = QuantumCircuit(3, 3) + circ.cx(0, 2) + run_input = {"circuits": circ} + if isinstance(inst, Estimator): + run_input["observables"] = SparsePauliOp("ZZZ") + else: + circ.measure_all() + + inst.run(**run_input) diff --git a/test/unit/test_sampler.py b/test/unit/test_sampler.py index 9bb5e9c44..10c138ebf 100644 --- a/test/unit/test_sampler.py +++ b/test/unit/test_sampler.py @@ -12,11 +12,12 @@ """Tests for sampler class.""" -from qiskit_ibm_runtime import Sampler, Session +from qiskit import transpile + +from qiskit_ibm_runtime import Sampler from ..ibm_test_case import IBMTestCase -from ..utils import bell -from .mock.fake_runtime_service import FakeRuntimeService +from ..utils import bell, get_mocked_backend class TestSampler(IBMTestCase): @@ -28,14 +29,11 @@ def test_unsupported_values_for_sampler_options(self): {"resilience_level": 2, "optimization_level": 3}, {"optimization_level": 4, "resilience_level": 1}, ] + backend = get_mocked_backend() + circuit = transpile(bell(), backend=backend) - with Session( - service=FakeRuntimeService(channel="ibm_quantum", token="abc"), - backend="common_backend", - ) as session: - circuit = bell() - for bad_opt in options_bad: - inst = Sampler(session=session) - with self.assertRaises(ValueError) as exc: - _ = inst.run(circuit, **bad_opt) - self.assertIn(list(bad_opt.keys())[0], str(exc.exception)) + for bad_opt in options_bad: + sampler = Sampler(backend=backend) + with self.assertRaises(ValueError) as exc: + _ = sampler.run(circuit, **bad_opt) + self.assertIn(list(bad_opt.keys())[0], str(exc.exception)) diff --git a/test/utils.py b/test/utils.py index 041e6765c..b83b8be0e 100644 --- a/test/utils.py +++ b/test/utils.py @@ -25,7 +25,11 @@ from qiskit.qobj import QasmQobj from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.models import BackendStatus, BackendProperties, BackendConfiguration +from qiskit.providers.models import ( + BackendStatus, + BackendProperties, + BackendConfiguration, +) from qiskit.providers.backend import Backend from qiskit_ibm_runtime.hub_group_project import HubGroupProject @@ -277,22 +281,25 @@ def create_faulty_backend( def get_mocked_backend( - name: str = "ibm_gotham", configuration: Optional[BackendConfiguration] = None + name: str = "ibm_gotham", + configuration: Optional[Dict] = None, + properties: Optional[Dict] = None, + defaults: Optional[Dict] = None, ) -> IBMBackend: """Return a mock backend.""" - def _noop(*args, **kwargs): # pylint: disable=unused-argument - return None - mock_service = mock.MagicMock(spec=QiskitRuntimeService) mock_service._channel_strategy = None mock_api_client = mock.MagicMock() - if not configuration: - configuration = FakeManila().configuration() + configuration = ( + FakeManila().configuration() + if configuration is None + else BackendConfiguration.from_dict(configuration) + ) - mock_api_client.backend_properties = _noop - mock_api_client.backend_pulse_defaults = _noop + mock_api_client.backend_properties = lambda *args, **kwargs: properties + mock_api_client.backend_pulse_defaults = lambda *args, **kwargs: defaults mock_backend = IBMBackend( configuration=configuration, service=mock_service, api_client=mock_api_client )