From e70656bdb3475483db5df5779bbc6639ab6caad0 Mon Sep 17 00:00:00 2001 From: tzh476 Date: Thu, 4 Jun 2026 18:13:58 +0800 Subject: [PATCH 1/3] feat: load visual circuits from Python contexts Change-Id: I3b8a9eadb78dbe1f61b1ebb058a78bc1b0985a29 --- source/qdk_package/qdk/_context.py | 128 +++++++++++++++++++++++ source/qdk_package/qdk/_native.pyi | 18 ++++ source/qdk_package/src/interpreter.rs | 17 ++- source/qdk_package/tests/test_project.py | 45 +++++++- 4 files changed, 205 insertions(+), 3 deletions(-) diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index a0508de548..59dbf0cfe7 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -9,10 +9,12 @@ independent Q# environments to coexist. """ +import json import sys import types import weakref from dataclasses import make_dataclass +from pathlib import PurePath, PureWindowsPath from time import monotonic from typing import ( Any, @@ -46,6 +48,7 @@ TypeIR, TypeKind, UdtValue, + compile_visual_circuit_to_qsharp, ) from ._types import ( BitFlipNoise, @@ -82,6 +85,88 @@ def ipython_helper(): pass +def _path_stem(path: str) -> str: + path_type = PureWindowsPath if "\\" in path else PurePath + stem = path_type(path).stem + return stem or "circuit" + + +def _visual_circuit_signature(circuit_json: str, index: int) -> tuple[int, str, int]: + try: + data = json.loads(circuit_json) + except json.JSONDecodeError as err: + raise QSharpError(f"Failed to parse visual circuit JSON: {err}") from err + + circuits = data.get("circuits") if isinstance(data, dict) else None + if not isinstance(circuits, list) or len(circuits) == 0: + raise QSharpError("Visual circuit files must contain at least one circuit.") + + if not isinstance(index, int) or isinstance(index, bool): + raise QSharpError("Visual circuit index must be an integer.") + if index < 0 or index >= len(circuits): + raise QSharpError( + f"Visual circuit index {index} is out of range for {len(circuits)} circuits." + ) + + circuit = circuits[index] + if not isinstance(circuit, dict): + raise QSharpError("Visual circuit JSON contains an invalid circuit.") + + qubits = circuit.get("qubits", []) + if not isinstance(qubits, list): + raise QSharpError("Visual circuit JSON contains an invalid qubit register.") + + result_count = 0 + for qubit in qubits: + if not isinstance(qubit, dict): + raise QSharpError("Visual circuit JSON contains an invalid qubit.") + num_results = qubit.get("numResults", 0) + if ( + not isinstance(num_results, int) + or isinstance(num_results, bool) + or num_results < 0 + ): + raise QSharpError("Visual circuit JSON contains an invalid result count.") + result_count += num_results + + if result_count == 0: + return len(qubits), "Unit", len(circuits) + if result_count == 1: + return len(qubits), "Result", len(circuits) + return len(qubits), "Result[]", len(circuits) + + +def _visual_circuit_wrapper_source( + operation_name: str, wrapper_name: str, qubit_count: int, return_type: str +) -> str: + if qubit_count == 0: + allocation = "" + call = f"{operation_name}()" + reset = "" + else: + allocation = f" use qs = Qubit[{qubit_count}];\n" + call = f"{operation_name}(qs)" + reset = " ResetAll(qs);\n" + + if return_type == "Unit": + return ( + f"operation {wrapper_name}() : Unit {{\n" + f"{allocation}" + f" {call};\n" + f"{reset}" + "}\n" + ) + + return ( + f"operation {wrapper_name}() : {return_type} {{\n" + f"{allocation}" + f" let result = {call};\n" + f"{reset}" + " return result;\n" + "}\n" + ) + + def make_class_rec(qsharp_type: TypeIR) -> type: class_name = qsharp_type.unwrap_udt().name fields = {} @@ -154,6 +239,7 @@ class Context: code: Any _disposed: bool _is_global_context: bool + _loaded_circuit_count: int def __init__( self, @@ -189,6 +275,7 @@ def __init__( self._disposed = False self._is_global_context = _is_global_context self._target_profile = target_profile + self._loaded_circuit_count = 0 if _is_global_context: self.code = code @@ -466,6 +553,47 @@ def _check_same_context_struct(self, struct: Any) -> None: if getattr(struct_type, "_qdk_context") is not self: raise QSharpError("This struct belongs to a different Context. ") + def load_circuit(self, path: str, *, index: int = 0) -> Callable: + """ + Loads a visual circuit file as a callable in this context. + + :param path: Path to a ``.qsc`` visual circuit file. + :keyword index: Index of the circuit to return when the file contains + multiple circuits. Defaults to ``0``. + :return: A zero-argument Q# callable that allocates the circuit qubits, + applies the visual circuit, resets the qubits, and returns any + measurement results. + :rtype: Callable + :raises QSharpError: If the file cannot be read or converted to Q#. + """ + from ._fs import read_file, resolve + + resolved_path = resolve(".", path) + try: + _, circuit_json = read_file(resolved_path) + except Exception as err: + raise QSharpError(f"Error reading visual circuit file {resolved_path}.") from err + + qubit_count, return_type, circuit_count = _visual_circuit_signature( + circuit_json, index + ) + unique_name = f"{_path_stem(resolved_path)}_{self._loaded_circuit_count}" + self._loaded_circuit_count += 1 + + operation_name, generated_source = compile_visual_circuit_to_qsharp( + unique_name, circuit_json + ) + circuit_operation_name = ( + operation_name if circuit_count == 1 else f"{operation_name}{index}" + ) + wrapper_name = f"{circuit_operation_name}_Entry" + wrapper_source = _visual_circuit_wrapper_source( + circuit_operation_name, wrapper_name, qubit_count, return_type + ) + + self.eval(f"{generated_source}\n{wrapper_source}") + return getattr(self.code, wrapper_name) + def eval( self, source: str, diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index d1aab18ad8..2278f0e2f3 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -532,6 +532,24 @@ def physical_estimates(logical_resources: str, params: str) -> str: """ ... +def compile_visual_circuit_to_qsharp( + file_name: str, contents: str +) -> Tuple[str, str]: + """ + Converts a visual circuit file to Q# source. + + .. note:: + This call is not intended to be used directly by the user. + It is intended to be used by the Python wrapper which will handle + file loading and callable registration. + + :param file_name: The base name to use for the generated operation. + :param contents: The visual circuit JSON contents. + :return: The sanitized operation name and generated Q# source. + :rtype: Tuple[str, str] + """ + ... + def circuit_qasm_program( source: str, read_file: Callable[[str], Tuple[str, str]], diff --git a/source/qdk_package/src/interpreter.rs b/source/qdk_package/src/interpreter.rs index 4cc65de9c8..88839e49ef 100644 --- a/source/qdk_package/src/interpreter.rs +++ b/source/qdk_package/src/interpreter.rs @@ -15,7 +15,7 @@ use crate::{ interop::{ circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp, create_filesystem_from_py, get_operation_name, get_output_semantics, get_program_type, - get_search_path, resource_estimate_qasm_program, run_qasm_program, + get_search_path, resource_estimate_qasm_program, run_qasm_program, sanitize_name, }, interpreter::data_interop::{ PrimitiveKind, TypeIR, TypeKind, UdtFields, UdtIR, UdtValue, collect_udt_fields, @@ -45,7 +45,7 @@ use pyo3::{ }; use qsc::{ LanguageFeatures, PackageType, SourceMap, - circuit::TracerConfig, + circuit::{TracerConfig, circuits_to_qsharp}, error::WithSource, fir::{self}, hir::ty::{Prim, Ty}, @@ -152,6 +152,7 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(circuit_qasm_program, m)?)?; m.add_function(wrap_pyfunction!(compile_qasm_program_to_qir, m)?)?; m.add_function(wrap_pyfunction!(compile_qasm_to_qsharp, m)?)?; + m.add_function(wrap_pyfunction!(compile_visual_circuit_to_qsharp, m)?)?; Ok(()) } @@ -1130,6 +1131,18 @@ fn extract_callable_value(py: Python, callable: &Py) -> PyResult { } } +#[pyfunction] +pub fn compile_visual_circuit_to_qsharp( + file_name: &str, + circuit_json: &str, +) -> PyResult<(String, String)> { + let operation_name = sanitize_name(file_name); + match circuits_to_qsharp(&operation_name, circuit_json) { + Ok(source) => Ok((operation_name, source)), + Err(error) => Err(QSharpError::new_err(error)), + } +} + #[pyfunction] pub fn physical_estimates(logical_resources: &str, job_params: &str) -> PyResult { match re::estimate_physical_resources_from_json(logical_resources, job_params) { diff --git a/source/qdk_package/tests/test_project.py b/source/qdk_package/tests/test_project.py index 1808e2f80b..1fbf4aab5b 100644 --- a/source/qdk_package/tests/test_project.py +++ b/source/qdk_package/tests/test_project.py @@ -1,9 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import pytest +import json import os +import pytest + @pytest.fixture def qsharp(): @@ -80,6 +82,26 @@ def test_circuit(qsharp) -> None: assert result == qsharp.Result.Zero +def test_load_circuit(qsharp) -> None: + import qdk + + ctx = qdk.Context() + circuit = ctx.load_circuit("/standalone/circuit.qsc") + assert ctx.run(circuit, 1) == [qsharp.Result.Zero] + assert ctx.circuit(circuit) is not None + + +def test_load_circuit_from_multiple_circuit_file(qsharp) -> None: + import qdk + + ctx = qdk.Context() + first_circuit = ctx.load_circuit("/standalone/multiple_circuits.qsc") + second_circuit = ctx.load_circuit("/standalone/multiple_circuits.qsc", index=1) + + assert ctx.run(first_circuit, 1) == [qsharp.Result.Zero] + assert ctx.run(second_circuit, 1) == [qsharp.Result.One] + + def test_src_package_udt(qsharp) -> None: import qdk.code @@ -94,6 +116,23 @@ def test_src_package_udt(qsharp) -> None: ) as f: circuit_qsc_contents = f.read() +multiple_circuits = json.loads(circuit_qsc_contents) +second_circuit = json.loads(circuit_qsc_contents)["circuits"][0] +second_circuit["componentGrid"].insert( + 0, + { + "components": [ + { + "kind": "unitary", + "gate": "X", + "targets": [{"qubit": 0}], + } + ] + }, +) +multiple_circuits["circuits"].append(second_circuit) +multiple_circuits_qsc_contents = json.dumps(multiple_circuits) + memfs = { "": { "good": { @@ -182,6 +221,10 @@ def test_src_package_udt(qsharp) -> None: }, "qsharp.json": "{}", }, + "standalone": { + "circuit.qsc": circuit_qsc_contents, + "multiple_circuits.qsc": multiple_circuits_qsc_contents, + }, } } From c53b195aaa9ad26ef7b37459441f28ce339752e3 Mon Sep 17 00:00:00 2001 From: tzh476 Date: Fri, 5 Jun 2026 03:49:27 +0800 Subject: [PATCH 2/3] fix: suppress vetted visual circuit eval alert Change-Id: Id6234644a0f42eab2b6e6472df0464521947c71a --- source/qdk_package/qdk/_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index 59dbf0cfe7..6d5876c593 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -591,7 +591,10 @@ def load_circuit(self, path: str, *, index: int = 0) -> Callable: circuit_operation_name, wrapper_name, qubit_count, return_type ) - self.eval(f"{generated_source}\n{wrapper_source}") + # generated_source is produced by the native visual-circuit compiler; + # wrapper_source is assembled from sanitized operation names and type metadata. + eval_source = f"{generated_source}\n{wrapper_source}" + self.eval(eval_source) # DevSkim: ignore DS189424 return getattr(self.code, wrapper_name) def eval( From cc107d855e8babe5b2ff957445ec0f96ad72ffb4 Mon Sep 17 00:00:00 2001 From: tzh476 Date: Sat, 6 Jun 2026 06:30:35 +0800 Subject: [PATCH 3/3] fix: align visual circuit import API Change-Id: I962310d4ae6aaf624aee2632d0d0f5bb7b589328 --- source/qdk_package/qdk/_context.py | 47 ++++++++++++++++++------ source/qdk_package/tests/test_project.py | 22 ++++++++--- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index 6d5876c593..843a6bbad7 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -41,6 +41,7 @@ Output, Pauli, PrimitiveKind, + ProgramType, QSharpError, Result, StateDumpData, @@ -553,21 +554,38 @@ def _check_same_context_struct(self, struct: Any) -> None: if getattr(struct_type, "_qdk_context") is not self: raise QSharpError("This struct belongs to a different Context. ") - def load_circuit(self, path: str, *, index: int = 0) -> Callable: + def import_circuit( + self, + path: str, + *, + index: int = 0, + program_type: ProgramType = ProgramType.File, + ) -> Callable: """ - Loads a visual circuit file as a callable in this context. + Imports a visual circuit file as a callable in this context. :param path: Path to a ``.qsc`` visual circuit file. :keyword index: Index of the circuit to return when the file contains multiple circuits. Defaults to ``0``. - :return: A zero-argument Q# callable that allocates the circuit qubits, - applies the visual circuit, resets the qubits, and returns any - measurement results. + :keyword program_type: Controls how the circuit is imported: + ``ProgramType.File`` (default) imports a zero-argument wrapper that + allocates the circuit qubits, applies the visual circuit, resets the + qubits, and returns any measurement results. ``ProgramType.Operation`` + imports the visual circuit operation itself, which takes a + ``Qubit[]`` argument and is intended for composition from Q# entry + expressions. + :return: The imported Q# callable. :rtype: Callable :raises QSharpError: If the file cannot be read or converted to Q#. """ from ._fs import read_file, resolve + if program_type not in (ProgramType.File, ProgramType.Operation): + raise QSharpError( + "Visual circuit import supports ProgramType.File and " + "ProgramType.Operation only." + ) + resolved_path = resolve(".", path) try: _, circuit_json = read_file(resolved_path) @@ -586,16 +604,21 @@ def load_circuit(self, path: str, *, index: int = 0) -> Callable: circuit_operation_name = ( operation_name if circuit_count == 1 else f"{operation_name}{index}" ) - wrapper_name = f"{circuit_operation_name}_Entry" - wrapper_source = _visual_circuit_wrapper_source( - circuit_operation_name, wrapper_name, qubit_count, return_type - ) + eval_source = generated_source + callable_name = circuit_operation_name + if program_type == ProgramType.File: + wrapper_name = f"{circuit_operation_name}_Entry" + wrapper_source = _visual_circuit_wrapper_source( + circuit_operation_name, wrapper_name, qubit_count, return_type + ) + eval_source = f"{generated_source}\n{wrapper_source}" + callable_name = wrapper_name # generated_source is produced by the native visual-circuit compiler; - # wrapper_source is assembled from sanitized operation names and type metadata. - eval_source = f"{generated_source}\n{wrapper_source}" + # wrapper_source, when present, is assembled from sanitized operation + # names and type metadata. self.eval(eval_source) # DevSkim: ignore DS189424 - return getattr(self.code, wrapper_name) + return getattr(self.code, callable_name) def eval( self, diff --git a/source/qdk_package/tests/test_project.py b/source/qdk_package/tests/test_project.py index 1fbf4aab5b..195c177e79 100644 --- a/source/qdk_package/tests/test_project.py +++ b/source/qdk_package/tests/test_project.py @@ -82,26 +82,38 @@ def test_circuit(qsharp) -> None: assert result == qsharp.Result.Zero -def test_load_circuit(qsharp) -> None: +def test_import_circuit(qsharp) -> None: import qdk ctx = qdk.Context() - circuit = ctx.load_circuit("/standalone/circuit.qsc") + circuit = ctx.import_circuit("/standalone/circuit.qsc") assert ctx.run(circuit, 1) == [qsharp.Result.Zero] assert ctx.circuit(circuit) is not None -def test_load_circuit_from_multiple_circuit_file(qsharp) -> None: +def test_import_circuit_from_multiple_circuit_file(qsharp) -> None: import qdk ctx = qdk.Context() - first_circuit = ctx.load_circuit("/standalone/multiple_circuits.qsc") - second_circuit = ctx.load_circuit("/standalone/multiple_circuits.qsc", index=1) + first_circuit = ctx.import_circuit("/standalone/multiple_circuits.qsc") + second_circuit = ctx.import_circuit("/standalone/multiple_circuits.qsc", index=1) assert ctx.run(first_circuit, 1) == [qsharp.Result.Zero] assert ctx.run(second_circuit, 1) == [qsharp.Result.One] +def test_import_circuit_as_operation(qsharp) -> None: + import qdk + from qdk._native import ProgramType + + ctx = qdk.Context() + circuit = ctx.import_circuit( + "/standalone/circuit.qsc", program_type=ProgramType.Operation + ) + assert circuit is not None + assert ctx.run("{ use qs = Qubit[1]; circuit_0(qs) }", 1) == [qsharp.Result.Zero] + + def test_src_package_udt(qsharp) -> None: import qdk.code