From 5f88f8918ace5912cdd52789174bbc77e93a52c2 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Thu, 24 Sep 2026 21:28:16 +0300 Subject: [PATCH 1/8] capture ucc in qiskit context --- pyproject.toml | 1 + src/q8s/runtime/mlflow/qiskit/autologging.py | 286 +--------------- src/q8s/runtime/mlflow/qiskit/utils.py | 324 +++++++++++++++++++ src/q8s/runtime/mlflow/ucc/__init__.py | 20 ++ src/q8s/runtime/mlflow/ucc/autologging.py | 53 +++ 5 files changed, 400 insertions(+), 284 deletions(-) create mode 100644 src/q8s/runtime/mlflow/qiskit/utils.py create mode 100644 src/q8s/runtime/mlflow/ucc/__init__.py create mode 100644 src/q8s/runtime/mlflow/ucc/autologging.py diff --git a/pyproject.toml b/pyproject.toml index 905aaae..bd9ada1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ development = ["pre-commit", "pytest", "pytest-cov"] qiskit = ["qiskit", "qiskit-aer"] qrisp = ["qrisp"] +ucc = ["ucc"] test = ["mqt.bench", "qiskit<2.2", "mlflow", "iqm-client[qiskit]"] [build-system] diff --git a/src/q8s/runtime/mlflow/qiskit/autologging.py b/src/q8s/runtime/mlflow/qiskit/autologging.py index 629f22d..026d2e8 100644 --- a/src/q8s/runtime/mlflow/qiskit/autologging.py +++ b/src/q8s/runtime/mlflow/qiskit/autologging.py @@ -14,289 +14,7 @@ # # SPDX-License-Identifier: Apache-2.0 -import contextvars -import tempfile -import time -from pathlib import Path -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 q8s.runtime.mlflow.qiskit.utils import create_autolog -from q8s.runtime.mlflow.qiskit.transpiler import ( - find_stage_by_id, - process_staged_pass_manager, -) -from q8s.runtime.qprov.graphs import plot_transpilation_timeline -from q8s.runtime.qprov.record import ( - CompilationProvenance, - QProvRecord, - QuantumCircuitProvenance, - QuantumComputerProvenance, -) - -CONTEXT_NAME = "qiskit_mlflow_autologging_context" - -_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( - CONTEXT_NAME, - default=QProvRecord( - compilation=CompilationProvenance( - compiler="qiskit", compiler_version=qiskit.version.VERSION - ) - ), -) - - -def get_context() -> QProvRecord: - ctx = _current_context.get() - if ctx is None: - raise RuntimeError("No active autolog context") - return ctx - - -@autologging_integration("qiskit") -def autolog( - # log_passes=True, - disable=False, - silent=False, - extra_tags=None, -) -> None: - """Enables the Qiskit autologging integration. - - This function is idempotent and can be called multiple times. The integration will - only be enabled once. - """ - - def patched_staged_pass_manager_run( - original: StagedPassManager.run, # type: ignore[no-untyped-def] - instance: StagedPassManager, - *args, - **kwargs, - ): - if disable: - return original(*args, **kwargs) - - print("Qiskit autologging integration is enabled for StagedPassManager.run.") - - start = time.perf_counter() - - circuit: QuantumCircuit = args[0] - - ctx = _current_context.get() - - ctx.circuit = QuantumCircuitProvenance( - circuit_id=str(id(circuit)), - name=circuit.name, - num_qubits=circuit.num_qubits, - depth=circuit.depth(), - width=circuit.width(), - size=circuit.size(), - gate_counts=dict(circuit.count_ops()), - ) - - if ctx is None or not isinstance(ctx, QProvRecord): - raise RuntimeError( - "No active autolog context. Please call transpile() first." - ) - - if not silent: - print("Logging Qiskit transpilation run to MLflow.") - - kwargs["callback"] = callback - - ctx.compilation.metadata["stages_pass_info"] = process_staged_pass_manager( - instance - ) - - history = original(instance, *args, **kwargs) - - ctx.compilation.duration_s = time.perf_counter() - start - - return history - - safe_patch( - "qiskit", - qiskit.transpiler.StagedPassManager, - "run", - patched_staged_pass_manager_run, - manage_run=False, - extra_tags=extra_tags, - ) - - def patched_generate_preset_pass_manager( - original, # type: ignore[no-untyped-def] - *args, - **kwargs, - ): - """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): - raise RuntimeError( - "No active autolog context. Please call transpile() first." - ) - - optimization_level = kwargs.get("optimization_level", None) - - if optimization_level is not None: - ctx.compilation.optimization_level = optimization_level - mlflow.log_param("optimization_level", optimization_level) - - seed_transpiler = kwargs.get("seed_transpiler", None) - - if seed_transpiler is not None: - ctx.compilation.seed_transpiler = seed_transpiler - mlflow.log_param("seed_transpiler", seed_transpiler) - - backend = kwargs.get("backend", None) - - if backend is not None: - ctx.quantum_computer = QuantumComputerProvenance( - provider="IQM", - backend_name=backend.name, - num_qubits=getattr(backend, "num_qubits", None), - ) - mlflow.log_param("backend_name", backend.name) - - return original(*args, **kwargs) - - safe_patch( - "qiskit", - qiskit.transpiler, - "generate_preset_pass_manager", - patched_generate_preset_pass_manager, - manage_run=False, - extra_tags=extra_tags, - ) - - def patched_backend_run(original, self, *args, **kwargs): - job = original(self, *args, **kwargs) - - job_cls = job.__class__ - - def patched_result(original_result, job_self, *r_args, **r_kwargs): - result = original_result(job_self, *r_args, **r_kwargs) - - # log result here - try: - result.get_counts() - # print("counts:", counts) - # mlflow.log_dict(counts, "qiskit/result_counts.json") - except Exception: - pass - - return result - - safe_patch( - "qiskit", - job_cls, - "result", - patched_result, - ) - - return job - - safe_patch( - "qiskit", - AerSimulator, - "run", - patched_backend_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( - "qiskit", - ActiveRun, - "__exit__", - patched_activerun_exit, - manage_run=False, - extra_tags=extra_tags, - ) - - -def callback(pass_, dag, time, property_set, count): - """Callback function for logging pass information during transpilation.""" - name = pass_.__class__.__name__ - - ctx = _current_context.get() - - if ctx is None or not isinstance(ctx, QProvRecord): - raise RuntimeError("No active autolog context. Please call transpile() first.") - - pass_metadata = { - "depth": dag.depth(), - "size": dag.size(), - "stage": find_stage_by_id( - ctx.compilation.metadata["stages_pass_info"], id(pass_) - ), - } - - ctx.compilation.add_pass( - pass_name=name, - pass_index=count, - pass_duration_s=time, - pass_metadata=pass_metadata, - ) - - -def log_to_mlflow(record: QProvRecord): - """Logs the QProvRecord to MLflow.""" - - with tempfile.TemporaryDirectory() as tmpdir: - tmp = Path(tmpdir) - transpilation_timeline = tmp / "transpilation_timeline.png" - - qprov_file = tmp / "qprov_record.json" - qprov_file.write_text(record.to_json(indent=2, sort_keys=True)) - - fig = plot_transpilation_timeline(list(record.compilation.passes)) - fig.savefig(transpilation_timeline, bbox_inches="tight") - - run = mlflow.active_run() - if run is None: - raise RuntimeError("No active MLflow run. Please start a run first.") - - passes = sorted(record.compilation.passes, key=lambda p: p.pass_index) - - mlflow.log_metric("passes_count", len(passes)) - 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 - ) - mlflow.log_metric( - "circuit_size", passes[-1].pass_metadata.get("size", 0) if passes else 0 - ) - - mlflow.log_artifacts(tmp) +autolog = create_autolog() diff --git a/src/q8s/runtime/mlflow/qiskit/utils.py b/src/q8s/runtime/mlflow/qiskit/utils.py new file mode 100644 index 0000000..7bd80b0 --- /dev/null +++ b/src/q8s/runtime/mlflow/qiskit/utils.py @@ -0,0 +1,324 @@ +# 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 logging +import tempfile +import time +from importlib.metadata import version +from pathlib import Path +from typing import Callable + +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 q8s.runtime.mlflow.qiskit.transpiler import ( + find_stage_by_id, + process_staged_pass_manager, +) +from q8s.runtime.qprov.graphs import plot_transpilation_timeline +from q8s.runtime.qprov.record import ( + CompilationProvenance, + QProvRecord, + QuantumCircuitProvenance, + QuantumComputerProvenance, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +CONTEXT_NAME = "qiskit_mlflow_autologging_context" + +_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( + CONTEXT_NAME, + default=QProvRecord( + compilation=CompilationProvenance( + compiler="qiskit", compiler_version=version("qiskit") + ) + ), +) + + +def get_context() -> QProvRecord: + ctx = _current_context.get() + if ctx is None: + raise RuntimeError("No active autolog context") + return ctx + + +def create_autolog(integration_name: str = "qiskit") -> Callable[..., None]: + + @autologging_integration(integration_name) + def autolog( + # log_passes=True, + disable=False, + silent=False, + extra_tags=None, + ) -> None: + """Enables the Qiskit autologging integration. + + This function is idempotent and can be called multiple times. The integration + will only be enabled once. + """ + + _patch_qiskit(disable=disable, silent=silent, extra_tags=extra_tags) + + return autolog + + +def _patch_qiskit( + disable=False, + silent=False, + extra_tags=None, +): + def patched_staged_pass_manager_run( + original: StagedPassManager.run, # type: ignore[no-untyped-def] + instance: StagedPassManager, + *args, + **kwargs, + ): + if disable: + return original(*args, **kwargs) + + print("Qiskit autologging integration is enabled for StagedPassManager.run.") + + start = time.perf_counter() + + circuit: QuantumCircuit = args[0][0] if isinstance(args[0], list) else args[0] + + ctx = _current_context.get() + + ctx.circuit = QuantumCircuitProvenance( + circuit_id=str(id(circuit)), + name=circuit.name, + num_qubits=circuit.num_qubits, + depth=circuit.depth(), + width=circuit.width(), + size=circuit.size(), + gate_counts=dict(circuit.count_ops()), + ) + + if ctx is None or not isinstance(ctx, QProvRecord): + raise RuntimeError( + "No active autolog context. Please call transpile() first." + ) + + if not silent: + print("Logging Qiskit transpilation run to MLflow.") + + kwargs["callback"] = callback + + ctx.compilation.metadata["stages_pass_info"] = process_staged_pass_manager( + instance + ) + + history = original(instance, *args, **kwargs) + + ctx.compilation.duration_s = time.perf_counter() - start + + return history + + safe_patch( + "qiskit", + qiskit.transpiler.StagedPassManager, + "run", + patched_staged_pass_manager_run, + manage_run=False, + extra_tags=extra_tags, + ) + + logger.debug("patched StagedPassManager.run") + + def patched_generate_preset_pass_manager( + original, # type: ignore[no-untyped-def] + *args, + **kwargs, + ): + """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): + raise RuntimeError( + "No active autolog context. Please call transpile() first." + ) + + optimization_level = kwargs.get("optimization_level", None) + + if optimization_level is not None: + ctx.compilation.optimization_level = optimization_level + mlflow.log_param("optimization_level", optimization_level) + + seed_transpiler = kwargs.get("seed_transpiler", None) + + if seed_transpiler is not None: + ctx.compilation.seed_transpiler = seed_transpiler + mlflow.log_param("seed_transpiler", seed_transpiler) + + backend = kwargs.get("backend", None) + + if backend is not None: + ctx.quantum_computer = QuantumComputerProvenance( + provider="IQM", + backend_name=backend.name, + num_qubits=getattr(backend, "num_qubits", None), + ) + mlflow.log_param("backend_name", backend.name) + + return original(*args, **kwargs) + + safe_patch( + "qiskit", + qiskit.transpiler, + "generate_preset_pass_manager", + patched_generate_preset_pass_manager, + manage_run=False, + extra_tags=extra_tags, + ) + + logger.debug("Patched generate_preset_pass_manager") + + def patched_backend_run(original, self, *args, **kwargs): + job = original(self, *args, **kwargs) + + job_cls = job.__class__ + + def patched_result(original_result, job_self, *r_args, **r_kwargs): + result = original_result(job_self, *r_args, **r_kwargs) + + # log result here + try: + result.get_counts() + # print("counts:", counts) + # mlflow.log_dict(counts, "qiskit/result_counts.json") + except Exception: + pass + + return result + + safe_patch( + "qiskit", + job_cls, + "result", + patched_result, + ) + + return job + + safe_patch( + "qiskit", + AerSimulator, + "run", + patched_backend_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( + "qiskit", + ActiveRun, + "__exit__", + patched_activerun_exit, + manage_run=False, + extra_tags=extra_tags, + ) + + +def callback(pass_, dag, time, property_set, count): + """Callback function for logging pass information during transpilation.""" + name = pass_.__class__.__name__ + + ctx = _current_context.get() + + if ctx is None or not isinstance(ctx, QProvRecord): + raise RuntimeError("No active autolog context. Please call transpile() first.") + + pass_metadata = { + "depth": dag.depth(), + "size": dag.size(), + "stage": find_stage_by_id( + ctx.compilation.metadata["stages_pass_info"], id(pass_) + ), + } + + ctx.compilation.add_pass( + pass_name=name, + pass_index=count, + pass_duration_s=time, + pass_metadata=pass_metadata, + ) + + +def log_to_mlflow(record: QProvRecord): + """Logs the QProvRecord to MLflow.""" + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + transpilation_timeline = tmp / "transpilation_timeline.png" + + qprov_file = tmp / "qprov_record.json" + qprov_file.write_text(record.to_json(indent=2, sort_keys=True)) + + fig = plot_transpilation_timeline(list(record.compilation.passes)) + fig.savefig(transpilation_timeline, bbox_inches="tight") + + run = mlflow.active_run() + if run is None: + raise RuntimeError("No active MLflow run. Please start a run first.") + + passes = sorted(record.compilation.passes, key=lambda p: p.pass_index) + + mlflow.log_metric("passes_count", len(passes)) + 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 + ) + mlflow.log_metric( + "circuit_size", passes[-1].pass_metadata.get("size", 0) if passes else 0 + ) + + mlflow.log_artifacts(tmp) diff --git a/src/q8s/runtime/mlflow/ucc/__init__.py b/src/q8s/runtime/mlflow/ucc/__init__.py new file mode 100644 index 0000000..83cb504 --- /dev/null +++ b/src/q8s/runtime/mlflow/ucc/__init__.py @@ -0,0 +1,20 @@ +# 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 integration for ucc.""" + +from .autologging import autolog + +__all__ = ["autolog"] diff --git a/src/q8s/runtime/mlflow/ucc/autologging.py b/src/q8s/runtime/mlflow/ucc/autologging.py new file mode 100644 index 0000000..67fe947 --- /dev/null +++ b/src/q8s/runtime/mlflow/ucc/autologging.py @@ -0,0 +1,53 @@ +# 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 +from importlib.metadata import version + +from mlflow.utils.autologging_utils import autologging_integration + +from q8s.runtime.mlflow.qiskit.autologging import autolog as qiskit_autolog +from q8s.runtime.qprov.record import CompilationProvenance, QProvRecord + +CONTEXT_NAME = "ucc_mlflow_autologging_context" +_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( + CONTEXT_NAME, + default=QProvRecord( + compilation=CompilationProvenance( + compiler="ucc", compiler_version=version("ucc") + ) + ), +) + + +def get_context() -> QProvRecord: + ctx = _current_context.get() + if ctx is None: + raise RuntimeError("No active autolog context") + return ctx + + +@autologging_integration("ucc") +def autolog( + disable=False, + silent=False, +) -> None: + """Enables autologging for ucc.""" + + if disable: + return + + qiskit_autolog() From e778e2c0562c5fd78f88e7cd00b5249e94fb5558 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Sun, 27 Sep 2026 21:40:43 +0300 Subject: [PATCH 2/8] patch qiskit based on integrations (qiskit, ucc) --- src/q8s/runtime/mlflow/qiskit/__init__.py | 3 +- src/q8s/runtime/mlflow/qiskit/autologging.py | 2 +- src/q8s/runtime/mlflow/qiskit/utils.py | 99 +++++++++++--------- src/q8s/runtime/mlflow/ucc/autologging.py | 53 +++++------ 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/q8s/runtime/mlflow/qiskit/__init__.py b/src/q8s/runtime/mlflow/qiskit/__init__.py index d0b96fe..a9089f4 100644 --- a/src/q8s/runtime/mlflow/qiskit/__init__.py +++ b/src/q8s/runtime/mlflow/qiskit/__init__.py @@ -16,5 +16,6 @@ """MLflow autologging integration for Qiskit.""" from q8s.runtime.mlflow.qiskit.autologging import autolog +from q8s.runtime.mlflow.qiskit.utils import create_autolog -__all__ = ["autolog"] +__all__ = ["autolog", "create_autolog"] diff --git a/src/q8s/runtime/mlflow/qiskit/autologging.py b/src/q8s/runtime/mlflow/qiskit/autologging.py index 026d2e8..7c63b1d 100644 --- a/src/q8s/runtime/mlflow/qiskit/autologging.py +++ b/src/q8s/runtime/mlflow/qiskit/autologging.py @@ -17,4 +17,4 @@ from q8s.runtime.mlflow.qiskit.utils import create_autolog -autolog = create_autolog() +autolog = create_autolog(integration_name="qiskit") diff --git a/src/q8s/runtime/mlflow/qiskit/utils.py b/src/q8s/runtime/mlflow/qiskit/utils.py index 7bd80b0..9d76eda 100644 --- a/src/q8s/runtime/mlflow/qiskit/utils.py +++ b/src/q8s/runtime/mlflow/qiskit/utils.py @@ -45,24 +45,6 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -CONTEXT_NAME = "qiskit_mlflow_autologging_context" - -_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( - CONTEXT_NAME, - default=QProvRecord( - compilation=CompilationProvenance( - compiler="qiskit", compiler_version=version("qiskit") - ) - ), -) - - -def get_context() -> QProvRecord: - ctx = _current_context.get() - if ctx is None: - raise RuntimeError("No active autolog context") - return ctx - def create_autolog(integration_name: str = "qiskit") -> Callable[..., None]: @@ -79,16 +61,41 @@ def autolog( will only be enabled once. """ - _patch_qiskit(disable=disable, silent=silent, extra_tags=extra_tags) + _patch_qiskit( + integration_name=integration_name, + disable=disable, + silent=silent, + extra_tags=extra_tags, + ) return autolog def _patch_qiskit( + integration_name: str, disable=False, silent=False, extra_tags=None, ): + CONTEXT_NAME = integration_name + "_mlflow_autologging_context" + + _current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( + CONTEXT_NAME, + default=QProvRecord( + compilation=CompilationProvenance( + compiler=integration_name, compiler_version=version(integration_name) + ) + ), + ) + + print("Patched") + + def get_context() -> QProvRecord: + ctx = _current_context.get() + if ctx is None: + raise RuntimeError("No active autolog context") + return ctx + def patched_staged_pass_manager_run( original: StagedPassManager.run, # type: ignore[no-untyped-def] instance: StagedPassManager, @@ -137,7 +144,7 @@ def patched_staged_pass_manager_run( return history safe_patch( - "qiskit", + integration_name, qiskit.transpiler.StagedPassManager, "run", patched_staged_pass_manager_run, @@ -200,7 +207,7 @@ def patched_generate_preset_pass_manager( return original(*args, **kwargs) safe_patch( - "qiskit", + integration_name, qiskit.transpiler, "generate_preset_pass_manager", patched_generate_preset_pass_manager, @@ -238,7 +245,7 @@ def patched_result(original_result, job_self, *r_args, **r_kwargs): return job safe_patch( - "qiskit", + integration_name, AerSimulator, "run", patched_backend_run, @@ -259,7 +266,7 @@ def patched_activerun_exit(original, *args, **kwargs): return original(*args, **kwargs) safe_patch( - "qiskit", + integration_name, ActiveRun, "__exit__", patched_activerun_exit, @@ -267,30 +274,31 @@ def patched_activerun_exit(original, *args, **kwargs): extra_tags=extra_tags, ) + def callback(pass_, dag, time, property_set, count): + """Callback function for logging pass information during transpilation.""" + name = pass_.__class__.__name__ -def callback(pass_, dag, time, property_set, count): - """Callback function for logging pass information during transpilation.""" - name = pass_.__class__.__name__ - - ctx = _current_context.get() - - if ctx is None or not isinstance(ctx, QProvRecord): - raise RuntimeError("No active autolog context. Please call transpile() first.") + ctx = _current_context.get() - pass_metadata = { - "depth": dag.depth(), - "size": dag.size(), - "stage": find_stage_by_id( - ctx.compilation.metadata["stages_pass_info"], id(pass_) - ), - } + if ctx is None or not isinstance(ctx, QProvRecord): + raise RuntimeError( + "No active autolog context. Please call transpile() first." + ) - ctx.compilation.add_pass( - pass_name=name, - pass_index=count, - pass_duration_s=time, - pass_metadata=pass_metadata, - ) + pass_metadata = { + "depth": dag.depth(), + "size": dag.size(), + "stage": find_stage_by_id( + ctx.compilation.metadata["stages_pass_info"], id(pass_) + ), + } + + ctx.compilation.add_pass( + pass_name=name, + pass_index=count, + pass_duration_s=time, + pass_metadata=pass_metadata, + ) def log_to_mlflow(record: QProvRecord): @@ -315,7 +323,8 @@ def log_to_mlflow(record: QProvRecord): mlflow.log_metric("passes_count", len(passes)) 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 + "circuit_depth", + passes[-1].pass_metadata.get("depth", 0) if passes else 0, ) mlflow.log_metric( "circuit_size", passes[-1].pass_metadata.get("size", 0) if passes else 0 diff --git a/src/q8s/runtime/mlflow/ucc/autologging.py b/src/q8s/runtime/mlflow/ucc/autologging.py index 67fe947..532b724 100644 --- a/src/q8s/runtime/mlflow/ucc/autologging.py +++ b/src/q8s/runtime/mlflow/ucc/autologging.py @@ -14,40 +14,37 @@ # # SPDX-License-Identifier: Apache-2.0 -import contextvars -from importlib.metadata import version -from mlflow.utils.autologging_utils import autologging_integration +from q8s.runtime.mlflow.qiskit.autologging import create_autolog -from q8s.runtime.mlflow.qiskit.autologging import autolog as qiskit_autolog -from q8s.runtime.qprov.record import CompilationProvenance, QProvRecord +# CONTEXT_NAME = "ucc_mlflow_autologging_context" +# _current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( +# CONTEXT_NAME, +# default=QProvRecord( +# compilation=CompilationProvenance( +# compiler="ucc", compiler_version=version("ucc") +# ) +# ), +# ) -CONTEXT_NAME = "ucc_mlflow_autologging_context" -_current_context: contextvars.ContextVar[QProvRecord] = contextvars.ContextVar( - CONTEXT_NAME, - default=QProvRecord( - compilation=CompilationProvenance( - compiler="ucc", compiler_version=version("ucc") - ) - ), -) +# def get_context() -> QProvRecord: +# ctx = _current_context.get() +# if ctx is None: +# raise RuntimeError("No active autolog context") +# return ctx -def get_context() -> QProvRecord: - ctx = _current_context.get() - if ctx is None: - raise RuntimeError("No active autolog context") - return ctx +# @autologging_integration("ucc") +# def autolog( +# disable=False, +# silent=False, +# ) -> None: +# """Enables autologging for ucc.""" -@autologging_integration("ucc") -def autolog( - disable=False, - silent=False, -) -> None: - """Enables autologging for ucc.""" +# if disable: +# return - if disable: - return +# qiskit_autolog(integration_name="ucc") - qiskit_autolog() +autolog = create_autolog(integration_name="ucc") From fb7d66dc123c0eb40a2cc25ab1df9555ae8e4a41 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Sun, 27 Sep 2026 21:49:28 +0300 Subject: [PATCH 3/8] fix import --- src/q8s/runtime/mlflow/qrisp/autologging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/q8s/runtime/mlflow/qrisp/autologging.py b/src/q8s/runtime/mlflow/qrisp/autologging.py index 8af8eb9..980a6cb 100644 --- a/src/q8s/runtime/mlflow/qrisp/autologging.py +++ b/src/q8s/runtime/mlflow/qrisp/autologging.py @@ -22,7 +22,7 @@ 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.mlflow.qiskit.utils import log_to_mlflow from q8s.runtime.qprov.record import ( CompilationProvenance, QProvRecord, From 40c9322f48242e3333acc5dc86dd332c2de504d5 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Wed, 30 Sep 2026 11:51:18 +0300 Subject: [PATCH 4/8] added qiskit integration tests --- tests/test_qiskit_transpiler.py | 104 +++++++++++ tests/test_qiskit_utils.py | 306 ++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 tests/test_qiskit_transpiler.py create mode 100644 tests/test_qiskit_utils.py diff --git a/tests/test_qiskit_transpiler.py b/tests/test_qiskit_transpiler.py new file mode 100644 index 0000000..b860b53 --- /dev/null +++ b/tests/test_qiskit_transpiler.py @@ -0,0 +1,104 @@ +# 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 qiskit.passmanager import FlowControllerLinear +from qiskit.transpiler import ( + ConditionalController, + DoWhileController, + PassManager, + StagedPassManager, +) +from qiskit.transpiler.passes import Depth, Size + +from q8s.runtime.mlflow.qiskit.transpiler import ( + find_stage_by_id, + process_pass, + process_staged_pass_manager, +) + + +class TestProcessPass(unittest.TestCase): + def test_pass_updates_existing_list_without_duplicates(self): + task, existing_task = Depth(), Size() + stage_info = [id(existing_task)] + expected = stage_info + [id(task)] + + self.assertIs(process_pass(task, stage_info), stage_info) + process_pass(task, stage_info) + + self.assertEqual(stage_info, expected) + + def test_nested_controllers_collect_unique_passes_in_order(self): + depth, size = Depth(), Size() + task = FlowControllerLinear( + [ + depth, + ConditionalController( + [DoWhileController([size, depth], do_while=lambda _: False)], + condition=lambda _: False, + ), + ] + ) + + self.assertEqual(process_pass(task, []), [id(depth), id(size)]) + + def test_empty_controller(self): + self.assertEqual(process_pass(FlowControllerLinear([]), []), []) + + def test_unknown_task_raises(self): + with self.assertRaisesRegex(ValueError, "Unknown task type"): + process_pass(object(), []) + + +class TestProcessStagedPassManager(unittest.TestCase): + def test_collects_expanded_stages_and_skips_unset_stages(self): + depth, size = Depth(), Size() + manager = StagedPassManager( + stages=["init", "optimization"], + pre_init=PassManager([depth]), + init=PassManager([depth, size, depth]), + post_init=PassManager(), + ) + + self.assertEqual( + process_staged_pass_manager(manager), + { + "pre_init": [id(depth)], + "init": [id(depth), id(size)], + "post_init": [], + }, + ) + + def test_manager_without_passes(self): + self.assertEqual(process_staged_pass_manager(StagedPassManager()), {}) + + +class TestFindStageById(unittest.TestCase): + def test_returns_first_matching_stage(self): + stages = {"init": [1, 2], "optimization": [2, 3]} + + self.assertEqual(find_stage_by_id(stages, 2), "init") + self.assertEqual(find_stage_by_id(stages, 3), "optimization") + + def test_missing_id_returns_none(self): + self.assertIsNone(find_stage_by_id({"init": [1]}, 2)) + self.assertIsNone(find_stage_by_id({}, 1)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_qiskit_utils.py b/tests/test_qiskit_utils.py new file mode 100644 index 0000000..6c9ad49 --- /dev/null +++ b/tests/test_qiskit_utils.py @@ -0,0 +1,306 @@ +# 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 contextlib import ExitStack +from types import SimpleNamespace +from unittest.mock import Mock, call, patch + +from mlflow.utils import autologging_utils + +from q8s.runtime.mlflow.qiskit import utils + + +class TestCreateAutolog(unittest.TestCase): + def setUp(self): + stack = ExitStack() + self.addCleanup(stack.close) + stack.enter_context(patch.dict(autologging_utils.AUTOLOGGING_INTEGRATIONS)) + stack.enter_context(patch.object(autologging_utils, "revert_patches")) + self.patch_qiskit = stack.enter_context(patch.object(utils, "_patch_qiskit")) + + def test_factory_defers_patching_until_called_with_defaults(self): + autolog = utils.create_autolog() + + self.assertEqual(autolog.integration_name, "qiskit") + self.patch_qiskit.assert_not_called() + self.assertIsNone(autolog()) + + self.patch_qiskit.assert_called_once_with( + integration_name="qiskit", disable=False, silent=False, extra_tags=None + ) + + def test_custom_integration_forwards_options_and_registers_configuration(self): + autolog = utils.create_autolog("custom_qiskit") + tags = {"team": "quantum"} + + autolog(silent=True, extra_tags=tags) + + self.assertEqual(autolog.integration_name, "custom_qiskit") + self.patch_qiskit.assert_called_once_with( + integration_name="custom_qiskit", + disable=False, + silent=True, + extra_tags=tags, + ) + self.assertEqual( + autologging_utils.AUTOLOGGING_INTEGRATIONS["custom_qiskit"], + {"disable": False, "silent": True, "extra_tags": tags}, + ) + + def test_disabled_autolog_does_not_patch(self): + autolog = utils.create_autolog() + + self.assertIsNone(autolog(disable=True)) + + self.patch_qiskit.assert_not_called() + self.assertTrue(autologging_utils.get_autologging_config("qiskit", "disable")) + + def test_repeated_calls_can_disable_and_reenable_autologging(self): + autolog = utils.create_autolog() + autolog() + self.patch_qiskit.reset_mock() + + autolog(disable=True) + self.patch_qiskit.assert_not_called() + autolog() + + self.patch_qiskit.assert_called_once_with( + integration_name="qiskit", disable=False, silent=False, extra_tags=None + ) + self.assertFalse(autologging_utils.get_autologging_config("qiskit", "disable")) + + +class TestPatchQiskit(unittest.TestCase): + def setUp(self): + stack = ExitStack() + self.addCleanup(stack.close) + self.safe_patch = stack.enter_context(patch.object(utils, "safe_patch")) + self.version = stack.enter_context( + patch.object(utils, "version", return_value="1.2.3") + ) + self.context_factory = stack.enter_context( + patch.object(utils.contextvars, "ContextVar") + ) + self.active_run = stack.enter_context(patch.object(utils.mlflow, "active_run")) + self.log_param = stack.enter_context(patch.object(utils.mlflow, "log_param")) + self.log_record = stack.enter_context(patch.object(utils, "log_to_mlflow")) + stack.enter_context(patch("builtins.print")) + self.install() + + def install(self, **kwargs): + self.safe_patch.reset_mock() + utils._patch_qiskit("qiskit", **kwargs) + self.record = self.context_factory.call_args.kwargs["default"] + self.context_factory.return_value.get.return_value = self.record + self.patches = { + (entry.args[1], entry.args[2]): entry.args[3] + for entry in self.safe_patch.call_args_list + } + + def test_registers_patches_and_initializes_provenance(self): + tags = {"team": "quantum"} + self.install(extra_tags=tags) + + self.version.assert_called_with("qiskit") + self.assertEqual( + self.context_factory.call_args.args, + ("qiskit_mlflow_autologging_context",), + ) + self.assertEqual(self.record.compilation.compiler, "qiskit") + self.assertEqual(self.record.compilation.compiler_version, "1.2.3") + self.assertEqual( + set(self.patches), + { + (utils.StagedPassManager, "run"), + (utils.qiskit.transpiler, "generate_preset_pass_manager"), + (utils.AerSimulator, "run"), + (utils.ActiveRun, "__exit__"), + }, + ) + for entry in self.safe_patch.call_args_list: + self.assertEqual(entry.args[0], "qiskit") + if entry.args[1] is not utils.AerSimulator: + self.assertEqual( + entry.kwargs, {"manage_run": False, "extra_tags": tags} + ) + + def test_pass_manager_records_circuit_duration_and_callback_pass(self): + circuit = utils.QuantumCircuit(2, name="bell") + circuit.h(0) + circuit.cx(0, 1) + manager = Mock() + transpiler_pass = Mock() + dag = Mock() + dag.depth.return_value = 3 + dag.size.return_value = 4 + stages = {"example": "stage information"} + + for as_list in (False, True): + with self.subTest(as_list=as_list): + self.install(silent=True) + circuits = [circuit] if as_list else circuit + original = Mock(return_value="transpiled") + with ( + patch.object(utils.time, "perf_counter", side_effect=[10, 12.5]), + patch.object( + utils, "process_staged_pass_manager", return_value=stages + ) as process, + patch.object( + utils, "find_stage_by_id", return_value="routing" + ) as find, + ): + result = self.patches[utils.StagedPassManager, "run"]( + original, manager, circuits, num_processes=1 + ) + callback = original.call_args.kwargs["callback"] + original.assert_called_once_with( + manager, circuits, num_processes=1, callback=callback + ) + callback(transpiler_pass, dag, 0.25, {}, 2) + process.assert_called_once_with(manager) + find.assert_called_once_with(stages, id(transpiler_pass)) + + self.assertEqual(result, "transpiled") + provenance = self.record.circuit + self.assertEqual(provenance.circuit_id, str(id(circuit))) + self.assertEqual(provenance.name, "bell") + self.assertEqual(provenance.num_qubits, 2) + self.assertEqual(provenance.depth, 2) + self.assertEqual(provenance.width, 2) + self.assertEqual(provenance.size, 2) + self.assertEqual(provenance.gate_counts, {"h": 1, "cx": 1}) + self.assertEqual(self.record.compilation.duration_s, 2.5) + self.assertEqual( + self.record.compilation.metadata["stages_pass_info"], stages + ) + (recorded_pass,) = self.record.compilation.passes + self.assertEqual( + recorded_pass.pass_name, type(transpiler_pass).__name__ + ) + self.assertEqual(recorded_pass.pass_index, 2) + self.assertEqual(recorded_pass.pass_duration_s, 0.25) + self.assertEqual( + recorded_pass.pass_metadata, + {"depth": 3, "size": 4, "stage": "routing"}, + ) + self.context_factory.return_value.get.return_value = None + with self.assertRaisesRegex(RuntimeError, "No active autolog context"): + callback(transpiler_pass, dag, 0.25, {}, 3) + + def test_disabled_pass_manager_skips_provenance(self): + self.install(disable=True) + original = Mock() + result = self.patches[utils.StagedPassManager, "run"]( + original, Mock(), "circuit", callback="callback" + ) + original.assert_called_once_with("circuit", callback="callback") + self.assertIs(result, original.return_value) + self.assertIsNone(self.record.circuit) + + def test_preset_manager_records_options_including_zero(self): + backend = SimpleNamespace(name="simulator", num_qubits=5) + original = Mock() + result = self.patches[utils.qiskit.transpiler, "generate_preset_pass_manager"]( + original, optimization_level=0, seed_transpiler=0, backend=backend + ) + original.assert_called_once_with( + optimization_level=0, seed_transpiler=0, backend=backend + ) + self.assertIs(result, original.return_value) + self.assertEqual(self.record.compilation.optimization_level, 0) + self.assertEqual(self.record.compilation.seed_transpiler, 0) + self.assertEqual(self.record.quantum_computer.backend_name, "simulator") + self.assertEqual(self.record.quantum_computer.num_qubits, 5) + self.log_param.assert_has_calls( + [ + call("optimization_level", 0), + call("seed_transpiler", 0), + call("backend_name", "simulator"), + ] + ) + + def test_preset_manager_without_options_does_not_log_parameters(self): + original = Mock() + result = self.patches[utils.qiskit.transpiler, "generate_preset_pass_manager"]( + original + ) + original.assert_called_once_with() + self.assertIs(result, original.return_value) + self.log_param.assert_not_called() + self.assertIsNone(self.record.quantum_computer) + + def test_preset_manager_requires_active_run(self): + self.active_run.return_value = None + original = Mock() + with self.assertRaisesRegex(RuntimeError, "No active MLflow run"): + self.patches[utils.qiskit.transpiler, "generate_preset_pass_manager"]( + original + ) + original.assert_not_called() + + def test_preset_manager_and_run_exit_reject_invalid_context(self): + for context in (None, object()): + for key in ( + (utils.qiskit.transpiler, "generate_preset_pass_manager"), + (utils.ActiveRun, "__exit__"), + ): + with self.subTest(context=context, target=key): + self.context_factory.return_value.get.return_value = context + original = Mock() + with self.assertRaisesRegex( + RuntimeError, "No active autolog context" + ): + self.patches[key](original) + original.assert_not_called() + self.log_record.assert_not_called() + + def test_backend_run_patches_result_and_preserves_return_values(self): + backend, job = Mock(), Mock() + original = Mock(return_value=job) + result = self.patches[utils.AerSimulator, "run"]( + original, backend, "circuit", shots=100 + ) + original.assert_called_once_with(backend, "circuit", shots=100) + self.assertIs(result, job) + registration = self.safe_patch.call_args + self.assertEqual(registration.args[:3], ("qiskit", type(job), "result")) + patched_result = registration.args[3] + for error in (None, ValueError("No counts")): + with self.subTest(error=error): + counts_result = Mock() + counts_result.get_counts.side_effect = error + original_result = Mock(return_value=counts_result) + self.assertIs( + patched_result(original_result, job, "argument", timeout=5), + counts_result, + ) + original_result.assert_called_once_with(job, "argument", timeout=5) + counts_result.get_counts.assert_called_once_with() + + def test_run_exit_logs_record_before_delegating(self): + original = Mock( + side_effect=lambda *args: self.log_record.assert_called_once_with( + self.record + ) + ) + run = Mock() + self.patches[utils.ActiveRun, "__exit__"](original, run, None, None, None) + original.assert_called_once_with(run, None, None, None) + + +if __name__ == "__main__": + unittest.main() From 552d61c10aca23d441f5b902a425f3dd2936efe4 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Wed, 30 Sep 2026 11:59:34 +0300 Subject: [PATCH 5/8] updated workflow to run node24 --- .github/workflows/publish.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 68dade5..03e66e7 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -18,9 +18,9 @@ jobs: - "3.13" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -49,9 +49,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: Install pypa/build From 608cc4474759aa97e4678ca5caec8937b004c704 Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Wed, 30 Sep 2026 12:03:12 +0300 Subject: [PATCH 6/8] updated step to node24 --- .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 03e66e7..431b8d3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -63,7 +63,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-package-distributions path: dist/ From 7b1896a4cdb38bc9fdce88548d1906fd6c823b6e Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Fri, 2 Oct 2026 10:06:12 +0300 Subject: [PATCH 7/8] imporoved docs and logging --- README.md | 75 ++++++++++++++++++-------- src/q8s/runtime/mlflow/qiskit/utils.py | 10 ++-- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ff0594c..24616db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `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. +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), [UCC](https://ucc.readthedocs.io/en/latest/) 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 @@ -17,12 +17,13 @@ Support for individual quantum development kits can be installed using the corre ```bash pip install "q8s.runtime[qiskit]" pip install "q8s.runtime[qrisp]" +pip install "q8s.runtime[ucc]" ``` Multiple integrations can be installed together: ```bash -pip install "q8s.runtime[qiskit,qrisp]" +pip install "q8s.runtime[qiskit,qrisp,ucc]" ``` ## Integrations @@ -112,6 +113,32 @@ with mlflow.start_run(): optimized_qc = pm.run(qc) ``` +### UCC + +The UCC integration tracks the underlying Qiskit transpilation stages used by UCC through MLflow. + +Enable autologging before importing UCC's `compile` function, then compile within an active MLflow run: + +```python +import mlflow + +from q8s.runtime.mlflow.ucc import autolog + +autolog() + +from qiskit import QuantumCircuit +from ucc import compile + +qc = QuantumCircuit(2) +qc.h(0) +qc.cx(0, 1) + +mlflow.set_experiment("ucc-compilation") + +with mlflow.start_run(): + result = compile(qc) +``` + ## Capabilities ### Provenance @@ -120,15 +147,17 @@ with mlflow.start_run(): 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 | - | - | +| QProv category | Description | Qiskit | Qrisp | UCC | +| -------------------- | ----------------------------------------------------------------- | :----: | :---: | :-: | +| **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. +UCC collects circuit and pass metadata through the Qiskit instrumentation described above. The detailed tables below describe the direct Qiskit and Qrisp integrations. + ### Quantum Circuit Quantum Circuit provenance describes the structure and characteristics of the quantum circuit being executed. @@ -149,26 +178,26 @@ Circuit width represents the number of qubits used by the circuit, circuit depth 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 | ✓ | ✓ | +| QProv | Provenance attribute | Qiskit/UCC | 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 | ✓ | ✓ | +| Compiler provenance | Description | Qiskit/UCC | 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**. diff --git a/src/q8s/runtime/mlflow/qiskit/utils.py b/src/q8s/runtime/mlflow/qiskit/utils.py index 9d76eda..88688a5 100644 --- a/src/q8s/runtime/mlflow/qiskit/utils.py +++ b/src/q8s/runtime/mlflow/qiskit/utils.py @@ -88,8 +88,6 @@ def _patch_qiskit( ), ) - print("Patched") - def get_context() -> QProvRecord: ctx = _current_context.get() if ctx is None: @@ -105,7 +103,9 @@ def patched_staged_pass_manager_run( if disable: return original(*args, **kwargs) - print("Qiskit autologging integration is enabled for StagedPassManager.run.") + logger.info( + "Qiskit autologging integration is enabled for StagedPassManager.run." + ) start = time.perf_counter() @@ -129,7 +129,7 @@ def patched_staged_pass_manager_run( ) if not silent: - print("Logging Qiskit transpilation run to MLflow.") + logger.info("Logging Qiskit transpilation run to MLflow.") kwargs["callback"] = callback @@ -162,7 +162,7 @@ def patched_generate_preset_pass_manager( """Patch the generate_preset_pass_manager function to log the backend information to the QProvRecord.""" - print( + logger.info( "Qiskit autologging integration is enabled for generate_preset_pass_manager." ) From 35ba50242e84d8276f53d58f419e2edc80965fad Mon Sep 17 00:00:00 2001 From: Vlad Stirbu Date: Mon, 5 Oct 2026 13:15:50 +0300 Subject: [PATCH 8/8] contributing guideline and issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 39 ++++++++++ .github/ISSUE_TEMPLATE/feature_or_idea.md | 24 +++++++ CONTRIBUTING.md | 86 +++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_or_idea.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..49f6e4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Report a reproducible problem with q8s.runtime. +--- + +## Description + +Describe the bug clearly and briefly. + +## Steps to reproduce + +Include a minimal code example and the commands or steps needed to reproduce the problem. + +```python +# Minimal reproducible example +``` + +## Expected behavior + +What did you expect to happen? + +## Actual behavior + +What happened instead? Include relevant error messages or tracebacks. + +```text +Paste error messages or tracebacks here. +``` + +## Environment + +- Operating system: +- Python version: +- q8s.runtime version (or commit if installed from source): +- Relevant integrations and their versions (such as Qiskit, Qrisp, UCC, or MLflow): + +## Additional context + +Optional: include related issues, workarounds, or other details that help explain the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_or_idea.md b/.github/ISSUE_TEMPLATE/feature_or_idea.md new file mode 100644 index 0000000..bb114a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_or_idea.md @@ -0,0 +1,24 @@ +--- +name: Feature or idea +about: Discuss a new feature or improvement before starting implementation. +--- + +## Problem or use case + +What problem would this address? Describe who would benefit and how. + +## Proposed solution + +Describe the feature or improvement you have in mind. An example of the desired behavior or API is helpful. If you do not have a solution yet, describe the outcome you want. + +## Alternatives considered + +Describe any alternatives or existing workarounds you have considered. + +## Additional context + +Optional: include examples, related issues, or relevant integrations. + +## Interest in contributing + +Optional: let us know whether you would like to help implement the idea after discussion. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..335ddd8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing + +## Report bugs + +Report bugs using this repository's GitHub Issues. Search existing issues first to avoid duplicates, then open an issue using the **Bug report** template. + +Include a minimal reproducible example, the behavior you expected, what actually happened, and any relevant error messages or tracebacks. Provide your operating system, Python version, `q8s.runtime` version, and versions of any relevant integrations. + +## Propose features and new ideas + +Discuss new features in a GitHub issue before starting implementation. Search existing issues first, then open an issue using the **Feature or idea** template. Describe the problem or use case, your proposed solution, and any alternatives you have considered so maintainers and contributors can discuss the scope and approach. + +## Development setup + +Run the following commands from the root of your local `q8s-runtime` checkout. You will need Git and Python 3.10 or newer; optional integrations may require a newer Python version. + +## Create a virtual environment + +On macOS or Linux: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +On Windows with PowerShell: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +``` + +Activate this environment whenever you open a new terminal to work on the project. + +## Install in editable mode + +Install the package and development tools into the active environment: + +```bash +python -m pip install --upgrade pip +python -m pip install -e ".[development]" +``` + +Editable mode makes changes under `src/` available without reinstalling the package. The `development` extra includes `pre-commit`, `pytest`, and `pytest-cov`. + +For integration development and the test suite, install the additional dependencies: + +```bash +python -m pip install -e ".[development,test,qiskit,qrisp,ucc,matplotlib]" +``` + +The extras are defined in [pyproject.toml](pyproject.toml). You can select only the integrations needed for your work when running a subset of the tests. + +## Configure pre-commit + +Install the repository's Git hooks: + +```bash +pre-commit install +``` + +Check all existing files once after setup: + +```bash +pre-commit run --all-files +``` + +The first run downloads and prepares the hook environments, so it requires network access and can take longer. Hooks are configured in [.pre-commit-config.yaml](.pre-commit-config.yaml) and include formatting, import sorting, linting, file checks, and Python license headers. + +The hooks run automatically on staged files when you commit. If a hook modifies files, review and stage those changes, then retry the commit. Run `pre-commit run --all-files` before submitting changes. + +## Run tests + +With the test and integration dependencies installed: + +```bash +python -m pytest +``` + +To run a specific test file: + +```bash +python -m pytest tests/test_qiskit_utils.py +``` + +When you finish working, leave the virtual environment with `deactivate`.