Skip to content

✨ Introducing Qiskit's Target as our main Device representation and Remove OQC Lucy #560

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

Open
wants to merge 48 commits into
base: main
Choose a base branch
from

Conversation

nquetschlich
Copy link
Collaborator

@nquetschlich nquetschlich commented May 13, 2025

This PR introduces the Qiskit Target object as our new device representation (resolves issue #553).

Furthermore, the deprecated OQC Lucy device was removed.
Additonally, the FakeBackends are not used anymore to be compatible with Qiskit v2.

@nquetschlich nquetschlich added enhancement New feature or request feature New feature or request labels May 13, 2025
@github-project-automation github-project-automation bot moved this to In Progress in MQT Applications May 13, 2025
# Conflicts:
#	src/mqt/bench/benchmark_generation.py
#	src/mqt/bench/devices/__init__.py
#	src/mqt/bench/devices/device.py
#	src/mqt/bench/output.py
Copy link

codecov bot commented May 13, 2025

@burgholzer burgholzer added major Part of a major release refactor Anything related to refactoring the code base labels May 15, 2025
@burgholzer burgholzer linked an issue May 15, 2025 that may be closed by this pull request
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

Thanks @nquetschlich for spearheading this 🙏🏼
I have not made it through the whole PR yet, but I think I got enough of an idea to suggest targeted (pun intended) improvements.
I hope the inline comments roughly make sense and give you some direction on where this could/should be heading.

Comment on lines 72 to 73
gateset: Target | None = None,
device: Target | None = None,
Copy link
Member

Choose a reason for hiding this comment

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

There is no need to distinguish between these anymore.
It is totally sufficient to have a single target: Target | None = None parameter.
The native gates level will just extract the gateset from the target.
The mapped level will use the full target.
The other levels do not use the information at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed.

@@ -90,10 +90,10 @@ def generate_filename(
base = f"{benchmark_name}_{level}"

if level == "nativegates" and gateset is not None and opt_level is not None:
return f"{base}_{gateset.name}_opt{opt_level}_{num_qubits}"
return f"{base}_{gateset.description}_opt{opt_level}_{num_qubits}"
Copy link
Member

Choose a reason for hiding this comment

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

I am fairly sure the description needs some kind of sanitization.
Many descriptions of targets in the wild will use spaces ("Hardware Provider XZY Target").
Based on the above comment, the gateset should be a serialized/stringified version of the gateset and the device description should be a sanitized version of the description.

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 added a simple sanitization.

@@ -219,7 +219,7 @@ def get_indep_level(
@overload
def get_native_gates_level(
qc: QuantumCircuit,
gateset: Gateset,
gateset: Target,
Copy link
Member

Choose a reason for hiding this comment

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

I'd simply refer to this as target everywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed and adjusted it. However, for the get_benchmark method I think it is more easily understandable to still call it gateset. I would like to discuss this and some other points in person.

Comment on lines 292 to 293
gates = {inst.name for inst, _ in gateset.instructions}
print(gates)
Copy link
Member

Choose a reason for hiding this comment

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

There is a print statement here, which I am pretty surprised that ruff did not complain about or optimize away. Maybe worth checking the config.

Other than that, we should simply be able to use *target.operation_names in the call down 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.

Thanks for catching that, your suggestions works well.

Copy link
Member

Choose a reason for hiding this comment

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

I would argue that this file can be removed as I believe there should be no need to carry all these calibration files with us here.

Comment on lines 10 to 16
# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM
# Copyright (c) 2025 Munich Quantum Software Company GmbH
# All rights reserved.
#
# SPDX-License-Identifier: MIT
#
# Licensed under the MIT License
Copy link
Member

Choose a reason for hiding this comment

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

What happened here?

Suggested change
# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM
# Copyright (c) 2025 Munich Quantum Software Company GmbH
# All rights reserved.
#
# SPDX-License-Identifier: MIT
#
# Licensed under the MIT License

Please also fix in the other files.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed it.

Comment on lines 35 to 49
with calibration_path.open() as json_file:
calib = json.load(json_file)

class IonQAria1(IonQDevice):
"""IonQ Aria1 device."""
target = Target(num_qubits=calib["num_qubits"], description=calib["name"])
num_qubits = calib["num_qubits"]
connectivity = calib["connectivity"]

def __init__(self) -> None:
"""Initialize the IonQ Aria1 device."""
super().__init__(get_device_calibration_path("ionq_aria1"))
# Gate durations and fidelities
oneq_fidelity = calib["fidelity"]["1q"]["mean"]
twoq_fidelity = calib["fidelity"]["2q"]["mean"]
spam_fidelity = calib["fidelity"]["spam"]["mean"]

oneq_duration = calib["timing"]["1q"]
twoq_duration = calib["timing"]["2q"]
readout_duration = calib["timing"]["readout"]
Copy link
Member

Choose a reason for hiding this comment

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

I would be very tempted to simply hard code the calibration file values here.
We have never updated them. Since their creation. And they are fairly specific to a particular device.
We could simply link to the origin of the data.
Or, wild and crazy idea: Simply remove all the calibration information for now to streamline the implementation and then come up with a good strategy to selectively add it back wherever feasible.
MQT Bench was not using that information so far anyway.

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 like the idea and would like to discuss the consequences in terms of MQT Predictor in person. Let's then decide based on that discussion.

Comment on lines 88 to 89
if target:
gateset = sorted({str(inst.name) for inst, _ in target.instructions})
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if target:
gateset = sorted({str(inst.name) for inst, _ in target.instructions})
if target:
gateset = target.operation_names

or similar

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, followed that suggestion.

lines.append(f"// Used gateset: {gateset}")
c_map = target.build_coupling_map() if target else None
Copy link
Member

Choose a reason for hiding this comment

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

Pretty sure this function will need an additional level input.
Because otherwise, this will add a coupling map even if a circuit is only transpiled to the native gates level

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 don't think so, since the build_coupling_map() functions returns None according to its docstring if there are no connectivity constraints present in the target (see https://github.com/Qiskit/qiskit/blob/ee4095933e688efb794e679b3feda658fefdec9b/qiskit/transpiler/target.py#L545).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feature New feature or request major Part of a major release refactor Anything related to refactoring the code base
Projects
None yet
Development

Successfully merging this pull request may close these issues.

✨ Replace devices with Qiskit Target`s
2 participants