Skip to content

Commit

Permalink
Update ISA validation (#1414)
Browse files Browse the repository at this point in the history
* check coupling map

* restore stack level

* mypy

* remove layout check

* check width

* allow pulse gates

* add release note
  • Loading branch information
jyu00 authored Feb 23, 2024
1 parent 76d1d13 commit 08c419b
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 30 deletions.
2 changes: 1 addition & 1 deletion qiskit_ibm_runtime/base_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJo
if (
self._backend
and isinstance(self._backend, IBMBackend)
and isinstance(self._backend._service, QiskitRuntimeService)
and isinstance(self._backend.service, QiskitRuntimeService)
and hasattr(self._backend, "target")
):
validate_isa_circuits(primitive_inputs["circuits"], self._backend.target)
Expand Down
59 changes: 37 additions & 22 deletions qiskit_ibm_runtime/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,43 +33,58 @@
from qiskit_ibm_runtime.exceptions import IBMInputValueError


def is_isa_circuit(circuit: QuantumCircuit, target: Target) -> bool:
def is_isa_circuit(circuit: QuantumCircuit, target: Target) -> str:
"""Checks if the circuit is an ISA circuit, meaning that it has a layout and that it
only uses instructions that exist in the target.
Args:
circuit: A single QuantumCircuit
target: A Qiskit Target
target: The backend target
Returns:
Boolean True if the circuit is an ISA circuit
Message on why the circuit is not an ISA circuit, if applicable.
"""
if circuit.layout is None:
return False
if circuit.num_qubits > target.num_qubits:
return (
f"The circuit has {circuit.num_qubits} qubits "
f"but the target system requires {target.num_qubits} qubits."
)

for instruction in circuit.data:
name = instruction.operation.name
qargs = tuple(circuit.find_bit(x).index for x in instruction.qubits)
if not target.instruction_supported(name, qargs) and name != "barrier":
return False
return True
if (
not target.instruction_supported(name, qargs)
and name != "barrier"
and not circuit.has_calibration_for(instruction)
):
return (
f"The instruction {name} on qubits {qargs} is not supported by the target system."
)
return ""


def validate_isa_circuits(circuits: Sequence[QuantumCircuit], target: Target) -> None:
"""Validate if all circuits are ISA circuits
Args:
circuits: A list of QuantumCircuits
target: A Qiskit Target
Raises:
NonISACircuitsError if some of the circuits are not ISA circuits
circuits: A list of QuantumCircuits.
target: The backend target
"""
if not all(is_isa_circuit(circuit, target) for circuit in circuits):
warnings.warn(
"Circuits that do not match the target hardware definition will no longer be supported "
"after March 1, 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,
)
for circuit in circuits:
message = is_isa_circuit(circuit, target)
if message:
warnings.warn(
message
+ " Circuits that do not match the target hardware definition will no longer be "
"supported after March 1, 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,
)
break


def validate_job_tags(job_tags: Optional[List[str]]) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Fixes the check for ISA circuits to allow pulse gates and circuits that
don't have layout.
76 changes: 73 additions & 3 deletions test/unit/test_ibm_primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
from unittest.mock import MagicMock, patch
from dataclasses import asdict
from typing import Dict
import warnings

from qiskit import transpile
from ddt import data, ddt
from qiskit import transpile, pulse
from qiskit.circuit import QuantumCircuit

from qiskit.pulse.library import Gaussian
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManila
from qiskit.providers.models.backendconfiguration import QasmBackendConfiguration

from qiskit_ibm_runtime.fake_provider import FakeManila
from qiskit_ibm_runtime import (
Sampler,
Estimator,
Expand All @@ -41,6 +44,7 @@
dict_keys_equal,
create_faulty_backend,
bell,
get_mocked_backend,
)


Expand All @@ -51,6 +55,7 @@ class MockSession(Session):
_instance = None


@ddt
class TestPrimitives(IBMTestCase):
"""Class for testing the Sampler and Estimator classes."""

Expand Down Expand Up @@ -847,6 +852,71 @@ def test_no_raise_skip_transpilation(self):
sampler.run(transpiled)
mock_run.assert_called_once()

@data(Sampler, Estimator)
def test_abstract_circuits(self, primitive):
"""Test passing in abstract circuit."""
backend = get_mocked_backend()
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()

with self.assertWarnsRegex(DeprecationWarning, "target hardware"):
inst.run(**run_input)

@data(Sampler, Estimator)
def test_abstract_circuits_backend_no_coupling_map(self, primitive):
"""Test passing in abstract circuits to a backend with no coupling map."""

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)
circ = QuantumCircuit(2, 2)
circ.cx(0, 1)
transpiled = transpile(circ, backend=backend)
run_input = {"circuits": transpiled}
if isinstance(inst, Estimator):
run_input["observables"] = SparsePauliOp("ZZ")
else:
transpiled.measure_all()

with warnings.catch_warnings(record=True) as warns:
warnings.simplefilter("always")
inst.run(**run_input)
self.assertFalse(warns)

@data(Sampler, Estimator)
def test_pulse_gates_is_isa(self, primitive):
"""Test passing circuits with pulse gates is considered ISA."""
backend = get_mocked_backend()
inst = primitive(backend=backend)

circuit = QuantumCircuit(1)
circuit.h(0)
with pulse.build(backend, name="hadamard") as h_q0:
pulse.play(Gaussian(duration=64, amp=0.5, sigma=8), pulse.drive_channel(0))
circuit.add_calibration("h", [0], h_q0)

run_input = {"circuits": circuit}
if isinstance(inst, Estimator):
run_input["observables"] = SparsePauliOp("Z")
else:
circuit.measure_all()

with warnings.catch_warnings(record=True) as warns:
warnings.simplefilter("always")
inst.run(**run_input)
self.assertFalse(warns)

def _update_dict(self, dict1, dict2):
for key, val in dict1.items():
if isinstance(val, dict):
Expand Down
28 changes: 24 additions & 4 deletions test/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@
import time
import unittest
from unittest import mock
from typing import Dict, Optional, Any
from typing import Dict, Optional
from datetime import datetime

from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.compiler import transpile, assemble
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
from qiskit.providers.models import BackendStatus, BackendProperties, BackendConfiguration
from qiskit.providers.backend import Backend

from qiskit_ibm_runtime.hub_group_project import HubGroupProject
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime.ibm_backend import IBMBackend
from qiskit_ibm_runtime.runtime_job import RuntimeJob
from qiskit_ibm_runtime.exceptions import RuntimeInvalidStateError
from qiskit_ibm_runtime.fake_provider import FakeManila


def setup_test_logging(logger: logging.Logger, filename: str) -> None:
Expand Down Expand Up @@ -274,11 +276,29 @@ def create_faulty_backend(
return out_backend


def get_mocked_backend(name: str = "ibm_gotham") -> Any:
def get_mocked_backend(
name: str = "ibm_gotham", configuration: Optional[BackendConfiguration] = None
) -> IBMBackend:
"""Return a mock backend."""
mock_backend = mock.MagicMock(spec=IBMBackend)

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()

mock_api_client.backend_properties = _noop
mock_api_client.backend_pulse_defaults = _noop
mock_backend = IBMBackend(
configuration=configuration, service=mock_service, api_client=mock_api_client
)
mock_backend.name = name
mock_backend._instance = None

return mock_backend


Expand Down

0 comments on commit 08c419b

Please sign in to comment.