diff --git a/README.md b/README.md index 571f956..bf91bb5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/api/factory.md b/docs/api/factory.md index 48fe167..2558776 100644 --- a/docs/api/factory.md +++ b/docs/api/factory.md @@ -1,4 +1,3 @@ -!!! warning - This page is under construction. +# Factory ::: qadence2_ir.factory diff --git a/docs/api/factory_tools.md b/docs/api/factory_tools.md index f55ec98..48a10a3 100644 --- a/docs/api/factory_tools.md +++ b/docs/api/factory_tools.md @@ -1,5 +1,3 @@ -!!! warning - This page is under construction. - +# Factory Tools ::: qadence2_ir.factory_tools diff --git a/docs/api/index.md b/docs/api/index.md index aea97e4..6b7d184 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -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. diff --git a/docs/api/irast.md b/docs/api/irast.md index 3f3bc7c..3b38975 100644 --- a/docs/api/irast.md +++ b/docs/api/irast.md @@ -1,4 +1,3 @@ -!!! warning - This page is under construction. +# IRAST ::: qadence2_ir.irast diff --git a/docs/api/irbuilder.md b/docs/api/irbuilder.md index 1227aa1..e9faa9f 100644 --- a/docs/api/irbuilder.md +++ b/docs/api/irbuilder.md @@ -1,4 +1,3 @@ -!!! warning - This page is under construction. +# IR Builder ::: qadence2_ir.irbuilder diff --git a/docs/api/types.md b/docs/api/types.md index 37bbcc5..de2fc1b 100644 --- a/docs/api/types.md +++ b/docs/api/types.md @@ -1,4 +1,3 @@ -!!! warning - This page is under construction. +# Types ::: qadence2_ir.types diff --git a/docs/contents/challenges.md b/docs/contents/challenges.md new file mode 100644 index 0000000..107c431 --- /dev/null +++ b/docs/contents/challenges.md @@ -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. diff --git a/docs/contents/compute_stack.md b/docs/contents/compute_stack.md new file mode 100644 index 0000000..e817a77 --- /dev/null +++ b/docs/contents/compute_stack.md @@ -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. diff --git a/docs/contents/index.md b/docs/contents/index.md new file mode 100644 index 0000000..43624d6 --- /dev/null +++ b/docs/contents/index.md @@ -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. diff --git a/docs/contents/ir_structure.md b/docs/contents/ir_structure.md new file mode 100644 index 0000000..9fb5f2f --- /dev/null +++ b/docs/contents/ir_structure.md @@ -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. diff --git a/docs/contents/qadence2_stack.png b/docs/contents/qadence2_stack.png new file mode 100644 index 0000000..31112b6 Binary files /dev/null and b/docs/contents/qadence2_stack.png differ diff --git a/docs/contents/tbd.md b/docs/contents/tbd.md deleted file mode 100644 index 1d66995..0000000 --- a/docs/contents/tbd.md +++ /dev/null @@ -1,2 +0,0 @@ -!!! warning - This page is under construction. diff --git a/docs/tutorials/examples.md b/docs/tutorials/examples.md new file mode 100644 index 0000000..df5c0a5 --- /dev/null +++ b/docs/tutorials/examples.md @@ -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), + ], +) +``` diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md deleted file mode 100644 index 1d66995..0000000 --- a/docs/tutorials/index.md +++ /dev/null @@ -1,2 +0,0 @@ -!!! warning - This page is under construction. diff --git a/mkdocs.yml b/mkdocs.yml index 318b58b..e054ba5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/qadence2_ir/factory.py b/qadence2_ir/factory.py index ee6e791..26f776e 100644 --- a/qadence2_ir/factory.py +++ b/qadence2_ir/factory.py @@ -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 @@ -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`). """ diff --git a/qadence2_ir/factory_tools.py b/qadence2_ir/factory_tools.py index f1a6f0a..629864d 100644 --- a/qadence2_ir/factory_tools.py +++ b/qadence2_ir/factory_tools.py @@ -1,3 +1,9 @@ +"""Tools to process an `AST` as used by the compiler factory in `qadence2-ir.factory`. + +This module defines a collection of functions that manipulate an `AST`. Using these AST manipulation +tools the factory function in `qadence2-ir.factory` can build the instruction list from a given AST. +""" + from __future__ import annotations from functools import reduce @@ -8,23 +14,23 @@ def filter_ast(predicate: Callable[[AST], bool], ast: AST) -> Iterable[AST]: - """Filter the elements of the AST according to the `predicate` function. + """Filters the elements of the AST according to the `predicate` function. Args: - predicate: A function to check if a specific property is present in the `ast`. - ast: A parsed tree containing the sequence of instructions to be added to the `Model`. + predicate: A function that checks if a specific property is present in the `ast`. + ast: A parsed AST containing the sequence of instructions to be added to the `Model`. - Return: + Returns: An iterable and flattened version of the AST that contains the selected elements. Example: - ``` - >>> ast = AST.binar_op("/", AST.numeric(2), AST.callable("fn", AST.numeric(3))) + + ```python + >>> ast = AST.div(AST.numeric(2), AST.callable("fn", AST.numeric(3))) >>> list(filter_ast(lambda x: x.is_numeric, ast)) [AST.numeric(2), AST.numeric(3)] - ```` + ``` """ - # TODO edit example: binar_op is not available in AST if predicate(ast): yield ast @@ -37,18 +43,18 @@ def filter_ast(predicate: Callable[[AST], bool], ast: AST) -> Iterable[AST]: def flatten_ast(ast: AST) -> Iterable[AST]: - """Returns an interable and flattened version of the AST. + """Returns an iterable and flattened version of the AST. Args: ast: A parsed tree containing the sequence of instructions to be added to the `Model`. - Return: + Returns: An iterable and flattened version of the AST. The arguments of operations/functions will appear before the operation/function. Example: - ``` - >>> ast = AST.binar_op("/", AST.numeric(2), AST.callable("fn", AST.numeric(3))) + ```python + >>> ast = AST.div(AST.numeric(2), AST.callable("fn", AST.numeric(3))) >>> list(flatten_ast(ast)) [ AST.numeric(2), @@ -74,7 +80,7 @@ def extract_inputs_variables(ast: AST) -> dict[str, Alloc]: Args: ast: A parsed tree containing the sequence of instructions to be added to the `Model`. - Return: + Returns: A dictionary with the variables names as keys and their respective allocation instructions as values. """ @@ -83,11 +89,11 @@ def extract_inputs_variables(ast: AST) -> dict[str, Alloc]: def to_alloc(inputs: dict[str, Alloc], ast: AST) -> dict[str, Alloc]: - """If the `ast` is an input variable, add it to the inputs to be allocated - if not present yet. + """If the `ast` is an input variable, add it to the inputs to be allocated if not present yet. Args: - inputs: A dictionary containing pairs of variables and their allocation instructions. + inputs: A dictionary containing pairs of variables and their allocation instructions, which + are already allocated. ast: A parsed tree containing the sequence of instructions to be added to the `Model`. Return @@ -107,13 +113,12 @@ def to_alloc(inputs: dict[str, Alloc], ast: AST) -> dict[str, Alloc]: def build_instructions(ast: AST) -> list[QuInstruct | Assign]: - """Converts a sequence of instructions in the AST form into a list of Model - instructions. + """Converts an AST into a list of `Model` instructions. Args: ast: A parsed tree containing the sequence of instructions to be added to the `Model`. - Return: + Returns: A list of quantum operations and temporary static single-assigned variables. Temporary variables store the outcomes of classical operations and are used as arguments for parametric quantum operations. @@ -131,12 +136,11 @@ def to_instruct( memoise: dict[AST, Load], single_assign_index: int, ) -> tuple[list[QuInstruct | Assign], dict[AST, Load], int]: - """Add the `ast` to the `instructions_list` if `ast` is a classical function - or a quantum instruction. + """Adds the `ast` to the `instructions_list` if it is a `Call` or `QuInstruct`. - When the `ast` is a classical function, it uses the `single_assign_index` to - assign the call to a temporary variable using memoisation to avoid duplicated - assignments. + When the `ast` is a classical function, it uses the `single_assign_index` to assign the call to + a temporary variable using memoisation to avoid duplicated assignments. If the `ast` is a + quantum instruction, the instruction will be added to the instruction list. Args: ast: A parsed tree containing the sequence of instructions to be added to the `Model`. @@ -147,8 +151,8 @@ def to_instruct( single_assign_index: The index to be used by the next temporary variable assignement. Tempmorary variables are labled from "%0" to "%n". - Return: - A tuple consists of an updated list of instructions and assignments, a dictionary of pairs + Returns: + A tuple consisting of an updated list of instructions and assignments, a dictionary of pairs of AST objects and temporary variables, and the updated index for the next assignment. """ diff --git a/qadence2_ir/irast.py b/qadence2_ir/irast.py index 681582a..8485384 100644 --- a/qadence2_ir/irast.py +++ b/qadence2_ir/irast.py @@ -1,3 +1,11 @@ +"""Definition of the abstract syntax tree (AST) for Qadence 2 IR. + +This module defines the abstract syntax tree (AST) in Qadence 2 IR. The AST is used in the front- +end to IR compilation. Implementations of `IRBuilder`, defined in `qadence2-ir.irbuilder`, +translate front-end code to the AST defined here. Then, the tools in `qadence2-ir.factory_tools` +can be used to manipulate an `AST` instance and build valid IR instructions from the AST. +""" + from __future__ import annotations from enum import Flag, auto @@ -9,24 +17,24 @@ class AST: - """A class to keep a clean version of the instruction sequence to be converted - into a list of Model instructions. + """Represents an instruction sequence as an abstract syntax tree (AST). - The initilization of this class must be done using the specific constructors. + The initialization of this class must be done using the specific constructors. Constructors: - AST.numeric(value): For numerical values. - AST.input_variable(name, size, trainable): For literal variables. - AST.callable(fn_name, *args): For classical functions. - AST.support(target, control): For qubit indices. - AST.quantum_op(name, support, *args): For quantum operators with and without parameters. - AST.sequence(*q_ops): For sequences of quantum operations. - AST.add(lhs, rhs): For addition, lhs + rhs. - AST.sub(lhs, rhs): For subtraction, lhs - rhs. - AST.mul(lhs, rhs): For multiplication, lhs * rhs. - AST.div(lhs, rhs): For division, lhs / rhs. - AST.rem(lhs, rhs): For remainder, lhs % rhs. - AST.pow(base, power): For power, base ** power. + + - AST.numeric(value): For numerical values. + - AST.input_variable(name, size, trainable): For literal variables. + - AST.callable(fn_name, *args): For classical functions. + - AST.support(target, control): For qubit indices. + - AST.quantum_op(name, support, *args): For quantum operators with and without parameters. + - AST.sequence(*q_ops): For sequences of quantum operations. + - AST.add(lhs, rhs): For addition, lhs + rhs. + - AST.sub(lhs, rhs): For subtraction, lhs - rhs. + - AST.mul(lhs, rhs): For multiplication, lhs * rhs. + - AST.div(lhs, rhs): For division, lhs / rhs. + - AST.rem(lhs, rhs): For remainder, lhs % rhs. + - AST.pow(base, power): For power, base ** power. """ class Tag(Flag): @@ -61,8 +69,21 @@ def attrs(self) -> dict[Any, Any]: # Constructors @classmethod def __construct__(cls, tag: Tag, head: str, *args: Any, **attrs: Any) -> AST: - """To void arbitrary initialisation, the user must use one of the standard constructors - provided. This method hides the initilisation from the regular `__new__` to enforce that. + """Base constructor method. + + To void arbitrary initialization, this class must be initialized with one of the standard + constructors provided. This method hides the initialization from the regular `__new__` to + enforce that. + + Args: + tag: A Tag indicating the AST type. Has one of the following values: `QuantumOperator`, + `Sequence`, `Support`, `Call`, `InputVariable`, `Numeric`. + head: A string identifier of the AST object. + args: Optional arguments for specific constructors. + attrs: Optional attributes for specific constructors. + + Returns: + ast: A newly constructed AST object. """ token = super().__new__(cls) @@ -77,7 +98,10 @@ def numeric(cls, value: complex | float) -> AST: """Create an AST-numeric object. Args: - value: Numerical value to be converted in the Qadence-IR AST. + value: Numerical value to be converted in the Qadence2-IR AST. + + Returns: + ast: A numerical value AST. """ return cls.__construct__(cls.Tag.Numeric, "", value) @@ -87,13 +111,16 @@ def input_variable(cls, name: str, size: int, trainable: bool, **attributes: Any """Create an AST-input variable. Args: - name: Variable's name. + name: The variable's name. size: Number of slots to be reserved for the variable, 1 for scalar values and n>1 for array variables. - trainable: A boolean flag to indicate if the variable is intend to be optimised or - used as a constand during the run. + trainable: A boolean flag to indicate if the variable is intend to be optimized or + used as a constant during the run. attributes: Extra flags, values or dictionaries that can provide more context to the backends. + + Returns: + ast: An input variable AST. """ return cls.__construct__(cls.Tag.InputVariable, name, size, trainable, **attributes) @@ -103,8 +130,11 @@ def callable(cls, name: str, *args: AST) -> AST: """Create an AST-function object. Args: - name: Function name. + name: The function name. args: Arguments to be passed to the function. + + Returns: + ast: A callable AST. """ return cls.__construct__(cls.Tag.Call, name, *args) @@ -117,6 +147,9 @@ def support(cls, target: tuple[int, ...], control: tuple[int, ...]) -> AST: Args: target: A tuple of indices a quantum operator is acting on. control: A tuple of indices a quantum operator uses as control qubits. + + Returns: + ast: A support AST. """ return cls.__construct__(cls.Tag.Support, "", target, control) @@ -140,6 +173,9 @@ def quantum_op( operators like Puali gates are treated as a parametric operator with no arguments. attributes: Extra flags, values or dictionaries that can provide more context to the backends. + + Returns: + ast: A quantum operator AST. """ support = cls.support(target, control) @@ -152,6 +188,9 @@ def sequence(cls, *quantum_operators: Any) -> AST: Args: quantum_operators: Sequence of quantum operators to be applied by the backend in the given order. + + Returns: + ast: An AST-sequence of quantum operators. """ return cls.__construct__(cls.Tag.Sequence, "", *quantum_operators) @@ -164,6 +203,9 @@ def add(cls, lhs: AST, rhs: AST) -> AST: Args: lhs: Left-hand side operand. rhs: Right-hand side operand. + + Returns: + ast: An AST callable adding `lhs` and `rhs`. """ return cls.callable("add", lhs, rhs) @@ -175,6 +217,9 @@ def sub(cls, lhs: AST, rhs: AST) -> AST: Args: lhs: Left-hand side operand. rhs: Right-hand side operand. + + Returns: + ast: An AST callable subtracting `rhs` from `lhs`. """ return cls.callable("sub", lhs, rhs) @@ -186,6 +231,9 @@ def mul(cls, lhs: AST, rhs: AST) -> AST: Args: lhs: Left-hand side operand. rhs: Right-hand side operand. + + Returns: + ast: An AST callable multiplying `lhs` and `rhs`. """ return cls.callable("mul", lhs, rhs) @@ -197,6 +245,9 @@ def div(cls, lhs: AST, rhs: AST) -> AST: Args: lhs: Left-hand side operand. rhs: Right-hand side operand. + + Returns: + ast: An AST callable dividing `lhs` by `rhs`. """ return cls.callable("div", lhs, rhs) @@ -208,6 +259,9 @@ def rem(cls, lhs: AST, rhs: AST) -> AST: Args: lhs: Left-hand side operand. rhs: Right-hand side operand. + + Returns: + ast: An AST callable; remainder or `lhs` and `rhs`. """ return cls.callable("rem", lhs, rhs) @@ -219,6 +273,9 @@ def pow(cls, base: AST, power: AST) -> AST: Args: base: Base operand. power: Power operand. + + Returns: + ast: An AST callable of `base` to the power `power`. """ return cls.callable("pow", base, power) diff --git a/qadence2_ir/irbuilder.py b/qadence2_ir/irbuilder.py index a9fb2a4..eb7278a 100644 --- a/qadence2_ir/irbuilder.py +++ b/qadence2_ir/irbuilder.py @@ -1,3 +1,11 @@ +"""Defines an ABC for implementing IR builders. + +This module defines the interface to be used by Qadence 2 IR front-ends to compile to IR. A front- +end must implement an `IRBuilder` for the front-end specific input type, so that +`ir_compiler_factory`, defined in `qadence2-ir.factory` can generate a compiler function specific +to the front-end. +""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -8,32 +16,61 @@ class IRBuilder(ABC, Generic[InputType]): - """A base class to help create new input forms for Qadence2-IR. + """Defines the interface of Qadence 2 IR builders for building IR code from front-end input. - Implementing this class allows the function `ir_compiler_factory` to generate - the `compile_to_model` function for a new custom input format. + An `IRBuilder` implementation can be used by the `ir_compiler_factory` function, defined in + `qadence2-ir.factory` to build a compiler function that generates IR code from a specific + input type as created by a Qadence 2 front-end. + When subclassing this class, specify the `InputType` that is expected for the implementation, + i.e. the object type that the specific front-end generates. + This class is responsible for extracting information about the register, directives, other + settings and the AST from front-end generated input. """ @staticmethod @abstractmethod def set_register(input_obj: InputType) -> AllocQubits: - """Used by the factory to define/extract/infere the qubits register primarily from the - `input_obj`. + """Returns a register definition based on an input object. + + Args: + input_obj: Input for the compilation to IR native to a specific front-end. + + Returns: + A register definition that is extracted or inferred from `input_obj`. """ @staticmethod @abstractmethod def set_directives(input_obj: InputType) -> Attributes: - """Use by the factory to set the QPU directives from the input.""" + """Returns directives based on an input object. + + Args: + input_obj: Input for the compilation to IR native to a specific front-end. + + Returns: + A specification of all directives that could be extracted from `input_obj`. + """ @staticmethod @abstractmethod def settings(input_obj: InputType) -> Attributes: - """Used by the factory to define general settings for simulation and data purposes.""" + """Returns settings based on an input object. + + Args: + input_obj: Input for the compilation to IR native to a specific front-end. + + Returns: + A specification of all settings that could be extracted from `input_obj`. + """ @staticmethod @abstractmethod def parse_sequence(input_obj: InputType) -> AST: - """Used by the factory to parse a sequence operations acting on the qubit - register (e.g., quantum circuits, pulse sequences, etc). + """Returns an AST definition that represents the operations in input object. + + Args: + input_obj: Input for the compilation to IR native to a specific front-end. + + Returns: + An AST definition that represents the operations defined in an `input_obj`. """ diff --git a/qadence2_ir/types.py b/qadence2_ir/types.py index fde8777..028a97c 100644 --- a/qadence2_ir/types.py +++ b/qadence2_ir/types.py @@ -1,16 +1,38 @@ +"""Definition of all types supported in the IR. + +This module is the defacto definition of the IR. A unit of IR code can only consist of instances of +the classes defined in this module. An IR `Model` defines a complete set of instructions for a +backend to execute as a task. The instructions a `Model` can take can be divided into two groups; +classical and quantum related instructions: + +- Classical: + - `Alloc`: Allocates memory for holding variable values. + - `Assign`: Assigns a value to a variable. + - `Load`: Retrieves a value from a variable. + - `Call`: Executes a classical function. +- Quantum: + - `AllocQubits`: Allocates qubits for the computation. + - `QuInstruct`: Executes a quantum instruction. + - `Support`: Defines on which qubit(s) and with what role, target or control, a `QuInstruct` + should act. +""" + from __future__ import annotations from typing import Any, Literal class Alloc: - """ - Reserve one slot for a scaler parameter in the environment and n-slots for an array. The type of - the parameter is defined by the backend. + """Memory allocation for a parameter that is either a scalar value or an array of values. + + With this class an allocation of memory is made for a parameter that is a scalar value, if + `size = 1` or an array of values of length `n` if `size = n`. The type for the allocation is + defined by the backend, therefore it is not defined in the IR. Args: - size: Space occupied by the parameter. - trainable: Flag if the parameter can change during a training loop. + size: Number of values stored in the parameter, if `size = 1` the parameter is a scalar + value if `size > 1` the parameter is an array of values. + trainable: Indicates whether the parameter can be changed during a training loop. attributes: Extra flags and information to be used as instructions/suggestions by the backend. """ @@ -36,7 +58,12 @@ def __eq__(self, value: object) -> bool: class Assign: - """Push a variable to the environment and assign a value to it.""" + """Assignment of a value to a variable. + + Args: + variable_name: The name of the variable to assign a value to. + value: The value to be assigned to the variable. + """ def __init__(self, variable_name: str, value: Any) -> None: self.variable = variable_name @@ -55,7 +82,11 @@ def __eq__(self, value: object) -> bool: class Load: - """To recover the value of a given variable.""" + """Instruction to load the value of a variable. + + Args: + variable_name: The name of the variable to load. + """ def __init__(self, variable_name: str) -> None: self.variable = variable_name @@ -71,7 +102,12 @@ def __eq__(self, value: object) -> bool: class Call: - """Indicates the call of classical functions only.""" + """Instruction to call a classical function. + + Args: + identifier: The identifier of the function to call, i.e. its name. + args: The arguments that the function should be called with. + """ def __init__(self, identifier: str, *args: Any) -> None: self.identifier = identifier @@ -94,14 +130,15 @@ def __eq__(self, value: object) -> bool: class Support: - """Generic representation of the qubit support. For single qubit operations, a multiple index - support indicates apply the operation for each index in the support. + """Instruction that specifies the target and optionally control of a `QuInstruct`. - Both target and control lists must be ordered! + Generic representation of qubit support, specifying a target and control for a quantum + instruction. For single qubit operations, a multiple index support indicates apply the + operation for each index in the support. Both target and control lists must be ordered! Args: - target = Index or indices where the operation is applied. - control = Index or indices to which the operation is conditioned to. + target: Index or indices of qubits to which the operation is applied. + control: Index or indices of qubits which to which the operation is conditioned to. """ def __init__( @@ -136,7 +173,7 @@ def __eq__(self, value: object) -> bool: class QuInstruct: - """An abstract representation of a QPU instruction. + """Instruction to apply a quantum operation to one or more qubit(s). Args: name: The instruction name compatible with the standard instruction set. @@ -171,14 +208,13 @@ def __eq__(self, value: object) -> bool: class AllocQubits: - """ - Describes the register configuration in a neutral-atoms device. + """Allocation of qubit in a register of a neutral-atom QPU. Args: - num_qubits: Number of atoms to be allocated. - qubit_positions: A list of discrete coordinates for 2D grid with (0,0) position at center - of the grid. A list of indices in a linear register. An empty list will indicate the - backend is free to define the topology for devices that implement logical qubits. + num_qubits: Number of qubits, i.e atoms, to be allocated. + qubit_positions: A list of discrete coordinates for 2D grid, where (0,0) is the position at + center of the grid. An empty list will indicate the backend is free to define the + topology for devices that implement logical qubits. grid_type: Allows to select the coordinates sets for 2D grids as "square" (orthogonal) or "triangular" (skew). A "linear" will allow the backend to define the shape of the register. When the `grid_type` is `None` the backend uses its default structure @@ -238,14 +274,15 @@ def __eq__(self, value: object) -> bool: class Model: - """Aggregates the minimal information to construct sequence of instructions in a quantum device. - The structure is mainly focused in neutral atoms devices but its agnostic nature may make it - suitable for any quantum device. + """Aggregates the minimal information to construct sequence of instructions to execute on a QPU. + + This class defines a unit of IR code. The structure of the class is mainly focused in neutral + atoms devices but its agnostic nature may make it suitable for any quantum device. Args: - register: Describe the atomic arrangement of the neutral atom register. - instructions: A list of abstract instructions with their arguments with which a backend - can execute a sequence. + register: Describes the atomic arrangement of the neutral atom register. + instructions: A list of abstract instructions with their arguments that a backend can use + to execute a sequence. directives: A dictionary containing QPU related options. For instance, it can be used to set the Rydberg level to be used or whether to allow digital-analog operations in the sequence.