Skip to content

Commit

Permalink
[Docs] Add contents to the docs (#21)
Browse files Browse the repository at this point in the history
In this PR the contents of the documentation pages will be filled.

- [x] Improve docstrings
- [x] Write general documentation pages

---------

Co-authored-by: Kaonan Micadei <[email protected]>
Co-authored-by: RolandMacDoland <[email protected]>
  • Loading branch information
3 people authored Dec 9, 2024
1 parent 8e501c0 commit b600f7f
Show file tree
Hide file tree
Showing 21 changed files with 476 additions and 108 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ Before making a contribution, please review our [code of conduct](docs/getting_s

## License

Qadence Expressions is a free and open source software package, released under the Apache License, Version 2.0.
Qadence 2 IR is a free and open source software package, released under the Apache License, Version 2.0.
3 changes: 1 addition & 2 deletions docs/api/factory.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
!!! warning
This page is under construction.
# Factory

::: qadence2_ir.factory
4 changes: 1 addition & 3 deletions docs/api/factory_tools.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
!!! warning
This page is under construction.

# Factory Tools

::: qadence2_ir.factory_tools
16 changes: 14 additions & 2 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
!!! warning
This page is under construction.
# API Reference

Here you can find the API specification for Qadence 2 IR.
There is a page for each module in the Qadence 2 IR package, in which all class and function definitions are documented.
The API reference is particularly useful to check the behavior of classes and functions, and to get information on arguments, attributes and other details.

Qadence 2 IR has 5 modules that are each responsible for different aspects of the IR.
For more information, see their dedicated pages:

- [`qadence2-ir.factory`](./factory.md): Defines a factory function that creates a compile function.
- [`qadence2-ir.types`](./types.md): Defines the valid types to be used in Qadence 2 IR code.
- [`qadence2-ir.irast`](./irast.md): Defines the AST that is used in front-end to IR compilation.
- [`qadence2-ir.irbuilder`](./irbuilder.md): Defines the interface for front-ends compilation.
- [`qadence2-ir.factory_tools`](./factory_tools.md): Defines tools for processing AST objects during compilation.
3 changes: 1 addition & 2 deletions docs/api/irast.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
!!! warning
This page is under construction.
# IRAST

::: qadence2_ir.irast
3 changes: 1 addition & 2 deletions docs/api/irbuilder.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
!!! warning
This page is under construction.
# IR Builder

::: qadence2_ir.irbuilder
3 changes: 1 addition & 2 deletions docs/api/types.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
!!! warning
This page is under construction.
# Types

::: qadence2_ir.types
13 changes: 13 additions & 0 deletions docs/contents/challenges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Challenges

As pointed out [here](./ir_structure.md), digital and analog algorithms diverge in how they handle register topology. Contrary to classical computing, where the resource allocation is typically left to the OS to control, the quantum resources are explicit in this analog quantum computating IR.

For digital devices and circuit-based algorithms, the register topology is important mostly during the compilation phase to reduce the number of SWAP gates applied. Analog algorithms and devices, on the other hand, rely on the topology to ensure the proper interaction between qubits regarding connectivity and strength. That led us to consider including the abstract representation of the register (either by unitless coordinates or connectivity graph) as part of the IR.

However, register preparation doesn’t represent an instruction in the sense of runtime since it needs to be loaded before the sequence starts and (for analog algorithms) cannot be changed during execution. Even if shuttling is available, the initial register configuration needs to be known to properly evaluate the atoms’ movement since such action will affect the connectivity of the register.

Besides the register, other elements like the SLM used to target individual qubits are part of the “booting”/resources allocation that is not directly connected to the register but cannot be addressed as regular instructions. Still, its presence may affect the behavior of specific pulses, which motivated the inclusion of a “Directives” section on the IR.

Primitive operations are another challenge in neutral atoms. The analog nature of the algorithms and device makes it difficult to clearly define “primitive operations”. Elementary structures like a pulse corresponding to the neutral atom Hamiltonian and an idle/wait instruction to let the qubits interact under free coupling (without drive).

To avoid define a fixed set of operations that may not reflect the hardware capabilities and to avoid constant changes in the IR definition to include new primitives, the instructions’ names are passed as labels like `QuInstrunct("dyn_pulse", …)` and `QuInstruct("rx", …)` instead of `Pulse(…)` and `RX(…)`. This may change in the future. However, right now, this flexibility allows us to explore the hardware's capabilities without being held by a particular set of instructions.
10 changes: 10 additions & 0 deletions docs/contents/compute_stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Compute Stack

Pasqal’s compute stack comprises four layers, as shown in Figure 1. A user defines the quantum computation using one of the front-ends: Qadence 2 Expressions or PQL. The computation to be executed is processed from layer to layer and eventually executed on the hardware. In each layer, a quantum computation is expressed in a specific data structure. The higher up the layer is in the stack, the more hardware details are abstracted away.

The top layer is user-facing, with the highest level of abstraction. For each front-end, a compiler exists that compiles the computation in Qadence 2 IR. See the section IR structure for more details on its definition. The low-level compilation process targets a backend, either a QPU or a simulator, and compiles the computation into code that can run on the targeted backend. The backend itself takes care of executing the computation on the hardware.

It's important to note that the Qadence 2 IR layer spreads over the full width of the stack, meaning that all front-ends can compile to it and any backend can be targeted from it. The two-step compilation approach reduces the coupling between elements in the stack significantly and makes the codebase, therefore, more maintainable.

![Qadence 2 stack](qadence2_stack.png)
**Figure 1:** The Qadence 2 software stack.
2 changes: 2 additions & 0 deletions docs/contents/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Qadence2 IR
Qadence 2 IR is a Pasqal initiative, to define an intermediate representation structure for neutral atom devices. The structure captures the key elements of the platform while remaining agnostic regarding hardware specifications. The goal is to simplify instruction building of analog quantum algorithms, enabling optimized instructions and compilation processes of task-specific algorithms to different platforms. By using an agnostic instruction set, Qadence 2 IR allows digital and analog instructions to work together, extending its usability to the digital-analog paradigm. The IR uses static single-assignment to simplify differentiability when running simulations.
25 changes: 25 additions & 0 deletions docs/contents/ir_structure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# IR Structure
The main idea for Qadence 2 IR is to provide an abstract neutral atom device model.

Neutral atom devices usually rely on absolute values like laser power, atomic spacing in micrometers, and nanosecond pulse duration. Since the interaction between atoms and, therefore, the execution of algorithms in such devices are heavily influenced by those parameters, having a well-tuned algorithm for a specific device is desired. With that in mind, the IR definition should be independent of device-specific parameters, leaving their implementation to the backend. At the same time, the front-end compilation pipeline must build the IR from the algorithm and bridges with the backend at the low-level compilation pipeline, accessing its runtime resources.

Analog-relevant data such as the qubit register, parametric symbols and quantum instructions are wrapped in the IR, enabling each backend to handle them case-by-case. In particular, the instructions will provide only minimal information, such as the qubit support and the instruction label, i.e., which quantum operator or instruction is being applied, and the backend must provide some implementation for it.

The IR Model is split in four sections:

- Inputs
- Instructions
- Register
- Directives

The *Inputs* section is responsible for declaring the classical data and flagging them as trainable or not. This information is desired to ensure that only the parameters used in the machine learning training steps are considered for differentiability.

The *Instruction* section holds the sequence of classical computation via static single-assignment to avoid duplicate computation and help the differentiability instructions.

Quantum operations are passed as labels instead of fixed primitives (see the Challenges section). The IR definition is independent of device-specific parameters and leaves their configuration to the compiler, which builds an IR algorithm into instructions that contain the device-specific parameters.

The *Register* section holds either an abstract description of how the atoms are placed on the register or a connectivity graph, depending on the type of algorithm. Algorithms that don’t require customized registers are allowed to pass only the number of qubits. This process is delegated to the backend compiler to decide the best strategies to organize the atoms whenever possible.

The *Directives* section holds other device critical information for resource allocation like SLM mask target for individual qubit addressability.

Resource allocation, such as Registers and Directives, is usually not expected in an intermediate representation. However, as described before, those elements can affect algorithm design and pulse execution. The Challenges section presents more details about them.
Binary file added docs/contents/qadence2_stack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions docs/contents/tbd.md

This file was deleted.

171 changes: 171 additions & 0 deletions docs/tutorials/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Examples
The following examples were generate to present some possible algorithms and may not be
fully implementable in the hardware at the moment.

## Example digital input.
```python
Model(
register = AllocQubits(2),
directives = { # for QPU options
},
inputs = {
"x": Alloc(1, trainable=True),
},
instructions = [

# data encoding
Assign("%0", Call("mul", 0.5, Load("x"))),
QuInstruction("rx", Support(target=(0,)), Load("%0")),

# cnot
QuInstruction("x", Support(target=(1,), control=(0,))),
],
)
```

## Example digital-analog input.
```python
Model(
register = AllocQubits(4),
directives = {
"dmm": {
"targets": [0, 1, 2, 3],
"weights": "equal",
}
},
settings = {},
inputs = {
"theta": Alloc(4, trainable=False)
"duration": Alloc(1, trainable=True)
"omega": Alloc(5, trainable=True)
},
instrunctions = [
# Tower feature map
Assign("%0", Call("mul", 0.31831, Load("theta")),
QuInstruct("set_dmm", Support.target_all(), Load("%0"))
QuInstruct("rx", Support.target_all(), 1.570796),
QuInstruct("dyn_local_pulse", Support.target_all(), 2.0),
QuInstruct("rx", Support.target_all(), -1.570796),

# Entanglement
QuInstruct("dyn_interact", Support.target_all(), 2.5),

# Trainable layer
QuInstruct("dyn_pulse", Support.target_all(), Load("duration"), Load("omega"), 0.0, 0.0),
],
)
```

## Example analog input.
```python
Model(
register = AllocQubits(
num_qubits = 4,
qubits_positions = [
(-2, 1), (-1, 0), (0, 0), (1, -1)
],

# optional parameters
grid_type = "triangular",
grid_scale = 1.0,
),
directives = {
"dmm": {
"targets": [0, 3],
"weights": [0.5, 1.0],
}
},
inputs = {
"duration": Alloc(1, trainable=False, attrs={"time_parameter": True}),
"omega": Alloc(4, trainable=True),
"delta": Alloc(3, trainable=True),
},
instrunctions = [
QuInstruct(
"dyn_pulse",
Support.target_all(),
Load("duration"),
Load("omega"),
Load("delta"),
0.0, # phase
),
QuInstruction(
"dyn_local_phase",
Support(target=(0, 1)), # match with dmm targets
1.2, # duration
attrs={
"concurrent": True, # starts with the previous pulse
}
),
],
)
```

## Example analog input (alternative)
This example is intend to be used with backends that either support crossing-lattice or similar
algorithms, or gridless backends (e.g. PyQ).
```python
Model(
register = AllocQubits(
num_qubits = 4,
connectivity = {
(0, 1): 1.2,
(0, 3): 0.9,
(1, 2): 1.4,
(2, 3): 2.1,
}
),
directives = {
"dmm": {
"targets": [0, 3],
"weights": [0.5, 1.0],
}
},
inputs = {
"duration": Alloc(1, trainable=False, attrs={"time_parameter": True}),
"omega": Alloc(4, trainable=True),
"delta": Alloc(3, trainable=True),
},
instrunctions = [
QuInstruct(
"dyn_pulse",
Support.target_all(),
Load("duration"),
Load("omega"),
Load("delta"),
0.0, # phase
),
QuInstruction(
"dyn_local_phase",
Support(target=(0, 1)), # match with dmm targets
attrs={
"concurrent": True, # starts with the previous pulse
"duration": 1.2,
}
),
],
)
```

```python
Model(
register=AllocQubits(
num_qubits=3,
connectivity={(0,1): 1., (0,2): .5, (1,2): .5},
),
directives={
"dmm": {"targets": [0, 1]}
},
inputs={
't': Alloc(1, trainable=True)
},
instructions=[
# The presence of the `dmm` allows a single qubit operation by
# dynamic decoupling the others two qubits.
QuInstruct('x', Support(target=(2,))),

Assign('%0', Mul(1.57, Load('t')),
QuInstruct('dyn_pulse', target_all(), Load('%0'), 1.0),
],
)
```
2 changes: 0 additions & 2 deletions docs/tutorials/index.md

This file was deleted.

7 changes: 5 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ nav:
- License: getting_started/LICENSE.md

- Contents:
- TBD: contents/tbd.md
- contents/index.md
- Compute Stack: contents/compute_stack.md
- IR Structure: contents/ir_structure.md
- Challenges: contents/challenges.md

- Tutorials:
- Tutorials: tutorials/index.md
- Tutorials: tutorials/examples.md

- API:
- api/index.md
Expand Down
17 changes: 12 additions & 5 deletions qadence2_ir/factory.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""This module defines a factory method that creates a compiler function based on an `IRBuilder`.
The compiler function, that can be generated using the factory, should be used to to compile a
certain type of input, based on the front-end that is being used, to IR code. This is the first
step of compilation, which is followed by a compilation from IR to the targeted backend.
"""

from __future__ import annotations

from typing import Callable
Expand All @@ -9,17 +16,17 @@


def ir_compiler_factory(builder: IRBuilder[InputType]) -> Callable[[InputType], Model]:
"""Use an IRBuilder[InputType] to create an IR compiler function that converts an input of type
`InputType` and returns a Model.
"""Constructs an IR compiler function for a specific input type by using an `IRBuilder`.
The IR compiler must be named 'compile_to_model' by convention to ensure accessibility to other
engines in the framework.
The factory function uses an `IRBuilder[InputType]` to create an IR compiler function that
converts an input of type `InputType` and returns a Model. The IR compiler must be named
'compile_to_model' by convention to ensure accessibility to other engines in the framework.
Args:
builder: A concrete implementation of the generic class `IRBuilder` for a particular
`InputType`.
Return:
Returns:
A function that compiles an `InputType` object to the Qadence-IR (`Model`).
"""

Expand Down
Loading

0 comments on commit b600f7f

Please sign in to comment.