Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update calibration and mixin tests to work with BackendV2 #900

Merged
merged 13 commits into from
Mar 27, 2023
27 changes: 21 additions & 6 deletions qiskit_experiments/framework/backend_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ def meas_freqs(self):
"""Returns the backend's measurement stimulus frequencies.

.. note::
Currently BackendV2 does not have access to this data.

The qiskit-terra base classes do not provide this information as a
standard backend property, but it is available from some providers
in the data returned by the ``Backend.defaults()`` method.
"""
if self._v1:
return getattr(self._backend.defaults(), "meas_freq_est", [])
elif self._v2:
# meas_freq_est is currently not part of the BackendV2
if not hasattr(self._backend, "defaults"):
return []
return []
return getattr(self._backend.defaults(), "meas_freq_est", [])

@property
def num_qubits(self):
Expand Down Expand Up @@ -262,3 +262,18 @@ def is_simulator(self):
return True

return False

def qubit_t1(self, qubit: int) -> float:
"""Return the T1 value for a qubit from the backend properties

Args:
qubit: the qubit index to return T1 for

Returns:
The T1 value
"""
if self._v1:
return self._backend.properties().qubit_property(qubit)["T1"][0]
if self._v2:
return self._backend.qubit_properties(qubit).t1
return float("nan")
2 changes: 1 addition & 1 deletion qiskit_experiments/framework/restless_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def _t1_check(self, rep_delay: float) -> bool:

try:
t1_values = [
self._backend.properties().qubit_property(physical_qubit)["T1"][0]
self._backend_data.qubit_t1(physical_qubit)
for physical_qubit in self._physical_qubits
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,38 @@ def __init__(

Raises:
QiskitError: If no frequencies are given and absolute frequencies are desired and
no backend is given.
no backend is given or the backend does not have default measurement frequencies.
"""
analysis = ResonatorSpectroscopyAnalysis()

if frequencies is None:
frequencies = np.linspace(-20.0e6, 20.0e6, 51)

if absolute:
if backend is None:
raise QiskitError(
"Cannot automatically compute absolute frequencies without a backend."
)

center_freq = BackendData(backend).meas_freqs[physical_qubits[0]]
frequencies += center_freq
frequencies += self._get_backend_meas_freq(
BackendData(backend) if backend is not None else None,
physical_qubits[0],
)

super().__init__(
physical_qubits, frequencies, backend, absolute, analysis, **experiment_options
)

@staticmethod
def _get_backend_meas_freq(backend_data: Optional[BackendData], qubit: int) -> float:
wshanks marked this conversation as resolved.
Show resolved Hide resolved
"""Get backend meas_freq with error checking"""
if backend_data is None:
raise QiskitError(
"Cannot automatically compute absolute frequencies without a backend."
)

if len(backend_data.meas_freqs) < qubit + 1:
raise QiskitError(
"Cannot retrieve default measurement frequencies from backend. "
"Please set frequencies explicitly or set `absolute` to `False`."
)
return backend_data.meas_freqs[qubit]

@property
def _backend_center_frequency(self) -> float:
"""Returns the center frequency of the experiment.
Expand All @@ -205,10 +217,7 @@ def _backend_center_frequency(self) -> float:
Raises:
QiskitError: If the experiment does not have a backend set.
"""
if self.backend is None:
raise QiskitError("backend not set. Cannot call center_frequency.")

return self._backend_data.meas_freqs[self.physical_qubits[0]]
return self._get_backend_meas_freq(self._backend_data, self.physical_qubits[0])

def _template_circuit(self) -> QuantumCircuit:
"""Return the template quantum circuit."""
Expand Down
51 changes: 48 additions & 3 deletions qiskit_experiments/test/mock_iq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import numpy as np

from qiskit import QuantumCircuit
from qiskit.circuit.library import XGate, SXGate
from qiskit.result import Result
from qiskit.providers.fake_provider import FakeOpenPulse2Q
from qiskit.providers.fake_provider.fake_backend import FakeBackendV2

from qiskit.qobj.utils import MeasLevel
from qiskit_experiments.exceptions import QiskitError
Expand All @@ -31,7 +33,49 @@
)


class MockRestlessBackend(FakeOpenPulse2Q):
class FakeOpenPulse2QV2(FakeBackendV2):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the reason not to use BackendV2Converter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is that I started this PR before it existed. I wonder if it would work or would suffer from the issues that I hack around below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this:

from qiskit.providers.fake_provider import FakeOpenPulse2Q
from qiskit.providers.backend_compat import BackendV2Converter

class FakeOpenPulse2QV2(BackendV2Converter):
    def __init__(self):
        v1backend = FakeOpenPulse2Q()
        super().__init__(v1backend)

but I encountered several test errors. A lot of them were this problem:

  File "/home/wshanks/Documents/Code/qiskit-experiments/test/library/characterization/test_half_angle.py", line 39, in test_end_to_end
    exp_data = hac.run(backend)                                                                                                                                
  File "/home/wshanks/Documents/Code/qiskit-experiments/qiskit_experiments/framework/base_experiment.py", line 238, in run
    transpiled_circuits = experiment._transpiled_circuits()                                                                                                    
  File "/home/wshanks/Documents/Code/qiskit-experiments/qiskit_experiments/framework/base_experiment.py", line 319, in _transpiled_circuits
    transpiled = transpile(self.circuits(), self.backend, **transpile_opts)                                                                                    
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/compiler/transpiler.py", line 326, in transpile
    unique_transpile_args, shared_args = _parse_transpile_args(                                                                                                
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/compiler/transpiler.py", line 656, in _parse_transpile_args                       
    inst_map = _parse_inst_map(inst_map, backend)                                                                                                              
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/compiler/transpiler.py", line 818, in _parse_inst_map                             
    inst_map = backend.target.instruction_schedule_map()                                                                                                       
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/providers/backend_compat.py", line 244, in target                                 
    self._target = convert_to_target(                                                                                                                          
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/providers/backend_compat.py", line 87, in convert_to_target                       
    duration=properties.readout_length(qubit),                                                                                                                 
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/providers/models/backendproperties.py", line 453, in readout_length
    return self.qubit_property(qubit, "readout_length")[0]  # Throw away datetime at index 1                                                    
  File "/home/wshanks/.conda/envs/qiskit/lib/python3.10/site-packages/qiskit/providers/models/backendproperties.py", line 389, in qubit_property
    raise BackendPropertyError(                                                                                                                                
qiskit.providers.exceptions.BackendPropertyError: "Couldn't find the property 'readout_length' for qubit 0." 

FakeBackendV2 follows a different code path from BackendConverter. They use different convert_to_target functions. BackendConverter assumes that readout_length is in the properties here:

https://github.com/Qiskit/qiskit-terra/blob/a1a67a11741000f8f3b6d6a8766dab7e3cd06847/qiskit/providers/backend_compat.py#L86-L89

while FakeBackendV2 checks for readout_length but does not error if it is not there:

https://github.com/Qiskit/qiskit-terra/blob/a1a67a11741000f8f3b6d6a8766dab7e3cd06847/qiskit/providers/fake_provider/utils/backend_converter.py#L80-L85

I tried working around that with this (thisi is also working around errors from description and max_experiments being missing):

from qiskit.providers.fake_provider import FakeOpenPulse2Q
from qiskit.providers.backend_compat import BackendV2Converter


class FakeOpenPulse2QV2(BackendV2Converter):
    def __init__(self):
        backend_v1 = FakeOpenPulse2Q()
        backend_v1.configuration().description = "FakeOpenPulse2Q"
        backend_v1.configuration().max_experiments = 100
        cmd_def = backend_v1.defaults().cmd_def
        measure = next(c for c in cmd_def if c.name == "measure")
        readout_length = max(getattr(i, "duration", 0) for i in measure.sequence)
        for qubit in backend_v1._properties._qubits.values():
            qubit["readout_length"] = (readout_length * backend_v1.configuration().dt, qubit["readout_error"][1])
        super().__init__(backend_v1, add_delay=True)
        self._defaults = backend_v1._defaults

but I still had further errors. The next one that needed to be addressed was that u2 and id were in the basis gates but not in the properties which led to an error in a transpiler pass.

At that point, I decided it was more trouble than it was worth to get this working. Maybe BackendConverter should be more robust, but likely FakeOpenPulse2Q just does not have complete enough metadata to match what is expected of a BackendV1 instance. We could fix that but likely it would be better just to make a BackendV2 equivalent class directly in terra.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Will. This is good to know. I agree we should have Fake V2 in Terra in future.

"""BackendV2 version of FakeOpenPulse2Q"""

def __init__(self):
# FakeOpenPulse2Q has its data hard-coded rather than in json files. We
# prepopulate the dicts so that FakeBackendV2 does not try to load them
# from files.
#
# We have to use a hack to populate _conf_dict
backend_v1 = FakeOpenPulse2Q()
self._conf_dict_real = backend_v1._configuration.to_dict()
super().__init__()
self._props_dict = backend_v1._properties.to_dict()
self._defs_dict = backend_v1._defaults.to_dict()
self._defaults = backend_v1._defaults

# Workaround a bug in FakeOpenPulse2Q. It defines u1 on qubit 1 in the
# cmd_def in the defaults json file but not in the gates in the
wshanks marked this conversation as resolved.
Show resolved Hide resolved
# properties json. The code FakeBackendV2 uses to build the Target
wshanks marked this conversation as resolved.
Show resolved Hide resolved
# assumes these two are consistent.
u1_0 = next(g for g in self._props_dict["gates"] if g["gate"] == "u1")
self._props_dict["gates"].append(u1_0.copy())
self._props_dict["gates"][-1]["qubits"] = [1]

@property
def _conf_dict(self):
# FakeBackendV2 sets this in __init__. As a hack, we use this property
# to prevent it from overriding our values.
return self._conf_dict_real

@_conf_dict.setter
def _conf_dict(self, value):
pass

# This method is not defined in the base class as we would like to avoid
# relying on it as much as necessary. Individual tests should add it when
# necessary.
# def defaults(self):
# """Pulse defaults"""
# return self._defaults


class MockRestlessBackend(FakeOpenPulse2QV2):
"""An abstract backend for testing that can mock restless data."""

def __init__(self, rng_seed: int = 0):
Expand Down Expand Up @@ -145,7 +189,8 @@ def __init__(
self._angle_per_gate = angle_per_gate
super().__init__(rng_seed=rng_seed)

self.configuration().basis_gates.extend(["sx", "x"])
self.target.add_instruction(SXGate(), properties={(0,): None})
self.target.add_instruction(XGate(), properties={(0,): None})

def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]):
"""Compute the probabilities of being in the excited state or
Expand All @@ -171,7 +216,7 @@ def _compute_outcome_probabilities(self, circuits: List[QuantumCircuit]):
self._precomputed_probabilities[(idx, "01")] = [prob_1, prob_0, 0, 0]


class MockIQBackend(FakeOpenPulse2Q):
class MockIQBackend(FakeOpenPulse2QV2):
"""A mock backend for testing with IQ data."""

def __init__(
Expand Down
10 changes: 10 additions & 0 deletions releasenotes/notes/calibration-backendv2-e564f466eb1c9999.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
- |
Updated querying of :class:`~qiskit.providers.Backend` metadata to support
the look up of qubit T1 and measurement drive frequency, in order to
support :class:`~qiskit.providers.BackendV2` backends. The look up of the
latter is ``qiskit-ibm-provider`` specific. This change fixes errors
failing to find these properties when using :class:`.ResonatorSpectroscopy`
(issue `#1099 <https://github.com/Qiskit/qiskit-experiments/issues/1099>_`)
and when using restless measurements with ``BackendV2`` backends.
35 changes: 3 additions & 32 deletions test/calibration/test_update_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,17 @@
from test.base import QiskitExperimentsTestCase
import numpy as np

from qiskit import pulse
from qiskit.circuit import Parameter
from qiskit.qobj.utils import MeasLevel
from qiskit.providers.fake_provider import FakeAthensV2
from qiskit.qobj.utils import MeasLevel

from qiskit_experiments.framework import BackendData
from qiskit_experiments.library import QubitSpectroscopy
from qiskit_experiments.calibration_management.calibrations import Calibrations
from qiskit_experiments.calibration_management.update_library import Frequency
from qiskit_experiments.test.mock_iq_backend import MockIQBackend
from qiskit_experiments.test.mock_iq_helpers import MockIQSpectroscopyHelper as SpectroscopyHelper


class TestAmplitudeUpdate(QiskitExperimentsTestCase):
"""Test the update functions in the update library."""

def setUp(self):
"""Setup amplitude values."""
super().setUp()
self.cals = Calibrations(coupling_map=[])
self.qubit = 1

axp = Parameter("amp")
chan = Parameter("ch0")
with pulse.build(name="xp") as xp:
pulse.play(pulse.Gaussian(duration=160, amp=axp, sigma=40), pulse.DriveChannel(chan))

ax90p = Parameter("amp")
with pulse.build(name="x90p") as x90p:
pulse.play(pulse.Gaussian(duration=160, amp=ax90p, sigma=40), pulse.DriveChannel(chan))

self.x90p = x90p

self.cals.add_schedule(xp, num_qubits=1)
self.cals.add_schedule(x90p, num_qubits=1)
self.cals.add_parameter_value(0.2, "amp", self.qubit, "xp")
self.cals.add_parameter_value(0.1, "amp", self.qubit, "x90p")


class TestFrequencyUpdate(QiskitExperimentsTestCase):
"""Test the frequency update function in the update library."""

Expand All @@ -67,10 +40,8 @@ def test_frequency(self):
iq_cluster_width=[0.2],
),
)
backend._configuration.basis_gates = ["x"]
backend._configuration.timing_constraints = {"granularity": 16}

freq01 = backend.defaults().qubit_freq_est[qubit]
freq01 = BackendData(backend).drive_freqs[qubit]
frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21)

spec = QubitSpectroscopy([qubit], frequencies)
Expand Down
12 changes: 8 additions & 4 deletions test/library/calibration/test_fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ddt import ddt, data

from qiskit import pulse, transpile
from qiskit.circuit import Gate
from qiskit.circuit.library import XGate, SXGate
from qiskit.pulse import DriveChannel, Drag

Expand All @@ -41,10 +42,11 @@ def test_end_to_end_under_rotation(self, pi_ratio):
"""Test the experiment end to end."""

amp_exp = FineXAmplitude([0])
amp_exp.set_transpile_options(basis_gates=["x", "sx"])

error = -np.pi * pi_ratio
backend = MockIQBackend(FineAmpHelper(error, np.pi, "x"))
backend.target.add_instruction(XGate(), properties={(0,): None})
backend.target.add_instruction(SXGate(), properties={(0,): None})

expdata = amp_exp.run(backend)
self.assertExperimentDone(expdata)
Expand All @@ -61,10 +63,11 @@ def test_end_to_end_over_rotation(self, pi_ratio):
"""Test the experiment end to end."""

amp_exp = FineXAmplitude([0])
amp_exp.set_transpile_options(basis_gates=["x", "sx"])

error = np.pi * pi_ratio
backend = MockIQBackend(FineAmpHelper(error, np.pi, "x"))
backend.target.add_instruction(XGate(), properties={(0,): None})
backend.target.add_instruction(SXGate(), properties={(0,): None})
expdata = amp_exp.run(backend)
self.assertExperimentDone(expdata)
result = expdata.analysis_results(1)
Expand All @@ -87,6 +90,7 @@ def test_end_to_end(self, pi_ratio):
error = -np.pi * pi_ratio
amp_exp = FineZXAmplitude((0, 1))
backend = MockIQBackend(FineAmpHelper(error, np.pi / 2, "szx"))
backend.target.add_instruction(Gate("szx", 2, []), properties={(0, 1): None})

expdata = amp_exp.run(backend)
self.assertExperimentDone(expdata)
Expand Down Expand Up @@ -207,8 +211,8 @@ def setUp(self):
library = FixedFrequencyTransmon()

self.backend = MockIQBackend(FineAmpHelper(-np.pi * 0.07, np.pi, "xp"))
self.backend.configuration().basis_gates.append("sx")
self.backend.configuration().basis_gates.append("x")
self.backend.target.add_instruction(SXGate(), properties={(0,): None})
self.backend.target.add_instruction(XGate(), properties={(0,): None})
self.cals = Calibrations.from_backend(self.backend, libraries=[library])

def test_cal_options(self):
Expand Down
8 changes: 4 additions & 4 deletions test/library/calibration/test_fine_frequency.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
FineFrequency,
FineFrequencyCal,
)
from qiskit_experiments.framework import BackendData
from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon
from qiskit_experiments.calibration_management import Calibrations
from qiskit_experiments.framework import BackendData
from qiskit_experiments.test.mock_iq_backend import MockIQBackend
from qiskit_experiments.test.mock_iq_helpers import MockIQFineFreqHelper as FineFreqHelper

Expand Down Expand Up @@ -53,7 +53,7 @@ def test_end_to_end(self, freq_shift):
"""Test the experiment end to end."""
exp_helper = FineFreqHelper(sx_duration=self.sx_duration, freq_shift=freq_shift)
backend = MockIQBackend(exp_helper)
exp_helper.dt = backend.configuration().dt
exp_helper.dt = BackendData(backend).dt

freq_exp = FineFrequency([0], 160, backend)
freq_exp.set_transpile_options(inst_map=self.inst_map)
Expand All @@ -62,7 +62,7 @@ def test_end_to_end(self, freq_shift):
self.assertExperimentDone(expdata)
result = expdata.analysis_results(1)
d_theta = result.value.n
dt = backend.configuration().dt
dt = BackendData(backend).dt
d_freq = d_theta / (2 * np.pi * self.sx_duration * dt)

tol = 0.01e6
Expand All @@ -75,7 +75,7 @@ def test_calibration_version(self):

exp_helper = FineFreqHelper(sx_duration=self.sx_duration, freq_shift=0.1e6)
backend = MockIQBackend(exp_helper)
exp_helper.dt = backend.configuration().dt
exp_helper.dt = BackendData(backend).dt

fine_freq = FineFrequencyCal([0], self.cals, backend)
armonk_freq = BackendData(FakeArmonkV2()).drive_freqs[0]
Expand Down
2 changes: 1 addition & 1 deletion test/library/calibration/test_ramsey_xy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from qiskit_experiments.calibration_management.calibrations import Calibrations
from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon
from qiskit_experiments.framework import BaseAnalysis, AnalysisStatus, BackendData
from qiskit_experiments.framework import AnalysisStatus, BackendData, BaseAnalysis
from qiskit_experiments.library import RamseyXY, FrequencyCal
from qiskit_experiments.test.mock_iq_backend import MockIQBackend
from qiskit_experiments.test.mock_iq_helpers import MockIQRamseyXYHelper as RamseyXYHelper
Expand Down
Loading