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

# ADAPT QAOA

> **[View on GitHub](https://github.com/Classiq/classiq-library/blob/main/applications/optimization/adapt_qaoa/adapt_qaoa.ipynb)**
>
> Open this notebook in GitHub to run it yourself

The notebook demonstrates the solution of a generic optimization problem using both the "vanilla" QAOA (Quantum Approximate Optimization Algorithm) and the adaptive version

## Hamiltonian Encoding

The optimization  objective function is encoded into a Hamiltonian function formulated as a weighted sum of Pauli strings:

$$
H = \sum_k{c_kP_k}
$$

with each pauli string, $P_k$, composed of a Pauli operation ($\sigma\in\left\{I,X,Y,Z\right\}$) applied to each of the qubits in the register.

For example, $XIIZ$, applies a Pauli $X$ to the first qubit and a Pauli $Z$ to the fourth qubit

The encoding of the objective start by defining the function with binary variables, $x_i\in\left\{1,-1\right\}$, and then defining Pauli strings with $Z_i$ corresponding to the original variables. If the objective variables' domain is $x_i\in\left\{0,1\right\}$, a transformation is used: $x_i=\left(1-Z_i\right)/2$ (commonly referred to as *spin to bit* transformation)

Since the encoding results in Pauli strings composed only of $I$, and $Z$, the Pauli string is called *diagonal*, this has the following benefits:

* **Easy exponentiation** - the exponentiation results in simple to implement single or multi-qubit Z rotations
* **No superposition mixing** - the diagonal Hamiltonian does not create entanglement or rotate between basis states, enabling computation basis measurement
* **Efficient measurement** - all $\left\{I,Z\right\}^{\otimes n}$ strings commute, allowing for a single simultaneous measurement of all strings

```python theme={null}
import matplotlib.pyplot as plt
import numpy as np
import scipy
from tqdm import tqdm

import classiq
from classiq import *
```

#

## QMod Tip

In the following cell the variable `h` is set to a diagonal hamiltonian using a self explanatory syntax that sums sparse Pauli strings, that is only the non identity Paulis are specified.

For example the first line, `- 0.1247375 * Pauli.Z(0) * Pauli.Z(1)` creates the string `ZZIII` with the weight `-0.1247375`.

The length of the string, is inferred from the largest qubit index in the sum.

The resulating variable has the type [SparsePauliOp](https://docs.classiq.io/latest/qmod-reference/api-reference/classical-types/?h=sparsepauliop#classiq.qmod.builtins.structs.SparsePauliOp)

For the optimization function a *dual-use* function is used for the Hamiltonian.

This function takes a argument that can be either a `QArray` or a `list[int]`.

This allows the same function to be used both to:

* compute the cost expectation on the measurement results which are binary numbers ($0,1$)
* create a quantum observable in the circuit, resulting in quantum gates

the additional `cost_func` function negates the `hamiltonian` function since the problem involves finding the maximum value, but the optimization procedure (`scipy.optimize.minimize`) performs a minimization

```python theme={null}
from sympy import sympify

from classiq.qmod.symbolic_expr import SymbolicExpr

QFloatExr = SymbolicExpr


h = (
    -0.561775 * Pauli.Z(0)
    - 1.238125 * Pauli.Z(1)
    - 1.131450 * Pauli.Z(2)
    - 0.681350 * Pauli.Z(3)
    - 0.713775 * Pauli.Z(4)
    - 0.979450 * Pauli.Z(5)
    - 1.145025 * Pauli.Z(6)
    - 0.774450 * Pauli.Z(7)
    + 0.5 * Pauli.Z(0) * Pauli.Z(1)
    + 0.5 * Pauli.Z(0) * Pauli.Z(2)
    - 0.188125 * Pauli.Z(0) * Pauli.Z(4)
    - 0.200100 * Pauli.Z(0) * Pauli.Z(5)
    + 0.5 * Pauli.Z(1) * Pauli.Z(3)
    + 0.234550 * Pauli.Z(1) * Pauli.Z(6)
    + 0.053575 * Pauli.Z(1) * Pauli.Z(7)
    + 0.5 * Pauli.Z(2) * Pauli.Z(3)
    - 0.048100 * Pauli.Z(2) * Pauli.Z(4)
    + 0.229550 * Pauli.Z(2) * Pauli.Z(5)
    - 0.039525 * Pauli.Z(3) * Pauli.Z(6)
    - 0.229125 * Pauli.Z(3) * Pauli.Z(7)
    + 0.5 * Pauli.Z(4) * Pauli.Z(5)
    + 0.5 * Pauli.Z(4) * Pauli.Z(6)
    + 0.5 * Pauli.Z(5) * Pauli.Z(7)
    + 0.5 * Pauli.Z(6) * Pauli.Z(7)
)


def hamiltonian(v: QArray | list[int]) -> QFloatExr:
    # linear Z_i coefficients (diagonal of hh)
    single_weights = [
        -0.561775,  # Z0
        -1.238125,  # Z1
        -1.13145,  # Z2
        -0.68135,  # Z3
        -0.713775,  # Z4
        -0.97945,  # Z5
        -1.145025,  # Z6
        -0.77445,  # Z7
    ]

    # quadratic ZZ coefficients (off-diagonal of hh for i<j)
    quadratic_pairs = [
        (0, 1),
        (0, 2),
        (0, 4),
        (0, 5),
        (1, 3),
        (1, 6),
        (1, 7),
        (2, 3),
        (2, 4),
        (2, 5),
        (3, 6),
        (3, 7),
        (4, 5),
        (4, 6),
        (5, 7),
        (6, 7),
    ]
    quadratic_weights = [
        0.5,
        0.5,
        -0.188125,
        -0.2001,
        0.5,
        0.23455,
        0.053575,
        0.5,
        -0.0481,
        0.22955,
        -0.039525,
        -0.229125,
        0.5,
        0.5,
        0.5,
        0.5,
    ]

    def z_value(bit: int | QBit) -> QFloatExr:
        # maps v_i in {0,1} to Z_i in {+1,-1}
        return sympify(1.0) - 2 * bit

    ham = sympify(0.0)  # no constant term in your SparsePauliOp

    # ZZ terms
    for (i, j), w in zip(quadratic_pairs, quadratic_weights):
        ham += w * z_value(v[i]) * z_value(v[j])

    # Z terms
    for i, w in enumerate(single_weights):
        ham += w * z_value(v[i])

    return ham


def cost_func(v: QArray | list[int]) -> QFloatExr:
    return hamiltonian(v)  # negative sign for maximization
```

## The Cost Layer

As mentioned above, since the Hamiltonian is diagonal, its exponentiation is straightforward and amounts to phase addition due to Z rotations.\
This is accomplished with the [phase](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase.md?h=phase) function, which results in an efficient quantum implementation of $e^{-i \gamma H_c}$.

```python theme={null}
@qfunc
def cost_layer(gamma: CReal, v: QArray):
    phase(cost_func(v), gamma)
```

## The Mixer Layer

The QAOA mixer is defined as $H_M=\sum{X_i}$, and its exponentiaion is simply a $R_X$ rotation of the same angle ($\beta$) on all the qubits

```python theme={null}
@qfunc
def mixer_layer(beta: CReal, qba: QArray):
    apply_to_all(lambda q: RX(2 * beta, q), qba)
```

## The QAOA Ansatz

The parametric QAOA ansatz is composed of $p$ pairs of exponentiated Mixer and Cost Hamiltonians:

$$
U\left(\vec{\beta},\vec{\gamma}\right)=e^{-i \beta_p H_M}e^{-i \gamma_p H_C}\ldots e^{-i \beta_2 H_M}e^{-i \gamma_1 H_C}e^{-i \beta_2 H_M}e^{-i \gamma_1 H_C}
$$

```python theme={null}
@qfunc
def qaoa_ansatz(
    gammas: CArray[CReal],
    betas: CArray[CReal],
    qba: QArray,
):
    n = gammas.len
    assert n == betas.len, "Number of gamma and beta parameters must be equal"
    repeat(n, lambda i: (cost_layer(gammas[i], qba), mixer_layer(betas[i], qba)))
```

## Assemble the Full QAOA Algorithm

We select $p=3$ layers for the QAOA.

The quantum program takes the $2p$ parameters as a single list (because the `scipy.optimize.minimize` function manipulate a 1-D array of parameters), and the quantum register of the $5$ qubits.

The qubits are initialized to $\left|+\right\rangle^{\otimes 5}$, with a `hadamard_transform` and then the `qaoa_ansatz`, $U\left(\vec{\beta},\vec{\gamma}\right)$, is applied

```python theme={null}
from typing import Final

NUM_LAYERS: Final[int] = 3
NUM_QUBITS: Final[int] = 8


@qfunc
def main(
    params: CArray[
        CReal, NUM_LAYERS * 2  # type: ignore
    ],  # Execution parameters (first half: gammas, second half: betas)
    v: Output[QArray[QBit, NUM_QUBITS]],  # type: ignore
):
    allocate(v)
    hadamard_transform(v)
    gammas = params[0:NUM_LAYERS]
    betas = params[NUM_LAYERS : NUM_LAYERS * 2]

    qaoa_ansatz(gammas, betas, v)
```

```python theme={null}

from classiq.execution.execution_session import ExecutionSession
from classiq.synthesis import show, synthesize

custom_hardware_settings = CustomHardwareSettings(basis_gates=["cz", "rz", "rx", "ry"])
preferences = Preferences(custom_hardware_settings=custom_hardware_settings)

qprog = synthesize(main, preferences=preferences)

es = ExecutionSession(qprog)
# show(qprog)
```

```python theme={null}

metrics = classiq.get_transpiled_circuit_metrics(qprog)
# circuit width
print(f"circuit width: {metrics.width}")
# circuit depth
print(f"circuit depth: {metrics.depth}")
```

> [!NOTE]
>
> **Output:**
>
> ```
> circuit width: 8
>   circuit depth: 211
>
>
> ```

## Classical Optimization - "Vanilla" QAOA

The $\vec{\beta}$, and $\vec{\gamma}$ parameters are tuned until the objective function converges.

The results of the parameters, and the solution vector are shown below

```python theme={null}
cost_trace = []
params_history = []


def objective_func(params, es):
    cost_estimation = es.estimate_cost(
        lambda state: cost_func(state["v"]), {"params": params.tolist()}
    )
    cost_trace.append(cost_estimation)
    params_history.append(params.copy())
    return cost_estimation
```

```python theme={null}

MAX_ITERATIONS = 60

initial_params = np.concatenate(
    (np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS))
)

with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar:

    def progress_bar(xk: np.ndarray) -> None:
        pbar.update(1)

    obj = lambda params: objective_func(params, es)

    optimization_results = scipy.optimize.minimize(
        fun=obj,
        x0=initial_params,
        method="COBYLA",
        options={"maxiter": MAX_ITERATIONS},
        callback=progress_bar,
    )

# Sample the circuit using the optimized parameters
res = es.sample({"params": optimization_results.x.tolist()})
es.close()

print(f"Optimized parameters: {optimization_results.x.tolist()}")
```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimization Progress:  42%|████▏     | 25/60 [02:46<03:52,  6.65s/it]
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimized parameters: [0.06465698944998352, 0.4936891361733595, 1.9842498691312849, 1.0062689717287894, 0.5693726640965514, 0.10641213100641594]
>
>
> ```

Plotting the convergence graph:

```python theme={null}
plt.plot(cost_trace)
plt.xlabel("Iterations")
plt.ylabel("Cost")
plt.title("Cost convergence")
plt.show()
```

![output](https://mintcdn.com/classiq/CosGEwtV-iL_6OKk/explore/applications/optimization/adapt_qaoa/adapt_qaoa_files/adapt_qaoa_1.png?fit=max\&auto=format\&n=CosGEwtV-iL_6OKk\&q=85\&s=7d5f4020232779c20b1863d6423298a1)

## Displaying and Discussing the Results

```python theme={null}
print(f"Optimized parameters: {optimization_results.x.tolist()}")
sorted_counts = sorted(
    (row for _, row in res.iterrows()), key=lambda row: cost_func(row["v"])
)
num_shots = res["counts"].sum()
for sampled in sorted_counts[:10]:
    solution = sampled["v"]
    probability = sampled["counts"] / num_shots
    cost_value = cost_func(solution)
    print(f"solution={solution} probability={probability:.3f} cost={cost_value:.3f}")
```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimized parameters: [0.06465698944998352, 0.4936891361733595, 1.9842498691312849, 1.0062689717287894, 0.5693726640965514, 0.10641213100641594]
>   solution=[1, 0, 0, 1, 1, 0, 0, 1] probability=0.031 cost=-5.482
>   solution=[1, 0, 0, 1, 0, 1, 1, 0] probability=0.011 cost=-4.771
>   solution=[1, 0, 0, 1, 0, 0, 0, 1] probability=0.020 cost=-4.629
>   solution=[0, 0, 0, 1, 1, 0, 0, 1] probability=0.016 cost=-4.629
>   solution=[0, 0, 0, 1, 0, 0, 0, 1] probability=0.034 cost=-4.529
>   solution=[1, 0, 0, 1, 0, 1, 0, 0] probability=0.027 cost=-4.513
>   solution=[1, 0, 0, 0, 0, 1, 1, 0] probability=0.014 cost=-4.513
>   solution=[1, 0, 0, 0, 1, 0, 0, 1] probability=0.021 cost=-4.465
>   solution=[1, 0, 0, 1, 1, 0, 0, 0] probability=0.012 cost=-4.465
>   solution=[1, 0, 0, 0, 0, 1, 0, 0] probability=0.041 cost=-4.413
>
>
> ```

## ADAPT QAOA

The algorithm, based on the paper [An adaptive quantum approximate optimization algorithm for solving combinatorial problems on a quantum computer](https://arxiv.org/pdf/2005.10258), modifies QAOA by using different *mixer* layers, instead of the original $H_M=\sum_i{X_i}$.

The algorithm defines a pool of potential mixer layers, that are simple to implement, and can match the problem better.

The motivation to the algorithm is rooted in *STA* (shortcut to adiabaticity), with the hope of achieving better convergence than the "vanilla" QAOA.

The algorithm proceeds incrementally:

* initially, a single mixer layer is used (the original $H_M$) and the parameters are optimized
* all the potential mixer layers, $A_j$, are tested as candidates.

The criterion is the resulting gradient of the commutator of a potential mixer layer with the cost Hamiltonian, applied to the current circuit:

$$
\nabla A_j = \left\langle\psi^{\left(k-1\right)}\right|e^{i \gamma_0 H_C}\left[H_C,A_j\right]e^{-i \gamma_0 H_C}\left|\psi^{\left(k-1\right)}\right\rangle
$$

* the mixer layer with the largest gradient is selected, the circuit grows by one layer, and the process continues (optimizing parameters and checking gradients)
* the process terminates when the scaled norm of the gradient vector drops below a threshold

#

## The Mixer Pool

The pool includes the default sum-of-X layer, as well as a sum-of-Y layer, followed by several single qubit layers with X and Y Paulis, and some two-qubit gates that add entanglement at the cost of a slightly more complicated layer.

Several heuristics are suggested for building mixer pools based on the problem definition.

A larger pool has a potential to find more efficient circuits, at the cost of computing many *mixer gradients* at each iteration.

Each kind of mixer is generated programmatically over all `NUM_QUBITS` qubits (or qubit pairs).

```python theme={null}
from functools import reduce
from itertools import combinations
from operator import add


def global_mixer(pauli) -> SparsePauliOp:
    return reduce(add, (pauli(q) for q in range(NUM_QUBITS)))


def single_qubit_mixers(pauli) -> list[SparsePauliOp]:
    return [1.0 * pauli(q) for q in range(NUM_QUBITS)]


def two_qubit_mixers(pauli) -> list[SparsePauliOp]:
    return [1.0 * pauli(i) * pauli(j) for i, j in combinations(range(NUM_QUBITS), 2)]


global_x_mixer = global_mixer(Pauli.X)
global_y_mixer = global_mixer(Pauli.Y)
single_x_mixers = single_qubit_mixers(Pauli.X)
single_y_mixers = single_qubit_mixers(Pauli.Y)
xx_mixers = two_qubit_mixers(Pauli.X)
yy_mixers = two_qubit_mixers(Pauli.Y)


mixer_pool: list[SparsePauliOp] = [
    global_x_mixer,
    global_y_mixer,
    *single_x_mixers,
    *single_y_mixers,
    *xx_mixers,
    *yy_mixers,
]
```

The gradient of a mixer is computed by taking the circuit, $e^{-i \gamma_0 H_C}\left|\psi^{\left(k-1\right)}\right\rangle$, provided by the `es` execution session and the `params`, and measuring the commutation of the mixer with the Hamiltonian:

$$
\nabla A_j=\left\langle\psi^{\left(k-1\right)}\right|e^{i \gamma_0 H_C}\left[H_C,A_j\right] e^{-i \gamma_0 H_C}\left|\psi^{\left(k-1\right)}\right\rangle
$$

Since `ExecutionSession.observe` accepts a *list* of observables and estimates all of them in a single execution job, the commutator of every mixer in the pool is built up front, and all the resulting gradients are estimated together in one `observe` call.

```python theme={null}
def mixer_commutator(mixer: SparsePauliOp, hamiltonian: SparsePauliOp) -> SparsePauliOp:
    mix_h_comm = commutator(mixer, hamiltonian)
    # observe() requires the operand width to match the circuit's qubit count
    return SparsePauliOp(
        mix_h_comm.terms, num_qubits=max(mixer.num_qubits, hamiltonian.num_qubits)
    )
```

## Classical Optimization

* ADAPT QAOA

The implementation of the ADAPT QAOA uses the same hybrid quantum-classical structure to optimize the ansatz parameters ($\vec{\beta},\vec{\gamma}$)
The differences are:

* The `ansatz mixers` list is incrementally populated with mixer layers, and used in the ansatz
* Following each convergence, the gradients of all mixers are computed, and either a new mixer is added, or the adaptive process concludes

#

## QMod Tip

Since the mixer is no longer a sum of single bit Pauli strings (the default mixer can be written as: $XII\ldots I+IXI\ldots I+IIX\ldots I+III\ldots X$), the exponent is not a trivial Pauli rotation (applying $R_X\left(\beta\right)$ to each qubit) and the exponentiation $e^{-i \beta A_j}$ has to be computed.

An efficient and simple construct is the [suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/core_library/exponentiation/?h=suzuki_trotter#classiq.qmod.builtins.functions.exponentiation.commuting_paulis_exponent) function that exponentiates a `SparsePauliOp` (a weighted sum of Pauli strings) with a given coefficient.

The `adaptive_mixer_layer` function uses this construct: `suzuki_trotter(ansatz_mixers[mixer_idx], beta, 1, 1, qba)`, where the third and fourth arguments (`1,1`) specify the order and number of repetitions of the trotterization, which can be safely set to $1$ for simple Pauli strings.

Another variant is the [multi\_suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/core_library/exponentiation/?h=multi_suzuki_trotter#classiq.qmod.builtins.functions.exponentiation.multi_suzuki_trotter) function that exponentiates a sum of Hamiltonians: $e^{-i\left(H_1t_1+H_2t_2+\ldots+H_nt_n\right)}$

```python theme={null}
MAX_ITERATIONS = 60

last_params = []

ansatz_mixers: list[int] = []
# start with default mixer
ansatz_mixers.append(0)


# gradient norm tolerance for stopping criterion
tol: Final[float] = 0.027

while True:  # loop until gradient norm exceeds tolerance (break-ing out of the loop)
    # number of layers (initially=1), each layer is a pair of mixer and cost Hamiltonians

    p = len(ansatz_mixers)

    # adiabatic inspired initialization of beta and gamma
    initial_params = np.concatenate((np.linspace(0, 1, p), np.linspace(1, 0, p)))

    # trace and history for analysis and plotting
    cost_trace = []
    params_history = []

    @qfunc
    def adaptive_mixer_layer(mixer_idx: int, beta: CReal, qba: QArray):
        suzuki_trotter(mixer_pool[ansatz_mixers[mixer_idx]], beta, 1, 1, qba)

    @qfunc
    def qaoa_adaptive_ansatz(
        gammas: CArray[CReal],
        betas: CArray[CReal],
        qba: QArray,
    ):
        n = gammas.len
        assert n == betas.len, "Number of gamma and beta parameters must be equal"
        print(f"Building ansatz with {n} layers.")
        for i in range(n):
            cost_layer(gammas[i], qba)
            adaptive_mixer_layer(i, betas[i], qba)

    @qfunc
    def main(
        params: CArray[
            CReal, p * 2  # type: ignore
        ],  # Execution parameters (first half: gammas, second half: betas), used later by the sample method
        v: Output[QArray[QBit, NUM_QUBITS]],  # type: ignore
    ):
        allocate(v)
        hadamard_transform(v)
        gammas = params[0:p]
        betas = params[p : p * 2]
        qaoa_adaptive_ansatz(gammas, betas, v)

    custom_hardware_settings = CustomHardwareSettings(
        basis_gates=["cz", "rz", "rx", "ry"]
    )
    preferences = Preferences(custom_hardware_settings=custom_hardware_settings)

    qprog = synthesize(main, preferences=preferences)

    with ExecutionSession(quantum_program=qprog) as es:
        with tqdm(
            total=MAX_ITERATIONS,
            desc=f"Optimization Progress for {p} layered circuit",
            leave=True,
        ) as pbar:

            def progress_bar(xk: np.ndarray) -> None:
                pbar.update(1)

            def obj(params):
                return objective_func(params, es)

            optimization_results = scipy.optimize.minimize(
                fun=obj,
                x0=initial_params,
                method="COBYLA",
                options={"maxiter": MAX_ITERATIONS, "rhobeg": 0.5},
                callback=progress_bar,
            )

    print(
        f"Completed optimization for {p} layered circuit. objective value: {optimization_results.fun}"
    )

    params = optimization_results.x.tolist()
    last_params = params[:]  # copy for final execution

    # compute gradients of mixer pool elements
    gamma_0 = 0.01  # (or any value below 0.1)

    @qfunc
    def main(
        params: CArray[
            CReal, p * 2  # type: ignore
        ],  # Execution parameters (first half: gammas, second half: betas), used later by the sample method
        v: Output[QArray[QBit, NUM_QUBITS]],  # type: ignore
    ):
        allocate(v)
        hadamard_transform(v)
        gammas = params[0:p]
        betas = params[p : p * 2]

        qaoa_adaptive_ansatz(gammas, betas, v)
        cost_layer(gamma_0, v)

    print("Computing mixer gradients...")
    custom_hardware_settings = CustomHardwareSettings(
        basis_gates=["cz", "rz", "rx", "ry"]
    )
    preferences = Preferences(custom_hardware_settings=custom_hardware_settings)

    qprog_grads = synthesize(main, preferences=preferences)

    mixer_commutators = [mixer_commutator(mp, h) for mp in mixer_pool]
    with ExecutionSession(qprog_grads) as es:
        gradient_results = es.observe(mixer_commutators, {"params": params})
    gradients = np.abs(np.array([result.value for result in gradient_results]))

    scaled_gradient_norm = np.linalg.norm(gradients) / len(gradients)
    print(f"{scaled_gradient_norm=}")

    if scaled_gradient_norm < tol:
        break

    g_idx = np.argmax(gradients)
    print(f"Selected mixer index: {g_idx}")

    ansatz_mixers.append(g_idx)
```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Building ansatz with 1 layers.
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimization Progress for 1 layered circuit:  38%|███▊      | 23/60 [02:09<03:27,  5.61s/it]
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Completed optimization for 1 layered circuit. objective value: -3.5955269042968747
>   Computing mixer gradients...
>
> Building ansatz with 1 layers.
>   scaled_gradient_norm=np.float64(0.027545160133732996)
>   Selected mixer index: 1
>   Building ansatz with 2 layers.
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimization Progress for 2 layered circuit:  40%|████      | 24/60 [02:37<03:56,  6.57s/it]
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Completed optimization for 2 layered circuit. objective value: -3.8337818847656258
>   Computing mixer gradients...
>
> Building ansatz with 2 layers.
>   scaled_gradient_norm=np.float64(0.06023831551845875)
>   Selected mixer index: 0
>   Building ansatz with 3 layers.
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimization Progress for 3 layered circuit:  63%|██████▎   | 38/60 [04:02<02:20,  6.37s/it]
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Completed optimization for 3 layered circuit. objective value: -4.0034943847656255
>   Computing mixer gradients...
>
> Building ansatz with 3 layers.
>   scaled_gradient_norm=np.float64(0.040593424720682926)
>   Selected mixer index: 1
>   Building ansatz with 4 layers.
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimization Progress for 4 layered circuit:  45%|████▌     | 27/60 [07:42<09:24, 17.11s/it]
>
>
> ```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Completed optimization for 4 layered circuit. objective value: -0.474186181640625
>   Computing mixer gradients...
>
> Building ansatz with 4 layers.
>   scaled_gradient_norm=np.float64(0.021402686019953886)
>
>
> ```

```python theme={null}
# Sample the circuit using the optimized parameters
df = sample(qprog, parameters={"params": last_params})
```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Submitting job to simulator
>   Job: https://platform.classiq.io/jobs/241874ec-0626-4f30-a6b4-95c33cddbd0c
>
>
> ```

Plotting the convergence graph:

```python theme={null}
plt.plot(cost_trace)
plt.xlabel("Iterations")
plt.ylabel("Cost")
plt.title("Cost convergence")
plt.show()
```

![output](https://mintcdn.com/classiq/CosGEwtV-iL_6OKk/explore/applications/optimization/adapt_qaoa/adapt_qaoa_files/adapt_qaoa_2.png?fit=max\&auto=format\&n=CosGEwtV-iL_6OKk\&q=85\&s=9e82a4d29f26260c42da51a6df6134dc)

```python theme={null}
df
```

&#x20;<table border="1"> <thead> <tr> <th /> <th>v</th> <th>counts</th> <th>probability</th> <th>bitstring</th> </tr> </thead> <tbody> <tr> <th>0</th> <td>\[0, 1, 0, 1, 0, 0, 1, 1]</td> <td>56</td> <td>0.027344</td> <td>11001010</td> </tr> <tr> <th>1</th> <td>\[1, 0, 1, 0, 1, 1, 0, 0]</td> <td>43</td> <td>0.020996</td> <td>00110101</td> </tr> <tr> <th>2</th> <td>\[1, 1, 0, 0, 1, 1, 0, 0]</td> <td>42</td> <td>0.020508</td> <td>00110011</td> </tr> <tr> <th>3</th> <td>\[1, 1, 0, 0, 1, 0, 1, 0]</td> <td>40</td> <td>0.019531</td> <td>01010011</td> </tr> <tr> <th>4</th> <td>\[0, 0, 1, 0, 0, 1, 0, 0]</td> <td>38</td> <td>0.018555</td> <td>00100100</td> </tr> <tr> <th>...</th> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> <tr> <th>220</th> <td>\[1, 1, 0, 1, 1, 0, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11011011</td> </tr> <tr> <th>221</th> <td>\[0, 0, 1, 0, 0, 1, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11100100</td> </tr> <tr> <th>222</th> <td>\[0, 1, 1, 0, 0, 1, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11100110</td> </tr> <tr> <th>223</th> <td>\[0, 0, 0, 1, 0, 1, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11101000</td> </tr> <tr> <th>224</th> <td>\[1, 0, 0, 1, 1, 1, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11111001</td> </tr> </tbody> </table> 225 rows × 4 columns

```python theme={null}
print(f"Optimized parameters: {optimization_results.x.tolist()}")
sorted_counts = sorted(
    (row for _, row in df.iterrows()), key=lambda row: cost_func(row["v"])
)
num_shots = df["counts"].sum()
for sampled in sorted_counts[:10]:
    solution = sampled["v"]
    probability = sampled["counts"] / num_shots
    cost_value = cost_func(solution)
    print(f"solution={solution} probability={probability:.3f} cost={cost_value:.3f}")
```

> [!NOTE]
>
> **Output:**
>
> ```
>
> Optimized parameters: [0.9686227876407876, 0.9515144639904436, 0.5447912504090217, 0.993362347276416, 1.5313164878931926, 0.6592657157373645, 0.854356667003189, -0.007123951959039967]
>   solution=[1, 0, 0, 1, 1, 0, 0, 1] probability=0.004 cost=-5.482
>   solution=[1, 0, 0, 1, 0, 1, 1, 0] probability=0.000 cost=-4.771
>   solution=[1, 0, 0, 1, 0, 0, 0, 1] probability=0.001 cost=-4.629
>   solution=[0, 0, 0, 1, 1, 0, 0, 1] probability=0.003 cost=-4.629
>   solution=[0, 0, 0, 1, 0, 0, 0, 1] probability=0.002 cost=-4.529
>   solution=[1, 0, 0, 0, 0, 1, 1, 0] probability=0.001 cost=-4.513
>   solution=[1, 0, 0, 0, 1, 0, 0, 1] probability=0.002 cost=-4.465
>   solution=[1, 0, 0, 1, 1, 0, 0, 0] probability=0.001 cost=-4.465
>   solution=[1, 0, 0, 0, 0, 1, 0, 0] probability=0.002 cost=-4.413
>   solution=[1, 0, 0, 0, 1, 0, 0, 0] probability=0.000 cost=-4.365
>
>
> ```

```python theme={null}
df.sort_values(by="probability", ascending=False)
```

&#x20;<table border="1"> <thead> <tr> <th /> <th>v</th> <th>counts</th> <th>probability</th> <th>bitstring</th> </tr> </thead> <tbody> <tr> <th>0</th> <td>\[0, 1, 0, 1, 0, 0, 1, 1]</td> <td>56</td> <td>0.027344</td> <td>11001010</td> </tr> <tr> <th>1</th> <td>\[1, 0, 1, 0, 1, 1, 0, 0]</td> <td>43</td> <td>0.020996</td> <td>00110101</td> </tr> <tr> <th>2</th> <td>\[1, 1, 0, 0, 1, 1, 0, 0]</td> <td>42</td> <td>0.020508</td> <td>00110011</td> </tr> <tr> <th>3</th> <td>\[1, 1, 0, 0, 1, 0, 1, 0]</td> <td>40</td> <td>0.019531</td> <td>01010011</td> </tr> <tr> <th>4</th> <td>\[0, 0, 1, 0, 0, 1, 0, 0]</td> <td>38</td> <td>0.018555</td> <td>00100100</td> </tr> <tr> <th>...</th> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> <tr> <th>201</th> <td>\[0, 1, 0, 1, 0, 1, 0, 0]</td> <td>1</td> <td>0.000488</td> <td>00101010</td> </tr> <tr> <th>200</th> <td>\[1, 1, 1, 0, 1, 0, 0, 0]</td> <td>1</td> <td>0.000488</td> <td>00010111</td> </tr> <tr> <th>199</th> <td>\[1, 0, 0, 0, 1, 0, 0, 0]</td> <td>1</td> <td>0.000488</td> <td>00010001</td> </tr> <tr> <th>198</th> <td>\[1, 1, 0, 1, 0, 0, 0, 0]</td> <td>1</td> <td>0.000488</td> <td>00001011</td> </tr> <tr> <th>224</th> <td>\[1, 0, 0, 1, 1, 1, 1, 1]</td> <td>1</td> <td>0.000488</td> <td>11111001</td> </tr> </tbody> </table> 225 rows × 4 columns


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.