-
Notifications
You must be signed in to change notification settings - Fork 16
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
✨ Added free IBM hardware (kyiv
, brisbane
, sherbrooke
) to CLI
#381
base: main
Are you sure you want to change the base?
Changes from all commits
51798a2
1e24499
bd833ef
238b51b
1b09b1b
5108263
fc56294
0a9c027
8149136
6ef47b9
398a563
b3fc240
893d5a5
c3638aa
837992e
29d5f1d
ecb09c9
604bbc4
c6dfc74
a0a5f83
fda5425
90fbd8d
ef414ec
b74578a
057ee1a
881207a
38b04e5
094950f
072be89
d2a2ce0
40ed22b
cbf8c54
566d470
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another high-level comment: At the moment, we are storing kind of proprietary calibration files here. And we store them differently for every provider. It would be great to unify that hardware description format for all the devices here. Which would also allow to share a lot of the code for parsing the calibration files. (This could be converted to an issue to tackle separately) |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This duplicates quite a lot of the code from the One possible idea in that regard would be to kind of refactor the device handling to remove the (Given the scope of this request, this might also be better tackled in a separate follow-up issue. However, at least some kind of de-duplication would be nice. Maybe that also helps with getting a bit better coverage) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
"""Module to manage open-access IBM devices.""" | ||
|
||
from __future__ import annotations | ||
|
||
import json | ||
from typing import TYPE_CHECKING, TypedDict, cast | ||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
from qiskit.providers.models import BackendProperties | ||
from qiskit.transpiler import Target | ||
|
||
from mqt.bench.devices import Device, DeviceCalibration, Provider | ||
|
||
|
||
class QubitProperties(TypedDict): | ||
"""Class to store the properties of a single qubit.""" | ||
|
||
T1: float # us | ||
T2: float # us | ||
eRO: float | ||
tRO: float # ns | ||
eID: float | ||
eSX: float | ||
eX: float | ||
eECR: dict[str, float] | ||
tECR: dict[str, float] # ns | ||
|
||
|
||
class IBMOpenAccessCalibration(TypedDict): | ||
"""Class to store the calibration data of an open-access IBM device.""" | ||
|
||
name: str | ||
basis_gates: list[str] | ||
num_qubits: int | ||
connectivity: list[list[int]] | ||
properties: dict[str, QubitProperties] | ||
|
||
|
||
class IBMOpenAccessProvider(Provider): | ||
"""Class to manage open-access IBM devices.""" | ||
|
||
provider_name = "ibm_open_access" | ||
|
||
@classmethod | ||
def get_available_device_names(cls) -> list[str]: | ||
"""Get the names of all available open-access IBM devices.""" | ||
return ["ibm_kyiv", "ibm_brisbane", "ibm_sherbrooke"] # NOTE: update when adding new devices | ||
|
||
@classmethod | ||
def get_native_gates(cls) -> list[str]: | ||
"""Get a list of provider specific native gates.""" | ||
return ["id", "rz", "sx", "x", "ecr", "measure", "barrier"] # ibm_kyiv, ibm_brisbane, ibm_sherbrooke | ||
|
||
@classmethod | ||
def import_backend(cls, path: Path) -> Device: | ||
"""Import an open-access IBM backend. | ||
|
||
Arguments: | ||
path: the path to the JSON file containing the calibration data. | ||
|
||
Returns: the Device object | ||
""" | ||
with path.open() as json_file: | ||
open_access_ibm_calibration = cast(IBMOpenAccessCalibration, json.load(json_file)) | ||
|
||
device = Device() | ||
device.name = open_access_ibm_calibration["name"] | ||
device.num_qubits = open_access_ibm_calibration["num_qubits"] | ||
device.basis_gates = open_access_ibm_calibration["basis_gates"] | ||
device.coupling_map = list(open_access_ibm_calibration["connectivity"]) | ||
|
||
calibration = DeviceCalibration() | ||
for qubit in range(device.num_qubits): | ||
calibration.single_qubit_gate_fidelity[qubit] = { | ||
"id": 1 - open_access_ibm_calibration["properties"][str(qubit)]["eID"], | ||
"rz": 1, # rz is always perfect | ||
"sx": 1 - open_access_ibm_calibration["properties"][str(qubit)]["eSX"], | ||
"x": 1 - open_access_ibm_calibration["properties"][str(qubit)]["eX"], | ||
} | ||
calibration.readout_fidelity[qubit] = 1 - open_access_ibm_calibration["properties"][str(qubit)]["eRO"] | ||
# data in nanoseconds, convert to SI unit (seconds) | ||
calibration.readout_duration[qubit] = open_access_ibm_calibration["properties"][str(qubit)]["tRO"] * 1e-9 | ||
# data in microseconds, convert to SI unit (seconds) | ||
calibration.t1[qubit] = open_access_ibm_calibration["properties"][str(qubit)]["T1"] * 1e-6 | ||
calibration.t2[qubit] = open_access_ibm_calibration["properties"][str(qubit)]["T2"] * 1e-6 | ||
|
||
for qubit1, qubit2 in device.coupling_map: | ||
edge = f"{qubit1}_{qubit2}" | ||
|
||
error = open_access_ibm_calibration["properties"][str(qubit1)]["eECR"][edge] | ||
calibration.two_qubit_gate_fidelity[qubit1, qubit2] = {"ecr": 1 - error} | ||
|
||
# data in nanoseconds, convert to SI unit (seconds) | ||
duration = open_access_ibm_calibration["properties"][str(qubit1)]["eECR"][edge] * 1e-9 | ||
calibration.two_qubit_gate_duration[qubit1, qubit2] = {"ecr": duration} | ||
|
||
device.calibration = calibration | ||
return device | ||
|
||
@classmethod | ||
def __import_backend_properties(cls, backend_properties: BackendProperties) -> DeviceCalibration: | ||
"""Import calibration data from a Qiskit `BackendProperties` object. | ||
|
||
Arguments: | ||
backend_properties: the Qiskit `BackendProperties` object. | ||
|
||
Returns: Collection of calibration data | ||
""" | ||
calibration = DeviceCalibration() | ||
num_qubits = len(backend_properties.qubits) | ||
|
||
for qubit in range(num_qubits): | ||
calibration.t1[qubit] = cast(float, backend_properties.t1(qubit)) | ||
calibration.t2[qubit] = cast(float, backend_properties.t2(qubit)) | ||
calibration.readout_fidelity[qubit] = 1 - cast(float, backend_properties.readout_error(qubit)) | ||
calibration.readout_duration[qubit] = cast(float, backend_properties.readout_length(qubit)) | ||
|
||
calibration.single_qubit_gate_fidelity = {qubit: {} for qubit in range(num_qubits)} | ||
calibration.single_qubit_gate_duration = {qubit: {} for qubit in range(num_qubits)} | ||
for gate in backend_properties.gates: | ||
# Skip `reset` gate as its error information is not exposed. | ||
if gate.gate == "reset": | ||
continue | ||
|
||
error: float = backend_properties.gate_error(gate.gate, gate.qubits) | ||
duration: float = backend_properties.gate_length(gate.gate, gate.qubits) | ||
if len(gate.qubits) == 1: | ||
qubit = gate.qubits[0] | ||
calibration.single_qubit_gate_fidelity[qubit][gate.gate] = 1 - error | ||
calibration.single_qubit_gate_duration[qubit][gate.gate] = duration | ||
elif len(gate.qubits) == 2: | ||
qubit1, qubit2 = gate.qubits | ||
if (qubit1, qubit2) not in calibration.two_qubit_gate_fidelity: | ||
calibration.two_qubit_gate_fidelity[qubit1, qubit2] = {} | ||
calibration.two_qubit_gate_fidelity[qubit1, qubit2][gate.gate] = 1 - error | ||
|
||
if (qubit1, qubit2) not in calibration.two_qubit_gate_duration: | ||
calibration.two_qubit_gate_duration[qubit1, qubit2] = {} | ||
calibration.two_qubit_gate_duration[qubit1, qubit2][gate.gate] = duration | ||
|
||
return calibration | ||
|
||
@classmethod | ||
def __import_target(cls, target: Target) -> DeviceCalibration: | ||
"""Import calibration data from a Qiskit `Target` object. | ||
|
||
Arguments: | ||
target: the Qiskit `Target` object. | ||
|
||
Returns: Collection of calibration data | ||
""" | ||
calibration = DeviceCalibration() | ||
num_qubits = len(target.qubit_properties) | ||
|
||
for qubit in range(num_qubits): | ||
qubit_props = target.qubit_properties[qubit] | ||
calibration.t1[qubit] = cast(float, qubit_props.t1) | ||
calibration.t2[qubit] = cast(float, qubit_props.t2) | ||
|
||
calibration.single_qubit_gate_fidelity = {qubit: {} for qubit in range(num_qubits)} | ||
calibration.single_qubit_gate_duration = {qubit: {} for qubit in range(num_qubits)} | ||
coupling_map = target.build_coupling_map().get_edges() | ||
calibration.two_qubit_gate_fidelity = {(qubit1, qubit2): {} for qubit1, qubit2 in coupling_map} | ||
calibration.two_qubit_gate_duration = {(qubit1, qubit2): {} for qubit1, qubit2 in coupling_map} | ||
for instruction, qargs in target.instructions: | ||
# Skip `reset` and `delay` gate as their error information is not exposed. | ||
if instruction.name == "reset" or instruction.name == "delay": | ||
continue | ||
|
||
instruction_props = target[instruction.name][qargs] | ||
error: float = instruction_props.error | ||
duration: float = instruction_props.duration | ||
qubit = qargs[0] | ||
if instruction.name == "measure": | ||
calibration.readout_fidelity[qubit] = 1 - error | ||
calibration.readout_duration[qubit] = duration | ||
elif len(qargs) == 1: | ||
calibration.single_qubit_gate_fidelity[qubit][instruction.name] = 1 - error | ||
calibration.single_qubit_gate_duration[qubit][instruction.name] = duration | ||
elif len(qargs) == 2: | ||
qubit1, qubit2 = qargs | ||
calibration.two_qubit_gate_fidelity[qubit1, qubit2][instruction.name] = 1 - error | ||
calibration.two_qubit_gate_duration[qubit1, qubit2][instruction.name] = duration | ||
|
||
return calibration | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -165,6 +165,7 @@ def get_openqasm_gates() -> list[str]: | |
"c3x", | ||
"c3sqrtx", | ||
"c4x", | ||
"ecr", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't be adding to the list of OpenQASM gates here, as, in fact, the In the long run, we definitely want to get rid of that whole list of gates here and move towards dumping OpenQASM 3 programs instead of OpenQASM 2. |
||
] | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a side note on the calibration files in general.
We seem to be missing timing information (how long a certain gate takes) for single-qubit gates in most (if not all) of the calibration files.
I believe the original IBM calibration files contain that kind of information.
(This could be converted to an issue to tackle separately)