> ## Documentation Index
> Fetch the complete documentation index at: https://prod-mint.classiq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Quantum Layer

The Classiq engine exports the `QLayer` object, which inherits from `torch.nn.Module` (like most objects in the `torch.nn` namespace), and it acts like one.

The `QLayer` object is defined like this:

[comment]: DO_NOT_TEST

```python theme={null}
class QLayer(nn.Module):
    def __init__(
        self,
        quantum_program: QuantumProgram,
        execute: ExecuteFunction,
        post_process: PostProcessFunction,
    ) -> None: ...
```

Or,

[comment]: DO_NOT_TEST

```python theme={null}
class QLayer(nn.Module):
    def __init__(
        self,
        quantum_program: QuantumProgram,
        post_process: PostProcessFunction,
    ) -> None: ...
```

The first parameter, `quantum_program`, is the result of [synthesizing a quantum model](/user-guide/synthesis/index).
Note that the parameters are assumed to follow the API stated in [qnn](/user-guide/applications/qml/qnn/qnn).

The second parameter is a callable which is responsible for executing the quantum program, usually with [`execute_qnn`](#execution).
It takes a `QuantumProgram` and `MultipleArguments` (a list of arguments sets to assign to the quantum program parameters) as inputs, and returns a `ResultsCollection`.
Note that this argument can be left out, as demonstrated in the second code block.
If it is not supplied, the layer will create an `ExecutionSession` and sample the quantum program automagically.

<Accordion title="note">
  In order to properly close the `ExecutionSession`, if it was created, call the `teardown` method of `QLayer`.
</Accordion>

The third parameter is a callable which is responsible for post-processing each execution result. It takes a `SavedResult` as input, process it and returns a `Tensor`.

## Saving and loading models (checkpointing)

For hybrid QNN models that include a `QLayer`, prefer saving and loading only the weights:

[comment]: DO_NOT_TEST

```python theme={null}
# Save
torch.save(model.state_dict(), path)

# Load (recreate model structure first, then load weights)
model = Net(...)  # same structure as when saved
model.load_state_dict(torch.load(path))
```

Whole-model pickle (`torch.save(model, path)`) is supported, but `post_process` and the execution path cannot be pickled when they are local functions, lambdas, or closures (e.g. defined inside `__init__`). In that case they are omitted from the checkpoint. After loading:

1. Call `layer.register_post_process(your_post_process)` on each `QLayer` that had a non-serializable `post_process` before calling `forward()`.
2. If you know `post_process` is not serializable, you can pass `serializable_post_process=False` when constructing the `QLayer` to skip pickling it and avoid a warning on save.

## Example callables

An example of such callables:

[comment]: DO_NOT_TEST

```python theme={null}
import torch

from classiq.applications.qnn.types import (
    MultipleArguments,
    SavedResult,
    ResultsCollection,
)

from classiq import execute_qnn
from classiq.synthesis import QuantumProgram


def execute(
    quantum_program: QuantumProgram, arguments: MultipleArguments
) -> ResultsCollection:
    return execute_qnn(quantum_program, arguments)


def post_process(result: SavedResult) -> torch.Tensor:
    # for example, post-processing can take some value out of `result.value.counts`, which is a `dict`
    value = _post_process_result(result)
    return torch.tensor(value)
```

## Execution

To facilitate the execution of your quantum layer, we supply the utility function `execute_qnn`.
It enables you to easily execute a batch of input arguments, and instruct whether you want the sample results or the estimation results according to a specific observable.

The inputs for `execute_qnn` are:

* `quantum_program` of type `QuantumProgram`
* `arguments` of type `MultipleArguments`
* (optionally) `observable` of type `PauliOperator`.

The function returns a `ResultsCollection`, which is a list of `SavedResult` objects (see [Execution Results](/user-guide/execution/index#results) for more information).

The type of each `SavedResult` depends on the `observable` input:

* If no `observable` were given, the type would be `ExecutionDetails`.
* Otherwise, the type would be `EstimationResult`.

<Accordion title="note">
  If only one observable was given, `execute_qnn` will estimate the execution of all batched arguments with this observable.

  If more than one observable was given, their number should match the number of batched arguments, and each execution with a set of arguments will be estimated with the matching observable.
</Accordion>

### Examples

[comment]: DO_NOT_TEST

```python theme={null}
# Execute and return the sample results
def execute(
    quantum_program: QuantumProgram, arguments: MultipleArguments
) -> ResultsCollection:
    return execute_qnn(quantum_program, arguments)


# Execute and return the estimation results according to a specific observable
def execute(
    quantum_program: QuantumProgram, arguments: MultipleArguments
) -> ResultsCollection:
    return execute_qnn(
        quantum_program,
        arguments,
        observable=PauliOperator(
            pauli_list=[("II", 1 / 2), ("IZ", -1 / 2), ("ZI", -1 / 2)]
        ),
    )
```

## Behind the Scenes

Behind the scenes, the `QLayer` handles the following actions:

* Processing of the PQC
* Initializing and tracking of parameters
* Passing the inputs and weights (as multi-dimensional tensors) to the execution function
* Passing the results from the execution function to the post-processing function
* Gradient calculation on the PQC
