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

Backend v2 basic compatibility #843

Merged
merged 63 commits into from
Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
b2c9c07
Basic fixes enabling running of randomized benchmarking experiment
gadial Jul 6, 2022
bd6129a
Beginning a switch to V2 backends in tests
gadial Jul 10, 2022
82bdd4f
Added failing test due to handling of control channels in calibrations
gadial Jul 13, 2022
3d8cb82
Control channel and coupling map working for backendv2 (requires Terr…
gadial Jul 13, 2022
bc9eaf7
Tests for existance of `drive_freq` and `meas_freq`
gadial Jul 13, 2022
f00a03f
Test ramsey xy converted to fake v2 backend; known problem with calib…
gadial Jul 14, 2022
0b8ddb1
Readout error tests converted to V2; raises a bug in composite experi…
gadial Jul 14, 2022
ee37de3
Fix to parallel experiment to correctly get coupling map for v2 backends
gadial Jul 14, 2022
7df5715
Merge branch 'main' into backend_v2_basic_compatibility
gadial Jul 18, 2022
c321e62
Finishing the merge with the new service PR
gadial Jul 18, 2022
72a0520
Fixes to experiment_data to handle backend v2
gadial Jul 18, 2022
bbfd6cb
Switch RB tests to backend v2
gadial Jul 18, 2022
d6746f2
Switched fine drag experiment to v2 - no problems
gadial Jul 18, 2022
1353d5e
Switched fine frequency experiment to v2 - should not work yet
gadial Jul 18, 2022
ff1cd58
Getting qubit frequency from backend v2 but dropping measurement freq…
gadial Jul 21, 2022
c61d2af
Linting
gadial Jul 21, 2022
742bce5
Merge branch 'main' into backend_v2_basic_compatibility
gadial Jul 21, 2022
c759b0a
Merge branch 'main' into backend_v2_basic_compatibility
gadial Jul 24, 2022
5357394
Cross resonance tests are now working with v2; starting to switch all…
gadial Jul 25, 2022
e347bce
Switching to `BackendData` usage
gadial Jul 25, 2022
0050f08
Switching to `BackendData` usage
gadial Jul 25, 2022
457f317
Moving more experiments to v2
gadial Jul 25, 2022
4ef8b0e
Linting
gadial Jul 25, 2022
531d79b
Further fixes in calibrations
gadial Jul 25, 2022
e4d4b4f
Fixes in test_ramsey_xy.py
gadial Jul 25, 2022
8bf2834
Fix in test_fine_frequency.py
gadial Jul 25, 2022
ce5e957
Linting
gadial Jul 25, 2022
b91d413
Fixing noisy delay backendv1 reference and added a failing test for t…
gadial Jul 27, 2022
a7484fb
Non-fixable usage in restless mixin
gadial Jul 27, 2022
a3f4ccf
Added test_circuit_with_backend to t1 tests and added the relevant fixes
gadial Jul 28, 2022
696029c
Removed granularity setting and fixed the tests accordingly
gadial Jul 28, 2022
6d37794
Default granularity is 1
gadial Jul 28, 2022
aede51c
dt default value 1
gadial Jul 28, 2022
637a3e2
Added getters for other target fields
gadial Jul 28, 2022
700f4aa
refactored max_experiments to max_circuits
gadial Jul 28, 2022
3c3ae0e
drive_freq and meas_freq name refactor
gadial Jul 28, 2022
a88310b
Refactored BackendData to be a standard class
gadial Jul 28, 2022
70e72da
Hack to enable working with BackendV2 until Terra gives native access…
gadial Jul 28, 2022
a991a5f
Linting
gadial Jul 28, 2022
ddfe164
Switch to is_simulator for other experiments
gadial Jul 28, 2022
7c03484
Fix additional usages of backendv1
gadial Jul 28, 2022
ce7d7c9
Linting
gadial Jul 28, 2022
ac145da
Returning default dt leads to failing tests, so disable for now
gadial Jul 28, 2022
4e859db
resonator_spectroscopy.py fixes
gadial Jul 28, 2022
9d55456
default() usage fix in qubit_spectroscopy.py
gadial Jul 28, 2022
bdce7ad
drive_freq fix
gadial Jul 28, 2022
ba81c63
Catch NotImplementedError
gadial Jul 28, 2022
d227163
Fixed pulse data parsing
gadial Jul 28, 2022
27ab9f3
backend name access fix
gadial Jul 28, 2022
9936c57
Lint
gadial Jul 28, 2022
4032962
Update qiskit_experiments/framework/backend_data.py
gadial Jul 31, 2022
dec211d
Update qiskit_experiments/framework/backend_data.py
gadial Jul 31, 2022
5a04917
Typo fix
gadial Aug 1, 2022
0225eeb
Release note
gadial Aug 1, 2022
11c3461
Update qiskit-ibm-experiment version
gadial Aug 1, 2022
683ab07
Extended note about the problem with `is_simulator` in `BackendV2`
gadial Aug 2, 2022
1ce5d4d
Merge branch 'main' into backend_v2_basic_compatibility
gadial Aug 3, 2022
8416303
Update terra version
gadial Aug 3, 2022
a30eeb3
Remove `control_channels` from `BackendData` to avoid exposing inner …
gadial Aug 3, 2022
5100fe7
Added additional support for `drive_channel`, `measure_channel` and `…
gadial Aug 3, 2022
2dfed83
Linting
gadial Aug 3, 2022
b91e74b
control_channel now relies on the backends control_channel method whe…
gadial Aug 3, 2022
1fa5284
More robust code for now
gadial Aug 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions qiskit_experiments/calibration_management/calibrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
)
from qiskit.pulse.channels import PulseChannel
from qiskit.circuit import Parameter, ParameterExpression
from qiskit.providers.backend import BackendV1 as Backend
from qiskit.providers.backend import Backend

from qiskit_experiments.exceptions import CalibrationError
from qiskit_experiments.calibration_management.basis_gate_library import BasisGateLibrary
Expand All @@ -49,6 +49,7 @@
ParameterValueType,
ScheduleKey,
)
from qiskit_experiments.framework import BackendData


class Calibrations:
Expand Down Expand Up @@ -257,26 +258,23 @@ def from_backend(
Returns:
An instance of Calibrations instantiated from a backend.
"""
if hasattr(backend, "name") and hasattr(backend.name, "__call__"):
backend_name = backend.name()
else:
backend_name = None
backend_data = BackendData(backend)

cals = Calibrations(
getattr(backend.configuration(), "coupling_map", []),
getattr(backend.configuration(), "control_channels", None),
backend_data.coupling_map,
backend_data.control_channels,
library,
libraries,
add_parameter_defaults,
backend_name,
getattr(backend, "version", None),
backend_data.name,
backend_data.version,
)

if add_parameter_defaults:
for qubit, freq in enumerate(getattr(backend.defaults(), "qubit_freq_est", [])):
for qubit, freq in enumerate(backend_data.drive_freqs):
cals.add_parameter_value(freq, cals.drive_freq, qubit, update_inst_map=False)

for meas, freq in enumerate(getattr(backend.defaults(), "meas_freq_est", [])):
for meas, freq in enumerate(backend_data.meas_freqs):
cals.add_parameter_value(freq, cals.meas_freq, meas, update_inst_map=False)

# Update the instruction schedule map after adding all parameter values.
Expand Down
1 change: 1 addition & 0 deletions qiskit_experiments/framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
.. _create-experiment:
"""
from qiskit.providers.options import Options
from qiskit_experiments.framework.backend_data import BackendData
from qiskit_experiments.framework.analysis_result import AnalysisResult
from qiskit_experiments.framework.experiment_data import (
ExperimentStatus,
Expand Down
210 changes: 210 additions & 0 deletions qiskit_experiments/framework/backend_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Backend data access helper class

Since `BackendV1` and `BackendV2` do not share the same interface, this
class unifies data access for various data fields.
"""
from qiskit.providers.models import PulseBackendConfiguration
from qiskit.providers import BackendV1, BackendV2
from qiskit.providers.fake_provider import fake_backend, FakeBackendV2, FakeBackend


class BackendData:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is just using a class as a namespace, so I don't see any fundamental difference from defining a set of functions. I would turn this into an actual class with constructor taking a backend, so that you can store this backend configuration as an experiment or run options, i.e. note that currently we cannot serialize backend and thus hard-coded run/transpile options in a backend object are all discarded in the loaded experiment.

Perhaps it's useful to override the base experiment _set_backend method to internally generate this data and set the data instance to the experiment instance. Then all experiment subclasses can easily access these configuration data, e.g.

self._backend_data.granularity

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the suggestion of making this a full class that takes a backend as a constructor argument and then making most of the methods into properties. Setting it up from _set_backend makes sense as well.

Making it serializable is also nice, though I don't know if it is worth it to serialize and attach the backend data to each experiment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For now we don't attach anything to experiment instance IIRC. These will be separately saved as an artifact.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I'll do it as the last touch on the PR, in case we get more design ideas by then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, done.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is really nice to have this compatibility layer for transitioning to BackendV2. I wonder what is the life cycle of this class? Does it provide enough utility that we keep it after BackendV1 is deprecated? If not, should we try to keep the methods here as close to BackendV2 as possible so that in the future we could swap BackendData usage out for direct access of the BackendV2 instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My hope is that after deprecating BackendV1 we can stop using this class, but that's actually the reason I preferred to use static functions called with explicitly using the BackendData name, so it'll be easier to refactor this out. However, I can also try the _backend_data approach; if all goes well, the references to this field could be changed with references to the backend itself.

"""Class for providing joint interface for accessing backend data"""

def __init__(self, backend):
"""Inits the backend and verifies version"""
self._backend = backend
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think backend can be discarded after parsing all necessary data. I like _parse_additional_data logic. Can you do the same for other fields and remove self._v1 and self._v2? Having these member may indicate this object is "sensitive" to backend version, but the aim of this object is to return the same data regardless of backend version.

You can write a dispatcher to do it cleanly. Actually functools provides a method dispatcher but available only > 3.8. Using this pattern makes this object robust to future update of backend, i.e. V3. You can write backend parser with dispatcher and save parsed values as protected members. Then you can provide property methods to indicate these are kind of static values.

Copy link
Collaborator

@wshanks wshanks Jul 29, 2022

Choose a reason for hiding this comment

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

I like the idea of pulling out all the data in __init__ so that the other methods can be common to V1 and V2. One question though -- would we need to use this compatibility class for any property of the backend that is allowed to be modified? Nothing in the class currently should be modified. max_circuits is an edge case which we have previously decided can be changed to help with job splitting: #638 (comment). However, modifying max_experiments like that comment describes is a hack according to this comment: Qiskit/qiskit-ibm-provider#361 (comment) (but it's the only way in qiskit-experiments to get a job split on a smaller number of circuits with BackendV1; with V2 the max_circuits_per_job run option can be used -- actually the provider supports it but I think BaseExperiment needs to be updated to support it as well).

Besides max_circuits/max_experiments, is there anything else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am very hesitant to accept the parsing solution. The backend comes from an outside source, and I don't know if and when it might be changed. I think of BackendData as a mediator class, which hides the differences between v1 and v2, not as something that somehow enables us to forget about the backend.

I considered using dispatcher but for now I actually prefer to keep the code as explicit as possible - I don't think it's large enough to drive us into using more abstract code.

Copy link
Collaborator

@nkanazawa1989 nkanazawa1989 Aug 1, 2022

Choose a reason for hiding this comment

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

Fair enough, but I don't understand why

I don't know if and when it might be changed

this becomes a problem for parsing. Actually backend is bit weird object because it is basically "configuration" + "interface for job submission". So current code allows us to write

my_expr._backend_data._backend.run(...)

though it is not expected to call protected members (I meant, this is no longer "data", but this is not strong objection).

self._v1 = isinstance(backend, BackendV1)
self._v2 = isinstance(backend, BackendV2)
if self._v2:
self._parse_additional_data()

def _parse_additional_data(self):
# data specific parsing not done yet in qiskit-terra
if self._backend._conf_dict["open_pulse"]:
if "u_channel_lo" not in self._backend._conf_dict:
self._backend._conf_dict["u_channel_lo"] = [] # to avoid terra bug
self._pulse_conf = PulseBackendConfiguration.from_dict(self._backend._conf_dict)

@property
def name(self):
"""Returns the backend name"""
if self._v1:
return self._backend.name()
elif self._v2:
return self._backend.name
return str(self._backend)

def control_channel(self, qubits):
"""Returns the backend control channels"""
if self._v1:
return self._backend.configuration().control(qubits)
elif self._v2:
try:
return self._backend.control_channel(qubits)
except (AttributeError, NotImplementedError):
return self._pulse_conf.control_channels[qubits]
return None

@property
def granularity(self):
"""Returns the backend's time constraint granularity"""
try:
if self._v1:
return self._backend.configuration().timing_constraints.get("granularity", 1)
elif self._v2:
return self._backend.target.granularity
except AttributeError:
return 1
return 1

@property
def min_length(self):
"""Returns the backend's time constraint minimum duration"""
try:
if self._v1:
return self._backend.configuration().timing_constraints.get("min_length", 0)
elif self._v2:
return self._backend.target.min_length
except AttributeError:
return 0
return 0

@property
def pulse_alignment(self):
"""Returns the backend's time constraint pulse alignment"""
try:
if self._v1:
return self._backend.configuration().timing_constraints.get("pulse_alignment", 1)
elif self._v2:
return self._backend.target.pulse_alignment
except AttributeError:
return 1
return 1

@property
def aquire_alignment(self):
"""Returns the backend's time constraint acquire alignment"""
try:
if self._v1:
return self._backend.configuration().timing_constraints.get("aquire_alignment", 1)
elif self._v2:
return self._backend.target.aquire_alignment
except AttributeError:
gadial marked this conversation as resolved.
Show resolved Hide resolved
return 1
return 1

@property
def dt(self):
"""Returns the backend's input time resolution"""
if self._v1:
try:
return self._backend.configuration().dt
except AttributeError:
return None
elif self._v2:
return self._backend.dt
return None
gadial marked this conversation as resolved.
Show resolved Hide resolved

@property
def max_circuits(self):
"""Returns the backend's max experiments value"""
if self._v1:
return getattr(self._backend.configuration(), "max_experiments", None)
elif self._v2:
return self._backend.max_circuits
return None

@property
def coupling_map(self):
"""Returns the backend's coupling map"""
if self._v1:
return getattr(self._backend.configuration(), "coupling_map", [])
elif self._v2:
return list(self._backend.coupling_map.get_edges())
return []

@property
def control_channels(self):
"""Returns the backend's control channels"""
if self._v1:
return getattr(self._backend.configuration(), "control_channels", None)
elif self._v2:
try:
return self._backend.control_channels
except AttributeError:
return self._pulse_conf.control_channels
return None

@property
def version(self):
"""Returns the backend's version"""
if self._v1:
return getattr(self._backend, "version", None)
elif self._v2:
return self._backend.version
return None

@property
def provider(self):
"""Returns the backend's provider"""
if self._v1:
return getattr(self._backend, "provider", None)
elif self._v2:
return self._backend.provider
return None

@property
def drive_freqs(self):
"""Returns the backend's qubit frequency estimation"""
gadial marked this conversation as resolved.
Show resolved Hide resolved
if self._v1:
return getattr(self._backend.defaults(), "qubit_freq_est", [])
elif self._v2:
return [property.frequency for property in self._backend.target.qubit_properties]
return []

@property
def meas_freqs(self):
"""Returns the backend's measurement frequency estimation.
Note: currently BackendV2 does not have access to this data"""
gadial marked this conversation as resolved.
Show resolved Hide resolved
if self._v1:
return getattr(self._backend.defaults(), "meas_freq_est", [])
elif self._v2:
# meas_freq_est is currently not part of the BackendV2
return []
return []

@property
def num_qubits(self):
"""Returns the backend's number of qubits"""
if self._v1:
return self._backend.configuration().num_qubits
elif self._v2:
# meas_freq_est is currently not part of the BackendV2
return self._backend.num_qubits
return None

@property
def is_simulator(self):
"""Returns True given an indication the backend is a simulator
Note: for `BackendV2` we sometimes cannot be sure"""
gadial marked this conversation as resolved.
Show resolved Hide resolved
if self._v1:
if self._backend.configuration().simulator or isinstance(self._backend, FakeBackend):
return True
if self._v2:
if isinstance(self._backend, (FakeBackendV2, fake_backend.FakeBackendV2)):
return True

return False
8 changes: 5 additions & 3 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from qiskit.exceptions import QiskitError
from qiskit.qobj.utils import MeasLevel
from qiskit.providers.options import Options
from qiskit_experiments.framework import BackendData
from qiskit_experiments.framework.store_init_args import StoreInitArgs
from qiskit_experiments.framework.base_analysis import BaseAnalysis
from qiskit_experiments.framework.experiment_data import ExperimentData
Expand Down Expand Up @@ -140,6 +141,7 @@ def _set_backend(self, backend: Backend):
properties from the supplied backend if required.
"""
self._backend = backend
self._backend_data = BackendData(backend)

def copy(self) -> "BaseExperiment":
"""Return a copy of the experiment"""
Expand Down Expand Up @@ -331,11 +333,11 @@ def _finalize(self):
def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]:
"""Run circuits on backend as 1 or more jobs."""
# Run experiment jobs
max_experiments = getattr(self.backend.configuration(), "max_experiments", None)
if max_experiments and len(circuits) > max_experiments:
max_circuits = self._backend_data.max_circuits
if max_circuits and len(circuits) > max_circuits:
# Split jobs for backends that have a maximum job size
job_circuits = [
circuits[i : i + max_experiments] for i in range(0, len(circuits), max_experiments)
circuits[i : i + max_circuits] for i in range(0, len(circuits), max_circuits)
]
else:
# Run as single job
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def _combined_circuits(self, device_layout: bool) -> List[QuantumCircuit]:
# Work around for backend coupling map circuit inflation
coupling_map = getattr(self.transpile_options, "coupling_map", None)
if coupling_map is None and self.backend:
coupling_map = self.backend.configuration().coupling_map
coupling_map = self._backend_data.coupling_map
if coupling_map is not None:
num_qubits = 1 + max(*self.physical_qubits, np.max(coupling_map))
else:
Expand Down
61 changes: 31 additions & 30 deletions qiskit_experiments/framework/experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ThreadSafeList,
)
from qiskit_experiments.framework.analysis_result import AnalysisResult
from qiskit_experiments.framework import BackendData
from qiskit_experiments.database_service.exceptions import (
ExperimentDataError,
ExperimentEntryNotFound,
Expand Down Expand Up @@ -535,31 +536,28 @@ def _set_backend(self, new_backend: Backend, recursive: bool = True) -> None:
# defined independently from the setter to enable setting without autosave

self._backend = new_backend
if hasattr(new_backend, "name"):
self._db_data.backend = new_backend.name()
else:
self._backend_data = BackendData(new_backend)
self._db_data.backend = self._backend_data.name
if self._db_data.backend is None:
self._db_data.backend = str(new_backend)
if hasattr(new_backend, "provider"):
self._set_hgp_from_backend()
provider = self._backend_data.provider
if provider is not None:
self._set_hgp_from_provider(provider)
if recursive:
for data in self.child_data():
data._set_backend(new_backend)

def _set_hgp_from_backend(self):
if self.backend is not None and self.backend.provider() is not None:
try:
creds = self.backend.provider().credentials
hub = self._db_data.hub or creds.hub
group = self._db_data.group or creds.group
project = self._db_data.project or creds.project
self._db_data.hub = hub
self._db_data.group = group
self._db_data.project = project
except AttributeError:
LOG.warning(
"Unable to set hub/group/project backend %s ",
self.backend,
)
def _set_hgp_from_provider(self, provider):
try:
creds = provider.credentials
hub = self._db_data.hub or creds.hub
group = self._db_data.group or creds.group
project = self._db_data.project or creds.project
self._db_data.hub = hub
self._db_data.group = group
self._db_data.project = project
except AttributeError:
return

def _clear_results(self):
"""Delete all currently stored analysis results and figures"""
Expand Down Expand Up @@ -719,18 +717,21 @@ def add_jobs(
# Add futures for extracting finished job data
timeout_ids = []
for job in jobs:
jid = job.job_id()
if self.backend is not None and self.backend.name() != job.backend().name():
LOG.warning(
"Adding a job from a backend (%s) that is different "
"than the current backend (%s). "
"The new backend will be used, but "
"service is not changed if one already exists.",
job.backend(),
self.backend,
)
if self.backend is not None:
backend_name = BackendData(self.backend).name
job_backend_name = BackendData(job.backend()).name
if self.backend and backend_name != job_backend_name:
LOG.warning(
"Adding a job from a backend (%s) that is different "
"than the current backend (%s). "
"The new backend will be used, but "
"service is not changed if one already exists.",
job.backend(),
self.backend,
)
self.backend = job.backend()

jid = job.job_id()
if jid in self._jobs:
LOG.warning(
"Skipping duplicate job, a job with this ID already exists [Job ID: %s]", jid
Expand Down
Loading