Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: compilation and precompilation docs #55

Merged
merged 5 commits into from
Jun 4, 2024
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## What is bartiq

Bartiq is a library that allows to analyze a quantum routines and calculate symbolic expressions for quantum resource estimates (QRE).
Bartiq is a library that allows one to analyze quantum algorithms and calculate symbolic expressions for quantum resource estimates (QRE).

## Installation

Expand All @@ -19,7 +19,7 @@ pip install .

## Quick start

In bartiq we can take a quantum algorithm expressed as a collection of subroutines, each with it's costs expressed as symbolic expressions, and compile it to get cost expression for the whole algorithm.
In bartiq we can take a quantum algorithm expressed as a collection of subroutines, each with its costs expressed as symbolic expressions, and compile it to get cost expression for the whole algorithm.

As an example we can use Alias Sampling – an algorithm proposed by [Babbush et al.](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.8.041015). Here's how it's depicted in the paper:

Expand Down
62 changes: 61 additions & 1 deletion docs/concepts/compilation.md
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
# Compilation
# Compilation

Here we will describe how the compilation process in Bartiq works.
Please keep in mind, that while we try to keep this page up-to-date, in the end the code is the only source-of-truth. If you notice any discrepancies, between what's described here and how Bartiq behaves, please [create an issue](https://github.com/PsiQ/bartiq/issues)!

## Bird's eye perspective on compilation

Compilation consists of the following steps:

1. Precompilation - ensures that Routine has all the components needed for the rest of the compilation process.
2. Routine-to-function conversion – maps routine to functions (see glossary), on which compilation is performed.
3. Function compilation – compiles all the functions to a


## Step 1 – precompilation

This stage "precompiles" the routine by making a series of reasonable assumptions about what the user would have
wanted when they have not been explicit. For example, insertion of "trivial" routines for known subroutines or
"filling out" routines which have only been partially defined. It is expected that this precompilation stage will
grow as more sensible defaults are requested by users. For more details please visit [precompilation page](precompilation.md).

## Step 2 – routine to function conversion

Before being compiled, routines must first be mapped to compilable function objects (see glossary).
At this stage, we perform a number of steps to map the routines into functions that are ready for
compilation:

1. Map routines to "local" functions: first, we simply map the routines to functions in a local manner, whereby they
are unaware of their location within the full definition. To do this we convert `Routine` objects to `RoutineWithFunction`, which augments Routine class with a `symbolic_function` field.
2. Map "local" functions to "global" functions: we tell the functions where they live in the definition, thereby
making each parameter and any named functions unique within the definition by prepending it with its full path from the root.
3. Pull in input register sizes: in order to allow for input register sizes to be compiled properly, low-level input
register sizes must be renamed by the size parameters used by high-level registers. This process is referred to as
"pulling in" those high-level register sizes.
4. Push out output register sizes: to ensure correct compilation for output register sizes, those associated with
low-level routines must be "push out" to either other low-level routines or eventually high-level routines.
5. Parameter inheritance: lastly, any high-level routines parameters are passed down to low-level routines by
substituting the low-level routine parameters with the high-level ones.

Once this is complete, each subroutine has an associated function, which is expressed in terms of global variables.

## Step 3 – functions compilation

At the start of this stage routine is annotated with functions which contain all the required information in the global namespace. Compilation starts from the leafs, each routine can only be compiled once all of its children have been compiled ("bottom-up"). This stage contains a number of sub-steps:

1. Compile functions of self and children (non-leaves only): for leaves, no function compilation is necessary, but
non-leaves need to update information in associated function with the information contained in their children's functions.
2. Undo register size pulling and pushing: to ensure a correct local estimate, we must "undo" the pulling in and pushing
out of register size params done during routine-to-function conversion.
3. Derefence global namespace: again, to ensure a correct local routine definition, we dereference the global
namespace. This means dropping the path prefix from all the variables: e.g.: `root.child.gchild.N` would become `N`.
4. Map function to routine: lastly, once the correct local function has been compiled, we map the function object
back to a routine by updating routine's attribute and removing associated function object.


## Glossary

- **Function**: functions are objects defined with `SymbolicFunction` class. Each function has a defined set of inputs and outputs. Inputs are independent variables, whic are basically bare symbols, without any structure. Outputs are dependent variables, meaning they are variables defined by expressions which are dependent on the input variables.
- **Global and local**: in this context "global" means "defined at the root-level of the routine we compile". Local, on the contrary, means "defined for the subroutine currently being operated on". Intuitively, global parameters are those that correspond to the domain of the problem that we are trying to solve, e.g.: number of bits of precision of QPE. On the other hand, local parameters correspond to the implementation details of the algorithm we are analyzing, e.g.: size of the QFT routine in QPE. Compilation process allows us to express all the local parameters in the algorithm in terms of the global parameters.


30 changes: 29 additions & 1 deletion docs/concepts/precompilation.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
# Precompilation
# Precompilation

Precompilation is a collection of steps we perform prior to the proper compilation. They apply a series of reasonable assumptions about what the user would have wanted when they have not been explicit in defining their routine.

## Usage


By default precompilation is performed in the `compile_routine` method with a set of default stages. However, API for precompilation has been designed to make customizing precompilation stages easy.

First, user can define their own precompilation stages. All precompilation stages have the same interface – as inputs they take `Routine` and `SymbolicBackend` and they modify the provided `Routine` object in place. Therefore adding a custom stage requires simply writing a method which complies with this interface.

Second, the choice and order of the stages used can be changed by passing them explicitly through `precompilation_stages` argument of the `compile_routine` method.

Precompilation can also be performed outside of the `compile_routine` by using `precompile` method.


## Precompilation stages

`bartiq.precompilation` module currently contains the stages listed below. They are applied by default in the order provided.

1. Add default properties: adds default properties for the subroutines of certain type. Right now it only acts on the routines of type `merge`. If the size of the output port is not specified, it sets it to the sume of the sizes of the input ports.

2. Add default additive resources: if the user defined resource of type `additive` anywhere in any of the subroutines, it implies that this resource should be included in their parent as a sum of the resources of their children. An example of an additive resource is T gate – if a routine contains two children A and B, which have resource `T` defined, equal to `N_A` and `N_B`, after performing this stage, the parent will also have resource `T` with the value equal to `N_A + N_B`.

3. Add passthrough placeholder: passthrough is a situation where we have a routine, which has a connection that goes straight from the input to output port, without touching any subroutines on its way. Currently Bartiq can't handle such cases in the compilation process and in order to get around in the precompilation process we add a "virtual" routine of type `passthrough`, which is not associated with any real operation (it can be thought of as an "identity gate"), but which removes the passthrough from the topology of the routine.

4. Removing non-root non-leaf input register sizes: currently Bartiq cannot handle situation where port get a size assigned twice. For example, the first assigned comes from how the port is defined and the second comes from the connection (i.e. we derive the size of the port based on the fact that we know the size of the port on the other side of the connection). In order to alleviate this issue, in this precompilation step we set to `None` sizes of the input ports of all the routines which are neither root nor leaf.

5. Unroll wildcarded resources: if wildcard symbol (`~`) is detected in the resource, it gets replaced with the proper expression. An example usage would be when a routine has multiple children and we only want to add the resources of the children with the name fitting certain pattern, e.g.: `cost = sum(select_~.cost)` would only add `cost` for the children whose names start with `select_` and that have `cost` resource defined.
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

## Intro

Bartiq is a library that allows to analyze a quantum algorithms and calculate symbolic expressions for quantum resource estimates (QRE).
Bartiq is a library that allows one to analyze quantum algorithms and calculate symbolic expressions for quantum resource estimates (QRE).

## Installation

To install `bartiq` run: `pip install bartiq`. For more details follow instructions on the [installation page](installation.md).

## Quick start

In Bartiq we take a quantum algorithm expressed as a collection of subroutines, each with it's costs expressed as symbolic expressions, and compile it to get cost expression for the whole algorithm.
In Bartiq we take a quantum algorithm expressed as a collection of subroutines, each with its costs expressed as symbolic expressions, and compile it to get cost expression for the whole algorithm.

As an example we use Alias Sampling – an algorithm proposed by [Babbush et al.](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.8.041015). Here's how it's depicted in the paper:

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ Debugging `bartiq` is not always straightforward, so please see below for a numb
- If not, consider creating one!
- Submitting issue is the most transparent way to give us feedback – even if something works, but is extremely unintuitive, we want to make it easier to use. The goal of this tool is to save you time, not waste it on unhelpful error messages.

- If you see a `passthrough` in your error message, but you don't know where it came from, passthroughs are added automatically during one of the precompilation stages.
- If you see a `passthrough` in your error message, but you don't know where it came from, passthroughs are added automatically during one of the precompilation stages (see [precompilation page](concepts/precompilation.md) for more details).
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ nav:
- tutorials/index.md
- tutorials/01_basic_example.ipynb
- tutorials/02_alias_sampling_basic.ipynb
- Concepts:
- concepts/compilation.md
- concepts/precompilation.md
- troubleshooting.md
- limitations.md
- reference.md
Expand Down
74 changes: 2 additions & 72 deletions src/bartiq/compilation/_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,74 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Bartiq compilation module.
Glossary:

- Routine: a topological definition of a quantum circuit. In this definition, each quantum subroutine is represented
as a routine with input and/or output ports, representing the qubit registers the inputs and/or outputs
respectively. Qubit register flows between subroutines are represented by connections between subroutine ports.
Root-level ports represent input/output quantum registers of the top-level quantum routine.
Each subroutine also contains information about its quantum resource costs
- Estimate: the output of compiling a circuit with an assignment of routines. After compilation, each
routines's resources and register sizes now represent the cost of the routine in terms of the estimate's
high-level parameters (rather than the parameters of the original routine).

Estimate compilation is a complex procedure that involves a number of interacting, subtle steps.
At a high level there are three major stages to the process:

1. Routine precompilation
2. Routine-to-function conversion
3. Estimate compilation

Below describes the main concepts in each of these in more detail.

1. Routine precompilation
This stage "precompiles" the routine by making a series of reasonable assumptions about what the user would have
wanted when they have not been explicit. For example, insertion of "trivial" routines for known subroutines or
"filling out" routines which have only been partially defined. (It is expected that this precompilation stage will
grow as more sensible defaults are requested by users.)
This stage is applied via `run_estimate_precompilation`

2. Routine-to-function conversion
Before being compiled, routines must first be mapped to compilable function objects.
At this stage, we perform a number of steps to map the routines into functions that are ready for
compilation:

1. Map routines to "local" functions: first, we simply map the routines to functions in a local manner, whereby they
are unaware of their location within the full definition. We use RoutineWithFunction, which augments Routine
class with a `symbolic_function` field.
2. Map "local" functions to "global" functions: next, we tell the functions where they live in the definition, thereby
making each parameter and any named functions unique within the definition.
3. Pull in input register sizes: in order to allow for input register sizes to be compiled properly, low level input
register sizes must be renamed by the size parameters used by high-level registers. This process is referred to as
"pulling in" those high-level register sizes.
4. Push out output register sizes: to ensure correct compilation for output register sizes, those associated with
low-level routines must be "push out" to either other low-level routines or eventually high-level routines.
5. Parameter inheritance: lastly, any high-level routines parameters are passed down to low-level estimators by
substituting the low-level routine parameters with the high-level ones.

Once this is complete, the final function is stored back as one of the routine's attributes.
This stage is applied via `_map_estimators_to_functions`

3. Estimate compilation

Lastly, the entire estimate is compiled. Compilation occurs at a routine-local level, bottom-to-top, such that each
routine's estimate is compiled strictly after all its children's estimates have been compiled. This stage also
contains a number of sub-steps:

1. Compile functions of self and children (non-leaves only): for leaves, no function compilation is necessary, but for
non-leaves we must compile our own function with those of our direct descendent children.
2. Undo register size pulling and pushing: to ensure a correct local estimate, we must "undo" the pulling in and pushing
out of register size params done during routine-to-function conversion.
3. Derefence global namespace: again, to ensure a correct local routine definition, we dereference the global
namespace.
4. Map function to routine: lastly, once the correct local function has been compiled, we map the function object
back to a routine and update routine's attribute

This stage is applied via `_compile_routine_with_functions`

"""

from typing import Any, Optional, cast, overload

from .. import Port, Routine
Expand Down Expand Up @@ -155,22 +87,20 @@ def _compile_routine(
global_functions: Optional[list[str]] = None,
functions_map: Optional[FunctionsMap] = None,
):
# NOTE: Old compilation algorithm was written with the assumption that root is always an empty string.
# The fact that remove the name and then add it back at the end is a hack solution and needs to be fixed in future.
root_name = routine.name
routine.name = ""
precompile(routine, precompilation_stages=precompilation_stages, backend=backend)

# NOTE: This step must be completed BEFORE we start to compile the functions, as parents must be allowed to
# update their childrens' functions (to support parameter inheritance).
routine_with_functions = _add_func_to_routine(routine, global_functions, backend)
routine_with_functions = _add_function_to_routine(routine, global_functions, backend)

compiled_routine_with_funcs = _compile_routine_with_functions(routine_with_functions, functions_map, backend)
compiled_routine_with_funcs.name = root_name
return compiled_routine_with_funcs.to_routine()


def _add_func_to_routine(
def _add_function_to_routine(
routine: Routine, global_functions: Optional[list[str]], backend: SymbolicBackend[T_expr]
) -> RoutineWithFunction[T_expr]:
"""Converts each routine to a symbolic function."""
Expand Down
6 changes: 3 additions & 3 deletions src/bartiq/compilation/_symbolic_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .. import Port, Resource, ResourceType, Routine
from ..errors import BartiqCompilationError
from ..symbolics.backend import SymbolicBackend, T_expr
from ..symbolics.utilities import infer_subcosts
from ..symbolics.utilities import infer_subresources
from ..symbolics.variables import DependentVariable, IndependentVariable
from ._utilities import is_constant_int, is_single_parameter, split_equation
from .types import FunctionsMap, Number
Expand Down Expand Up @@ -522,8 +522,8 @@ def to_symbolic_function(routine: Routine, backend: SymbolicBackend[T_expr]) ->
routine: The routine to be mapped to a symbolic function.
backend: A backend used for manipulating symbolic expressions.
"""
subcosts = infer_subcosts(routine, backend)
inputs = [IndependentVariable.from_str(input_symbol) for input_symbol in list(routine.input_params) + subcosts]
subresources = infer_subresources(routine, backend)
inputs = [IndependentVariable.from_str(input_symbol) for input_symbol in list(routine.input_params) + subresources]

# NOTE: since multiple ports can have the same input size, this map defines a substitution for size parameters to
# the variable corresponding to the first register with said size. Given that such variables are suffixed by the
Expand Down
8 changes: 4 additions & 4 deletions src/bartiq/precompilation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
from ._core import default_precompilation_stages, precompile
from .stages import (
AddPassthroughPlaceholder,
add_default_additive_costs,
add_default_additive_resources,
add_default_properties,
remove_non_root_container_input_register_sizes,
unroll_wildcarded_costs,
unroll_wildcarded_resources,
)

__all__ = [
"precompile",
"default_precompilation_stages",
"remove_non_root_container_input_register_sizes",
"add_default_properties",
"add_default_additive_costs",
"unroll_wildcarded_costs",
"add_default_additive_resources",
"unroll_wildcarded_resources",
"AddPassthroughPlaceholder",
]
Loading
Loading