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

✨ Added free IBM hardware (kyiv, brisbane, sherbrooke) to CLI #381

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
51798a2
:truck: add calibration files for free access IBM devices.
Drewniok Sep 18, 2024
1e24499
:truck: add calibration files for free access IBM devices.
Drewniok Sep 18, 2024
bd833ef
:truck: support circuit generation for free access ibm hardware.
Drewniok Sep 18, 2024
238b51b
:art: small fix.
Drewniok Sep 18, 2024
1b09b1b
Merge branch 'main' into add_ibm_free_devices
Drewniok Sep 18, 2024
5108263
:art: renaming.
Drewniok Sep 18, 2024
fc56294
:art: renaming.
Drewniok Sep 18, 2024
0a9c027
:art: add file to convert ibm-csv to properties.
Drewniok Sep 18, 2024
8149136
:art: update parser.
Drewniok Sep 19, 2024
6ef47b9
:art: add line.
Drewniok Sep 19, 2024
398a563
:art: undo changes in ``backend.py``
Drewniok Sep 19, 2024
b3fc240
:white_check_mark: update tests.
Drewniok Sep 19, 2024
893d5a5
:green_heart: update CI to run on fork.
Drewniok Sep 19, 2024
c3638aa
:green_heart: update CI to run on fork.
Drewniok Sep 19, 2024
837992e
:white_check_mark: fix test.
Drewniok Sep 19, 2024
29d5f1d
:white_check_mark: fix test.
Drewniok Sep 19, 2024
ecb09c9
:white_check_mark: fix test.
Drewniok Sep 19, 2024
604bbc4
:white_check_mark: add tests.
Drewniok Sep 19, 2024
c6dfc74
:white_check_mark: fix tests.
Drewniok Sep 19, 2024
a0a5f83
:art: small fix.
Drewniok Sep 19, 2024
fda5425
:art: remove unused import of BackendV2
Drewniok Sep 19, 2024
90fbd8d
:art: revert CI changes.
Drewniok Sep 19, 2024
ef414ec
🎨 pre-commit fixes
pre-commit-ci[bot] Sep 19, 2024
b74578a
:art: addressing linting warnings and implementing best practices.
Drewniok Sep 19, 2024
057ee1a
Merge remote-tracking branch 'origin/add_ibm_free_devices' into add_i…
Drewniok Sep 19, 2024
881207a
🎨 pre-commit fixes
pre-commit-ci[bot] Sep 19, 2024
38b04e5
:art: small fix.
Drewniok Sep 19, 2024
094950f
🎨 pre-commit fixes
pre-commit-ci[bot] Sep 19, 2024
072be89
Merge branch 'main' into add_ibm_free_devices
Drewniok Sep 19, 2024
d2a2ce0
:white_check_mark: add more unit tests.
Drewniok Sep 19, 2024
40ed22b
🎨 pre-commit fixes
pre-commit-ci[bot] Sep 19, 2024
cbf8c54
:white_check_mark: small fix.
Drewniok Sep 19, 2024
566d470
🎨 pre-commit fixes
pre-commit-ci[bot] Sep 19, 2024
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
2,036 changes: 2,036 additions & 0 deletions src/mqt/bench/calibration_files/ibm_brisbane_calibration.json
Copy link
Member

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)

Copy link
Member

Choose a reason for hiding this comment

The 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.

2,024 changes: 2,024 additions & 0 deletions src/mqt/bench/calibration_files/ibm_kyiv_calibration.json

Large diffs are not rendered by default.

2,034 changes: 2,034 additions & 0 deletions src/mqt/bench/calibration_files/ibm_sherbrooke_calibration.json

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion src/mqt/bench/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .provider import Provider

from .ibm import IBMProvider
from .ibm_open_access import IBMOpenAccessProvider
from .ionq import IonQProvider
from .iqm import IQMProvider
from .oqc import OQCProvider
Expand All @@ -25,7 +26,15 @@ class NotFoundError(Exception):

def get_available_providers() -> list[Provider]:
"""Get a list of all available providers."""
return [IBMProvider(), IonQProvider(), OQCProvider(), RigettiProvider(), QuantinuumProvider(), IQMProvider()]
return [
IBMProvider(),
IBMOpenAccessProvider(),
IonQProvider(),
OQCProvider(),
RigettiProvider(),
QuantinuumProvider(),
IQMProvider(),
]


def get_available_provider_names() -> list[str]:
Expand Down Expand Up @@ -92,6 +101,7 @@ def get_device_by_name(device_name: str) -> Device:
__all__ = [
"Device",
"DeviceCalibration",
"IBMOpenAccessProvider",
"IBMProvider",
"IQMProvider",
"IonQProvider",
Expand Down
187 changes: 187 additions & 0 deletions src/mqt/bench/devices/ibm_open_access.py
Copy link
Member

Choose a reason for hiding this comment

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

This duplicates quite a lot of the code from the ibm provider. Do you see any way of unifying that to reduce this duplication. In essence, the only big difference between the regular ibm provider and the new one here is the native gate-set.
I feel it should be possible to unify the implementation a little bit here.

One possible idea in that regard would be to kind of refactor the device handling to remove the Provider concept and replace it with base classes of Devices that implement shared functionality.
This might take a little work, but I think in the this might be a little more scalable for the future.
In the mid-term (maybe earlier), a device should be described in a standardised fashion so there should really be no need to provide this Provider abstraction.
The list of supported gate-sets by MQT Bench (e.g., for the native gates level) is then simply the union of all the different gate sets provided by all available devices.

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

Check warning on line 112 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L111-L112

Added lines #L111 - L112 were not covered by tests

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

Check warning on line 118 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L114-L118

Added lines #L114 - L118 were not covered by tests

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:

Check warning on line 122 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L120-L122

Added lines #L120 - L122 were not covered by tests
# Skip `reset` gate as its error information is not exposed.
if gate.gate == "reset":
continue

Check warning on line 125 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L124-L125

Added lines #L124 - L125 were not covered by tests

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

Check warning on line 137 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L127-L137

Added lines #L127 - L137 were not covered by tests

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

Check warning on line 141 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L139-L141

Added lines #L139 - L141 were not covered by tests

return calibration

Check warning on line 143 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L143

Added line #L143 was not covered by tests

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

Check warning on line 155 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L154-L155

Added lines #L154 - L155 were not covered by tests

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)

Check warning on line 160 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L157-L160

Added lines #L157 - L160 were not covered by tests

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:

Check warning on line 167 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L162-L167

Added lines #L162 - L167 were not covered by tests
# Skip `reset` and `delay` gate as their error information is not exposed.
if instruction.name == "reset" or instruction.name == "delay":
continue

Check warning on line 170 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L169-L170

Added lines #L169 - L170 were not covered by tests

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

Check warning on line 185 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L172-L185

Added lines #L172 - L185 were not covered by tests

return calibration

Check warning on line 187 in src/mqt/bench/devices/ibm_open_access.py

View check run for this annotation

Codecov / codecov/patch

src/mqt/bench/devices/ibm_open_access.py#L187

Added line #L187 was not covered by tests
1 change: 1 addition & 0 deletions src/mqt/bench/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def get_openqasm_gates() -> list[str]:
"c3x",
"c3sqrtx",
"c4x",
"ecr",
Copy link
Member

Choose a reason for hiding this comment

The 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 ecr gate is not an OpenQASM 2 standard gate.
We handle a pretty similar case already for OQC devices (which also offer the ECR gate as their basis gate).
Any kind of handling of that gate in the newly added devices should, in a first stage, handle this similarly.

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.
However, I'd rather tackle this in a separate issue and use the same kind of special handling for the ECR gate here as for the OQC devices.

]


Expand Down
2 changes: 1 addition & 1 deletion tests/devices/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def test_device_calibration_errors() -> None:
def test_provider() -> None:
"""Test that all providers can be imported."""
for provider in get_available_providers():
assert provider.provider_name in ["ibm", "rigetti", "oqc", "ionq", "quantinuum", "iqm"]
assert provider.provider_name in ["ibm", "ibm_open_access", "rigetti", "oqc", "ionq", "quantinuum", "iqm"]

with pytest.raises(NotFoundError, match="Provider 'test' not found among available providers."):
get_provider_by_name("test")
Loading
Loading