# QAOA

This package is a flexible python implementation of the [Quantum Approximate Optimization Algorithm](https://arxiv.org/pdf/1411.4028.pdf) /[Quantum Alternating Operator ansatz](https://arxiv.org/pdf/1709.03489.pdf)  (QAOA) **aimed at researchers** to readily test the performance of a new ansatz, a new classical optimizer, etc. By default it uses qiskit as a backend.

Install with `pip install qaoa` or `pip install -e .`.

***
### Background
Given a **cost function** 
$$c: \lbrace 0, 1\rbrace^n \rightarrow \mathbb{R}$$
one defines a **problem Hamiltonian** $H_P$ through the action on computational basis states via

$$ H_P |x\rangle = c(x) |x\rangle,$$

which means that ground states minimize the cost function $c$.
Given a parametrized ansatz $| \gamma, \beta \rangle$, a classical optimizer is used to minimize the energy

$$ \langle \gamma, \beta | H_P | \gamma, \beta \rangle.$$

QAOA of depth $p$ consist of the following **ansatz**:

$$ |\gamma, \beta \rangle = \prod_{l=1}^p \left( U_M(\beta_l) U_P(\gamma_l)\right) | s\rangle, $$

where

- $U_P$ is a family of **phase**-separating operators,
- $U_M$ is a family of **mixing** operators, and
- $|s\rangle$ is a "simple" **initial** state.

In plain vanilla QAOA these have the form
$U_M(\beta_l)=e^{-i\beta_l X^{\otimes n}}$,  $U_P(\gamma_l)=e^{-i\gamma_l H_P}$, and the uniform superposition $| s \rangle = |+\rangle^{\otimes n}$ as initial state.

***
### Create a custom ansatz

In order to create a custom QAOA ansatz, one needs to specify a [problem](qaoa/problems/base_problem.py), a [mixer](qaoa/mixers/base_mixer.py), and an [initial state](qaoa/initialstates/base_initialstate.py). These base classes have an abstract method `def create_circuit:`which needs to be implemented. The problem base class additionally has an abstract method `def cost:`.

This library already contains several standard implementations.

- The following [problem](qaoa/problems/base_problem.py) cases are already available:
	- [Max k-CUT binary power of two](qaoa/problems/maxkcut_binary_powertwo.py) *
	- [Max k-CUT binary full H](qaoa/problems/maxkcut_binary_fullH.py)
	- [Max k-CUT binary one hot](qaoa/problems/maxkcut_one_hot_problem.py)
	- [QUBO](qaoa/problems/qubo_problem.py)
	- [Exact cover](qaoa/problems/exactcover_problem.py)
	- [Portfolio](qaoa/problems/portfolio_problem.py)
	- [Graph](qaoa/problems/graph_problem.py)
- The following [mixer](qaoa/mixers/base_mixer.py) cases are already available:
	- [X-mixer](qaoa/mixers/x_mixer.py)
	- [XY-mixer](qaoa/mixers/xy_mixer.py)
	- [Grover-mixer](qaoa/mixers/grover_mixer.py)
	- [Max k-CUT grover](qaoa/mixers/maxkcut_grover_mixer.py)
	- [Max k-CUT LX](qaoa/mixers/maxkcut_lx_mixer.py)
	- [X multi-angle mixer](qaoa/mixers/x_multiangle_mixer.py) *(one β per qubit)*
- The following [initial state](qaoa/initialstates/base_initialstate.py) cases are already available:
	- [Plus](qaoa/initialstates/plus_initialstate.py)
	- [Statevector](qaoa/initialstates/statevector_initialstate.py)
	- [Dicke](qaoa/initialstates/dicke_initialstate.py)
	- [Dicke 1- and 2-states superposition](qaoa/initialstates/dicke1_2_initialstate.py)
	- [Less than k](qaoa/initialstates/lessthank_initialstate.py)
	- [Max k-CUT feasible](qaoa/initialstates/maxkcut_feasible_initialstate.py)
	- [Plus parameterized](qaoa/initialstates/plus_parameterized_initialstate.py) *(|+⟩ with optimizable phase rotations)*

It is **very easy to extend this list** by providing  an implementation of a circuit/cost of the base classes mentioned above. Feel free to fork the repo and create a pull request :-)

To make an ansatz for the MaxCut problem, the X-mixer and the initial state $|+\rangle^{\otimes n}$  one can create an instance like this: 

	qaoa = QAOA(
		problem=problems.MaxKCutBinaryPowerOfTwo(G="some networkx instance", k_cuts=2),
		mixer=mixers.X(),
		initialstate=initialstates.Plus()
	)

*(can be used for the standard MaxCut with argument k_cuts=2, or use `problems.MaxCut(G)` for convenience)*
***
### Run optimization at depth $p$

For depth $p=1$ the expectation value can be sampled on an $n\times m$ Cartesian grid over the domain $[0,\gamma_\text{max}]\times[0,\beta_\text{max}]$ with:
		
	qaoa.sample_cost_landscape()
	
![Energy landscape](images/E.png  "Energy landscape")

Sampling high-dimensional target functions quickly becomes intractable for depth $p>1$. We therefore **iteratively increase the depth**. At each depth a **local optimization** algorithm, e.g. COBYLA, is used to find a local minimum. As **initial guess** the following is used:

- At depth $p=1$ initial parameters $(\gamma, \beta)$ are given by the lowest value of the sampled cost landscape. 
- At depth $p>1$ initial parameters $(\gamma, \beta)$ are based on an [interpolation-based heuristic](https://arxiv.org/pdf/1812.01041.pdf) of the optimal values at the previous depth.

Running this iterative local optimization to depth $p$ can be done by the following call:

	qaoa.optimize(depth=p)

The function will call `sample_cost_landscape` if not already done, before iteratively increasing the depth.

***
### Further parameters

QAOA supports the following keywords:

	qaoa = QAOA( ...,
		backend= ,
		noisemodel= ,
		optimizer= ,
		precision= ,
		shots= ,
		cvar=
	)

- `backend`: the backend to be used, defaults to `AerSimulator()` (from `qiskit_aer`)
- `noisemodel`: the noise model to be used, default to `None`,
- `optimizer`: a list of the optimizer to be used from qiskit-algorithms together with options, defaults to `[COBYLA, {}]`,
- `precision`: sampel until a certain precision of the expectation value is reached based on $\text{error}=\frac{\text{variance}}{\sqrt{\text{shots}}}$, defaults to `None`,
- `shots`: number of shots to be used, defaults to `1024`,
- `cvar`: the value for [conditional value at risk (CVAR)](https://arxiv.org/pdf/1907.04769.pdf), defaults to `1`, which are the standard moments.

***
### Extract results

Once `qaoa.optimize(depth=p)` is run, one can extract, the expectation value, variance, and parametres for each depth $1\leq i \leq p$ by respectively calling:

	qaoa.get_Exp(depth=i)
	qaoa.get_Var(depth=i)
	qaoa.get_gamma(depth=i)
	qaoa.get_beta(depth=i)

Additionally, for each depth every time the loss function is called, the **angles, expectation value, variance, maximum cost, minimum cost, **and** number of shots** are stored in 

	qaoa.optimization_results[i]

***
### Multi-Angle QAOA

Multi-angle QAOA allows components to use multiple parameters per layer, increasing expressibility:

- **Multi-angle mixer** (`XMultiAngle`): Each qubit gets its own independent β parameter
- **Parameterized initial state** (`PlusParameterized`): The initial state |+⟩ with optimizable per-qubit phase rotations

	qaoa = QAOA(
		problem=problems.MaxKCutBinaryPowerOfTwo(G="some networkx instance", k_cuts=2),
		mixer=mixers.XMultiAngle(),  # N_qubits beta parameters per layer
		initialstate=initialstates.Plus()
	)

The flat angle array format used by `hist()`, `getParametersToBind()`, and `interp()` is:

	[init_0, ..., init_{n-1},          # initial state params (0 for Plus)
	 gamma_{0,0}, ..., beta_{0,n-1},   # layer 0 params
	 gamma_{1,0}, ..., beta_{1,n-1},   # layer 1 params
	 ...]

For the standard single-parameter case, this reduces to `[gamma_0, beta_0, gamma_1, beta_1, ...]`.

Implement `get_num_parameters()` in a custom component to enable multi-angle support. See [examples/MultiAngle](examples/MultiAngle/) for a complete example.

***
### Example use cases

See [examples here](examples/).


***
### Minimizing depth of phase separating operator

Assuming all-to-all connectivity of qubits, one can minimize the depth of the circuit of the phase separating operator by solving the problem of minimum edge colouring. This is implemtend in [GraphHandler](qaoa/util/graphutils.py) and gets automatically invoked. An [example](examples/MaxCut/MinimalDepth.ipynb) output is this

![this graph](images/minimal_depth.png  "Edge Coloring")


***
### Tensorize mixers
To tensorize a mixer, i.e. decomposing the mixer into a tensor product of unitaries that is 
performed on each qubit, one can call the tensor class with the arguments of mixer and number of qubits in subpart.

For example, for the standard MaxCut problem above where the X mixer was used, one could find the tensor by writing:

	tensorized_mixer = Tensor(mixer.X(), number_of_qubits_of_subpart)
<!--find out the number of qubits we want here -->

***
### Talk to an agent
In the code, there is also included an "agent" folder, which has implemented a specialized QAOA agent. It is possible to ask the agent for either code that implements, or explanations about, the QAOA package.

There are two ways to run the agents. Either one can run the agent in the terminal, or run an interface.

To run the agent directly from the terminal, one can simply open the agent folder and run the file planner.py. It is then possible to ask it questions as inputs from the terminal.

To run the interface, open the agent folder, and run streamlit by typing: streamlit run interface.py
The following window will pop up:

<img src="images\interface.png" alt="QAOA Agent interface startup page" width=500/>
<br><br>
It is then possible to ask questions in the text box. An example could be:
<img src="images\interface_output.png" alt="QAOA Agent interface with question page" width=500/>

For the agent application the following dependencies are needed: langchain, langchain_community, langchain_chroma, langchain_openai and optionally streamlit (for the interface). Additionally, an API-KEY is needed. 


***
### Acknowledgement
We would like to thank for funding of the work by the Research Council of Norway through project number 33202.
