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
26 changes: 5 additions & 21 deletions source/pip/benchmarks/bench_qre.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import timeit
from dataclasses import dataclass, KW_ONLY, field
from qsharp.qre.models import AQREGateBased, SurfaceCode
from qsharp.qre._enumeration import _enumerate_instances


Expand Down Expand Up @@ -35,30 +36,13 @@ def bench_enumerate_isas():
# Add the tests directory to sys.path to import test_qre
# TODO: Remove this once the models in test_qre are moved to a proper module
sys.path.append(os.path.join(os.path.dirname(__file__), "../tests"))
import test_qre # type: ignore
from test_qre import ExampleLogicalFactory, ExampleFactory # type: ignore

from qsharp.qre._isa_enumeration import (
Context,
ISAQuery,
ProductNode,
)

ctx = Context(architecture=test_qre.ExampleArchitecture())
ctx = AQREGateBased().context()

# Hierarchical factory using from_components
query = ProductNode(
sources=[
ISAQuery(test_qre.SurfaceCode),
ISAQuery(
test_qre.ExampleLogicalFactory,
source=ProductNode(
sources=[
ISAQuery(test_qre.SurfaceCode),
ISAQuery(test_qre.ExampleFactory),
]
),
),
]
query = SurfaceCode.q() * ExampleLogicalFactory.q(
source=SurfaceCode.q() * ExampleFactory.q()
)

number = 100
Expand Down
27 changes: 25 additions & 2 deletions source/pip/qsharp/qre/__init__.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from ._application import Application, QSharpApplication
from ._architecture import Architecture
from ._estimation import estimate
from ._instruction import (
LOGICAL,
PHYSICAL,
Encoding,
ISATransform,
constraint,
instruction,
ISATransform,
)
from ._isa_enumeration import ISAQuery, ISARefNode, ISA_ROOT
from ._qre import (
ISA,
InstructionFrontier,
Constraint,
ConstraintBound,
EstimationResult,
FactoryResult,
ISARequirements,
Block,
Trace,
block_linear_function,
constant_function,
linear_function,
)
from ._architecture import Architecture
from ._trace import LatticeSurgery, PSSPC, TraceQuery

__all__ = [
"block_linear_function",
"constant_function",
"constraint",
"estimate",
"instruction",
"linear_function",
"Application",
"Architecture",
"Block",
"Constraint",
"ConstraintBound",
"Encoding",
"EstimationResult",
"FactoryResult",
"InstructionFrontier",
"ISA",
"ISA_ROOT",
"ISAQuery",
"ISARefNode",
"ISARequirements",
"ISATransform",
"LatticeSurgery",
"PSSPC",
"QSharpApplication",
"Trace",
"TraceQuery",
"LOGICAL",
"PHYSICAL",
]
139 changes: 139 additions & 0 deletions source/pip/qsharp/qre/_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from __future__ import annotations

import types
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import (
Any,
Callable,
ClassVar,
Generic,
Protocol,
TypeVar,
Generator,
get_type_hints,
cast,
)

from .._qsharp import logical_counts
from ..estimator import LogicalCounts
from ._enumeration import _enumerate_instances
from ._qre import Trace
from .instruction_ids import CCX, MEAS_Z, RZ, T


class DataclassProtocol(Protocol):
__dataclass_fields__: ClassVar[dict]


TraceParameters = TypeVar("TraceParameters", DataclassProtocol, types.NoneType)


class Application(ABC, Generic[TraceParameters]):
"""
An application defines a class of quantum computation problems along with a
method to generate traces for specific problem instances.

We distinguish between application and trace parameters. The application
parameters define which particular instance of the application we want to
consider. The trace parameters define how to generate a trace. They
change the specific way in which we solve the problem, but not the problem
itself.

For example, in quantum cryptography, the application parameters could
define the key size for an RSA prime product, while the trace parameters
define which algorithm to use to break the cryptography, as well as
parameters therein.
"""

@abstractmethod
def get_trace(self, parameters: TraceParameters) -> Trace:
"""Return the trace corresponding to this application."""

def context(self, **kwargs) -> _Context:
"""Create a new enumeration context for this application."""
return _Context(self, **kwargs)

def enumerate_traces(
self,
**kwargs,
) -> Generator[Trace, None, None]:
"""Yields all traces of an application given its dataclass parameters."""

param_type = get_type_hints(self.__class__.get_trace).get("parameters")
if param_type is types.NoneType:
yield self.get_trace(None) # type: ignore
return

if isinstance(param_type, TypeVar):
for c in param_type.__constraints__:
if c is not types.NoneType:
param_type = c
break
for parameters in _enumerate_instances(cast(type, param_type), **kwargs):
yield self.get_trace(parameters)


class _Context:
application: Application
kwargs: dict[str, Any]

def __init__(self, application: Application, **kwargs):
self.application = application
self.kwargs = kwargs


@dataclass
class QSharpApplication(Application[None]):
def __init__(self, entry_expr: str | Callable | LogicalCounts):
self._entry_expr = entry_expr

def get_trace(self, parameters: None = None) -> Trace:
if not isinstance(self._entry_expr, LogicalCounts):
self._counts = logical_counts(self._entry_expr)
else:
self._counts = self._entry_expr
return self._trace_from_logical_counts(self._counts)

def _trace_from_logical_counts(self, counts: LogicalCounts) -> Trace:
ccx_count = counts.get("cczCount", 0) + counts.get("ccixCount", 0)

trace = Trace(counts.get("numQubits", 0))

rotation_count = counts.get("rotationCount", 0)
rotation_depth = counts.get("rotationDepth", rotation_count)

if rotation_count != 0:
if rotation_depth > 1:
rotations_per_layer = rotation_count // (rotation_depth - 1)
else:
rotations_per_layer = 0

last_layer = rotation_count - (rotations_per_layer * (rotation_depth - 1))

if rotations_per_layer != 0:
block = trace.add_block(repetitions=rotation_depth - 1)
for i in range(rotations_per_layer):
block.add_operation(RZ, [i])
block = trace.add_block()
for i in range(last_layer):
block.add_operation(RZ, [i])

if t_count := counts.get("tCount", 0):
block = trace.add_block(repetitions=t_count)
block.add_operation(T, [0])

if ccx_count:
block = trace.add_block(repetitions=ccx_count)
block.add_operation(CCX, [0, 1, 2])

if meas_count := counts.get("measurementCount", 0):
block = trace.add_block(repetitions=meas_count)
block.add_operation(MEAS_Z, [0])

# TODO: handle memory qubits

return trace
25 changes: 25 additions & 0 deletions source/pip/qsharp/qre/_architecture.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field

from ._qre import ISA

Expand All @@ -10,3 +13,25 @@ class Architecture(ABC):
@property
@abstractmethod
def provided_isa(self) -> ISA: ...

def context(self) -> _Context:
"""Create a new enumeration context for this architecture."""
return _Context(self.provided_isa)


@dataclass(slots=True, frozen=True)
class _Context:
"""
Context passed through enumeration, holding shared state.

Attributes:
root_isa: The root ISA for enumeration.
"""

root_isa: ISA
_bindings: dict[str, ISA] = field(default_factory=dict, repr=False)

def _with_binding(self, name: str, isa: ISA) -> _Context:
"""Return a new context with an additional binding (internal use)."""
new_bindings = {**self._bindings, name: isa}
return _Context(self.root_isa, new_bindings)
27 changes: 20 additions & 7 deletions source/pip/qsharp/qre/_enumeration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from typing import Generator, Type, TypeVar, Literal, get_args, get_origin
from typing import (
Generator,
Type,
TypeVar,
Literal,
get_args,
get_origin,
get_type_hints,
)
from dataclasses import MISSING
from itertools import product
from enum import Enum
Expand Down Expand Up @@ -57,8 +65,13 @@ class MyConfig:
yield cls(**kwargs)
return

for field in fields.values():
# Resolve type hints to handle stringified types from __future__.annotations
type_hints = get_type_hints(cls)

for field in fields.values(): # type: ignore
name = field.name
# Get resolved type or fallback to field.type
current_type = type_hints.get(name, field.type)

if name in kwargs:
val = kwargs[name]
Expand All @@ -83,16 +96,16 @@ class MyConfig:
values.append(domain)
continue

if field.type is bool:
if current_type is bool:
values.append([True, False])
continue

if isinstance(field.type, type) and issubclass(field.type, Enum):
values.append(list(field.type))
if isinstance(current_type, type) and issubclass(current_type, Enum):
values.append(list(current_type))
continue

if get_origin(field.type) is Literal:
values.append(list(get_args(field.type)))
if get_origin(current_type) is Literal:
values.append(list(get_args(current_type)))
continue

if field.default is not MISSING:
Expand Down
52 changes: 52 additions & 0 deletions source/pip/qsharp/qre/_estimation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from ._application import Application
from ._architecture import Architecture
from ._qre import EstimationCollection, estimate_parallel
from ._trace import TraceQuery
from ._isa_enumeration import ISAQuery


def estimate(
application: Application,
architecture: Architecture,
trace_query: TraceQuery,
isa_query: ISAQuery,
*,
max_error: float = 1.0,
) -> EstimationCollection:
"""
Estimate the resource requirements for a given application instance and
architecture.

The application instance might return multiple traces. Each of the traces
is transformed by the trace query, which applies several trace transforms in
sequence. Each transform may return multiple traces. Similarly, the
architecture's ISA is transformed by the ISA query, which applies several
ISA transforms in sequence, each of which may return multiple ISAs. The
estimation is performed for each combination of transformed trace and ISA.
The results are collected into an EstimationCollection and returned.

The collection only contains the results that are optimal with respect to
the total number of qubits and the total runtime.

Args:
application (Application): The quantum application to be estimated.
architecture (Architecture): The target quantum architecture.
trace_query (TraceQuery): The trace query to enumerate traces from the
application.
isa_query (ISAQuery): The ISA query to enumerate ISAs from the architecture.

Returns:
EstimationCollection: A collection of estimation results.
"""

app_ctx = application.context()
arch_ctx = architecture.context()

return estimate_parallel(
list(trace_query.enumerate(app_ctx)),
list(isa_query.enumerate(arch_ctx)),
max_error,
)
Loading