Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions source/qdk_package/qdk/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -39,13 +41,15 @@
Output,
Pauli,
PrimitiveKind,
ProgramType,
QSharpError,
Result,
StateDumpData,
TargetProfile,
TypeIR,
TypeKind,
UdtValue,
compile_visual_circuit_to_qsharp,
)
from ._types import (
BitFlipNoise,
Expand Down Expand Up @@ -82,6 +86,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 = {}
Expand Down Expand Up @@ -154,6 +240,7 @@ class Context:
code: Any
_disposed: bool
_is_global_context: bool
_loaded_circuit_count: int

def __init__(
self,
Expand Down Expand Up @@ -189,6 +276,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
Expand Down Expand Up @@ -466,6 +554,72 @@ 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 import_circuit(
self,
path: str,
*,
index: int = 0,
program_type: ProgramType = ProgramType.File,
) -> Callable:
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also after team discussion: it would be valuable for this public API to take a program_type parameter (ProgramType.File | ProgramType.Operation) to mirror how import_openqasm treats its input. The ProgramType enum should be accessible here via ._native

  • File (current behavior): treats the circuit as a standalone program, the returned callable takes no arguments and can be invoked directly from Python.

  • Operation: would skip the wrapper and register the circuit operation itself (which takes a Qubit[] parameter). This is useful for composing circuits inside larger Q# programs. However, since qubits have no Python representation, the callable could only be invoked via a string entry expression (e.g. ctx.run("{ use qs = Qubit[N]; myCircuit(qs) }", shots)), not via ctx.code.myCircuit(...). You can add a unit test for this to validate behavior. See how import_openqasm with ProgramType.Operation works for the precedent.

The default should be File if no program type is passed.

Sorry if this seems like scope creep - when we filed the issue, we hadn't yet ironed out all open questions, but now that we see the API all fleshed out, it became clear that having both options would be useful.

Thank you!

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``.
: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)
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}"
)
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, when present, is assembled from sanitized operation
# names and type metadata.
self.eval(eval_source) # DevSkim: ignore DS189424
return getattr(self.code, callable_name)

def eval(
self,
source: str,
Expand Down
18 changes: 18 additions & 0 deletions source/qdk_package/qdk/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand Down
17 changes: 15 additions & 2 deletions source/qdk_package/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -1130,6 +1131,18 @@ fn extract_callable_value(py: Python, callable: &Py<PyAny>) -> PyResult<Value> {
}
}

#[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<String> {
match re::estimate_physical_resources_from_json(logical_resources, job_params) {
Expand Down
57 changes: 56 additions & 1 deletion source/qdk_package/tests/test_project.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -80,6 +82,38 @@ def test_circuit(qsharp) -> None:
assert result == qsharp.Result.Zero


def test_import_circuit(qsharp) -> None:
import qdk

ctx = qdk.Context()
circuit = ctx.import_circuit("/standalone/circuit.qsc")
assert ctx.run(circuit, 1) == [qsharp.Result.Zero]
assert ctx.circuit(circuit) is not None


def test_import_circuit_from_multiple_circuit_file(qsharp) -> None:
import qdk

ctx = qdk.Context()
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

Expand All @@ -94,6 +128,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": {
Expand Down Expand Up @@ -182,6 +233,10 @@ def test_src_package_udt(qsharp) -> None:
},
"qsharp.json": "{}",
},
"standalone": {
"circuit.qsc": circuit_qsc_contents,
"multiple_circuits.qsc": multiple_circuits_qsc_contents,
},
}
}

Expand Down
Loading