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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion samples/qre/1_qre_input.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
" # obtain the memory/compute strategy and its name\n",
" strategy, strategy_name = parameters.strategy\n",
" # generate a trace from the Q# entry point with the specified parameters\n",
" trace = trace_from_entry_expr(qdk.code.Adder, self.bitsize, adder_fn, parameters.compute_fraction, strategy)\n",
" trace = trace_from_entry_expr(qdk.code.Adder, args=(self.bitsize, adder_fn, parameters.compute_fraction, strategy))\n",
"\n",
" # Set trace properties for later analysis and display\n",
" trace.set_property(ADDER, adder_name)\n",
Expand Down
19 changes: 19 additions & 0 deletions source/qdk_package/qdk/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ from typing import (
overload,
)

from .qre import Trace

# pylint: disable=unused-argument
# E302 is fighting with the formatter for number of blank lines
# flake8: noqa: E302
Expand Down Expand Up @@ -321,6 +323,23 @@ class Interpreter:
"""
...

def trace(
self,
entry_expr: Optional[str] = None,
callable: Optional[GlobalCallable | Closure] = None,
args: Optional[Any] = None,
) -> Trace:
"""
Builds a QRE trace for Q# source code.

:param entry_expr: The entry expression to run.
:param callable: The callable to run, if no entry expression is provided.
:param args: The arguments to pass to the callable, if any.

:returns trace: The resource-estimation trace.
"""
...

def set_quantum_seed(self, seed: Optional[int]) -> None:
"""
Sets the seed for the quantum random number generator.
Expand Down
2 changes: 1 addition & 1 deletion source/qdk_package/qdk/qre/application/_openqasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ def get_trace(self, parameters: None = None) -> Trace:
import_openqasm(self.program, name=name, program_type=ProgramType.File)
self.program = getattr(code.qasm_import, name)

return trace_from_entry_expr(self.program, *self.args)
return trace_from_entry_expr(self.program, args=self.args)
10 changes: 9 additions & 1 deletion source/qdk_package/qdk/qre/application/_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class QSharpApplication(Application[None]):
provided. Default is an empty tuple.
cache_dir (Path): Directory for caching compiled traces.
use_cache (bool): Whether to use the trace cache. Default is False.
use_trace_backend (bool): Whether to build traces directly from a Q# backend
instead of via logical counts. Default is False.
"""

entry_expr: str | Callable | LogicalCounts
Expand All @@ -38,6 +40,7 @@ class QSharpApplication(Application[None]):
default=Path.home() / ".cache" / "re3" / "qsharp", repr=False
)
use_cache: bool = field(default=False, repr=False)
use_trace_backend: bool = field(default=False, repr=False)

def __post_init__(self):
"""Log telemetry for QSharpApplication creation."""
Expand All @@ -57,4 +60,9 @@ def get_trace(self, parameters: None = None) -> Trace:
else:
cache_path = None

return trace_from_entry_expr_cached(self.entry_expr, cache_path, *self.args)
return trace_from_entry_expr_cached(
self.entry_expr,
cache_path=None if self.use_trace_backend else cache_path,
use_trace_backend=self.use_trace_backend,
args=self.args,
)
99 changes: 82 additions & 17 deletions source/qdk_package/qdk/qre/interop/_qsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
import time
from typing import Callable, Optional

from ..._interpreter import logical_counts
from ..._interpreter import (
Closure,
GlobalCallable,
_get_context_or_default,
logical_counts,
)
from ...estimator import LogicalCounts
from .._qre import Trace
from ..instruction_ids import CCX, MEAS_Z, RZ, T, READ_FROM_MEMORY, WRITE_TO_MEMORY
Expand Down Expand Up @@ -54,20 +59,11 @@ def _bucketize_rotation_counts(
return result


def trace_from_entry_expr(entry_expr: str | Callable | LogicalCounts, *args) -> Trace:
"""Convert a Q# entry expression into a resource-estimation Trace.

Evaluates the entry expression to obtain logical counts, then builds
a trace containing the corresponding quantum operations.

Args:
entry_expr (str or :class:`~typing.Callable` or :class:`~qdk.estimator.LogicalCounts`): A Q# entry expression
string, a callable, or pre-computed logical counts.
*args: The arguments to pass to the callable, if one is provided.

Returns:
:class:`~qdk.qre.Trace`: A trace representing the resource profile of the program.
"""
def _trace_from_entry_expr_using_logical_counts(
entry_expr: str | Callable | LogicalCounts,
args: tuple,
) -> Trace:
"""Build a Trace from logical counts."""

start = time.time_ns()
counts = (
Expand Down Expand Up @@ -125,8 +121,70 @@ def trace_from_entry_expr(entry_expr: str | Callable | LogicalCounts, *args) ->
return trace


def _trace_from_entry_expr_using_trace_builder(
entry_expr: str | Callable, args: tuple
) -> Trace:
"""Build a Trace directly from Q# execution via the native trace backend."""
context = _get_context_or_default(entry_expr)

start = time.time_ns()
if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"):
context._check_same_context_callable(entry_expr)
interpreter_args = context._python_args_to_interpreter_args(args)
trace = context._interpreter.trace(
callable=getattr(entry_expr, "__global_callable"), args=interpreter_args
)
elif isinstance(entry_expr, (GlobalCallable, Closure)):
interpreter_args = context._python_args_to_interpreter_args(args)
trace = context._interpreter.trace(callable=entry_expr, args=interpreter_args)
else:
assert isinstance(entry_expr, str)
trace = context._interpreter.trace(entry_expr=entry_expr)
evaluation_time = time.time_ns() - start

trace.set_property(EVALUATION_TIME, evaluation_time)
trace.set_property(ALGORITHM_COMPUTE_QUBITS, trace.compute_qubits)
trace.set_property(ALGORITHM_MEMORY_QUBITS, trace.memory_qubits or 0)
return trace


def trace_from_entry_expr(
entry_expr: str | Callable | LogicalCounts,
use_trace_backend: bool = False,
args: tuple = (),
) -> Trace:
"""Convert a Q# entry expression into a resource-estimation Trace.

Evaluates the entry expression to obtain logical counts, then builds
a trace containing the corresponding quantum operations.

Args:
entry_expr (str or :class:`~typing.Callable` or :class:`~qdk.estimator.LogicalCounts`): A Q# entry expression
string, a callable, or pre-computed logical counts.
use_trace_backend (bool): If True, uses the native Q# trace backend to
build the trace directly from execution. If False, derives the
trace from logical counts.
args (tuple): Positional arguments to pass to the callable entry
expression, if one is provided.

Returns:
:class:`~qdk.qre.Trace`: A trace representing the resource profile of the program.
"""
if use_trace_backend:
if isinstance(entry_expr, LogicalCounts):
raise TypeError(
"LogicalCounts input is not supported when use_trace_backend=True"
)
return _trace_from_entry_expr_using_trace_builder(entry_expr, args)
else:
return _trace_from_entry_expr_using_logical_counts(entry_expr, args)


def trace_from_entry_expr_cached(
entry_expr: str | Callable | LogicalCounts, cache_path: Optional[Path], *args
entry_expr: str | Callable | LogicalCounts,
cache_path: Optional[Path],
use_trace_backend: bool = False,
args: tuple = (),
) -> Trace:
"""Convert a Q# entry expression into a Trace, with optional caching.

Expand All @@ -139,14 +197,21 @@ def trace_from_entry_expr_cached(
string, a callable, or pre-computed logical counts.
cache_path (Optional[Path]): Path for reading/writing the cached
trace. If None, caching is disabled.
use_trace_backend (bool): Passed through to
``trace_from_entry_expr``. If True, uses the native trace backend;
otherwise uses the logical-counts-based path.
args (tuple): Positional arguments to pass to the callable entry
expression, if one is provided.

Returns:
:class:`~qdk.qre.Trace`: A trace representing the resource profile of the program.
"""
if cache_path and cache_path.exists():
return Trace.from_json(cache_path.read_text(encoding="utf-8"))

trace = trace_from_entry_expr(entry_expr, *args)
trace = trace_from_entry_expr(
entry_expr, use_trace_backend=use_trace_backend, args=args
)

if cache_path:
cache_path.parent.mkdir(parents=True, exist_ok=True)
Expand Down
29 changes: 29 additions & 0 deletions source/qdk_package/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,35 @@ impl Interpreter {
)),
}
}

#[pyo3(signature=(entry_expr=None, callable=None, args=None))]
fn trace(
&mut self,
py: Python,
entry_expr: Option<&str>,
callable: Option<Py<PyAny>>,
args: Option<Py<PyAny>>,
) -> PyResult<crate::qre::Trace> {
let results = if let Some(entry_expr) = entry_expr {
::qre::trace_expr(&mut self.interpreter, entry_expr)
} else {
let callable = callable.ok_or_else(|| {
QSharpError::new_err("either entry_expr or callable must be specified")
})?;
let callable = extract_callable_value(py, &callable)?;
let (input_ty, output_ty) = self
.interpreter
.global_callable_ty(&callable)
.ok_or(QSharpError::new_err("callable not found"))?;
let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
::qre::trace_call(&mut self.interpreter, callable, args)
};

match results {
Ok(trace) => Ok(crate::qre::Trace::from_qre_trace(trace)),
Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
}
}
}

fn args_to_values(
Expand Down
6 changes: 6 additions & 0 deletions source/qdk_package/src/qre.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,12 @@ impl FactoryResult {
#[pyclass]
pub struct Trace(qre::Trace);

impl Trace {
pub(crate) fn from_qre_trace(trace: qre::Trace) -> Self {
Self(trace)
}
}

#[pymethods]
impl Trace {
#[new]
Expand Down
104 changes: 104 additions & 0 deletions source/qdk_package/tests/qre/test_qsharp_interop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.


from qdk._context import Context
from qdk.qre import ISA, LOGICAL, PSSPC, LatticeSurgery, linear_function
from qdk.qre._qre import _ProvenanceGraph
from qdk.qre.application import QSharpApplication
from qdk.qre.instruction_ids import (
CCX,
CX,
LATTICE_SURGERY,
MEAS_RESET_Z,
MEAS_Z,
PAULI_X,
T,
)

SMALL_PROGRAM = """
{
use (a, b, c) = (Qubit(), Qubit(), Qubit());
X(a);
CNOT(a, b);
CCNOT(a, b, c);
let _ = M(a);
MResetZ(b);
}
"""


def _make_basic_isa() -> ISA:
graph = _ProvenanceGraph()
return graph.make_isa(
[
graph.add_instruction(
LATTICE_SURGERY,
encoding=LOGICAL,
arity=None,
time=1000,
space=linear_function(50),
error_rate=linear_function(1e-6),
),
graph.add_instruction(
T, encoding=LOGICAL, time=1000, space=400, error_rate=1e-8
),
]
)


def test_qsharp_trace_backend_from_entry_expr():
app = QSharpApplication(SMALL_PROGRAM, use_trace_backend=True)
trace = app.get_trace()

assert trace.compute_qubits == 3
assert trace.total_qubits == 3
assert trace.num_gates == 5
assert trace.depth == 4
assert {c.id for c in trace.required_isa} == {
PAULI_X,
CX,
CCX,
MEAS_Z,
MEAS_RESET_Z,
}

transformed = PSSPC(num_ts_per_rotation=16, ccx_magic_states=False).transform(trace)
assert transformed is not None
transformed = LatticeSurgery().transform(transformed)
assert transformed is not None

result = transformed.estimate(_make_basic_isa(), max_error=float("inf"))
assert result is not None
assert result.qubits > 0
assert result.runtime > 0


def test_qsharp_trace_backend_from_callable(context: Context):
context.eval(
"""
namespace TestTraceInterop {
operation Entry() : Unit {
Comment thread
fedimser marked this conversation as resolved.
Dismissed
use (a, b, c) = (Qubit(), Qubit(), Qubit());
X(a);
CNOT(a, b);
CCNOT(a, b, c);
let _ = M(a);
MResetZ(b);
}
}
"""
)

app = QSharpApplication(context.code.TestTraceInterop.Entry, use_trace_backend=True)
trace = app.get_trace()

assert trace.compute_qubits == 3
assert trace.num_gates == 5
assert {c.id for c in trace.required_isa} == {
PAULI_X,
CX,
CCX,
MEAS_Z,
MEAS_RESET_Z,
}
3 changes: 3 additions & 0 deletions source/qre/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ edition.workspace = true
license.workspace = true

[dependencies]
qsc = { path = "../compiler/qsc" }
miette = { workspace = true }
num-traits = { workspace = true }
rustc-hash = { workspace = true }
probability = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }


[dev-dependencies]
indoc = { workspace = true }

[lints]
workspace = true
Loading
Loading