From cf3a86a36083ca37ad0ae9dc0076911f77becc6a Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 14:29:44 +0300 Subject: [PATCH 01/13] initial qrips partial implementation that for PassManager --- .flake8 | 2 + .pre-commit-config.yaml | 77 ++++++++++++++ pyproject.toml | 7 ++ src/q8s/runtime/mlflow/qiskit/autologging.py | 43 ++++---- src/q8s/runtime/mlflow/qrisp/__init__.py | 5 + src/q8s/runtime/mlflow/qrisp/autologging.py | 82 +++++++++++++++ src/q8s/runtime/qprov/record.py | 23 ++++- test-mlflow.py | 11 +- test-qrisp.py | 29 ++++++ tests/test_qrisp_autologging.py | 100 +++++++++++++++++++ 10 files changed, 354 insertions(+), 25 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 src/q8s/runtime/mlflow/qrisp/__init__.py create mode 100644 src/q8s/runtime/mlflow/qrisp/autologging.py create mode 100644 test-qrisp.py create mode 100644 tests/test_qrisp_autologging.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7da1f96 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ed4725b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,77 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + # - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +## If you want to automatically "modernize" your Python code: +# - repo: https://github.com/asottile/pyupgrade +# rev: v3.17.0 +# hooks: +# - id: pyupgrade +# args: ['--py38-plus'] + +## If you want to avoid flake8 errors due to unused vars or imports: +# - repo: https://github.com/PyCQA/autoflake +# rev: v2.3.1 +# hooks: +# - id: autoflake +# args: [ +# --in-place, +# --remove-all-unused-imports, +# --remove-unused-variables, +# ] + +- repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 + hooks: + - id: docformatter + args: + - --in-place + - --wrap-summaries=88 + - --wrap-descriptions=88 + +- repo: https://github.com/PyCQA/isort + rev: 7.0.0 + hooks: + - id: isort + args: ["--profile", "black", "--filter-files"] + +- repo: https://github.com/psf/black + rev: 25.11.0 + hooks: + - id: black + language_version: python3 + +# If like to embrace black styles even in the docs: +- repo: https://github.com/asottile/blacken-docs + rev: 1.18.0 + hooks: + - id: blacken-docs + additional_dependencies: [black] + +- repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + ## You can add flake8 plugins via `additional_dependencies`: + # additional_dependencies: [flake8-bugbear] + +## Check for misspells in documentation files: +# - repo: https://github.com/codespell-project/codespell +# rev: v2.3.0 +# hooks: +# - id: codespell diff --git a/pyproject.toml b/pyproject.toml index 1b2a816..a0b3853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "q8s.runtime" version = "0.1.0" authors = [{ name = "Vlad Stirbu", email = "vstirbu@gmail.com" }] description = "A runtime library for q8s workloads" +readme = "README.md" requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", @@ -28,6 +29,8 @@ dependencies = ["mlflow-skinny", "dataclasses-json"] [project.optional-dependencies] matplotlib = ["matplotlib"] +development = ["pre-commit", "pytest", "pytest-cov"] + test = ["mqt.bench", "qiskit<2.2", "mlflow", "iqm-client[qiskit]"] [build-system] requires = ["setuptools >= 61.0"] @@ -35,3 +38,7 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] diff --git a/src/q8s/runtime/mlflow/qiskit/autologging.py b/src/q8s/runtime/mlflow/qiskit/autologging.py index 4e90818..20ae09a 100644 --- a/src/q8s/runtime/mlflow/qiskit/autologging.py +++ b/src/q8s/runtime/mlflow/qiskit/autologging.py @@ -3,14 +3,14 @@ import time from pathlib import Path -from mlflow.tracking.fluent import ActiveRun +import mlflow import qiskit +from mlflow.tracking.fluent import ActiveRun +from mlflow.utils.autologging_utils import autologging_integration, safe_patch from qiskit.circuit import QuantumCircuit from qiskit.transpiler import StagedPassManager from qiskit_aer import AerSimulator -from mlflow.utils.autologging_utils import autologging_integration, safe_patch - from q8s.runtime.mlflow.qiskit.transpiler import ( find_stage_by_id, process_staged_pass_manager, @@ -49,10 +49,10 @@ def autolog( silent=False, extra_tags=None, ) -> None: - """ - Enables the Qiskit autologging integration. + """Enables the Qiskit autologging integration. - This function is idempotent and can be called multiple times. The integration will only be enabled once. + This function is idempotent and can be called multiple times. The integration will + only be enabled once. """ def patched_staged_pass_manager_run( @@ -116,13 +116,22 @@ def patched_generate_preset_pass_manager( *args, **kwargs, ): - """ - Patch the generate_preset_pass_manager function to log the backend information to the QProvRecord. - """ + """Patch the generate_preset_pass_manager function to log the backend + information to the QProvRecord.""" + print( "Qiskit autologging integration is enabled for generate_preset_pass_manager." ) + run = mlflow.active_run() + + if run is None: + # noqa: E501 + raise RuntimeError( + """No active MLflow run. `generate_preset_pass_manager` must be + called within an active MLflow run.""" + ) + ctx = _current_context.get() if ctx is None or not isinstance(ctx, QProvRecord): @@ -173,7 +182,7 @@ def patched_result(original_result, job_self, *r_args, **r_kwargs): # log result here try: - counts = result.get_counts() + result.get_counts() # print("counts:", counts) # mlflow.log_dict(counts, "qiskit/result_counts.json") except Exception: @@ -198,9 +207,8 @@ def patched_result(original_result, job_self, *r_args, **r_kwargs): ) def patched_activerun_exit(original, *args, **kwargs): - """ - Patch the __exit__ method of ActiveRun to log the QProvRecord to MLflow when the run ends. - """ + """Patch the __exit__ method of ActiveRun to log the QProvRecord to MLflow when + the run ends.""" ctx = _current_context.get() if ctx is None or not isinstance(ctx, QProvRecord): @@ -223,9 +231,7 @@ def patched_activerun_exit(original, *args, **kwargs): def callback(pass_, dag, time, property_set, count): - """ - Callback function for logging pass information during transpilation. - """ + """Callback function for logging pass information during transpilation.""" name = pass_.__class__.__name__ ctx = _current_context.get() @@ -250,10 +256,7 @@ def callback(pass_, dag, time, property_set, count): def log_to_mlflow(record: QProvRecord): - """ - Logs the QProvRecord to MLflow. - """ - import mlflow + """Logs the QProvRecord to MLflow.""" with tempfile.TemporaryDirectory() as tmpdir: tmp = Path(tmpdir) diff --git a/src/q8s/runtime/mlflow/qrisp/__init__.py b/src/q8s/runtime/mlflow/qrisp/__init__.py new file mode 100644 index 0000000..4e0b878 --- /dev/null +++ b/src/q8s/runtime/mlflow/qrisp/__init__.py @@ -0,0 +1,5 @@ +"""Qrip MLflow autologging integration.""" + +from q8s.runtime.mlflow.qrisp.autologging import autolog + +__all__ = ["autolog"] diff --git a/src/q8s/runtime/mlflow/qrisp/autologging.py b/src/q8s/runtime/mlflow/qrisp/autologging.py new file mode 100644 index 0000000..d81ad2f --- /dev/null +++ b/src/q8s/runtime/mlflow/qrisp/autologging.py @@ -0,0 +1,82 @@ +import contextvars +import time +from importlib.metadata import version + +from mlflow.utils.autologging_utils import autologging_integration, safe_patch +from qrisp import PassManager, QuantumCircuit + +from q8s.runtime.qprov.record import ( + CompilationProvenance, + QProvRecord, + QuantumCircuitProvenance, +) + +CONTEXT_NAME = "qrisp_mlflow_autologging_context" +_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( + CONTEXT_NAME, + default=QProvRecord( + compilation=CompilationProvenance( + compiler="qrisp", compiler_version=version("qrisp") + ) + ), +) + + +def get_context() -> QProvRecord: + ctx = _current_context.get() + if ctx is None: + raise RuntimeError("No active autolog context") + return ctx + + +@autologging_integration("qrisp") +def autolog( + disable=False, + silent=False, +) -> None: + """Enables autologging for QRISPs.""" + + def patched_pass_manager_run( + original, # type: ignore[no-untyped-def] + instance, + *args, + **kwargs, + ): + """Patched version of PassManager.run that logs the execution of passes and + their provenance.""" + + print("QRISPs autologging integration is enabled for PassManager.run.") + + ctx = _current_context.get() + + circuit: QuantumCircuit = args[0] + + ctx.circuit = QuantumCircuitProvenance( + circuit_id=str(id(circuit)), + num_qubits=circuit.num_qubits(), + depth=circuit.depth(), + width=len(circuit.qubits), + gate_counts=circuit.count_ops(), + ) + + for circuit_pass in instance._passes: + print( + f"Executing pass: {circuit_pass.__name__} with args: {args}, kwargs: {kwargs} " + ) + + start = time.perf_counter() + + history = original(instance, *args, **kwargs) + + end = time.perf_counter() + + ctx.compilation.duration_s = end - start + + print(ctx.to_json(indent=2)) + + return history + + if disable: + return + + safe_patch("qrisp", PassManager, "run", patched_pass_manager_run) diff --git a/src/q8s/runtime/qprov/record.py b/src/q8s/runtime/qprov/record.py index 1da37f5..c1366c2 100644 --- a/src/q8s/runtime/qprov/record.py +++ b/src/q8s/runtime/qprov/record.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass, field, asdict +from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any from uuid import uuid4 @@ -106,6 +106,27 @@ class ExecutionProvenance: @dataclass_json @dataclass class QProvRecord: + """A record of the provenance of a quantum circuit execution, including information + about the circuit, the quantum computer, the compilation process, and the execution + results. + + Attributes: + circuit : QuantumCircuitProvenance | None + The provenance information of the quantum circuit. + quantum_computer : QuantumComputerProvenance | None + The provenance information of the quantum computer used for execution. + compilation : CompilationProvenance | None + The provenance information of the compilation process. + execution : ExecutionProvenance | None + The provenance information of the execution results. + created_at : datetime + The timestamp when the record was created. + record_id : str + A unique identifier for the record. + metadata : dict[str, Any] + Additional metadata associated with the record. + """ + circuit: QuantumCircuitProvenance | None = None quantum_computer: QuantumComputerProvenance | None = None compilation: CompilationProvenance | None = None diff --git a/test-mlflow.py b/test-mlflow.py index fbf5c8e..4355584 100644 --- a/test-mlflow.py +++ b/test-mlflow.py @@ -1,5 +1,8 @@ +# flake8: noqa: E402 + import mlflow from mqt.bench import BenchmarkLevel, get_benchmark + from q8s.runtime.mlflow.qiskit import autolog autolog() @@ -17,12 +20,12 @@ backend = IQMFakeAphrodite() -manager = generate_preset_pass_manager( - optimization_level=3, - backend=backend, -) with mlflow.start_run(): + manager = generate_preset_pass_manager( + optimization_level=3, + backend=backend, + ) tqc = manager.run(qc) diff --git a/test-qrisp.py b/test-qrisp.py new file mode 100644 index 0000000..86a8aba --- /dev/null +++ b/test-qrisp.py @@ -0,0 +1,29 @@ +# flake8: noqa: E402 + +from qrisp import ( + PassManager, + QuantumCircuit, + combine_single_qubit_gates, + commute_swaps, + fuse_adjacents, +) + +from q8s.runtime.mlflow.qrisp import autolog + +autolog() + +qc = QuantumCircuit(2) +qc.cx(0, 1) +qc.cx(0, 1) # Self-inverse — will be cancelled +qc.h(0) +qc.h(0) # Another self-inverse pair + +print("Before:", qc, sep="\n") + +pm = PassManager() +pm += fuse_adjacents +pm += commute_swaps +pm += combine_single_qubit_gates + +optimized_qc = pm.run(qc) +print("After:", optimized_qc, sep="\n") diff --git a/tests/test_qrisp_autologging.py b/tests/test_qrisp_autologging.py new file mode 100644 index 0000000..ee132ac --- /dev/null +++ b/tests/test_qrisp_autologging.py @@ -0,0 +1,100 @@ +import unittest +from unittest.mock import patch + +from qrisp import QuantumCircuit + +import q8s.runtime.mlflow.qrisp.autologging as autologging +from q8s.runtime.qprov.record import CompilationProvenance, QProvRecord + + +class TestAutolog(unittest.TestCase): + def _patched_run(self): + with patch.object(autologging, "safe_patch") as safe_patch: + autologging.autolog(disable=False) + + safe_patch.assert_called_once() + return safe_patch.call_args.args[3] + + def _make_circuit(self): + """Creates a simple quantum circuit for testing. + + Returns: + QuantumCircuit: A simple quantum circuit with 2 qubits and 4 gates. + ┌───┐┌───┐ + qb_58: ──■────■──┤ H ├┤ H ├ + ┌─┴─┐┌─┴─┐└───┘└───┘ + qb_59: ┤ X ├┤ X ├────────── + └───┘└───┘ + """ + from qrisp import QuantumCircuit + + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(0, 1) # Self-inverse — will be cancelled + qc.h(0) + qc.h(0) # Another self-inverse pair + + return qc + + def _make_pass_manager(self): + from qrisp import ( + PassManager, + combine_single_qubit_gates, + commute_swaps, + fuse_adjacents, + ) + + pm = PassManager() + pm += fuse_adjacents + pm += commute_swaps + pm += combine_single_qubit_gates + + return pm + + def test_disabled_autolog_does_not_install_patch(self): + with patch.object(autologging, "safe_patch") as safe_patch: + autologging.autolog(disable=True) + + safe_patch.assert_not_called() + + def test_enabled_autolog_installs_patch(self): + with patch.object(autologging, "safe_patch") as safe_patch: + autologging.autolog(disable=False) + + safe_patch.assert_called_once() + + def test_patch_records_circuit_and_compilation_duration(self): + from qrisp import PassManager + + original_run = PassManager.run + + try: + with ( + patch.object(autologging.time, "perf_counter", side_effect=[1.0, 1.25]), + patch("builtins.print"), + ): + autologging.autolog(disable=False) + + circuit = self._make_circuit() + pass_manager = self._make_pass_manager() + context = QProvRecord( + compilation=CompilationProvenance(compiler="qrisp") + ) + token = autologging._current_context.set(context) + + result = pass_manager.run(circuit) + autologging._current_context.reset(token) + finally: + PassManager.run = original_run + + self.assertIsInstance(result, QuantumCircuit) + self.assertAlmostEqual(context.compilation.duration_s, 0.25) + self.assertEqual(context.circuit.circuit_id, str(id(circuit))) + self.assertEqual(context.circuit.num_qubits, 2) + self.assertEqual(context.circuit.depth, 4) + self.assertEqual(context.circuit.width, 2) + self.assertEqual(context.circuit.gate_counts, {"h": 2, "cx": 2}) + + +if __name__ == "__main__": + unittest.main() From 9ffffd9554896511d6553910a2d8e49ebd2e8e3f Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 16:29:16 +0300 Subject: [PATCH 02/13] capture pass information --- src/q8s/runtime/mlflow/qiskit/__init__.py | 4 +++ src/q8s/runtime/mlflow/qrisp/__init__.py | 2 +- src/q8s/runtime/mlflow/qrisp/autologging.py | 29 +++++++++++++----- test-qrisp.py | 9 ++++++ tests/test_qrisp_autologging.py | 33 ++++++++------------- 5 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/q8s/runtime/mlflow/qiskit/__init__.py b/src/q8s/runtime/mlflow/qiskit/__init__.py index 4cd53ad..8ee8816 100644 --- a/src/q8s/runtime/mlflow/qiskit/__init__.py +++ b/src/q8s/runtime/mlflow/qiskit/__init__.py @@ -1 +1,5 @@ +"""MLflow autologging integration for Qiskit.""" + from q8s.runtime.mlflow.qiskit.autologging import autolog + +__all__ = ["autolog"] diff --git a/src/q8s/runtime/mlflow/qrisp/__init__.py b/src/q8s/runtime/mlflow/qrisp/__init__.py index 4e0b878..a1e49c5 100644 --- a/src/q8s/runtime/mlflow/qrisp/__init__.py +++ b/src/q8s/runtime/mlflow/qrisp/__init__.py @@ -1,4 +1,4 @@ -"""Qrip MLflow autologging integration.""" +"""MLflow autologging integration for Qrisp.""" from q8s.runtime.mlflow.qrisp.autologging import autolog diff --git a/src/q8s/runtime/mlflow/qrisp/autologging.py b/src/q8s/runtime/mlflow/qrisp/autologging.py index d81ad2f..c912299 100644 --- a/src/q8s/runtime/mlflow/qrisp/autologging.py +++ b/src/q8s/runtime/mlflow/qrisp/autologging.py @@ -38,7 +38,7 @@ def autolog( def patched_pass_manager_run( original, # type: ignore[no-untyped-def] - instance, + instance: PassManager, *args, **kwargs, ): @@ -59,22 +59,37 @@ def patched_pass_manager_run( gate_counts=circuit.count_ops(), ) - for circuit_pass in instance._passes: - print( - f"Executing pass: {circuit_pass.__name__} with args: {args}, kwargs: {kwargs} " - ) + result: QuantumCircuit = circuit start = time.perf_counter() - history = original(instance, *args, **kwargs) + # history = original(instance, *args, **kwargs) end = time.perf_counter() + for index, circuit_pass in enumerate(instance._passes): + pass_start = time.perf_counter() + + result = circuit_pass(result) + + pass_end = time.perf_counter() + + ctx.compilation.add_pass( + pass_name=circuit_pass.__name__, + pass_index=index, + pass_duration_s=pass_end - pass_start, + pass_metadata={ + "depth": result.depth(), + "size": sum(result.count_ops().values()), + "width": len(result.qubits), + }, + ) + ctx.compilation.duration_s = end - start print(ctx.to_json(indent=2)) - return history + return result if disable: return diff --git a/test-qrisp.py b/test-qrisp.py index 86a8aba..dd52a2c 100644 --- a/test-qrisp.py +++ b/test-qrisp.py @@ -9,6 +9,8 @@ ) from q8s.runtime.mlflow.qrisp import autolog +from q8s.runtime.mlflow.qrisp.autologging import get_context +from q8s.runtime.qprov.graphs import plot_transpilation_timeline autolog() @@ -27,3 +29,10 @@ optimized_qc = pm.run(qc) print("After:", optimized_qc, sep="\n") + +fig = plot_transpilation_timeline( + passes=get_context().compilation.passes, + figsize=(16, 8), +) + +fig.savefig("transpilation_timeline.png", dpi=300, bbox_inches="tight") diff --git a/tests/test_qrisp_autologging.py b/tests/test_qrisp_autologging.py index ee132ac..6e9f240 100644 --- a/tests/test_qrisp_autologging.py +++ b/tests/test_qrisp_autologging.py @@ -1,11 +1,13 @@ import unittest from unittest.mock import patch -from qrisp import QuantumCircuit +from qrisp import PassManager, QuantumCircuit import q8s.runtime.mlflow.qrisp.autologging as autologging from q8s.runtime.qprov.record import CompilationProvenance, QProvRecord +original_run = PassManager.run + class TestAutolog(unittest.TestCase): def _patched_run(self): @@ -64,36 +66,27 @@ def test_enabled_autolog_installs_patch(self): safe_patch.assert_called_once() def test_patch_records_circuit_and_compilation_duration(self): - from qrisp import PassManager - - original_run = PassManager.run try: - with ( - patch.object(autologging.time, "perf_counter", side_effect=[1.0, 1.25]), - patch("builtins.print"), - ): - autologging.autolog(disable=False) - - circuit = self._make_circuit() - pass_manager = self._make_pass_manager() - context = QProvRecord( - compilation=CompilationProvenance(compiler="qrisp") - ) - token = autologging._current_context.set(context) - - result = pass_manager.run(circuit) - autologging._current_context.reset(token) + autologging.autolog(disable=False) + + circuit = self._make_circuit() + pass_manager = self._make_pass_manager() + context = QProvRecord(compilation=CompilationProvenance(compiler="qrisp")) + token = autologging._current_context.set(context) + + result = pass_manager.run(circuit) + autologging._current_context.reset(token) finally: PassManager.run = original_run self.assertIsInstance(result, QuantumCircuit) - self.assertAlmostEqual(context.compilation.duration_s, 0.25) self.assertEqual(context.circuit.circuit_id, str(id(circuit))) self.assertEqual(context.circuit.num_qubits, 2) self.assertEqual(context.circuit.depth, 4) self.assertEqual(context.circuit.width, 2) self.assertEqual(context.circuit.gate_counts, {"h": 2, "cx": 2}) + self.assertEqual(len(context.compilation.passes), 3) if __name__ == "__main__": From 5b4ffe9abd6b22fe6f7c40314dea69220e8d3312 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 16:50:36 +0300 Subject: [PATCH 03/13] added qrisp pass information and logging to mlflow on run exit --- src/q8s/runtime/mlflow/qiskit/autologging.py | 11 +---- src/q8s/runtime/mlflow/qrisp/autologging.py | 33 +++++++++++--- test-qrisp.py | 45 +++++++++++--------- tests/test_qrisp_autologging.py | 2 +- 4 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/q8s/runtime/mlflow/qiskit/autologging.py b/src/q8s/runtime/mlflow/qiskit/autologging.py index 20ae09a..ea6c3b9 100644 --- a/src/q8s/runtime/mlflow/qiskit/autologging.py +++ b/src/q8s/runtime/mlflow/qiskit/autologging.py @@ -272,17 +272,10 @@ def log_to_mlflow(record: QProvRecord): if run is None: raise RuntimeError("No active MLflow run. Please start a run first.") - ctx = _current_context.get() - - if ctx is None or not isinstance(ctx, QProvRecord): - raise RuntimeError( - "No active autolog context. Please call transpile() first." - ) - - passes = sorted(ctx.compilation.passes, key=lambda p: p.pass_index) + passes = sorted(record.compilation.passes, key=lambda p: p.pass_index) mlflow.log_metric("passes_count", len(passes)) - mlflow.log_metric("transpilation_duration", ctx.compilation.duration_s) + mlflow.log_metric("transpilation_duration", record.compilation.duration_s) mlflow.log_metric( "circuit_depth", passes[-1].pass_metadata.get("depth", 0) if passes else 0 ) diff --git a/src/q8s/runtime/mlflow/qrisp/autologging.py b/src/q8s/runtime/mlflow/qrisp/autologging.py index c912299..b1daba3 100644 --- a/src/q8s/runtime/mlflow/qrisp/autologging.py +++ b/src/q8s/runtime/mlflow/qrisp/autologging.py @@ -2,9 +2,11 @@ import time from importlib.metadata import version +from mlflow import ActiveRun from mlflow.utils.autologging_utils import autologging_integration, safe_patch from qrisp import PassManager, QuantumCircuit +from q8s.runtime.mlflow.qiskit.autologging import log_to_mlflow from q8s.runtime.qprov.record import ( CompilationProvenance, QProvRecord, @@ -36,6 +38,9 @@ def autolog( ) -> None: """Enables autologging for QRISPs.""" + if disable: + return + def patched_pass_manager_run( original, # type: ignore[no-untyped-def] instance: PassManager, @@ -87,11 +92,29 @@ def patched_pass_manager_run( ctx.compilation.duration_s = end - start - print(ctx.to_json(indent=2)) - return result - if disable: - return - safe_patch("qrisp", PassManager, "run", patched_pass_manager_run) + + def patched_activerun_exit(original, *args, **kwargs): + """Patch the __exit__ method of ActiveRun to log the QProvRecord to MLflow when + the run ends.""" + ctx = _current_context.get() + + if ctx is None or not isinstance(ctx, QProvRecord): + raise RuntimeError( + "No active autolog context. Please call transpile() first." + ) + + log_to_mlflow(ctx) + + return original(*args, **kwargs) + + safe_patch( + "qrisp", + ActiveRun, + "__exit__", + patched_activerun_exit, + manage_run=False, + # extra_tags=extra_tags, + ) diff --git a/test-qrisp.py b/test-qrisp.py index dd52a2c..13d80f4 100644 --- a/test-qrisp.py +++ b/test-qrisp.py @@ -1,5 +1,11 @@ # flake8: noqa: E402 +import mlflow + +from q8s.runtime.mlflow.qrisp import autolog + +autolog() + from qrisp import ( PassManager, QuantumCircuit, @@ -8,31 +14,32 @@ fuse_adjacents, ) -from q8s.runtime.mlflow.qrisp import autolog from q8s.runtime.mlflow.qrisp.autologging import get_context from q8s.runtime.qprov.graphs import plot_transpilation_timeline -autolog() +mlflow.set_experiment("qrisp-transpilation") -qc = QuantumCircuit(2) -qc.cx(0, 1) -qc.cx(0, 1) # Self-inverse — will be cancelled -qc.h(0) -qc.h(0) # Another self-inverse pair +with mlflow.start_run(): -print("Before:", qc, sep="\n") + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(0, 1) # Self-inverse — will be cancelled + qc.h(0) + qc.h(0) # Another self-inverse pair -pm = PassManager() -pm += fuse_adjacents -pm += commute_swaps -pm += combine_single_qubit_gates + # print("Before:", qc, sep="\n") -optimized_qc = pm.run(qc) -print("After:", optimized_qc, sep="\n") + pm = PassManager() + pm += fuse_adjacents + pm += commute_swaps + pm += combine_single_qubit_gates -fig = plot_transpilation_timeline( - passes=get_context().compilation.passes, - figsize=(16, 8), -) + optimized_qc = pm.run(qc) + # print("After:", optimized_qc, sep="\n") + + # fig = plot_transpilation_timeline( + # passes=get_context().compilation.passes, + # figsize=(16, 8), + # ) -fig.savefig("transpilation_timeline.png", dpi=300, bbox_inches="tight") + # fig.savefig("transpilation_timeline.png", dpi=300, bbox_inches="tight") diff --git a/tests/test_qrisp_autologging.py b/tests/test_qrisp_autologging.py index 6e9f240..686cdd1 100644 --- a/tests/test_qrisp_autologging.py +++ b/tests/test_qrisp_autologging.py @@ -63,7 +63,7 @@ def test_enabled_autolog_installs_patch(self): with patch.object(autologging, "safe_patch") as safe_patch: autologging.autolog(disable=False) - safe_patch.assert_called_once() + self.assertEqual(safe_patch.call_count, 2) def test_patch_records_circuit_and_compilation_duration(self): From 266e20dfebc7c4188fbd7be55f631c214349b728 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 17:59:42 +0300 Subject: [PATCH 04/13] updated readme --- README.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 3 ++ 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 01a08ab..beba81d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,39 @@ # q8s.runtime -A runtime library for q8s workloads. +`q8s.runtime` provides common runtime, provenance, and experiment-tracking capabilities for quantum software. + +The library provides a QDK-independent representation of quantum programs and their execution metadata, together with integrations for quantum development kits such as [Qiskit](https://www.ibm.com/quantum/qiskit) and [Qrisp](https://www.qrisp.eu/index.html). This makes it possible to collect and analyse execution and compilation information consistently across different quantum software stacks. ## Installation -You can install the q8s.runtime library using pip: +Install the core package using: ```bash pip install q8s.runtime ``` -## Features +Support for individual quantum development kits can be installed using the corresponding optional dependencies: + +```bash +pip install "q8s.runtime[qiskit]" +pip install "q8s.runtime[qrisp]" +``` + +Multiple integrations can be installed together: + +```bash +pip install "q8s.runtime[qiskit,qrisp]" +``` + +## Integrations + +### Qiskit + +The Qiskit integration provides adapters for Qiskit objects and automatic experiment tracking through MLflow. -### Qiskit autologging to MLflow +#### MLflow autologging + +Qiskit transpilation and execution can be automatically captured by enabling autologging: ```python import mlflow @@ -48,3 +69,93 @@ with mlflow.start_run(): result.get_counts() ``` + +The integration can capture information about the transpilation process, including individual transpiler passes and their associated metadata. + +### Qrisp + +The Qrisp integration converts Qrisp programs into the common `q8s.runtime` representation, allowing provenance and experiment information produced by Qrisp workflows to be handled using the same model as Qiskit workflows. + +```python +# Qrisp program +... + +# Convert/import into the q8s.runtime representation +... +``` + +## Capabilities + +### Provenance + +`q8s.runtime` uses the **QProv provenance model** to describe the information associated with the lifecycle of a quantum program. + +QProv organizes provenance information into four main categories: + +| QProv category | Description | Qiskit | Qrisp | +| -------------------- | ----------------------------------------------------------------- | :----: | :---: | +| **Quantum Circuit** | Structure and characteristics of the quantum circuit | ◐ | ◐ | +| **Quantum Computer** | Characteristics of the quantum computer or execution backend | - | - | +| **Compilation** | Transformation of a quantum circuit for a target quantum computer | ◐ | ◐ | +| **Execution** | Information associated with executing the compiled circuit | - | - | + +The availability of individual provenance attributes depends on the QDK, backend, provider, and application. + +### Quantum Circuit + +Quantum Circuit provenance describes the structure and characteristics of the quantum circuit being executed. + +| QProv | Provenance attribute | Qiskit | Qrisp | +| ------ | -------------------- | :----: | :---: | +| **Q1** | Gates | ✓ | ✓ | +| **Q2** | Measurements | ✓ | ✓ | +| **Q3** | Execution order | - | - | +| **Q4** | Circuit width | ✓ | ✓ | +| **Q5** | Circuit depth | ✓ | ✓ | +| **Q6** | Circuit size | ✓ | ✓ | +| **Q7** | Encoding | — | — | + +Circuit width represents the number of qubits used by the circuit, circuit depth describes the number of sequential operations required by the circuit, and circuit size describes its number of operations. + +### Compilation + +Compilation provenance describes how an abstract quantum circuit is transformed into a circuit that can be executed by a particular quantum computer. + +| QProv | Provenance attribute | Qiskit | Qrisp | +| ------ | -------------------- | :----: | :---: | +| **C1** | Qubit assignments | ✓ | ✓ | +| **C2** | Gate mappings | ✓ | ✓ | +| **C3** | Optimisation goal | ✓ | - | +| **C4** | Random seed | ✓ | - | +| **C5** | Compilation time | ✓ | ✓ | + +In addition to the QProv compilation attributes, `q8s.runtime` toolkit collects **fine-grained compiler provenance**. + +For each transpiler pass, the following information can be recorded: + +| Compiler provenance | Description | Qiskit | Qrisp | +| ------------------- | ------------------------------------------------- | :----: | :---: | +| **Pass index** | Position of the pass in the transpilation process | ✓ | ✓ | +| **Pass name** | Transpiler pass name | ✓ | ✓ | +| **Stage** | Stage of the staged pass manager | ✓ | - | +| **Duration** | Execution time of the pass | ✓ | ✓ | +| **Circuit depth** | Circuit depth after the pass | ✓ | ✓ | +| **Circuit size** | Circuit size after the pass | ✓ | ✓ | + +This extends QProv's compilation provenance with information about the internal compilation process and enables reconstruction and visualization of a **transpilation timeline**. + +**Legend:** ✓ supported · ◐ dependent on QDK/backend/application · — not currently collected + +## References + +The provenance model implemented by `q8s.runtime` is based on: + +> Weder, B., Breitenbücher, U., Leymann, F., and Wild, K. +> _Integrating quantum computing into workflow modeling and execution._ +> IET Quantum Communication. + +See the QProv publication for the complete provenance model and definitions. + +## License + +`q8s.runtime` is licensed under the Apache License 2.0. diff --git a/pyproject.toml b/pyproject.toml index a0b3853..53ecbc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,9 @@ matplotlib = ["matplotlib"] development = ["pre-commit", "pytest", "pytest-cov"] +qiskit = ["qiskit"] +qrisp = ["qrisp"] + test = ["mqt.bench", "qiskit<2.2", "mlflow", "iqm-client[qiskit]"] [build-system] requires = ["setuptools >= 61.0"] From 93d012c8dbba35ef8fd9acb8ae1cefb6cd90adeb Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 19:14:51 +0300 Subject: [PATCH 05/13] added better qrisp examples --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- test-qrisp.py | 59 +++++++++++++++++++++++++------------------ 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index beba81d..9537513 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,39 @@ The integration can capture information about the transpilation process, includi The Qrisp integration converts Qrisp programs into the common `q8s.runtime` representation, allowing provenance and experiment information produced by Qrisp workflows to be handled using the same model as Qiskit workflows. ```python -# Qrisp program -... +import mlflow + +from q8s.runtime.mlflow.qrisp import autolog + +autolog() + +from qrisp import ( + PassManager, + QuantumCircuit, + combine_single_qubit_gates, + commute_swaps, + fuse_adjacents, +) + +from q8s.runtime.mlflow.qrisp.autologging import get_context +from q8s.runtime.qprov.graphs import plot_transpilation_timeline + +mlflow.set_experiment("qrisp-transpilation") + +with mlflow.start_run(): + + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(0, 1) # Self-inverse — will be cancelled + qc.h(0) + qc.h(0) # Another self-inverse pair -# Convert/import into the q8s.runtime representation -... + pm = PassManager() + pm += fuse_adjacents + pm += commute_swaps + pm += combine_single_qubit_gates + + optimized_qc = pm.run(qc) ``` ## Capabilities @@ -159,3 +187,37 @@ See the QProv publication for the complete provenance model and definitions. ## License `q8s.runtime` is licensed under the Apache License 2.0. + +## Provenance + +### Quantum Computer + +Quantum Computer provenance describes the characteristics of the quantum computer on which a circuit is executed. + +| QProv | Provenance attribute | Qiskit | Qrisp | +| ------- | ------------------------- | :----: | :---: | +| **QC1** | Number of qubits | ✓ | ✓ | +| **QC2** | Decoherence times (T1/T2) | ◐ | ◐ | +| **QC3** | Qubit connectivity | ✓ | ✓ | +| **QC4** | Gate set | ✓ | ✓ | +| **QC5** | Gate fidelities | ◐ | ◐ | +| **QC6** | Gate times | ◐ | ◐ | +| **QC7** | Readout fidelities | ◐ | ◐ | + +The availability of hardware properties depends on the selected backend and provider. In particular, calibration information such as decoherence times, gate fidelities, and readout fidelities may not be exposed by every backend. + +### Execution + +Execution provenance captures information generated when a compiled quantum circuit is executed. + +| QProv | Provenance attribute | Qiskit | Qrisp | +| ------ | ------------------------ | :----: | :---: | +| **E1** | Input data | ✓ | ✓ | +| **E2** | Output data | ✓ | ✓ | +| **E3** | Number of shots | ✓ | ✓ | +| **E4** | Intermediate results | ◐ | ◐ | +| **E5** | Number of iterations | ◐ | ◐ | +| **E6** | Execution time | ✓ | ✓ | +| **E7** | Readout-error mitigation | ◐ | ◐ | + +Intermediate results and iteration counts are particularly relevant for hybrid and variational quantum algorithms and are available when exposed by the application or QDK. diff --git a/test-qrisp.py b/test-qrisp.py index 13d80f4..187327a 100644 --- a/test-qrisp.py +++ b/test-qrisp.py @@ -6,40 +6,51 @@ autolog() +from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite +from iqm.qrisp_iqm import create_iqm_pass_manager from qrisp import ( - PassManager, + Clbit, QuantumCircuit, - combine_single_qubit_gates, - commute_swaps, - fuse_adjacents, + Qubit, ) -from q8s.runtime.mlflow.qrisp.autologging import get_context -from q8s.runtime.qprov.graphs import plot_transpilation_timeline -mlflow.set_experiment("qrisp-transpilation") +def build_demo_circuit() -> QuantumCircuit: + """Build a small demo circuit with 2-qubit interactions. -with mlflow.start_run(): + https://docs.iqm.tech/iqm-client/user_guide_qrisp/plasma_sabre_tutorial.html#demo-circuit. + """ + qc = QuantumCircuit() + + # Give logical qubits distinctive names + for i in range(4): + qc.add_qubit(Qubit("original_qb_" + str(i))) + + for i in range(4): + qc.add_clbit(Clbit("c" + str(i))) - qc = QuantumCircuit(2) - qc.cx(0, 1) - qc.cx(0, 1) # Self-inverse — will be cancelled qc.h(0) - qc.h(0) # Another self-inverse pair + qc.cx(0, 1) + qc.ry(0.7, 2) + qc.cz(1, 2) + qc.cx(2, 3) + qc.s(1) + qc.cy(0, 2) - # print("Before:", qc, sep="\n") + # Add measurements for execution workflows + qc.measure(qc.qubits, qc.clbits) + return qc - pm = PassManager() - pm += fuse_adjacents - pm += commute_swaps - pm += combine_single_qubit_gates - optimized_qc = pm.run(qc) - # print("After:", optimized_qc, sep="\n") +mlflow.set_experiment("qrisp-transpilation") + +backend = IQMFakeAphrodite() + +connectivity = backend.target.build_coupling_map() - # fig = plot_transpilation_timeline( - # passes=get_context().compilation.passes, - # figsize=(16, 8), - # ) +with mlflow.start_run(): + qc = build_demo_circuit() + + pm = create_iqm_pass_manager(connectivity=connectivity) - # fig.savefig("transpilation_timeline.png", dpi=300, bbox_inches="tight") + optimized_qc = pm.run(qc) From ab375ca5af66109f2eb7d670396a49ed87439580 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 20:40:24 +0300 Subject: [PATCH 06/13] update --- README.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/README.md b/README.md index 9537513..a0be9a5 100644 --- a/README.md +++ b/README.md @@ -187,37 +187,3 @@ See the QProv publication for the complete provenance model and definitions. ## License `q8s.runtime` is licensed under the Apache License 2.0. - -## Provenance - -### Quantum Computer - -Quantum Computer provenance describes the characteristics of the quantum computer on which a circuit is executed. - -| QProv | Provenance attribute | Qiskit | Qrisp | -| ------- | ------------------------- | :----: | :---: | -| **QC1** | Number of qubits | ✓ | ✓ | -| **QC2** | Decoherence times (T1/T2) | ◐ | ◐ | -| **QC3** | Qubit connectivity | ✓ | ✓ | -| **QC4** | Gate set | ✓ | ✓ | -| **QC5** | Gate fidelities | ◐ | ◐ | -| **QC6** | Gate times | ◐ | ◐ | -| **QC7** | Readout fidelities | ◐ | ◐ | - -The availability of hardware properties depends on the selected backend and provider. In particular, calibration information such as decoherence times, gate fidelities, and readout fidelities may not be exposed by every backend. - -### Execution - -Execution provenance captures information generated when a compiled quantum circuit is executed. - -| QProv | Provenance attribute | Qiskit | Qrisp | -| ------ | ------------------------ | :----: | :---: | -| **E1** | Input data | ✓ | ✓ | -| **E2** | Output data | ✓ | ✓ | -| **E3** | Number of shots | ✓ | ✓ | -| **E4** | Intermediate results | ◐ | ◐ | -| **E5** | Number of iterations | ◐ | ◐ | -| **E6** | Execution time | ✓ | ✓ | -| **E7** | Readout-error mitigation | ◐ | ◐ | - -Intermediate results and iteration counts are particularly relevant for hybrid and variational quantum algorithms and are available when exposed by the application or QDK. From aa7d792982cc944daed4a917c55c5e2b33e26674 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 20:43:38 +0300 Subject: [PATCH 07/13] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0be9a5..ff0594c 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Quantum Circuit provenance describes the structure and characteristics of the qu | **Q4** | Circuit width | ✓ | ✓ | | **Q5** | Circuit depth | ✓ | ✓ | | **Q6** | Circuit size | ✓ | ✓ | -| **Q7** | Encoding | — | — | +| **Q7** | Encoding | - | - | Circuit width represents the number of qubits used by the circuit, circuit depth describes the number of sequential operations required by the circuit, and circuit size describes its number of operations. From e0d5a58dfc8e1cac1f469fe52a8cb46e95a8c392 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 20:46:09 +0300 Subject: [PATCH 08/13] enable tests --- .github/workflows/publish.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0fec11f..a4f8daa 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -24,17 +24,17 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - # - name: Install dependencies - # run: >- - # python3 -m - # pip install - # .[development] - # - name: Run tests with coverage - # run: >- - # python3 -m - # pytest - # --cov=q8s - # --cov-report=xml + - name: Install dependencies + run: >- + python3 -m + pip install + .[development,qiskit,qrisp] + - name: Run tests with coverage + run: >- + python3 -m + pytest + --cov=q8s + --cov-report=xml # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v5 # if: matrix.python-version == '3.10' @@ -158,4 +158,4 @@ jobs: with: repository-url: https://test.pypi.org/legacy/ skip-existing: true - verbose: true \ No newline at end of file + verbose: true From defa60e0fc132e16e11a4ab9bb81c46bb956312f Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 20:51:40 +0300 Subject: [PATCH 09/13] added qiskit-aer dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 53ecbc6..4ea343a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ matplotlib = ["matplotlib"] development = ["pre-commit", "pytest", "pytest-cov"] -qiskit = ["qiskit"] +qiskit = ["qiskit", "qiskit-aer"] qrisp = ["qrisp"] test = ["mqt.bench", "qiskit<2.2", "mlflow", "iqm-client[qiskit]"] From ce5847feb7925acb99c419c06f44f347e93a8e73 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 20:56:37 +0300 Subject: [PATCH 10/13] remove python 3.10 --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index a4f8daa..527263d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -13,7 +13,7 @@ jobs: strategy: matrix: python-version: - - "3.10" + # - "3.10" - "3.11" - "3.12" - "3.13" From b0d0bc069f408cbed295e1e04c0ccbd685bb21ca Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 21:02:54 +0300 Subject: [PATCH 11/13] fixed python version --- .github/workflows/publish.yaml | 3 +-- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 527263d..c87d8ab 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -13,7 +13,6 @@ jobs: strategy: matrix: python-version: - # - "3.10" - "3.11" - "3.12" - "3.13" @@ -33,7 +32,7 @@ jobs: run: >- python3 -m pytest - --cov=q8s + --cov=q8s.runtime --cov-report=xml # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v5 diff --git a/pyproject.toml b/pyproject.toml index 4ea343a..faeb851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", From bcebbaf0743ccab482ec2087e3404bacc2b108c4 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 23:14:23 +0300 Subject: [PATCH 12/13] added license header --- .gitignore | 2 +- .pre-commit-config.yaml | 26 +++++++++++++++ src/q8s/runtime/__init__.py | 16 +++++++++ src/q8s/runtime/matplotlib/backend.py | 18 +++++++++- src/q8s/runtime/mlflow/qiskit/__init__.py | 15 +++++++++ src/q8s/runtime/mlflow/qiskit/autologging.py | 16 +++++++++ src/q8s/runtime/mlflow/qiskit/transpiler.py | 35 +++++++++++++++----- src/q8s/runtime/mlflow/qrisp/__init__.py | 15 +++++++++ src/q8s/runtime/mlflow/qrisp/autologging.py | 16 +++++++++ src/q8s/runtime/qprov/graphs.py | 19 +++++++++-- src/q8s/runtime/qprov/record.py | 16 +++++++++ src/q8s/runtime/tracking.py | 22 ++++++++++-- tests/test_qrisp_autologging.py | 16 +++++++++ 13 files changed, 217 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index dfa239a..87cea00 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ mlartifacts/ mlflow.db .envrc -out.txt \ No newline at end of file +out.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed4725b..cf61214 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + exclude: '^docs/conf.py' repos: @@ -75,3 +89,15 @@ repos: # rev: v2.3.0 # hooks: # - id: codespell + +- repo: https://github.com/google/addlicense + rev: v1.2.0 + hooks: + - id: addlicense + args: ["-c", "Qubernetes Project", "-s", "*.py"] + # - -c + # - Qubernetes Project + # - -l + # - apache + # - "*.py" + files: ^(src|tests)/.*\.py$ diff --git a/src/q8s/runtime/__init__.py b/src/q8s/runtime/__init__.py index 387d5ae..5f0b272 100644 --- a/src/q8s/runtime/__init__.py +++ b/src/q8s/runtime/__init__.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + from importlib.metadata import version __version__ = version("q8s.runtime") diff --git a/src/q8s/runtime/matplotlib/backend.py b/src/q8s/runtime/matplotlib/backend.py index beb4163..a7ebb48 100644 --- a/src/q8s/runtime/matplotlib/backend.py +++ b/src/q8s/runtime/matplotlib/backend.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import base64 import io @@ -41,4 +57,4 @@ def show(): # Register the custom show function -matplotlib.backend_bases.show = show \ No newline at end of file +matplotlib.backend_bases.show = show diff --git a/src/q8s/runtime/mlflow/qiskit/__init__.py b/src/q8s/runtime/mlflow/qiskit/__init__.py index 8ee8816..d0b96fe 100644 --- a/src/q8s/runtime/mlflow/qiskit/__init__.py +++ b/src/q8s/runtime/mlflow/qiskit/__init__.py @@ -1,3 +1,18 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 """MLflow autologging integration for Qiskit.""" from q8s.runtime.mlflow.qiskit.autologging import autolog diff --git a/src/q8s/runtime/mlflow/qiskit/autologging.py b/src/q8s/runtime/mlflow/qiskit/autologging.py index ea6c3b9..629f22d 100644 --- a/src/q8s/runtime/mlflow/qiskit/autologging.py +++ b/src/q8s/runtime/mlflow/qiskit/autologging.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import contextvars import tempfile import time diff --git a/src/q8s/runtime/mlflow/qiskit/transpiler.py b/src/q8s/runtime/mlflow/qiskit/transpiler.py index b6d07ce..9b6fdaf 100644 --- a/src/q8s/runtime/mlflow/qiskit/transpiler.py +++ b/src/q8s/runtime/mlflow/qiskit/transpiler.py @@ -1,9 +1,25 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from qiskit.passmanager import FlowControllerLinear, GenericPass from qiskit.transpiler import ( ConditionalController, DoWhileController, StagedPassManager, ) -from qiskit.passmanager import FlowControllerLinear, GenericPass def process_pass( @@ -12,13 +28,15 @@ def process_pass( ), stage_info: list[int], ): - """ - Recursively process a pass or controller and collect the IDs of all passes in the stage_info list. + """Recursively process a pass or controller and collect the IDs of all passes in the + stage_info list. + Args: task: The pass or controller to process. stage_info: A list to collect the IDs of all passes. Returns: - The updated stage_info list containing the IDs of all passes.""" + The updated stage_info list containing the IDs of all passes. + """ if ( isinstance(task, ConditionalController) or isinstance(task, DoWhileController) @@ -36,8 +54,9 @@ def process_pass( def process_staged_pass_manager(manager: StagedPassManager) -> dict[str, list[int]]: - """ - Process a StagedPassManager and return a dictionary containing the stage names and their corresponding pass IDs. + """Process a StagedPassManager and return a dictionary containing the stage names + and their corresponding pass IDs. + Args: manager (StagedPassManager): The StagedPassManager to process. Returns: @@ -56,8 +75,8 @@ def process_staged_pass_manager(manager: StagedPassManager) -> dict[str, list[in def find_stage_by_id(stage_pass_info: dict[str, list[int]], pass_id: int) -> str | None: - """ - Find the stage name corresponding to a given pass ID in the staged pass manager. + """Find the stage name corresponding to a given pass ID in the staged pass manager. + Args: stage_pass_info (dict): A dictionary mapping stage names to lists of pass IDs. pass_id (int): The ID of the pass to find. diff --git a/src/q8s/runtime/mlflow/qrisp/__init__.py b/src/q8s/runtime/mlflow/qrisp/__init__.py index a1e49c5..601986f 100644 --- a/src/q8s/runtime/mlflow/qrisp/__init__.py +++ b/src/q8s/runtime/mlflow/qrisp/__init__.py @@ -1,3 +1,18 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 """MLflow autologging integration for Qrisp.""" from q8s.runtime.mlflow.qrisp.autologging import autolog diff --git a/src/q8s/runtime/mlflow/qrisp/autologging.py b/src/q8s/runtime/mlflow/qrisp/autologging.py index b1daba3..8af8eb9 100644 --- a/src/q8s/runtime/mlflow/qrisp/autologging.py +++ b/src/q8s/runtime/mlflow/qrisp/autologging.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import contextvars import time from importlib.metadata import version diff --git a/src/q8s/runtime/qprov/graphs.py b/src/q8s/runtime/qprov/graphs.py index 8e81ad4..05e5aa7 100644 --- a/src/q8s/runtime/qprov/graphs.py +++ b/src/q8s/runtime/qprov/graphs.py @@ -1,10 +1,25 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import matplotlib.pyplot as plt from matplotlib import colormaps def plot_transpilation_timeline(passes, figsize=(16, 8)): - """ - Plot a transpilation timeline. + """Plot a transpilation timeline. Parameters ---------- diff --git a/src/q8s/runtime/qprov/record.py b/src/q8s/runtime/qprov/record.py index c1366c2..7241099 100644 --- a/src/q8s/runtime/qprov/record.py +++ b/src/q8s/runtime/qprov/record.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + from __future__ import annotations from dataclasses import asdict, dataclass, field diff --git a/src/q8s/runtime/tracking.py b/src/q8s/runtime/tracking.py index 6df66df..8fd2f6c 100644 --- a/src/q8s/runtime/tracking.py +++ b/src/q8s/runtime/tracking.py @@ -1,8 +1,24 @@ -import time +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import json import tempfile -from pathlib import Path +import time from dataclasses import dataclass +from pathlib import Path from typing import Any import mlflow @@ -102,7 +118,7 @@ def _log_artifacts(self, original: QuantumCircuit, transpiled: QuantumCircuit): try: input_qasm.write_text(original.qasm()) output_qasm.write_text(transpiled.qasm()) - except AttributeError as e: + except AttributeError: from qiskit.qasm3 import dumps input_qasm.write_text(dumps(original)) diff --git a/tests/test_qrisp_autologging.py b/tests/test_qrisp_autologging.py index 686cdd1..ec81bb0 100644 --- a/tests/test_qrisp_autologging.py +++ b/tests/test_qrisp_autologging.py @@ -1,3 +1,19 @@ +# Copyright 2026 Qubernetes Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + import unittest from unittest.mock import patch From 61c1f1e9aca2c8341837db880c302144dc264743 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 17 Aug 2026 23:32:27 +0300 Subject: [PATCH 13/13] cleanup --- src/q8s/runtime/tracking.py | 149 ------------------------------------ test.py | 28 ------- 2 files changed, 177 deletions(-) delete mode 100644 src/q8s/runtime/tracking.py delete mode 100644 test.py diff --git a/src/q8s/runtime/tracking.py b/src/q8s/runtime/tracking.py deleted file mode 100644 index 8fd2f6c..0000000 --- a/src/q8s/runtime/tracking.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2026 Qubernetes Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 - -import json -import tempfile -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import mlflow -from qiskit import QuantumCircuit -from qiskit.transpiler import StagedPassManager, generate_preset_pass_manager - -from q8s.runtime.mlflow.qiskit.autologging import get_context - - -@dataclass -class MLflowTranspilationManager: - experiment_name: str = "qiskit-transpilation" - optimization_level: int = 1 - backend: Any | None = None - basis_gates: list[str] | None = None - coupling_map: Any | None = None - - def __post_init__(self): - mlflow.set_experiment(self.experiment_name) - - def transpile( - self, circuit: QuantumCircuit, run_name: str | None = None - ) -> QuantumCircuit: - with mlflow.start_run(run_name=run_name): - self._log_input(circuit) - - mlflow.log_params( - { - "optimization_level": self.optimization_level, - "backend": getattr(self.backend, "name", None) or str(self.backend), - "basis_gates": json.dumps(self.basis_gates), - "num_qubits_input": circuit.num_qubits, - } - ) - - pass_manager: StagedPassManager = generate_preset_pass_manager( - optimization_level=self.optimization_level, - backend=self.backend, - basis_gates=self.basis_gates, - coupling_map=self.coupling_map, - ) - - start = time.perf_counter() - transpiled = pass_manager.run(circuit) - elapsed = time.perf_counter() - start - - self._log_output(circuit, transpiled, elapsed) - self._log_artifacts(circuit, transpiled) - - return transpiled - - def _log_input(self, circuit: QuantumCircuit): - mlflow.log_metrics( - { - "input_depth": circuit.depth(), - "input_size": circuit.size(), - "input_width": circuit.width(), - "input_num_qubits": circuit.num_qubits, - "input_num_clbits": circuit.num_clbits, - } - ) - - for gate, count in circuit.count_ops().items(): - mlflow.log_metric(f"input_gate_{gate}", count) - - def _log_output( - self, - original: QuantumCircuit, - transpiled: QuantumCircuit, - elapsed: float, - ): - mlflow.log_metrics( - { - "transpilation_time_s": elapsed, - "output_depth": transpiled.depth(), - "output_size": transpiled.size(), - "output_width": transpiled.width(), - "output_num_qubits": transpiled.num_qubits, - "output_num_clbits": transpiled.num_clbits, - "depth_delta": transpiled.depth() - original.depth(), - "size_delta": transpiled.size() - original.size(), - } - ) - - for gate, count in transpiled.count_ops().items(): - mlflow.log_metric(f"output_gate_{gate}", count) - - def _log_artifacts(self, original: QuantumCircuit, transpiled: QuantumCircuit): - with tempfile.TemporaryDirectory() as tmpdir: - tmp = Path(tmpdir) - - input_qasm = tmp / "input_circuit.qasm" - output_qasm = tmp / "transpiled_circuit.qasm" - summary = tmp / "transpilation_summary.json" - qprov = tmp / "qprov_record.json" - - try: - input_qasm.write_text(original.qasm()) - output_qasm.write_text(transpiled.qasm()) - except AttributeError: - from qiskit.qasm3 import dumps - - input_qasm.write_text(dumps(original)) - output_qasm.write_text(dumps(transpiled)) - - summary.write_text( - json.dumps( - { - "input": { - "depth": original.depth(), - "size": original.size(), - "count_ops": dict(original.count_ops()), - }, - "output": { - "depth": transpiled.depth(), - "size": transpiled.size(), - "count_ops": dict(transpiled.count_ops()), - }, - }, - indent=2, - ) - ) - - ctx = get_context() - - qprov.write_text(ctx.to_json(indent=2, sort_keys=True)) - - mlflow.log_artifacts(tmp) diff --git a/test.py b/test.py deleted file mode 100644 index f4d3ce8..0000000 --- a/test.py +++ /dev/null @@ -1,28 +0,0 @@ -import q8s.runtime -from mqt.bench import BenchmarkLevel, get_benchmark - -from iqm.qiskit_iqm.fake_backends.fake_aphrodite import IQMFakeAphrodite - -from q8s.runtime.mlflow.qiskit import autolog - -autolog() - -from q8s.runtime.tracking import MLflowTranspilationManager - -print("q8s.runtime version:", q8s.runtime.__version__) - -qc = get_benchmark( - benchmark="ghz", - level=BenchmarkLevel.ALG, - circuit_size=12, -) - -backend = IQMFakeAphrodite() - -manager = MLflowTranspilationManager( - experiment_name="qubernetes-transpilation", - optimization_level=1, - backend=backend, -) - -tqc = manager.transpile(qc, run_name="qft-8-opt3")