Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 111 additions & 0 deletions .github/workflows/upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Qiskit Upstream Tests
on:
schedule:
# Run every Monday at 00:00 UTC
- cron: "0 0 * * 1"
pull_request:
paths:
- ".github/workflows/upstream.yml"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read
issues: write # Needed to create/update issues

jobs:
qiskit-upstream-tests:
name: 🐍⚛️
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
uses: cda-tum/mqt-workflows/.github/workflows/reusable-qiskit-upstream.yml@v1.7
with:
runs-on: ${{ matrix.os }}
setup-z3: true

create-issue-on-failure:
name: Create issue on failure
if: ${{ always() }}
needs: qiskit-upstream-tests
runs-on: ubuntu-latest
steps:
- name: Get latest Qiskit commit
id: qiskit-commit
run: |
QISKIT_COMMIT=$(curl -s https://api.github.com/repos/Qiskit/qiskit/commits/main | jq -r '.sha')
QISKIT_COMMIT_URL="https://github.com/Qiskit/qiskit/commit/${QISKIT_COMMIT}"
QISKIT_COMMIT_DATE=$(curl -s https://api.github.com/repos/Qiskit/qiskit/commits/${QISKIT_COMMIT} | jq -r '.commit.author.date')
echo "url=${QISKIT_COMMIT_URL}" >> $GITHUB_OUTPUT
echo "date=${QISKIT_COMMIT_DATE}" >> $GITHUB_OUTPUT

- name: Create or update issue
if: ${{ needs.qiskit-upstream-tests.result != 'success' }}
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const runId = context.runId;
const workflowRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
const testResult = '${{ needs.qiskit-upstream-tests.result }}';
const qiskitCommitUrl = '${{ steps.qiskit-commit.outputs.url }}';
const qiskitCommitDate = '${{ steps.qiskit-commit.outputs.date }}';

// Search for existing open issues with a specific title pattern
const issueTitle = "❌ Qiskit Upstream Tests Failure";

const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
});

// Find matching issues
const matchingIssue = issues.data.find(issue => issue.title === issueTitle);

const today = new Date().toISOString().split('T')[0];
const body = `## Qiskit Upstream Tests Failed on ${today}

The weekly Qiskit upstream test has failed.

### Workflow Details

- **Workflow Run**: [View Logs and Details](${workflowRunUrl})
- **Result**: \`${testResult}\`
- **Triggered by**: ${context.eventName}
- **Qiskit Commit Tested**: ${qiskitCommitUrl} (${qiskitCommitDate})

Please investigate and fix this issue to ensure compatibility with the latest version of Qiskit.

> This issue was automatically generated by a GitHub Action.
`;

// If we found an existing issue, update it, otherwise create a new one
if (matchingIssue) {
// Add a comment with the new failure
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: matchingIssue.number,
body: `New failure detected on ${today}.
- [View workflow run](${workflowRunUrl})
- **Qiskit Commit Tested**: ${qiskitCommitUrl} (${qiskitCommitDate})`
});

console.log(`Updated existing issue #${matchingIssue.number}`);
} else {
// Create a new issue
const newIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: issueTitle,
body: body,
labels: ['bug', 'python', 'dependencies']
});

console.log(`Created new issue #${newIssue.data.number}`);
}
32 changes: 26 additions & 6 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def _run_tests(
session: nox.Session,
*,
install_args: Sequence[str] = (),
run_args: Sequence[str] = (),
extra_command: Sequence[str] = (),
pytest_run_args: Sequence[str] = (),
) -> None:
env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
if os.environ.get("CI", None) and sys.platform == "win32":
Expand All @@ -57,7 +58,6 @@ def _run_tests(
"build",
"--only-group",
"test",
"--verbose",
# Build mqt-core from source to work around pybind believing that two
# compiled extensions might not be binary compatible.
# This will be fixed in a new pybind11 release that includes https://github.com/pybind/pybind11/pull/5439.
Expand All @@ -73,14 +73,23 @@ def _run_tests(
)
session.run(
"uv",
"run",
"sync",
"--inexact",
"--no-dev", # do not auto-install dev dependencies
"--no-build-isolation-package",
"mqt-qmap", # build the project without isolation
"--verbose",
*install_args,
env=env,
)
if extra_command:
session.run(*extra_command, env=env)
session.run(
"uv",
"run",
"--no-sync", # do not sync as everything is already installed
*install_args,
"pytest",
*run_args,
*pytest_run_args,
*session.posargs,
"--cov-config=pyproject.toml",
env=env,
Expand All @@ -99,13 +108,24 @@ def minimums(session: nox.Session) -> None:
_run_tests(
session,
install_args=["--resolution=lowest-direct"],
run_args=["-Wdefault"],
pytest_run_args=["-Wdefault"],
)
env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
session.run("uv", "tree", "--frozen", env=env)
session.run("uv", "lock", "--refresh", env=env)


@nox.session(reuse_venv=True, venv_backend="uv", python=PYTHON_ALL_VERSIONS)
def qiskit(session: nox.Session) -> None:
"""Tests against the latest version of Qiskit."""
_run_tests(
session,
extra_command=["uv", "pip", "install", "qiskit[qasm3-import] @ git+https://github.com/Qiskit/qiskit.git"],
)
env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}
session.run("uv", "pip", "show", "qiskit", env=env)


@nox.session(reuse_venv=True)
def docs(session: nox.Session) -> None:
"""Build the docs. Use "--non-interactive" to avoid serving. Pass "-b linkcheck" to check links."""
Expand Down
7 changes: 1 addition & 6 deletions src/mqt/qmap/load_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from typing import TYPE_CHECKING

from qiskit.providers.models import BackendProperties
from qiskit.transpiler.target import Target

if TYPE_CHECKING:
Expand All @@ -19,7 +18,7 @@ def __dir__() -> list[str]:
return __all__


def load_calibration(architecture: Architecture, calibration: str | Target | BackendProperties | None = None) -> None:
def load_calibration(architecture: Architecture, calibration: str | Target | None = None) -> None:
"""Load a calibration from a string, BackendProperties, or Target.

Args:
Expand All @@ -31,10 +30,6 @@ def load_calibration(architecture: Architecture, calibration: str | Target | Bac

if isinstance(calibration, str):
architecture.load_properties(calibration)
elif isinstance(calibration, BackendProperties):
from mqt.qmap.plugins.qiskit import import_backend_properties

architecture.load_properties(import_backend_properties(calibration))
elif isinstance(calibration, Target):
from mqt.qmap.plugins.qiskit import import_target

Expand Down
85 changes: 10 additions & 75 deletions src/mqt/qmap/plugins/qiskit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@

from typing import TYPE_CHECKING

from qiskit.providers import Backend, BackendV1, BackendV2, BackendV2Converter

if TYPE_CHECKING:
from qiskit.providers.models import BackendProperties
from qiskit.providers import BackendV2
from qiskit.transpiler import Target

from mqt.qmap import Architecture

__all__ = [
"import_backend",
"import_backend_properties",
"import_target",
]

Expand All @@ -23,66 +20,16 @@ def __dir__() -> list[str]:
return __all__


def import_backend(backend: Backend) -> Architecture:
"""Import a backend from qiskit.providers.Backend.

Args:
backend: The backend to import.

Returns:
The imported backend as an Architecture.

"""
if isinstance(backend, BackendV1):
import warnings

warnings.warn(
"The class ``qiskit.providers.backend.BackendV1`` is deprecated as of qiskit 1.2. "
"It will be removed in the 2.0 release, scheduled for 20 March 2025. "
"MQT QMAP will continue to support BackendV1 instances until they are removed from Qiskit. "
"Please use ``qiskit.providers.backend.BackendV2`` instances instead.",
DeprecationWarning,
stacklevel=2,
)

return import_backend_v2(BackendV2Converter(backend))
if isinstance(backend, BackendV2):
return import_backend_v2(backend)
msg = f"Backend type {type(backend)} not supported."
raise TypeError(msg)


def import_backend_properties(backend_properties: BackendProperties) -> Architecture.Properties:
"""Import backend properties from qiskit.providers.models.BackendProperties.

Args:
backend_properties: The backend properties to import.

Returns:
The imported backend properties as an Architecture.Properties object.
"""
props = Architecture.Properties()
props.name = backend_properties.backend_name
props.num_qubits = len(backend_properties.qubits)
for qubit in range(props.num_qubits):
props.set_t1(qubit, backend_properties.t1(qubit))
props.set_t2(qubit, backend_properties.t2(qubit))
props.set_frequency(qubit, backend_properties.frequency(qubit))
props.set_readout_error(qubit, backend_properties.readout_error(qubit))

for gate in backend_properties.gates:
if gate.gate == "reset":
continue
def import_backend(backend: BackendV2) -> Architecture:
"""Import a backend from qiskit.providers.BackendV2."""
arch = Architecture()
arch.name = backend.name
arch.num_qubits = backend.num_qubits
arch.coupling_map = set(backend.coupling_map.get_edges())
arch.properties = import_target(backend.target)
arch.properties.name = backend.name

if len(gate.qubits) == 1:
props.set_single_qubit_error(
gate.qubits[0], gate.gate, backend_properties.gate_error(gate.gate, gate.qubits)
)
elif len(gate.qubits) == 2:
props.set_two_qubit_error(
gate.qubits[0], gate.qubits[1], backend_properties.gate_error(gate.gate, gate.qubits), gate.gate
)
return props
return arch


def import_target(target: Target) -> Architecture.Properties:
Expand Down Expand Up @@ -116,15 +63,3 @@ def import_target(target: Target) -> Architecture.Properties:
props.set_two_qubit_error(qargs[0], qargs[1], instruction_props.error, instruction.name)

return props


def import_backend_v2(backend: BackendV2) -> Architecture:
"""Import a backend from qiskit.providers.BackendV2."""
arch = Architecture()
arch.name = backend.name
arch.num_qubits = backend.num_qubits
arch.coupling_map = set(backend.coupling_map.get_edges())
arch.properties = import_target(backend.target)
arch.properties.name = backend.name

return arch
16 changes: 1 addition & 15 deletions test/python/test_qiskit_backend_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest
from qiskit import QuantumCircuit
from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2
from qiskit.providers.fake_provider import GenericBackendV2

from mqt import qmap
from mqt.qcec import verify
Expand All @@ -27,20 +27,6 @@ def backend() -> GenericBackendV2:
return GenericBackendV2(num_qubits=5, coupling_map=[[0, 1], [1, 0], [1, 2], [2, 1], [1, 3], [3, 1], [3, 4], [4, 3]])


def test_backend_v1(example_circuit: QuantumCircuit) -> None:
"""Test that circuits can be mapped to Qiskit BackendV1 instances providing the new basis_gates."""
qc, results = qmap.compile(example_circuit, arch=Fake5QV1())
assert results.timeout is False
assert verify(example_circuit, qc).considered_equivalent()


def test_architecture_from_v1_backend_properties(example_circuit: QuantumCircuit) -> None:
"""Test that circuits can be mapped by simply providing the backend properties (the BackendV1 way)."""
qc, results = qmap.compile(example_circuit, arch=None, calibration=Fake5QV1().properties())
assert results.timeout is False
assert verify(example_circuit, qc).considered_equivalent()


def test_backend_v2(example_circuit: QuantumCircuit, backend: GenericBackendV2) -> None:
"""Test that circuits can be mapped to Qiskit BackendV1 instances providing the old basis_gates."""
qc, results = qmap.compile(example_circuit, arch=backend)
Expand Down