diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index a706c2e62666..028cf8895742 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -474,12 +474,25 @@ def qubit_properties( be a single integer for 1 qubit or a list of qubits and a list of :class:`~qiskit.provider.QubitProperties` objects will be returned in the same order + Returns: + qubit_properties: The :class:`~.QubitProperties` object for the + specified qubit. If a list of qubits is provided a list will be + returned. If properties are missing for a qubit this can be + ``None``. - raises: + Raises: NotImplementedError: if the backend doesn't support querying the qubit properties """ - raise NotImplementedError + # Since the target didn't always have a qubit properties attribute + # to ensure the behavior here is backwards compatible with earlier + # BacekendV2 implementations where this would raise a NotImplemented + # error. + if self.target.qubit_properties is None: + raise NotImplementedError + if isinstance(qubit, int): + return self.target.qubit_properties[qubit] + return [self.target.qubit_properties[q] for q in qubit] def drive_channel(self, qubit: int): """Return the drive channel for the given qubit. diff --git a/qiskit/test/mock/fake_backend_v2.py b/qiskit/test/mock/fake_backend_v2.py index 31b8698adcf2..6ec566ddc7cc 100644 --- a/qiskit/test/mock/fake_backend_v2.py +++ b/qiskit/test/mock/fake_backend_v2.py @@ -46,7 +46,11 @@ def __init__(self): online_date=datetime.datetime.utcnow(), backend_version="0.0.1", ) - self._target = Target() + self._qubit_properties = [ + QubitProperties(t1=63.48783e-6, t2=112.23246e-6, frequency=5.17538e9), + QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + ] + self._target = Target(qubit_properties=self._qubit_properties) self._theta = Parameter("theta") self._phi = Parameter("phi") self._lam = Parameter("lambda") @@ -79,10 +83,6 @@ def __init__(self): } self._target.add_instruction(ECRGate(), ecr_props) self.options.set_validator("shots", (1, 4096)) - self._qubit_properties = { - 0: QubitProperties(t1=63.48783e-6, t2=112.23246e-6, frequency=5.17538e9), - 1: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), - } @property def target(self): @@ -99,6 +99,10 @@ def _default_options(cls): def run(self, run_input, **options): raise NotImplementedError + +class FakeBackendV2LegacyQubitProps(FakeBackendV2): + """Fake backend that doesn't use qubit properties via the target.""" + def qubit_properties(self, qubit): if isinstance(qubit, int): return self._qubit_properties[qubit] @@ -116,7 +120,14 @@ def __init__(self, bidirectional=True): online_date=datetime.datetime.utcnow(), backend_version="0.0.1", ) - self._target = Target() + qubit_properties = [ + QubitProperties(t1=63.48783e-6, t2=112.23246e-6, frequency=5.17538e9), + QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), + ] + self._target = Target(qubit_properties=qubit_properties) self._theta = Parameter("theta") self._phi = Parameter("phi") self._lam = Parameter("lambda") @@ -153,13 +164,6 @@ def __init__(self, bidirectional=True): ecr_props[(3, 2)] = InstructionProperties(duration=5.52e-9, error=0.0000232115) self._target.add_instruction(ECRGate(), ecr_props) self.options.set_validator("shots", (1, 4096)) - self._qubit_properties = { - 0: QubitProperties(t1=63.48783e-6, t2=112.23246e-6, frequency=5.17538e9), - 1: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), - 2: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), - 3: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), - 4: QubitProperties(t1=73.09352e-6, t2=126.83382e-6, frequency=5.26722e9), - } @property def target(self): @@ -176,11 +180,6 @@ def _default_options(cls): def run(self, run_input, **options): raise NotImplementedError - def qubit_properties(self, qubit): - if isinstance(qubit, int): - return self._qubit_properties[qubit] - return [self._qubit_properties[i] for i in qubit] - class FakeBackendSimple(BackendV2): """A fake simple backend that wraps BasicAer to implement run().""" diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 1a03128cb9b9..19aafb51a1bc 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -437,3 +437,4 @@ from .instruction_durations import InstructionDurations from .target import Target from .target import InstructionProperties +from .target import QubitProperties diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index d5eb2bc2ce76..ed79704afd31 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -27,6 +27,10 @@ from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints +# import QubitProperties here to provide convenience alias for building a +# full target +from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import + logger = logging.getLogger(__name__) @@ -169,6 +173,7 @@ class Target(Mapping): "aquire_alignment", "_non_global_basis", "_non_global_strict_basis", + "qubit_properties", ) def __init__( @@ -180,6 +185,7 @@ def __init__( min_length=1, pulse_alignment=1, aquire_alignment=1, + qubit_properties=None, ): """ Create a new Target object @@ -208,6 +214,17 @@ def __init__( resolution of measure instruction starting time. Measure instruction should start at time which is a multiple of the alignment value. + qubit_properties (list): A list of :class:`~.QubitProperties` + objects defining the characteristics of each qubit on the + target device. If specified the length of this list must match + the number of qubits in the target, where the index in the list + matches the qubit number the properties are defined for. If some + qubits don't have properties available you can set that entry to + ``None`` + Raises: + ValueError: If both ``num_qubits`` and ``qubit_properties`` are both + defined and the value of ``num_qubits`` differs from the length of + ``qubit_properties``. """ self.num_qubits = num_qubits # A mapping of gate name -> gate instance @@ -227,6 +244,16 @@ def __init__( self.aquire_alignment = aquire_alignment self._non_global_basis = None self._non_global_strict_basis = None + if qubit_properties is not None: + if not self.num_qubits: + self.num_qubits = len(qubit_properties) + else: + if self.num_qubits != len(qubit_properties): + raise ValueError( + "The value of num_qubits specified does not match the " + "length of the input qubit_properties list" + ) + self.qubit_properties = qubit_properties def add_instruction(self, instruction, properties=None, name=None): """Add a new instruction to the :class:`~qiskit.transpiler.Target` diff --git a/releasenotes/notes/qubit-properties-target-6b1fb155a46cb942.yaml b/releasenotes/notes/qubit-properties-target-6b1fb155a46cb942.yaml new file mode 100644 index 000000000000..eb01b2bd1e6e --- /dev/null +++ b/releasenotes/notes/qubit-properties-target-6b1fb155a46cb942.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Added a new attribute, :attr:`~.Target.qubit_properties` to the + :class:`~.Target` class. This attribute contains a list of + :class:`~.QubitProperties` objects for each qubit in the target. + For example:: + + target.qubit_properties[2] + + will contain the :class:`~.QubitProperties` for qubit number 2 in the + target. + + For :class:`~.BackendV2` authors, if you were previously defining + :class:`~.QubitProperties` directly on your :class:`~.BackendV2` + implementation by overriding :meth:`.BackendV2.qubit_properties` this + will still work fine. However, if you do move the definition to the + underlying :class:`~.Target` object and remove the specialized + :meth:`.BackendV2.qubit_properties` implementation which will enable + using qubit properties in the transpiler and also maintain API compatibility + with your previous implementation diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 7f1880c4e241..63a809551700 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -24,7 +24,12 @@ from qiskit.compiler.transpiler import _parse_inst_map from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.test.base import QiskitTestCase -from qiskit.test.mock.fake_backend_v2 import FakeBackendV2, FakeBackend5QV2 +from qiskit.test.mock.fake_backend_v2 import ( + FakeBackendV2, + FakeBackend5QV2, + FakeBackendSimple, + FakeBackendV2LegacyQubitProps, +) from qiskit.test.mock.fake_mumbai_v2 import FakeMumbaiFractionalCX from qiskit.quantum_info import Operator @@ -53,6 +58,18 @@ def test_qubit_properties(self): self.assertEqual([126.83382e-6, 112.23246e-6], [x.t2 for x in props]) self.assertEqual([5.26722e9, 5.17538e9], [x.frequency for x in props]) + def test_legacy_qubit_properties(self): + """Test that qubit props work for backends not using properties in target.""" + props = FakeBackendV2LegacyQubitProps().qubit_properties([1, 0]) + self.assertEqual([73.09352e-6, 63.48783e-6], [x.t1 for x in props]) + self.assertEqual([126.83382e-6, 112.23246e-6], [x.t2 for x in props]) + self.assertEqual([5.26722e9, 5.17538e9], [x.frequency for x in props]) + + def test_no_qubit_properties_raises(self): + """Ensure that if no qubit properties are defined we raise correctly.""" + with self.assertRaises(NotImplementedError): + FakeBackendSimple().qubit_properties(0) + def test_option_bounds(self): """Test that option bounds are enforced.""" with self.assertRaises(ValueError) as cm: