diff --git a/docs/source/index.rst b/docs/source/index.rst index ad7ec5f5..937d4e23 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -171,6 +171,18 @@ Your first RQAOA workflow rqaoa_type can take two values which select elimination strategies. The user can choose between `adaptive` or `custom`. +Your first FQAOA workflow +------------------------- + +.. code-block:: python + + from openqaoa import FQAOA + fqaoa = FQAOA() + fqaoa.compile(qubo_problem, n_fermions) + fqaoa.optimize() + +FQAOA intrinsically imposes a hard constraint where the hamming weight is equal to n_fermions. + Factory mode ------------ The user is also free to directly access the source code without using the workflow API. @@ -303,8 +315,10 @@ Contents notebooks/12_testing_azure.ipynb notebooks/13_optimizers.ipynb notebooks/14_qaoa_benchmark.ipynb - notebooks/X_dumping_data.ipynb notebooks/15_Zero_Noise_Extrapolation.ipynb + notebooks/16_FQAOA_example.ipynb + notebooks/17_FQAOA_advanced_parameterization.ipynb + notebooks/X_dumping_data.ipynb Indices and tables ================== diff --git a/docs/source/openqaoa_core/fqaoa.rst b/docs/source/openqaoa_core/fqaoa.rst new file mode 100644 index 00000000..a6566c2e --- /dev/null +++ b/docs/source/openqaoa_core/fqaoa.rst @@ -0,0 +1,10 @@ +Fermionic QAOA functions +======================== + +A set of utility functions for FQAOA + +.. automodule:: openqaoa.algorithms.fqaoa.fqaoa_utils + :members: + :undoc-members: + :inherited-members: + diff --git a/docs/source/openqaoa_core/openqaoa_core_api.rst b/docs/source/openqaoa_core/openqaoa_core_api.rst index 2b1bf5e9..b7e89b35 100644 --- a/docs/source/openqaoa_core/openqaoa_core_api.rst +++ b/docs/source/openqaoa_core/openqaoa_core_api.rst @@ -6,9 +6,10 @@ OpenQAOA Core API Reference workflows rqaoa + fqaoa problems qaoaparameters backends logger_and_results optimizers - utilities \ No newline at end of file + utilities diff --git a/docs/source/openqaoa_core/workflows.rst b/docs/source/openqaoa_core/workflows.rst index 5690dafc..a8a422d5 100644 --- a/docs/source/openqaoa_core/workflows.rst +++ b/docs/source/openqaoa_core/workflows.rst @@ -1,7 +1,7 @@ Workflows ================================= -Workflows are a simple reference API to build complex quantum optimisations problems. Currently, it supports creations of `QAOA` and `Recursive QAOA` workflows. +Workflows are a simple reference API to build complex quantum optimisations problems. Currently, it supports creations of `QAOA`, `Recursive QAOA`, `Fermionic QAOA` workflows. Workflows are designed to aid the user to focus on the optimisation problem, while delegating the construction and the execution of the specific algorithm to `OpenQAOA` @@ -54,7 +54,11 @@ To choose the strategy, set the parameter ``rqaoa_type`` using the `set_rqaoa_pa :members: :undoc-members: :inherited-members: - + +.. autoclass:: openqaoa.algorithms.fqaoa.fqaoa_workflow.FQAOA + :members: + :undoc-members: + :inherited-members: RQAOA Workflow Properties ------------------------- diff --git a/examples/15_Zero_Noise_Extrapolation.ipynb b/examples/15_Zero_Noise_Extrapolation.ipynb index 25b938a4..83d8c7dc 100644 --- a/examples/15_Zero_Noise_Extrapolation.ipynb +++ b/examples/15_Zero_Noise_Extrapolation.ipynb @@ -5,7 +5,7 @@ "id": "9cef204d-fca2-4dbd-abbe-91fa08242061", "metadata": {}, "source": [ - "# Zero-Noise Extrapolation: an example workflow" + "# 15 - Zero-Noise Extrapolation: an example workflow" ] }, { diff --git a/examples/16_FQAOA_example.ipynb b/examples/16_FQAOA_example.ipynb new file mode 100644 index 00000000..a2933c7c --- /dev/null +++ b/examples/16_FQAOA_example.ipynb @@ -0,0 +1,453 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c5de313a", + "metadata": {}, + "source": [ + "# 16 - Fermionic QAOA (FQAOA)\n", + "\n", + "This notebook provides a brief introduction to Fermionic QAOA (FQAOA). \n", + "It shows how this technique is implemented in the OpenQAOA workflow by solving the constrained quadratic optimization problem, an NP-hard problem.\n", + "\n", + "## A brief introduction\n", + "\n", + "We present an implementation of a novel algorithm designed for solving combinatorial optimization problems with constraints, utilizing the principles of quantum computing. The algorithm, known as the FQAOA [[1](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071)\n", + ", [2](https://arxiv.org/pdf/2312.04710)]\n", + ", introduces a significant enhancement over traditional methods by leveraging fermion particle number preservation. This intrinsic property allows the algorithm to enforce constraints naturally throughout the optimization process, addressing a critical challenge in many combinatorial problems.\n", + "\n", + "### Key Features\n", + "- Constraint Handling: In contrast to conventional approaches, which treat constraints as soft constraints in the cost function, FQAOA enforces constraints intrinsically by preserving fermion particle numbers, thereby enhancing the overall performance of the optimization algorithm.\n", + "\n", + "- Design of FQAOA Ansatz: In this algorithm,\n", + "the mixer is designed so that any classical state can be reached by its multiple actions.\n", + "The initial state is set to a ground state of the mixer Hamiltonian satisfying the constraints of the problem.\n", + "\n", + "- Adiabatic Evolution: FQAOA effectively reduces to quantum adiabatic computation in the large limit of circuit depth, $p$, offering improved performance even for shallow circuits by optimizing parameters starting from fixed angles determined by Trotterized quantum adiabatic evolution.\n", + "\n", + "- Performance Advantage: Extensive numerical simulations demonstrate that FQAOA offers substantial performance benefits over existing methods, particularly in portfolio optimization problems.\n", + "\n", + "- Broad Applicability: The Hamiltonian design guideline benefits QAOA and extends to other algorithms like Grover adaptive search and quantum phase estimation, making it a versatile tool for solving constrained combinatorial optimization problems.\n", + "\n", + "This notebook describes the implementation of FQAOA, illustrates its application through an example portfolio optimization problem, and provides insight into FQAOA's superior performance in constrained combinatorial optimization tasks.\n", + "\n", + "### Quadratic Constrained Binary Optimization Problems\n", + "The constrained combinatorial optimization problem for a quadratic binary cost function $C_{\\boldsymbol x}$ can be written in the following form:\n", + "$${\\boldsymbol x}^* = \\arg \\min_{\\boldsymbol x} C_{\\boldsymbol x}\\qquad {\\rm s.t.} \\quad\\sum_{i=1}^{N} x_i = M,$$\n", + "with bit string ${\\boldsymbol x}\\in \\{0,1\\}^N$, where ${\\boldsymbol x}^*$ is the optimal solution.\n", + "This problem can be replaced by the minimum eigenvalue problem in the following steps.\n", + "\n", + "1. map the cost function $C_{\\boldsymbol x}$ to the cost Hamiltonian $\\hat{\\cal H}_C$ by $x_i\\rightarrow \\hat{n}_i$:\n", + "\\begin{eqnarray}\n", + "C_{\\boldsymbol x} &=& \\sum_i \\mu_i x_{i}+\\sum_{i,j} \\sigma_{i,j} x_{i}x_{j}\n", + "&\\quad\\mapsto\\quad&\\hat{\\cal H}_C=\\sum_i \\mu_i \\hat{n}_{i}+\\sum_{i,j} \\sigma_{i,j} \\hat{n}_{i_1}\\hat{n}_{i_2},\n", + "\\end{eqnarray}\n", + "where $\\hat{n}_i = \\hat{c}^\\dagger_i\\hat{c}_i$ is number operator and $\\hat{c}_i^\\dagger (\\hat{c}_i)$ is creation (annihilation) operator of fermion at $i$-th site.\n", + "\n", + "2. formulate eigenvalue problems for combinatorial optimization problem under the constraint:\n", + "\\begin{eqnarray}\n", + "\\hat{\\cal H}_C|x_1x_2\\cdots x_N\\rangle &=& C_{\\boldsymbol x}|x_1x_2\\cdots x_N\\rangle,\\\\\n", + "\\sum_{i=1}^{N} \\hat{n}_i|x_1x_2\\cdots x_N\\rangle &=& M|x_1x_2\\cdots x_N\\rangle,\n", + "\\end{eqnarray}\n", + "where $|x_1x_2\\cdots x_N\\rangle=(\\hat{c}^\\dagger_1)^{x_1}(\\hat{c}^\\dagger_2)^{x_2}\\cdots (\\hat{c}^\\dagger_N)^{x_N}|{\\rm vac}\\rangle$ is fermionic basis state and $|\\rm vac\\rangle$ is vacuum satisfying $\\hat{c}_i|\\rm vac\\rangle=0$.\n", + "\n", + "3. optimize FQAOA ansatz:\n", + "$$|\\psi_p({\\boldsymbol \\gamma}^*, {\\boldsymbol \\beta}^*)\\rangle \n", + "= \\left[\\prod_{j=1}^pU(\\hat{\\cal H}_M,\\beta_j^*){U}(\\hat{\\cal H}_C,\\gamma_j^*)\\right]\\hat{U}_{\\rm init}|{\\rm vac}\\rangle\n", + "\\qquad {\\rm by}\\quad\n", + "C_p({\\boldsymbol \\gamma}^*, {\\boldsymbol \\beta}^*)=\\min_{{\\boldsymbol \\gamma}, {\\boldsymbol \\beta}}C_p({\\boldsymbol \\gamma},{\\boldsymbol \\beta}),$$\n", + "$$\\qquad{\\rm where \\quad}C_p({\\boldsymbol \\gamma}, {\\boldsymbol \\beta}) = \\langle\\psi_p({\\boldsymbol \\gamma}, {\\boldsymbol \\beta})|\\hat{\\cal H}_C|\\psi_p({\\boldsymbol \\gamma}, {\\boldsymbol \\beta})\\rangle.$$\n", + "The variational parameters $({\\boldsymbol \\gamma}^*, {\\boldsymbol \\beta}^*)$ give the lowest cost value at QAOA level $p$.\n", + "\n", + "\n", + "### One-Dimensional Mixer Hamiltonian\n", + "\n", + "The FQAOA implemented on OpenQAOA employs mixer Hamiltonians on one-dimensional lattices. The main features of this computational model are summarized based on the study [[2]](https://arxiv.org/pdf/2312.04710):\n", + "\n", + "- Utilises a mixer Hamiltonian on a one-dimensional lattice dedicated to combinatorial optimization problems with equality constraints of the same coefficients.\n", + "\n", + "- Reduced Gate Operations: The new mixer Hamiltonian significantly reduces the number of gate operations in quantum circuits compared to previous studies [[1]](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071), thus improving computational efficiency.\n", + "\n", + "- Noise suppression: as demonstrated in a 16-qubit trapped-ion quantum computer on Amazon Braket, the proposed mixer Hamiltonian effectively suppresses noise and improves algorithm performance.\n", + "\n", + "As these features enhance the performance and reliability of FQAOA for solving constrained combinatorial optimization problems, we focus on FQAOA with one-dimensional mixer Hamiltonians.\n", + "\n", + "The specific mixer Hamiltonian $\\hat{\\cal H}_M$ on cyclic lattice to be implemented in OpenQAOA are as follow:\n", + "\n", + "\\begin{eqnarray}\n", + "\\hat{\\cal H}_M &=& -t\\sum_{i=1}^{N-1} (\\hat{c}^\\dagger_i\\hat{c}_{i+1}+\\hat{c}^\\dagger_{i+1}\\hat{c}_i)-t(-1)^{M-1}(\\hat{c}^\\dagger_N\\hat{c}_{1}+\\hat{c}^\\dagger_{1}\\hat{c}_N).\n", + "\\end{eqnarray}\n", + "These Hamiltonians can be diagonalized as:\n", + "$$\\hat{\\cal H}_M=\\sum_{i=1}^{N}\\varepsilon_i\\hat{\\gamma}_i^\\dagger\\hat{\\gamma}_i \\qquad {\\rm with} \\quad\n", + "\\hat{\\gamma}_i^\\dagger = \\sum_{j=1}^N[\\phi_0]_{i,j}\\hat{c}^\\dagger_j,$$\n", + "where $[\\phi_0]$ is the unitary matrix used for the diagonalization and $\\varepsilon_i$ is eigenvalue.\n", + "The formulation and validation of the model on cyclic lattice are detailed in the study [[2]](https://arxiv.org/pdf/2312.04710).\n", + "\n", + "### Initial State and Mixer for FQAOA\n", + "\n", + "We describe the specific calculation model utilized for our implementation of the FQAOA on cyclic lattice. \n", + "The initial state preparation unitary $\\hat{U}_{\\rm init}$ and the mixer $\\hat{U}_M$ used to implement the FQAOA are shown below.\n", + "\n", + "- initial state preparation unitary $\\hat{U}_{\\rm init}$ on cyclic lattice:\n", + "$$|\\phi_0\\rangle=\\hat{U}_{\\rm init}|{\\rm vac}\\rangle=\\left(\\prod_{i=1}^{M}\\hat{\\gamma}_i^\\dagger\\right)|{\\rm vac}\\rangle,\n", + "\\qquad{\\rm with}\\quad \\hat{U}_{\\rm init}=\\prod_{i=1}^{M}\\left(\\sum_{j=1}^N[\\phi_0]_{i,j}\\hat{c}^\\dagger_j\\right),$$\n", + "where $M$ is the number of fermions and $i$ indexes the eigenvalues $\\varepsilon_i$ in ascending order.\n", + "The implementation of the initial state preparation unitary on quantum circuit are shown in Refs. [[1](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071),[2](https://arxiv.org/pdf/2312.04710),[3](https://arxiv.org/pdf/1711.05395)].\n", + "\n", + "- mixing unitary $U(\\hat{\\cal H}_M, \\beta)$ on cyclic lattice:\n", + "\\begin{eqnarray}\n", + "U(\\hat{\\cal H}_M, \\beta) &\\sim&\n", + "\\exp\\left[i\\beta t(-1)^{M-1}\\left(\\hat{c}^{\\dagger}_{ND}\\hat{c}_{1}+\\hat{c}^{\\dagger}_{1}\\hat{c}_{ND}\\right)\\right]\\\\&&\\times\n", + "\\prod_{i\\ {\\rm even}}\\exp\\left[i\\beta t\\left(\\hat{c}^{\\dagger}_i\\hat{c}_{i+1}+\\hat{c}^{\\dagger}_{i+1}\\hat{c}_{i}\\right)\\right]\n", + "\\prod_{i\\ {\\rm odd}}\\exp\\left[i\\beta t\\left(\\hat{c}^{\\dagger}_i\\hat{c}_{i+1}+\\hat{c}^{\\dagger}_{i+1}\\hat{c}_{i}\\right)\\right],\n", + "\\end{eqnarray}\n", + "where the approximation by Trotter decomposition is applied, as certain hopping terms are non-commutative.\n", + "The implementation of the mixer unitary indicated on the right-hand side on quantum circuits is given in Refs. [[1](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071),\n", + "[2](https://arxiv.org/pdf/2312.04710), [4](https://arxiv.org/pdf/1709.03489), [5](https://arxiv.org/pdf/1904.09314)]." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d3e2b171-0013-4e2e-9801-8a6a58d50370", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib notebook\n", + "%matplotlib inline\n", + "\n", + "# Import the libraries needed to employ the QAOA and FQAOA quantum algorithm using OpenQAOA\n", + "from openqaoa import FQAOA\n", + "from openqaoa import QAOA\n", + "\n", + "# method to covnert a docplex model to a qubo problem\n", + "from openqaoa.problems import PortfolioOptimization\n", + "\n", + "# Import external libraries to present an manipulate the data\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "8d47957a-251f-4206-8199-2c551713a754", + "metadata": {}, + "source": [ + "## Portfolio Optimization\n", + "\n", + "In the following, the [portfolio optimization problem](https://en.wikipedia.org/wiki/Portfolio_optimization) is taken as a constrained quadratic optimization problem.\n", + "Start by creating an instance of the portfolio optimization problem, using the `random_instance` method of the `PortfolioOptimization`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0efc6710-2cfe-4a0d-bf2f-f9ef3aed2ec5", + "metadata": {}, + "outputs": [], + "source": [ + "# create a problem instance for portfolio optimization\n", + "num_assets = 4 # number of decision variables\n", + "budget = 2 # constraint on the sum of decision variables\n", + "problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo" + ] + }, + { + "cell_type": "markdown", + "id": "fe2621d4", + "metadata": {}, + "source": [ + "## Solving the problem" + ] + }, + { + "cell_type": "markdown", + "id": "f54c48a2", + "metadata": {}, + "source": [ + "The simplest QAOA and FQAOA workflows." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0ee8df0b-e25d-4f9b-8304-1bdff0acb64b", + "metadata": {}, + "outputs": [], + "source": [ + "# conventional QAOA workflow\n", + "qaoa = QAOA()\n", + "qaoa.compile(problem = problem)\n", + "qaoa.optimize()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "11087210-d89c-4b4d-a85a-576a1faad80f", + "metadata": {}, + "outputs": [], + "source": [ + "# FQAOA workflow\n", + "fqaoa = FQAOA()\n", + "fqaoa.compile(problem = problem, n_fermions = budget)\n", + "fqaoa.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "5bf46716-6f89-4e00-bbae-cbe8a991055e", + "metadata": {}, + "source": [ + "## Performance Evaluation of FQAOA\n", + "To evaluate the performance of FQAOA, we show expectation value of costs. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e02d09a9-6c15-4539-accd-c4df75692f74", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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RERE5BWY1BAREZFXYFJDREREXoFJDREREXkFJjVERETkFZjUELmps2fPQpIk81Tq7uDYsWO45ppr4OPjg549e1otI4TAvffei9DQUJfH747vYWNt3boVkiTVWlPHGebPn2/z+hK5MyY1RDbcddddkCQJixcvttj+9ddfQ5IkF0XlWvPmzYO/vz+OHz9usbZLTRs3bsSKFSvw7bffIiMjA926dWuW2O666y5MnDjRYltsbGyzxuCJJEnC119/bbHtiSeesHl9idwZkxqiOvj4+OC1115DXl6eq0NxmIqKika/9vTp0xgyZAji4uIQFhZms0x0dDQGDRqEqKgoKBSKRp+vqeRyuctj8EQBAQE2ry+RO2NSQ1SHUaNGISoqCq+++qrNMtaa6t966y3Ex8ebn5taEV555RVERkYiODgYCxcuRGVlJZ588kmEhoaiTZs2WL58ea3jHzt2DIMGDYKPjw+6deuGbdu2Wew/cuQIxo0bh4CAAERGRuKOO+5Adna2ef+IESMwe/ZsPProowgPD8fYsWOt1sNgMGDhwoVo06YN1Go1evbsiY0bN5r3S5KE/fv3Y+HChZAkCfPnz691jLvuugsPPfQQzp8/D0mSzO9BfHw83nrrLYuyPXv2tDiGJEn4+OOPMWnSJPj5+SEpKQnffPONxWuOHj2KG2+8ERqNBoGBgRg6dChOnz6N+fPnY+XKldiwYQMkSYIkSdi6davV7qdt27ahf//+UKvViI6OxjPPPIPKykqL9+vhhx/GU089hdDQUERFRVmt69U+/vhjdO7cGT4+PujUqRPee+89875Bgwbh6aeftih/5coVKJVKbN++HQDw6aefom/fvggMDERUVBSmTp2KrKwsm+dryOdu7969GD16NMLDwxEUFIThw4fjjz/+MO83lZ00aZLF9br62PV9Nkzv81dffYWRI0fCz88PycnJ2LVrl7nMuXPnMH78eISEhMDf3x9du3bF999/X+d7SmQvJjVEdZDL5XjllVewZMkSXLhwoUnH2rJlCy5duoTt27fjzTffxLx583DjjTciJCQEe/bswf3334/77ruv1nmefPJJPP744/jzzz8xcOBAjB8/Hjk5OQCA/Px8XHvttejVqxf27duHjRs34vLly5g8ebLFMVauXAmVSoXffvsNH3zwgdX43n77bfzf//0f/vnPf+LQoUMYO3Ys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IZ+7pKQkfPrpp0hNTcWePXswbdq0Wi1e8fHx+Pnnn5GZmWlz6gJHfDYeffRRbNq0CWlpafjjjz/wyy+/mJNzIkdhUkNkh4ULF9bqhujcuTPee+89LF26FMnJyfj999+t3hnUWIsXL8bixYuRnJyMHTt24JtvvkF4eDgAmP+C1uv1GDNmDLp3745HH30UwcHBdn9JP/zww5gzZw4ef/xxdO/eHRs3bsQ333yDpKSkJtdh7ty5GD58OG688UbccMMNmDhxIhITE+06RlhYGLZs2YKioiIMHz4cffr0wUcffWTu8pg5cyY6duyIvn37IiIiolbLAgC0bt0a33//PX7//XckJyfj/vvvxz/+8Q88//zzTarfPffcg48//hjLly9H9+7dMXz4cKxYscKipQYwdkEdPHgQQ4cORdu2bc3bIyIisGLFCnzxxRfo0qULFi9ejH/+8591nrMhn7tly5YhLy8PvXv3xh133IGHH34YrVq1sijzf//3f9i8eTNiY2PRq1cvq+dyxGdDr9dj1qxZ6Ny5M66//np06NDB4rZ3IkeQxNWdskREREQeiC01RERE5BWY1BAREZFXYFJDREREXoFJDREREXkFJjVERETkFZjUEBERkVdgUkNERERegUkNEREReQUmNUREROQVmNQQERGRV2BSQ0RERF6BSQ0RERF5hf8HiLxQOiddVJgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = ['QAOA', 'FQAOA']\n", + "\n", + "# plot cost history\n", + "fig, ax = plt.subplots()\n", + "for i, result in enumerate([qaoa.result, fqaoa.result]):\n", + " result.plot_cost(ax=ax, color=f'C{i}', label=labels[i])\n", + "ax.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e620d83e-0a6c-433a-af44-1b7a04f0d491", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimized expectation values of the cost\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MethodOptimized Cost $\\langle C_{\\boldsymbol x} \\rangle$
0QAOA-0.014343
1FQAOA-1.675643
\n", + "
" + ], + "text/plain": [ + " Method Optimized Cost $\\langle C_{\\boldsymbol x} \\rangle$\n", + "0 QAOA -0.014343 \n", + "1 FQAOA -1.675643 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# evaluate optimized expectation values of the cost\n", + "exp_cost_dict = {\n", + " 'Method': labels,\n", + " r'Optimized Cost $\\langle C_{\\boldsymbol x} \\rangle$':[qaoa.result.optimized['cost'], fqaoa.result.optimized['cost']]\n", + "}\n", + "df = pd.DataFrame(exp_cost_dict)\n", + "print('optimized expectation values of the cost')\n", + "display(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6373b123-b0d2-44db-a4d6-5d2ca44185d8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "probabilities of finding the best five optimal solutions\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Bitstring, $\\boldsymbol{x}$Cost, $C_{\\boldsymbol x}$QAOAFQAOA
01010-1.7979050.1448960.904789
11100-0.7204930.1249310.015386
20110-0.6020180.1229310.033298
31001-0.4719080.1207770.031587
40011-0.3623330.1191130.012593
\n", + "
" + ], + "text/plain": [ + " Bitstring, $\\boldsymbol{x}$ Cost, $C_{\\boldsymbol x}$ QAOA FQAOA\n", + "0 1010 -1.797905 0.144896 0.904789\n", + "1 1100 -0.720493 0.124931 0.015386\n", + "2 0110 -0.602018 0.122931 0.033298\n", + "3 1001 -0.471908 0.120777 0.031587\n", + "4 0011 -0.362333 0.119113 0.012593" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Print the best 5 solutions\n", + "qaoa_lowest5_dict = qaoa.result.lowest_cost_bitstrings(5)\n", + "fqaoa_lowest5_dict = fqaoa.result.lowest_cost_bitstrings(5)\n", + "\n", + "qaoa_dict = {\n", + " r'Bitstring, $\\boldsymbol{x}$': qaoa_lowest5_dict['solutions_bitstrings'],\n", + " r'Cost, $C_{\\boldsymbol x}$': qaoa_lowest5_dict['bitstrings_energies'],\n", + " labels[0]: qaoa_lowest5_dict['probabilities'],\n", + " labels[1]: fqaoa_lowest5_dict['probabilities']\n", + "}\n", + "df = pd.DataFrame(qaoa_dict)\n", + "print('probabilities of finding the best five optimal solutions')\n", + "display(df)" + ] + }, + { + "cell_type": "markdown", + "id": "8106c098-1fe7-43ad-b276-8aeeb57f0e7d", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] T. Yoshioka, K. Sasada, Y. Nakano, and K. Fujii, [Phys. Rev. Research 5, 023071 (2023).](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071), [arXiv:2301.10756 [quant-ph]](https://arxiv.org/pdf/2301.10756).\\\n", + "[2] T. Yoshioka, K. Sasada, Y. Nakano, and K. Fujii, [2023 IEEE International Conference on Quantum Computing and Engineering (QCE) 1, 300-306 (2023).](https://ieeexplore.ieee.org/document/10313662), [arXiv:2312.04710 [quant-ph]](https://arxiv.org/pdf/2312.04710).\\\n", + "[3] Z. Jiang, J. S. Kevin, K. Kechedzhi, V. N. Smelyanskiy, and S. Boixo, [Phys. Rev. Appl. 9, 044036 (2018).](https://journals.aps.org/prapplied/abstract/10.1103/PhysRevApplied.9.044036)[arXiv:1711.05395 [quant-ph]](https://arxiv.org/pdf/1711.05395).\\\n", + "[4] S. Hadfield, Z. Wang, B. O’Gorman, E. G. Rieffel, D. Venturelli, and R. Biswas, [algorithms 12, 34 (2019).](https://www.mdpi.com/1999-4893/12/2/34),[arXiv:1709.03489v2 [quant-ph]](https://arxiv.org/pdf/1709.03489).\\\n", + "[5] Z. Wang, N. C. Rubin, J. M. Dominy, and E. G. Rioeffel, [Phys. Rev. A 101, 012320 (2020).](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.101.012320),[arXiv:1904.09314v2 [quant-ph]](https://arxiv.org/pdf/1904.09314)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/17_FQAOA_advanced_parameterization.ipynb b/examples/17_FQAOA_advanced_parameterization.ipynb new file mode 100644 index 00000000..ebdff3e8 --- /dev/null +++ b/examples/17_FQAOA_advanced_parameterization.ipynb @@ -0,0 +1,320 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c5de313a", + "metadata": {}, + "source": [ + "# 17 - FQAOA circuit with advanced circuit parameterizations\n", + "\n", + "This notebook describes how to apply the annealing and Fourier parameter classes included in OpenQAOA to FQAOA frame work." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d3e2b171-0013-4e2e-9801-8a6a58d50370", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib notebook\n", + "%matplotlib inline\n", + "\n", + "# Import the libraries needed to employ the QAOA and FQAOA quantum algorithm using OpenQAOA\n", + "from openqaoa import FQAOA\n", + "from openqaoa import QAOA\n", + "\n", + "# method to covnert a docplex model to a qubo problem\n", + "from openqaoa.problems import PortfolioOptimization\n", + "\n", + "# Import external libraries to present an manipulate the data\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "bb6a05c9-82f7-431b-b60f-de61cd71d3cf", + "metadata": {}, + "source": [ + "Start by creating an instance of the portfolio optimization problem, using the `random_instance` method of the `PortfolioOptimization` class." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "06460e0b-e03f-46f3-8008-1ef9688f3e57", + "metadata": {}, + "outputs": [], + "source": [ + "# create a problem instance for portfolio optimization\n", + "num_assets = 4 # number of assets\n", + "budget = 2 # budget constraint value\n", + "problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo" + ] + }, + { + "cell_type": "markdown", + "id": "c94dc5c2-136b-4683-86d9-64448daeca05", + "metadata": {}, + "source": [ + "## Quantum Annealing with FQAOA\n", + "\n", + "The framework of Fermionic QAOA (FQAOA) covers the Quantum Annealing (QA) framework [[1]](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071). In this note, we demonstrate that QA with FQAOA works for constrained combinatorial optimization problems in practice and compare its performance with QA with conventional QAOA [[2]](https://arxiv.org/pdf/quant-ph/0001106)." + ] + }, + { + "cell_type": "markdown", + "id": "25e5ec42-907e-41d2-9170-78e153af1ecd", + "metadata": {}, + "source": [ + "### FQAOA Ansatz for QA\n", + "FQAOA supports a discretized form of quantum annealing.\n", + "Quantum annealing starts with a mixer Hamiltonian ground state and gradually evolves to a cost Hamiltonian ground state.\n", + "If the transformation can be performed slowly to infinity, it is guaranteed to reach the exact ground state of the cost Hamiltonian.\n", + "In practice, the transformation is performed over a finite time and we want to prepare the ground state with some high probability.\n", + "\n", + "The approximated ground state obtained by QA are as follows:\n", + "$$|\\psi(T)\\rangle = {\\cal T}\\exp\\left\\{ -i\\int_0^T \\left[\\left(1-\\frac{t}{T}\\right)\\hat{\\cal H}_M+\\frac{t}{T}\\hat{\\cal H}_C\\right] dt\\right\\}\\hat{U}_{\\rm init}|{\\rm vac}\\rangle,$$\n", + "where the cost $\\hat{\\cal H}_C$ and mixer $\\hat{\\cal H}_M$ Hamiltonians, and initial state preparation unitary $\\hat{U}_{\\rm init}$ are given in the notebook [`16 - Fermionic QAOA (FQAOA)`](https://github.com/tech-sketch/openqaoa/blob/yoshioka1128/dev_clean_fqaoa/examples/16_FQAOA_examples.ipynb). $T$ is annealing time and $\\cal T$ is time ordering product for $t$.\n", + "\n", + "The $|\\psi(T)\\rangle$ is approximated in the following form for calculation in quantum circuits [1, 2]:\n", + "$$|\\psi(T)\\rangle\\sim|\\psi_p({\\boldsymbol \\gamma}^{(0)}, {\\boldsymbol \\beta}^{(0)})\\rangle \n", + "= \\left[\\prod_{j=1}^pU(\\hat{\\cal H}_M,\\beta_j^{(0)}){U}(\\hat{\\cal H}_C,\\gamma_j^{(0)})\\right]\\hat{U}_{\\rm init}|{\\rm vac}\\rangle,$$\n", + "with\n", + "$$\\gamma_j^{(0)} = \\frac{2j-1}{2p}\\Delta t,\\qquad\n", + "\\beta_j^{(0)} = \\left(1-\\frac{2j-1}{2p}\\right)\\Delta t$$,\n", + "\\end{eqnarray}\n", + "where $\\Delta t$ is the unit of discretized annealing time, as $T=p\\Delta t$.\n", + "\n", + "In the FQAOA ansatz, the following constraints can be imposed on any integer $M$ smaller than the number of qubits $N$ as:\n", + "$$\\sum_{i=1}^{N} \\hat{n}_i|\\psi_p({\\boldsymbol \\gamma^{(0)}}, {\\boldsymbol \\beta}^{(0)})\\rangle = M|\\psi_p({\\boldsymbol \\gamma^{(0)}}, {\\boldsymbol \\beta}^{(0)})\\rangle,$$\n", + "where $\\hat{n}_i = \\hat{c}^\\dagger_i\\hat{c}_i$ is number operator and $\\hat{c}_i^\\dagger (\\hat{c}_i)$ is creation (annihilation) operator of fermion at $i$-th site.\n", + "\n", + "The FQAOA ansatz is also improved from $|\\psi_p({\\boldsymbol \\gamma}^{(0)}, {\\boldsymbol \\beta}^{(0)})\\rangle$ by setting ${\\boldsymbol \\gamma}^{(0)}, {\\boldsymbol \\beta}^{(0)}$ as initial parameters and running FQAOA. Thus, the performance of the FQAOA framework is guaranteed by QA." + ] + }, + { + "cell_type": "markdown", + "id": "f7eb209f-387b-4987-b642-e76f61168cf1", + "metadata": {}, + "source": [ + "### Running QA" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b1c8a507-e592-42c8-b29b-aff991a16f5d", + "metadata": {}, + "outputs": [], + "source": [ + "# set maximum annealing time\n", + "Tmax = 2.0" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "11087210-d89c-4b4d-a85a-576a1faad80f", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# QA using FQAOA ansatz\n", + "fqaoa_dt = 0.2\n", + "fqaoa_cost_list = []\n", + "for ip in range(1, int(Tmax/fqaoa_dt)+1):\n", + " fqaoa = FQAOA()\n", + " fqaoa.set_circuit_properties(p=ip, param_type='annealing', init_type='ramp', annealing_time=fqaoa_dt*ip)\n", + " fqaoa.set_classical_optimizer(maxiter=0)\n", + " fqaoa.compile(problem = problem, n_fermions = budget)\n", + " fqaoa.optimize()\n", + " fqaoa_cost_list.append([ip*fqaoa_dt, fqaoa.result.optimized['cost']])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0ee8df0b-e25d-4f9b-8304-1bdff0acb64b", + "metadata": {}, + "outputs": [], + "source": [ + "# QA using QAOA ansatz\n", + "qaoa_dt = 0.02\n", + "qaoa_cost_list = []\n", + "for ip in range(1, int(Tmax/qaoa_dt)+1):\n", + " qaoa = QAOA()\n", + " qaoa.set_circuit_properties(p=ip, param_type='annealing', init_type='ramp', annealing_time=qaoa_dt*ip)\n", + " qaoa.set_classical_optimizer(maxiter=0)\n", + " qaoa.compile(problem = problem)\n", + " qaoa.optimize()\n", + " qaoa_cost_list.append([ip*qaoa_dt, qaoa.result.optimized['cost']])" + ] + }, + { + "cell_type": "markdown", + "id": "ef9f898f-32fa-471d-8fb3-3d97a7b1b0d7", + "metadata": {}, + "source": [ + "### Performance Evaluation of FQAOA Ansatz in QA\n", + "\n", + "To evaluate the performance of the QA using FQAOA ansatz, we show expectation values of cost." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "51385cb6-0205-48ce-994f-5f4bbd6ce2af", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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yBAtLCxEQRDEzZs3a9dfvXq1zL7ff/990crKSrx27ZrOe7311luiVCoV4+LiKl07UU3hiBBRHVMqlQAAGxubSrXftWsXAJT5H/Prr78OANq5RPb29gCA33//HSqVqiZKxciRI6FSqbB9+3btur179yIzMxMjR47UrrO3t8fJkycRHx9fI+/7559/Ii0tDaNHj9auGz16NM6fP1/uKE/z5s3RvXt37fcuLi5o2rQpbt68Wabtiy++CEEQtN93794dJSUluH37NgDNKE9mZiZGjx6N1NRU7SKVStGxY0ccPHhQu62FhYX2zwUFBUhNTUWnTp0AoNqnBSdNmqQzf6i0X6V9OX36NNLS0jBt2jSYmT2Y5jl27Fg4ODhU6z2r6uG5Wvb29mjatCmsrKwwYsQI7fqmTZvC3t5e5xhs3boV3bt3h4ODg85n26dPH5SUlODIkSN1Uj/RwxiEiOqYra0tAM28msq4ffs2JBKJztU+AODu7g57e3vtL/CePXti+PDhCAsLg7OzM4YMGYIffvihzDyiqggODkazZs2wZcsW7botW7bA2dkZTz31lHbdp59+ikuXLsHb2xshISFYtGhRuSGksn788Uf4+flBoVAgJiYGMTExCAgIgKWlJTZs2FCmfcOGDcusc3BwQEZGRoVtS8NDadvr168DAJ566im4uLjoLHv37kVycrJ22/T0dLz66qtwc3ODhYUFXFxc4OfnBwDIysqqVt8rqq/0eP/758HMzAy+vr7Ves+qMDc31841K2VnZ4cGDRroBMzS9Q8fg+vXr2P37t1lPtc+ffoAgM5nS1RXeNUYUR2ztbWFp6cnLl26VKXt/v1LprzXt23bhhMnTuC3337Dnj17MHnyZHzxxRc4ceIErK2tq1XvyJEj8eGHHyI1NRU2Njb49ddfMXr0aJ3RiBEjRqB79+7YsWMH9u7di88++wyffPIJtm/fjgEDBlTp/ZRKJX777TcUFBSgcePGZV7fuHEjPvzwQ53PQyqVlrsvsZx5OhW1LZ20u379eri7u5dp9+9+Hz9+HG+++SZat24Na2trqNVq9O/fv9qTf6vSF314VH2VqVutViM0NBTz5s0rt22TJk2evECiKmIQItKDZ555Bt9//z0iIiLQuXPnx7b18fGBWq3G9evXERgYqF2flJSEzMxM+Pj46LTv1KkTOnXqhA8//BAbN27E2LFjsXnzZkydOrXCMFWekSNHIiwsDD///DPc3NygVCp1JmmX8vDwwIwZMzBjxgwkJyejbdu2+PDDD6schLZv346CggKsWLECzs7OOq9FR0fj3XffxbFjx9CtW7cq96UyAgICAACurq7akYryZGRk4MCBAwgLC8OCBQu060tHlGpL6fGOiYlB7969teuLi4tx69YtncvjDU1AQABycnIe+7kS1TWeGiPSg3nz5sHKygpTp05FUlJSmddv3LiBb775BgAwcOBAAMDXX3+t0+bLL78EAAwaNAiA5hfzv0cNWrduDQDa02OWlpYAUKW7NAcGBqJly5bYsmULtmzZAg8PD/To0UP7eklJSZnTQK6urvD09NQ5LZeamoqrV68iLy/vse/3448/wt/fHy+//DKee+45neWNN96AtbV1uafHakq/fv1ga2uLjz76qNy5VqWX2peOgPz7M//3capp7du3h5OTE1atWoXi4mLt+g0bNpR7KtCQjBgxAhEREdizZ0+Z1zIzM3X6Q1RXOCJEpAcBAQHYuHEjRo4cicDAQJ07Sx8/fhxbt27FxIkTAWjm6UyYMAHff/89MjMz0bNnT5w6dQpr167F0KFDtaMCa9euxfLlyzFs2DAEBAQgOzsbq1atgq2trTZMWVhYoHnz5tiyZQuaNGkCR0dHBAUFISgo6LH1jhw5EgsWLIC5uTmmTJkCieTB/6Gys7PRoEEDPPfccwgODoa1tTX279+Pf/75B1988YW23XfffYewsDAcPHjwkfe6iY+Px8GDB/HKK6+U+7pCoUC/fv2wdetWLF26tFbuemxra4sVK1bghRdeQNu2bTFq1Ci4uLggLi4Of/zxB7p27YrvvvsOtra26NGjBz799FOoVCp4eXlh7969OpeL1wa5XI5FixZh9uzZeOqppzBixAjcunUL4eHhCAgIqNaoX11588038euvv+KZZ57BxIkT0a5dO+Tm5uLixYvYtm0bbt26VWYUkKi2MQgR6cl//vMfXLhwAZ999hl++eUXrFixAgqFAq1atcIXX3yBadOmadv+73//g7+/P8LDw7Fjxw64u7tj/vz5WLhwobZNaUDavHkzkpKSYGdnh5CQEGzYsEE7gbd0X7Nnz8Zrr72GoqIiLFy4sFJB6N1330VeXp7O1WKAZpRpxowZ2Lt3L7Zv3w61Wo1GjRph+fLlmD59epU+k82bN0OtVmPw4MGPbDN48GD8/PPP+PPPP/Gf//ynSvuvrDFjxsDT0xMff/wxPvvsMxQWFsLLywvdu3fHpEmTtO02btyI2bNnY9myZRBFEX379sWff/4JT0/PWqmr1KxZsyCKIr744gu88cYbCA4Oxq+//opXXnnFoO8GbWlpicOHD+Ojjz7C1q1bsW7dOtja2qJJkyYICwuDnZ2dvkukekgQDWUGHhERVZtarYaLiwueffZZrFq1St/lEBkNzhEiIjIyBQUFZeYmrVu3Dunp6UbxMF0iQ8IRISIiI3Po0CG89tpreP755+Hk5ISzZ89i9erVCAwMxJkzZ/TyQFciY8U5QkRERsbX1xfe3t5YunQp0tPT4ejoiPHjx+Pjjz9mCCKqIo4IERERUb3FOUJERERUbzEIERERUb3FOUIVUKvViI+Ph42NjUHfqIyIiIgeEEUR2dnZ8PT01LkJ7L8xCFUgPj4e3t7e+i6DiIiIquHOnTto0KDBI19nEKqAjY0NAM0HaWtrW+39qFQq7N27F3379q2VxwIYAvbRNLCPpsHU+2jq/QPYxyelVCrh7e2t/T3+KAxCFSg9HWZra/vEQcjS0hK2trYm/QPNPho/9tE0mHofTb1/APtYUyqa1sLJ0kRERFRvMQgRERFRvcUgRERERPUW5wgRERFVk1qtRlFRUa3sW6VSwczMDAUFBSgpKamV99C3J+mjTCaDVCp94hqMKggdOXIEn332Gc6cOYOEhATs2LEDQ4cOfWT7Q4cOoXfv3mXWJyQkwN3dvRYrJSIiU1dUVITY2Fio1epa2b8oinB3d8edO3dM9j52T9pHe3t7uLu7P9HnY1RBKDc3F8HBwZg8eTKeffbZSm8XHR2tc8WXq6trbZRHRET1hCiKSEhIgFQqhbe392Nv2FddarUaOTk5sLa2rpX9G4Lq9lEUReTl5SE5ORkA4OHhUe0ajCoIDRgwAAMGDKjydq6urrC3t6/5goiIqF4qLi5GXl4ePD09YWlpWSvvUXrazdzc3KSDUHX7aGFhAQBITk6Gq6trtU+TGVUQqq7WrVujsLAQQUFBWLRoEbp27frItoWFhSgsLNR+r1QqAWjOY6pUqmrXULrtk+zD0LGPpoF9NA2m3kd996+wsBCiKMLMzKxWT42Vfq2t99C3J+2jubk5RFFEfn4+FAqFzmuV/dkQxNIqjIwgCBXOEYqOjsahQ4fQvn17FBYW4n//+x/Wr1+PkydPom3btuVus2jRIoSFhZVZv3HjxlpL/UREZFzMzMzg7u4Ob29vyOVyfZdTbxUVFeHOnTtITExEcXGxzmt5eXkYM2YMsrKyHntDZJMOQuXp2bMnGjZsiPXr15f7enkjQt7e3khNTX3iO0vv27cPoaGhJn2HUPbR+LGPpsHU+6jv/hUUFODOnTvw9fWFubl5rbxH6UNDTfmh30/ax4KCAty6dQve3t5ljoNSqYSzs3OFQahenBp7WEhICP7+++9Hvq5QKMoMrwGay/Rq4i9bTe3HkLGPpoF9NA2m3kd99a+kpASCIEAikdTa/J3SU0Wl72OKnrSPEokEgiCU+3NQ2Z8L0/xkHyMyMvKJZpcTEREZuzt37mDy5Mnw9PSEXC6Hj48PXn31VaSlpZVpu2nTJkilUsycObPcfaWnp2POnDnw8fGBXC6Hp6cnJk+ejLi4uHLbL1myBFKpFJ999lmN9qm6jCoI5eTkIDIyEpGRkQCA2NhYREZGaj/s+fPnY/z48dr2X3/9NX755RfExMTg0qVLmDNnDv76669HHsy6lKQswJ30PBQVm+YEOCIiMkw3b95E+/btcf36dWzatAkxMTFYuXIlDhw4gM6dOyM9PV2n/erVqzFv3jxs2rQJBQUFOq+lp6ejU6dO2L9/P1auXImYmBhs3rwZMTEx6NChA27evFnm/desWYN58+ZhzZo1tdrPyjKqU2OnT5/WuUHi3LlzAQATJkxAeHg4EhISdBJoUVERXn/9ddy7dw+WlpZo1aoV9u/fX+5NFuvafw/fxJpjsQAAZ2s53GzN4W5rjgYOFmjkao0AV2s0crWGi7XCZM8NExFR3Zs5cybkcjn27t2rvQS9YcOGaNOmDQICAvDOO+9gxYoVADQDDsePH8fPP/+MgwcPYvv27RgzZox2X++88w7i4+MRExOjvVFxw4YNsWfPHjRu3BgzZ87En3/+qW1/+PBh5OfnY/HixVi3bh2OHz+OoKCgOux9WUYVhHr16oXHze0ODw/X+X7evHmYN29eLVdVPaoSNeRSCYpK1EjNKUJqThEuxyvLtLOzkCHIyxatGtgjuIE9gr3t4G5rznBERGRARFFEvqpmH4OhVquRX1QCs6Lix86fsZBJK/07IT09HXv27MGHH36oDUGl3N3dMXbsWGzZsgXLly+HIAj44YcfMGjQINjZ2WHcuHFYvXq1Ngip1Wps3rwZY8eOLfO0BgsLC8yYMQPvvvsu0tPT4ejoCEAzujR69GjIZDKMHj0aa9aswZdfflmVj6XGGVUQMiXvDw3C4iEtkJ5bhERlAZKUBUjMKsTttFzEJOcgJiUHcel5yMpX4VhMGo7FPDhv625rjk7+jugS4IzOAU7wduRl/URE+pSvKkHzBXv08t5XFveDpbxyv86vX78OURQRGBhY7uuBgYHIyMhASkoKnJ2dER4ejm+//RYAMGrUKLz++uuIjY2Fn58fUlJSkJmZ+dh9iaKImJgYhISEQKlUYtu2bYiIiAAAjBs3Dt27d8fixYuf6KrsJ8UgpEeCIMDJWgEnawVaeNqVeb1AVYKY5BxcvJeF83cycf5uFq4lZSNRWYCdkfHYGRkPAPCyt0DPpi7oE+iKLgHOMJc9+UPoiIjIdFV05xy5XI59+/YhNzcXAwcOBAA4OzsjNDQUa9aswfvvv1/pfZXatGkTAgICEBwcDEBzs2MfHx/s2LFDr3N3GYQMmLlMiiAvOwR52WF0SEMAQH5RCc7FZeD4jTRE3EzD+TuZuJeZj40n47DxZBzMZRJ0a+SMPoFu6NfCHQ5WvNEXEVFts5BJcWVxvxrdp1qtRrYyGza2NhWeGqusRo0aQRAEREVFYdiwYWVej4qKgouLC+zt7bF69Wqkp6frnEJTq9W4cOECwsLCtO2ioqLKfa+oqCgIgoBGjRoB0JwWu3z5MszMzHT29+OPPzIIUeVZyKXo0sgZXRo5AwByC4txKjYdB64m4a+oZMRnFWB/VDL2RyXj3Z2X0L2xMwYHeyK0uRtszE33XiJERPokCEKlT09VllqtRrFcCku5WY3dR8jJyQmhoaFYvnw5XnvtNZ2Qk5iYiA0bNmDmzJlIS0vDL7/8gs2bN6NFixbaNiUlJejWrRv27t2L/v37Y8SIEdiwYQMWL16sM08oPz8fy5cvR79+/eDo6IiLFy/i9OnTOHTokHa+EACkpqbiqaeewtWrV9G8efMa6WNVMQgZOSuFGXo3c0XvZq4Qh4iISsjGgagk/HkpEVcSlDgYnYKD0SmQm0kQ2twNI9t7o1sjZ0gknGxNRFQffffdd+jSpQv69euHDz74AH5+frh8+TLefPNNNGnSBAsWLMD//vc/ODk5YcSIEWUmYg8cOBCrV69G//798dFHH+HAgQMIDQ3Fp59+iqCgIMTGxuLdd9+FSqXCsmXLAGhGg0JCQtCjRw+dfanVarRt2xZr1qzB559/XmefwcOM6j5C9HiCIKC5py1mP90Yu17tjv1ze2JOn8bwd7FCUbEaf1xIwPg1p9D904P4at813M3I03fJRERUxxo3box//vkH/v7+GDFiBHx8fDBgwAA0adIEx44dg7W1NdasWYNhw4aVezXa8OHD8euvvyI1NRVOTk44ceIEevfujZdeegkBAQEYMWIEAgICtO9RVFSEH3/8EcOHDy+3nsGDB2P9+vV6e4AuR4RMWCNXa8zp0wSvPt0Yl+OV2Hr6Dnacu4d7mfn45sB1LP3rOp5u5oqJXfzQtZETL8knIqonfH19dW45s3DhQnz55Ze4cOECOnXqhAsXLjxy2xEjRmDEiBHa752dnbF06VIsXbq03PZyuRypqamP3N+rr76K9957T2+PEWEQqgcEQdBOup4/MBB7Lidiyz93cPxGmnY+UWNXa4zv4otn23jBSsEfCyKi+iQsLAy+vr44ceIEQkJCTPbZZuXhb7x6xlwmxZDWXhjS2gs3UnKw7vgtbDtzF9eTc/Dezkv4fE80JnTxxcQuvnDkFWdERPXGpEmT9F2CXtSfyEdlBLhYI2xIECLefhoLnmkOHydLZOWrsPTAdXT9+C+E/XYZ9zLz9V0mERFRrWEQItiayzC5mx/+er0Xlo1piyAvW+SrSvDDsVvo+elBzN9+gROriYjIJDEIkZZUImBQKw/8Nqsb1k8JQZcAJxSrRWw6dQe9Pz+Ed3deREIWR4iIiMh0cI4QlSEIAro3dkH3xi44fSsdX+2/hmMxafjxRBx++ucuxnRsiNlPNYKTtULfpRIRET0RjgjRY7X3dcSGqZ2w+cVOCPFzRFGJGuHHb6HnZ4ew7GAM8otq9mnLREREdYlBiCqlk78TtrzYCT9O6YggL1vkFBbjsz3R6PX5QWw+FYcSdeUeukdERGRIGISo0gRBQLfGzvh1Zjd8M6o1GjhYIElZiLe2X8SgpUdxMjZd3yUSERFVCYMQVZlEImBIay8ceL0n3h0UCDsLGa4mZmPcmtP44ZqEl9wTEZHRYBCialOYSTG1uz8OvdEL4zo1hEQAItMk6PfNMXy17xoKVJw/RERkaCZOnAhBEMosMTExAIA7d+5g8uTJ8PT0hFwuh4+PD1599VWkpaWVu79NmzZBKpVi5syZ5b6enp6OOXPmwMfHB3K5HJ6enpg8eTLi4uLKbb9kyRJIpVJ89tlnNdPhCjAI0RNzsJLjg6Et8cuMzmhkq0ZhsRrfHLiOfl8fweFrKfouj4iI/qV///5ISEjQWfz8/HDz5k20b98e169fx6ZNmxATE4OVK1fiwIED6Ny5M9LTy06BWL16NebNm4dNmzahoKBA57X09HR06tQJ+/fvx8qVKxETE4PNmzcjJiYGHTp0wM2bN8vsb82aNZg3bx7WrFlTa/1/GIMQ1Zhm7jaY1VyNpSNbwc1WgdtpeZiw5hRmbjyLJGVBxTsgIqI6oVAo4O7urrOUjurI5XLs3bsXPXv2RMOGDTFgwADs378f9+7dwzvvvKOzn9jYWBw/fhxvvfUWmjRpgu3bt+u8/s477yA+Ph779+/HgAED0LBhQ/To0QN79uyBTCbDrFmzdNofPnwY+fn5WLx4MZRKJY4fP17rnwWDENUoQQAGBLnjwOu9MLmrHyQC8MeFBDz9xWGsi7gFNa8uIyJTJIpAUW7NL6q8ituINfPvanp6Ovbs2YMZM2bAwsJC5zV3d3eMHTsWW7ZsgfjQ+/3www8YNGgQ7OzsMG7cOKxevVr7mlqtxubNmzF27Fi4u7vr7M/CwgIzZszA3r17kZGRoV2/evVqjB49GjKZDKNHj9bZX23hDRWpVlgrzLBgcHM829YL7+y8hPN3MrHgl8v47Xw8Ph7eCgEu1voukYio5qjygI88a3SXEgD2lWn4djwgt6rSvn///XdYWz/4d3jAgAF44403IIoiAgMDy90mMDAQGRkZSElJgaurK9RqNcLDw/Htt98CAEaNGoXXX38dsbGx8PPzQ0pKCjIzMx+7P1EUcfPmTfj4+ECpVGLbtm2IiIgAAIwbNw7du3fHN998o1NrTeOIENWqIC87bJ/eBWH/aQEruRT/3MrAgG+OYtnBGKhK1Pouj4ioXurduzciIyO1y9KlS7WviRWMMMnlcgDAvn37kJubi4EDBwIAnJ2dERoaWmZuT0X7K7Vp0yYEBAQgODgYANC6dWv4+Phgy5Ytle5XdXBEiGqdVCJgQhdfPB3oind2XMLhayn4bE80/riQgC9GBCPQw1bfJRIRPRmZpWZkpgap1Woos7Nha2MDieQx4xYyyyrv28rKCo0aNdJZJ5fLIQgCoqKiMGzYsDLbREVFwcXFBfb29gA0p7HS09N1TqOp1WpcuHABYWFh2rZRUVHl1hAVFQVBEODv76/d3+XLl2FmZqazvzVr1mDKlClV7mNlcUSI6kwDB0uET+qAL0cEw95ShisJSvznu7+x7GAMijk6RETGTBA0p6dqepFZVtxGEGqkC05OTggNDcXy5cuRn697P7jExERs2LABEydOBACkpaXhl19+webNm3VGls6dO4eMjAzs3bsXEokEI0aMwMaNG5GYmKizv/z8fCxfvhx9+/aFg4MDLl68iNOnT+PQoUM6+zt06BAiIiJw9erVGuljeRiEqE4JgoBn2zbAvtd6IrS5G1QlIj7bE43hKyMQk5yj7/KIiOq17777DoWFhejXrx+OHDmCO3fuYPfu3QgNDUWTJk2wYMECAMD69evh5OSEESNGICgoSLsEBwdj4MCB2knOH330Edzd3REaGoo///wTd+7cwZEjR9CvXz+oVCp89913ADSXzIeEhKBHjx46++vRowc6dOhQq5OmGYRIL1xsFPj+hXb4ckQwbMzNcP5OJgYtPYo1f8fyyjIiIj1p3Lgx/vnnH/j7+2PEiBHw8fHBgAED0KRJExw7dkw7aXnNmjUYNmwYhHJGo4YPH45ff/0VqampcHJywokTJ9C7d2+89NJLCAgIwIgRIxAQEKB9n6KiImzYsAHDhw8vt6bhw4dj3bp1UKlUtdJnzhEivSkdHeoc4IR52y7g6PVULP79Cg5GJ+OL54Phamuu7xKJiExOeHj4Y1/39fXVabNw4UJ8+eWXuHDhAjp16gQAuHDhwiO3HzFiBEaMGKH93tnZGUuXLtWZkF1KrVZDLpcjOTn5kfOg5s2bh3nz5j225ifBESHSOw87C6ybHIL3hwbBXCbB0eup6Pf1Eey5nFjxxkREVKvCwsKwdOlSnDhxAmq16c3n5IgQGQRBEPBCJx909nfEq5sjcTleiZfWn8GoDt5YMLg5LOX8USUi0pdJkybpu4RawxEhMiiNXG2wY0ZXvNTTH4IAbP7nDgZ/+zeiEpT6Lo2IiEwQgxAZHLmZBPMHBGLD1I5ws1XgRkouhiw7hvURtyp9Yy4iIqLKYBAig9UlwBm7XumO3k1dUFSsxnu/XMb0H88iK692rhwgIqoq/udMv2ri82cQIoPmZK3Amokd8O6gQMikAnZfTsTApUdx/k6mvksjonpMKpUCAIqKivRcSf2Wl5cHAJDJZNXeB2egksETBAFTu/sjxM8RszaeQ1x6Hp5beRzvDmqO8Z19yr2PBRFRbTIzM4OlpSVSUlIgk8ke/wiMalKr1SgqKkJBQUGt7N8QVLePoigiLy8PycnJsLe31wbT6mAQIqPRqoE9fn+lG+ZtvYDdlxOx8NfLOHUrHR8/2xI25tX/3wARUVUJggAPDw/Exsbi9u3btfIeoigiPz8fFhYWJvsfvifto729Pdzd3Z+oBgYhMiq25jKsGNcWPxy7hY92ReGPCwm4Eq/EinFt0cydD28lorojl8vRuHHjWjs9plKpcOTIEfTo0eOJTv0Ysifpo0wme6KRoFIMQmR0BEHA5G5+aN3QHrM2nEVsai6GLTuOj4e3xJDWXvouj4jqEYlEAnPz2rkLvlQqRXFxMczNzU02CBlCH03zpCPVC20bOuCPV7qje2Nn5KtK8OrmSCz69TJUfJI9ERFVEoMQGTUHKznCJ4VgVu9GAIDw47cw+vsTSFYW6LkyIiIyBgxCZPSkEgFv9GuKVePbw0ZhhtO3MzDo279x5na6vksjIiIDxyBEJiO0uRt+nd0NTd1skJJdiFHfn8CmU3H6LouIiAwYgxCZFD9nK2yf0QUDgtyhKhExf/tFvL3jIoqKOW+IiIjKYhAik2OlMMPysW3xZr+mEARg48k4jFl1AsnZnDdERES6GITIJAmCgJm9G2HNhA6wMdfMGxry3TFcupel79KIiMiAMAiRSevdzBW/zOwKfxcrJGQV4LmVx/Hb+Xh9l0VERAaCQYhMnr+LNXbO7IqeTVxQoFJj9qZz+HJvNNRqPjWaiKi+YxCiesHWXIY1EztgWnc/AMDSv2IwfcMZ5BUV67kyIiLSJwYhqjekEgHvDGqOz58PhlwqwZ7LSXhuRQQSsvL1XRoREekJgxDVO8+1a4BNL3aCs7UcVxKU+M93x3D+Tqa+yyIiIj1gEKJ6qZ2PA3bM6Kq9+eKI/0bg9wucRE1EVN8wCFG95e1oiW3TO+OpZq4oLFZj1sZzWHrgOkSRk6iJiOoLBiGq12zMZVg1vj2mdNNMov5y3zW8/tN5FBaX6LkyIiKqCwxCVO9JJQLee6Y5PhwWBKlEwPZz9/DC6lPIyC3Sd2lERFTLjCoIHTlyBIMHD4anpycEQcDOnTsr3ObQoUNo27YtFAoFGjVqhPDw8Fqvk4zT2I4++GFiB9gozHAqNh3PrjiO2NRcfZdFRES1yKiCUG5uLoKDg7Fs2bJKtY+NjcWgQYPQu3dvREZGYs6cOZg6dSr27NlTy5WSserRxAXbpneBl70FYlNzMWz5MZyKTdd3WUREVEvM9F1AVQwYMAADBgyodPuVK1fCz88PX3zxBQAgMDAQf//9N7766iv069evtsokI9fU3QY7ZnbBtHVncP5OJsb97yQ+HxGM/wR76rs0IiKqYUYVhKoqIiICffr00VnXr18/zJkz55HbFBYWorCwUPu9UqkEAKhUKqhUqmrXUrrtk+zD0JlSHx3MpVg/sR1e33YR+6KS8cqmc4hLzcGkTl4ATKOPj2JKx/FR2EfjZ+r9A9jHmtp3RQTRSK8VFgQBO3bswNChQx/ZpkmTJpg0aRLmz5+vXbdr1y4MGjQIeXl5sLCwKLPNokWLEBYWVmb9xo0bYWlpWSO1k/FQi8AvtyU4lKA5i9zZVY3n/dSQGtVJZSKi+icvLw9jxoxBVlYWbG1tH9nOpEeEqmP+/PmYO3eu9nulUglvb2/07dv3sR9kRVQqFfbt24fQ0FDIZLKaKNXgmGofnwGw7kQcPtx1FRHJEmQUAutf7gl767JB2hSY6nF8GPto/Ey9fwD7+KRKz+hUxKSDkLu7O5KSknTWJSUlwdbWttzRIABQKBRQKBRl1stksho5SDW1H0Nmin2c0j0ADZ2s8cqms7iaBUxYF4nwSSFwtTXXd2m1xhSP47+xj8bP1PsHsI9Pss/KMOkB/s6dO+PAgQM66/bt24fOnTvrqSIyZqHN3fDj5A6wNhNxJSEbw5Yfx/WkbH2XRURET8CoglBOTg4iIyMRGRkJQHN5fGRkJOLi4gBoTmuNHz9e2/7ll1/GzZs3MW/ePFy9ehXLly/HTz/9hNdee00f5ZMJaNXADq+1LIGvkyXuZeZj+IrjOHEzTd9lERFRNRlVEDp9+jTatGmDNm3aAADmzp2LNm3aYMGCBQCAhIQEbSgCAD8/P/zxxx/Yt28fgoOD8cUXX+B///sfL52nJ+JsDmyZFoJ2Pg5QFhRj/OpT+O08H9hKRGSMjGqOUK9evR77QMzy7hrdq1cvnDt3rharovrI0UqODVM74rUtkfjzUiJmbzqHJGUBpnb313dpRERUBUY1IkRkSMxlUnw3pi0mdvEFAHzwRxQW/3YFarVR3pGCiKheYhAiegJSiYCFg5tj/oBmAIA1x2Ixe/M5FKj49HoiImPAIET0hARBwEs9A/DNqNaQSQX8cSEBE9acQla+6d4NlojIVDAIEdWQIa29sHZSCGwUZjgZm44RKyOQmFWg77KIiOgxGISIalCXRs7Y8lJnuNooEJ2UjWeXH8M13muIiMhgMQgR1bDmnrb4eXoX+LtYIT6rAM+tOI5/bqXruywiIioHgxBRLfB2tMTPL3dBm4b2UBYUY+z/TmL3pUR9l0VERP/CIERUSxys5Ng4tRP6BLqiqFiNGRvO4McTt/VdFhERPYRBiKgWWcilWDmuHUZ18IZaBN7deQlf7rv22BuDEhFR3WEQIqplZlIJljzbEq883RgAsPTAdby94xKKS9R6royIiBiEiOqAIAiYG9oEHwwNgiAAm07FYfqGs7zxIhGRnjEIEdWhcZ18sGJsW8jNJNh3JQkvrD6JrDzeeJGISF8YhIjqWP8gD6yfHAIbczP8cysDI/7LGy8SEekLgxCRHnT0d8LWlx/ceHH4iuOISc7Rd1lERPUOgxCRnjRzv3/jRWcr3MvMx3Mrj+NsXIa+yyIiqlcYhIj0yNvREtumd0Gwtz0y81QYs+oEDl5N1ndZRET1BoMQkZ45WsmxaVpH9GrqggKVGlPXncbPZ+7quywionqBQYjIAFjKzbBqfHs828YLJWoRr289j/8evsEbLxIR1TIGISIDIZNK8PnzwXixhz8AYMmfV/HBH1FQqxmGiIhqC4MQkQGRSAS8PTAQ7wwMBACs/jsWc3+KRFEx70JNRFQbGISIDNC0Hv74amQwzCQCdkbGY+q608gtLNZ3WUREJodBiMhADWvTAP+b0B4WMimOXEvBmFUnkJZTqO+yiIhMCoMQkQHr1dQVG6d1hIOlDOfvZuH5lRG4k56n77KIiEwGgxCRgWvT0AFbX+4CL3sL3EzNxfAVxxGVoNR3WUREJoFBiMgINHK1xs/Tu6Cpmw2Sswsx4r8ROHEzTd9lEREZPQYhIiPhbmeOn17qjBBfR2QXFGP8mlPYfSlB32URERk1BiEiI2JnKcO6KSHo29wNRcVqTN9wFj+euK3vsoiIjBaDEJGRMZdJsWJcO4wOaQhRBN7deQlf7rvGu1ATEVUDgxCREZJKBHw0LAivPt0YALD0wHW8veMSikt440UioqpgECIyUoIg4LXQJvhgaBAEAdh0Kg4zNpxFgapE36URERkNBiEiIzeukw9WjG0LuZkEe68k4YXVJ5GVp9J3WURERoFBiMgE9A/ywPrJIbAxN8M/tzLw/H+PIz4zX99lEREZPAYhIhPR0d8JW1/uDDdbBa4l5WD4iuO4lpSt77KIiAwagxCRCWnmbovtM7oiwMUKCVkFeG7FcfxzK13fZRERGSwGISIT42VvgW0vd0E7HwcoC4ox9n8nsftSor7LIiIySAxCRCbIwUqODVM7ok+g5saLMzacwXreeJGIqAwGISITZS6TYuW4thgd0hBqEXhv5yV8tucqb7xIRPQQBiEiE2YmleCjYUGYG9oEALDs4A28sfUCVLzxIhERAAYhIpMnCAJeeboxPhneElKJgJ/P3sWUtaeRW1is79KIiPSOQYionhjZoSFWjW8HC5kUR66lYNT3J5CSXajvsoiI9IpBiKgeeaqZGza92AmOVnJcvJeFZ1ccw42UHH2XRUSkNwxCRPVMa297/Dy9C3ycLHEnPR/DVxzHmdu81xAR1U8MQkT1kJ+zFX6e3gXBDeyQmafCmFW81xAR1U8MQkT1lLO1Apte7ISnm7misFiN6RvOIPxYrL7LIiKqUwxCRPWYpdwM/32hHcZ0bAhRBBb9dgUf746GmrcaIqJ6gkGIqJ4zk0rw4dAgvNmvKQBg9bHbWHtNgkJViZ4rIyKqfQxCRARBEDCzdyN8M6o1ZFIBkekSTAg/g4zcIn2XRkRUqxiEiEhrSGsv/DChHSykIs7EZWL4iuO4nZar77KIiGoNgxAR6ejo54hXg0rgaWeOm6m5GLb8OM7cztB3WUREtYJBiIjK8LAEtr7UEUFetkjPLcKYVSew62KCvssiIqpxDEJEVC5XGwW2vNgZfQI1l9fP2HAW3x+5wafXE5FJYRAiokeyUpjhvy+0x4TOPgCAj3ZdxTs7L6GYT68nIhPBIEREjyWVCFj0nxZ475nmEARg48k4TF57GtkFKn2XRkT0xIwuCC1btgy+vr4wNzdHx44dcerUqUe2DQ8PhyAIOou5uXkdVktkGgRBwJRufvjvuAdPr39uRQTuZuTpuzQioidiVEFoy5YtmDt3LhYuXIizZ88iODgY/fr1Q3Jy8iO3sbW1RUJCgna5fft2HVZMZFr6tnDHTy91hquNAtFJ2Ri67DjO38nUd1lERNVmVEHoyy+/xLRp0zBp0iQ0b94cK1euhKWlJdasWfPIbQRBgLu7u3Zxc3Orw4qJTE/LBnbYObMrmrnbIDWnECO/j+AVZURktIwmCBUVFeHMmTPo06ePdp1EIkGfPn0QERHxyO1ycnLg4+MDb29vDBkyBJcvX66LcolMmqe9BbZN74LeTV1QoNJcUbbsYAyvKCMio2Om7wIqKzU1FSUlJWVGdNzc3HD16tVyt2natCnWrFmDVq1aISsrC59//jm6dOmCy5cvo0GDBuVuU1hYiMLCQu33SqUSAKBSqaBSVX9yaOm2T7IPQ8c+mobK9lEhAVaMaY2Pd0cjPCIOn+2JxvVEJT4Y2gIKM8P+PxaPo/Ez9f4B7GNN7bsigmgk/4WLj4+Hl5cXjh8/js6dO2vXz5s3D4cPH8bJkycr3IdKpUJgYCBGjx6N999/v9w2ixYtQlhYWJn1GzduhKWlZfU7QGTC/k4U8HOsBGoI8LcRMaVpCaxl+q6KiOqzvLw8jBkzBllZWbC1tX1kO6MZEXJ2doZUKkVSUpLO+qSkJLi7u1dqHzKZDG3atEFMTMwj28yfPx9z587Vfq9UKuHt7Y2+ffs+9oOsiEqlwr59+xAaGgqZzDR/Q7CPpqE6fRwIYGBMGl7Zch43s4uxIsYa/x3XBk3cbGq32GricTR+pt4/gH18UqVndCpiNEFILpejXbt2OHDgAIYOHQoAUKvVOHDgAGbNmlWpfZSUlODixYsYOHDgI9soFAooFIoy62UyWY0cpJrajyFjH01DVfvYO9AdO2ZYYXL4acSl52HE96fw7Zg2eKqZ4V6gwONo/Ey9fwD7+CT7rAzDPpH/L3PnzsWqVauwdu1aREVFYfr06cjNzcWkSZMAAOPHj8f8+fO17RcvXoy9e/fi5s2bOHv2LMaNG4fbt29j6tSp+uoCkUlr5GqDX2Z2RUc/R+QWlWDK2tN8LAcRGTSjGRECgJEjRyIlJQULFixAYmIiWrdujd27d2snUMfFxUEieZDtMjIyMG3aNCQmJsLBwQHt2rXD8ePH0bx5c311gcjkOVjJsX5KRyz89RI2nbqDj3ZdxfWkHHwwLAgKM6m+yyMi0mFUQQgAZs2a9chTYYcOHdL5/quvvsJXX31VB1UR0cPkZhJ8NKwlGrva4IM/rmDrmbu4mZqLlePawcWm7KlnIiJ9MapTY0RkPARBwORuflgzsQNszM1w5nYG/vPd37h0L0vfpRERaTEIEVGt6tXUFTtndoW/ixUSsgrw3Mrj+O18vL7LIiICwCBERHUgwMUaO2Z0Rc8mmjtRz950Dp/tuYoSNSdRE5F+MQgRUZ2ws5BhzcQOeKmHPwBg2cEbmLbuNLLyTfeuuURk+BiEiKjOSCUC5g8MxNcjW0NhJsFfV5MxbNkxxCRn67s0IqqnGISIqM4NbeOFbS93gaedOW6m5mLosuPYdyWp4g2JiGoYgxAR6UXLBnb4dXY3dPRzRE5hMaatO40v912DmvOGiKgOMQgRkd44Wyvw49SOmNDZBwCw9MB1TFn7D+cNEVGdYRAiIr2SSSUIGxKEz58PhsJMgoPRKfjPd3/jamLlHphIRPQkGISIyCA8164Bfp7eBV72Fridlodhy47jV95viIhqGYMQERmMIC87/D67G7o3dka+qgSvbDqHsN8uo6hYre/SiMhEMQgRkUFxsJIjfFIIZvQKAAD8cOwWRq86gcSsAj1XRkSmiEGIiAyOVCJgXv9m+P6FdtrnlD3z7VEcv5Gq79KIyMQwCBGRwerbwh2/zeqGZu42SM0pwrj/ncTyQzG8xJ6IagyDEBEZNF9nK+yY0RXPtvWCWgQ+3R2NaetOIzOvSN+lEZEJYBAiIoNnIZfii+eDseTZlpCbSXDgajIGLf0bkXcy9V0aERk5BiEiMgqCIGB0SEPsmNEFPk6WuJeZj+dXHkf4sViIIk+VEVH1MAgRkVFp4WmH32Z3w4Agd6hKRCz67Qqm/3iWd6MmomphECIio2NrLsPysW2xcHBzyKQCdl9OxKClR3mqjIiqjEGIiIySIAiY1NUPP0/vgoaOlribkY/nVhzH/47e5KkyIqo0BiEiMmqtGtjj91e6YWBLdxSrRXzwRxSmrD2NtJxCfZdGREaAQYiIjJ6tuQzLxrTF+0ODIDeT4K+ryRjwzVEcj+ENGIno8RiEiMgkCIKAFzr54JeZXdHI1RrJ2YUYu/okPt19FaoSPquMiMrHIEREJiXQwxa/zuqK0SHeEEVg+aEbGPHfCMSl5em7NCIyQAxCRGRyLOVmWPJsK3w3pg1szM1wLi4TA5cexfazdzmRmoh0VCsILV68GHl5Zf93lZ+fj8WLFz9xUURENeGZVp7489Xu6ODrgJzCYsz96Txe2RwJJe85RET3VSsIhYWFIScnp8z6vLw8hIWFPXFRREQ1pYGDJTa/2Blv9G0CqUTAb+fjMXhZBGKU+q6MiAxBtYKQKIoQBKHM+vPnz8PR0fGJiyIiqklSiYBZTzXGtpc7w8fJEvFZBfjushSf7LmGwuISfZdHRHpUpSDk4OAAR0dHCIKAJk2awNHRUbvY2dkhNDQUI0aMqK1aiYieSJuGDvjjle54rq0XRAj439+3MOS7Y7iayOEhovrKrCqNv/76a4iiiMmTJyMsLAx2dnba1+RyOXx9fdG5c+caL5KIqKZYK8ywZFgL2OfGYftdc1xNzMZ/vj2GN/o1wZRu/pBKyo52E5HpqlIQmjBhAgDAz88PXbt2hZlZlTYnIjIYLR1FTBnaBe/9GoX9Ucn4aNdV7L+SjM+ebwUfJyt9l0dEdaRac4RsbGwQFRWl/f6XX37B0KFD8fbbb6OoqKjGiiMiqk3O1gqsGt8eHz/bElZyKU7dSseAb45i/YnbvMyeqJ6oVhB66aWXcO3aNQDAzZs3MXLkSFhaWmLr1q2YN29ejRZIRFSbBEHAqJCG2D2nBzr5OyKvqATv7byE8WtOIT4zX9/lEVEtq1YQunbtGlq3bg0A2Lp1K3r27ImNGzciPDwcP//8c03WR0RUJ7wdLbFxaicsHNwcCjMJjl5PRd+vjmDTqTiODhGZsGpfPq9Wa57ds3//fgwcOBAA4O3tjdRUPuSQiIyTRCJgUlc//Plqd7RtaI+cwmLM334R49ecwt0MPqKDyBRVKwi1b98eH3zwAdavX4/Dhw9j0KBBAIDY2Fi4ubnVaIFERHXN38UaW1/ugncHBWpHh/p9dQQ/nrgNtZqjQ0SmpFpB6Ouvv8bZs2cxa9YsvPPOO2jUqBEAYNu2bejSpUuNFkhEpA9SiYCp3f3x56vd0d7HAblFJXh35yWMXnUCsam5+i6PiGpIta5/b9WqFS5evFhm/WeffQapVPrERRERGQp/F2tseakz1h6/hc/2RONkbDr6f30Er4U2wdRufjCT8tnVRMbsiW4EdObMGe1l9M2bN0fbtm1rpCgiIkMilQiY3M0Poc3d8PaOizh6PRUf/3kVv1+IxyfDW6GFp13FOyEig1StIJScnIyRI0fi8OHDsLe3BwBkZmaid+/e2Lx5M1xcXGqyRiIig+DtaIl1k0Pw89l7eP/3K7h0T4n/fHcMU7r5YU6fxrCU8yazRMamWmO6s2fPRk5ODi5fvoz09HSkp6fj0qVLUCqVeOWVV2q6RiIigyEIAp5r1wD75/bEoFYeKFGL+P7ITYR+eQQHrybruzwiqqJqBaHdu3dj+fLlCAwM1K5r3rw5li1bhj///LPGiiMiMlQuNgosG9MWP0zsAC97C9zLzMek8H8wc+NZJCsL9F0eEVVStYKQWq2GTCYrs14mk2nvL0REVB/0buaKfXN7YFp3P0gE4I8LCXj6i8MIPxaLEl5qT2TwqhWEnnrqKbz66quIj4/Xrrt37x5ee+01PP300zVWHBGRMbCUm+GdQc3x66xuCPa2R3ZhMRb9dgVDlv2N83cy9V0eET1GtYLQd999B6VSCV9fXwQEBCAgIAB+fn5QKpX49ttva7pGIiKjEORlh+3Tu+CDoUGwNTfDpXtKDF1+DO/suIjMPD6QmsgQVesSB29vb5w9exb79+/H1atXAQCBgYHo06dPjRZHRGRspBIB4zr5oF8LdyzZFYXt5+5hw8k47LqYgHn9m2FEe29IJYK+yySi+6o0IvTXX3+hefPmUCqVEAQBoaGhmD17NmbPno0OHTqgRYsWOHr0aG3VSkRkNFxsFPhyZGtsfrETmrrZICNPhfnbL2LY8mOI5OkyIoNRpSD09ddfY9q0abC1tS3zmp2dHV566SV8+eWXNVYcEZGx6+TvhN9f6YYFzzSHjcIMF+5mYeiyY3hz63kkZ/PqMiJ9q1IQOn/+PPr37//I1/v27YszZ848cVFERKZEJpVgcjc/HHijJ55t6wUA2HrmLp76/DC+P3IDRcW82pZIX6oUhJKSksq9bL6UmZkZUlJSnrgoIiJT5Gpjji9HtMbP07sguIEdcgqL8dGuq+j39REciEqCKPJye6K6VqUg5OXlhUuXLj3y9QsXLsDDw+OJiyIiMmXtfBywY0ZXfP58MFxsFIhNzcWUtafxwupTiEpQ6rs8onqlSkFo4MCBeO+991BQUPa8dn5+PhYuXIhnnnmmxoojIjJVEonmUR0H3+iFl3sGQC6V4O+YVAxaehRv/XyB84eI6kiVLp9/9913sX37djRp0gSzZs1C06ZNAQBXr17FsmXLUFJSgnfeeadWCiUiMkXWCjO8NaAZxnZsiI93X8UfFxKw+Z87+O18PF7uGYAp3f34MFeiWlSlv11ubm44fvw4pk+fjvnz52vPZwuCgH79+mHZsmVwc3OrlUKJiEyZt6Mllo1pi8ld07H49yicv5OJL/Zdw48nb2NuaBM81473HyKqDVW+s7SPjw927dqF1NRUnDx5EidOnEBqaip27doFPz+/2qhRx7Jly+Dr6wtzc3N07NgRp06demz7rVu3olmzZjA3N0fLli2xa9euWq+RiKi62vk4Ysf0LvhmVGs0cLBAkrIQ//fzRQz45gj+usoJ1UQ1rVqP2AAABwcHdOjQASEhIXBwcKjJmh5py5YtmDt3LhYuXIizZ88iODgY/fr1Q3Jycrntjx8/jtGjR2PKlCk4d+4chg4diqFDhz52wjcRkb5JJAKGtPbCgdd74t1BgbCzkOFaUg4mh5/GyP+ewOlb6foukchkVDsI6cOXX36JadOmYdKkSWjevDlWrlwJS0tLrFmzptz233zzDfr3748333wTgYGBeP/999G2bVt89913dVw5EVHVKcykmNrdH0fe7I2XevhDYSbBqVvpeG5lBKau/QdXE3mFGdGTMpoZeEVFRThz5gzmz5+vXSeRSNCnTx9ERESUu01ERATmzp2rs65fv37YuXPnI9+nsLAQhYWF2u+VSs0/NCqVCiqVqtr1l277JPswdOyjaWAfDY+lDHgjtBHGhjTAdwdvYNvZe9gflYwDV5MxuKUHXnkqAD5OljrbGFsfq8rU+wewjzW174oYTRBKTU1FSUlJmcnYbm5u2ge//ltiYmK57RMTEx/5PkuWLEFYWFiZ9Xv37oWlpWU5W1TNvn37nngfho59NA3so2HqKgcaBQO74iSITJfg1wsJ+P1CPDq6iujbQA1HhW57Y+xjVZh6/wD2sbry8vIq1c5oglBdmT9/vs4oklKphLe3N/r27VvuM9YqS6VSYd++fQgNDX3s3bmNGftoGthH4zAJwKV7Snx9IAaHr6ciIlnA6TQpRnXwxkvdfeFoITX6Pj6OKRzDirCPT6b0jE5FjCYIOTs7QyqVIikpSWd9UlIS3N3dy93G3d29Su0BQKFQQKFQlFkvk8lq5CDV1H4MGftoGthHw9fG1wlrpzjh9K10fL43GidupmP9iTj8dPouRnVogIAi4+9jRUy9fwD7+CT7rAyjmSwtl8vRrl07HDhwQLtOrVbjwIED6Ny5c7nbdO7cWac9oBl+e1R7IiJj1N7XEZtf7IwNUzuivY8DCovVWBsRh/fPSvHBrqtIVvIu1USPYjQjQgAwd+5cTJgwAe3bt0dISAi+/vpr5ObmYtKkSQCA8ePHw8vLC0uWLAEAvPrqq+jZsye++OILDBo0CJs3b8bp06fx/fff67MbRES1omsjZ3QJcMKxmDR8uS8aZ+MysTYiDpv+uYtRHbzxcs8AeNpb6LtMIoNiVEFo5MiRSElJwYIFC5CYmIjWrVtj9+7d2gnRcXFxkEgeDHJ16dIFGzduxLvvvou3334bjRs3xs6dOxEUFKSvLhAR1SpBENCtsTNCfGzx1abdOJnrhLNxmVgXcRubTsVheNsGmNGrERo6PfnFH0SmwKiCEADMmjULs2bNKve1Q4cOlVn3/PPP4/nnn6/lqoiIDIsgCGhmL+K10R1w+o4S3x6IQcTNNGz+5w62nrmLwa08ML1XIzR1t9F3qUR6ZXRBiIiIKk8QBHQJcEaXAGecvpWOb/+KweFrKdgZGY+dkfF4upkrZvQOQDsfR32XSqQXDEJERPVEe19HrJ0cgkv3srDi0A3supSAA1c1N2YM8XXEiz388VQzV0j4cFeqRxiEiIjqmSAvOywb2xY3U3Lw/ZGb+PnsXZy6lY5Tt9LRyNUa07r7YWgbLyjMpPoulajWGc3l80REVLP8Xazx8fBWODrvKbzU0x82CjPEJOfg/36+iG6fHMSygzHIyC3Sd5lEtYpBiIionnO3M8f8AYE4Pv8pvD2wGdxtzZGSXYjP9kSj88cH8M6Oi7iRkqPvMolqBYMQEREBAGzMZXixRwCOzOuNL0cEo4WnLQpUamw4GYenvziMST+cwuFrKVCrRX2XSlRjOEeIiIh0yM0keLZtAwxr44UTN9Ox+u9YHLiahIPRKTgYnQJ/FytM7OKLZ9s2gLWCv0bIuPEnmIiIyiUIAjoHOKFzgBNiU3Ox9vgtbDtzFzdTcrHgl8v4bHc0hrdrgBc6+yDAxVrf5RJVC0+NERFRhfycrbDoPy0QMf8pLBrcHH7OVsguLEb48Vt4+ovDGPu/E9h9KRHFJWp9l0pUJRwRIiKiSrMxl2FiVz+M7+yLI9dT8OOJ2zhwNRnHYtJwLCYN7rbmGNnBG6NCvOFhx+eakeFjECIioiqTSAT0auqKXk1dcSc9DxtPxWHLP3eQqCzANweu49u/ruOpZm4Y27EhejRxgZQ3aSQDxSBERERPxNvREv/Xvxnm9GmM3ZcSseFkHE7FpmN/VBL2RyXB084cz7f3xvPtG6CBAx/2SoaFQYiIiGqEwkyKIa29MKS1F2KSs7Hx5B38fPYu4rM0o0RL/7qObo2cMapDQ/Rp7so7V5NBYBAiIqIa18jVBgsGN8e8/k2x53IitvxzB8dvpOHo9VQcvZ4Ke0sZhgR74rl23gjysoUg8NQZ6QeDEBER1Rpz2YNRori0PGw5HYefz9xDorIAayNuY23EbTRzt8Hwtg0wpLUnXG3N9V0y1TMMQkREVCcaOlnizX7NMDe0Kf6OScW2M3ex53IiriZm48NdUVjyZxS6NXbBs2280LeFGyzl/BVFtY8/ZUREVKekEgE9m7igZxMXZOWp8NuFeOw4dw9nbmfgyLUUHLmWAiu5FH1buGNIa090a+QMMylve0e1g0GIiIj0xs5ShnGdfDCukw9upeZix7l72HHuHuLS87R/drKSY1ArDwxp7Yk23g6Q8FJ8qkEMQkREZBB8na3wWmgTzOnTGGfjMvBrZDx+v5CAtNwirIu4jXURt+FpZ45BrTzQv7krRD77lWoAgxARERkUQRDQzscR7Xwc8e4zzXEsJhW/RMZj35UkxGcVYNXRWKw6GgsnhRRXZNfwTLAXWnrZ8cozqhYGISIiMlgyqUR7B+sCVQkORafg9wvxOBCVhLRCNb4/egvfH70FL3sLDGzpjgEtPdC6gT1Pn1GlMQgREZFRMJdJ0T/IHf2D3JGVm4+vNu9DssITB6NTcS8zXztS5GarQN/m7ujXwh0d/R0h40RregwGISIiMjqWcjO0cRYxcGAwikUJDl9Lxq6LifjrajKSlIVYf+I21p+4DTsLGZ5q5oo+gW7o0cQZNuYyfZdOBoZBiIiIjJqFXIr+QR7oH+SBwuISHL+Rhj2XErHvShLScou0V5/JpAI6BzijT6ArnmrmyueeEQAGISIiMiEKMyl6N3VF76au+HCYiDO3M7A/Kgn7riQhNjVXe5+iBb9cRlM3GzwV6Iqnm7miTUMHSDmvqF5iECIiIpMklQgI8XNEiJ8j3h4YiBspOdh3JQl/RSXj9O10RCdlIzopGysO3YCdhQzdGztrbvTY1AWuNnzUR33BIERERPVCgIs1Anpa4+WeAcjMK8Lhayn462oyDkWnICtfhd8vJOD3CwkAgBaetuje2AU9Gjujna8DFGZSPVdPtYVBiIiI6h17S7n2YbDFJWqcv5uJw9EpOHQtBRfuZuFyvBKX45VYefgGLGRSdPR3RLdGzujW2BlN3Wx4zyITwiBERET1mplUor2B49y+TZGaU4i/r6fiyPUUHL2eipTsQhyKTsGh6BQAgLO1HF0CnNG1kRM6+zvD29GCwciIMQgRERE9xNlagaFtvDC0jRdEUUR0UjaOXkvF3zGpOBWbjtScIvx6Ph6/no8HAHjZW6CTvxM6+Tuik78TvB15NZoxYRAiIiJ6BEEQ0MzdFs3cbTGthz+KitU4F5eBYzfScDwmFZF3MnEvMx8/n72Ln8/eBaAJRqWTtEP8HOHvbMURIwPGIERERFRJcjMJOvo7oaO/E+aGNkFeUTFO38pAxM00RNxIw8V7WbiXma+9dxEAOFnJ0d7XAR18HdHOxwFBXna827UBYRAiIiKqJku5GXo0cUGPJi4AgNzCYpyLy8Sp2DScjE3HuTuZSMstwp7LSdhzOQkAoDCTILiBPdr42KNtQwe0begAFxuFPrtRrzEIERER1RArhRm6NdZcXQYAhcUluHQvC//cysDpW+k4fTsDmXkqnLqVjlO30rXbNXCwQGtve7T2tkebhvZo4WkHXrBfNxiEiIiIaonCTKq9Ig09A6BWi7iZmouzcRk4F5eBs7czcS05G3cz8nE3I197HyMziYDGrtawV0ug/Ocu2vg4oombDeRmPKVW0xiEiIiI6ohEIqCRqzUauVpjRHtvAICyQIWLd7MQeScT5+IyEXknE6k5hYhKzAYgQcSvVwAAcqkETd1tEORlhyAvW7TwtEMzdxuYyzh29CQYhIiIiPTI1lyGro2c0bWR5nSaKIpIyCrA2Vtp2Hn0HPLMnXHpnhLKgmJcvJeFi/eytNtKBM0ds5t72qK5hy0CPWzRzMMGLtYKXqlWSQxCREREBkQQBHjaW8ClhRtKbqsxcGB7mJmZ4W5GPi7ey8Kl+2HocrwS6blFuJ6cg+vJOfglMl67DycrOZq626CZuy2aulujiZsNGrvZwFrBX/v/xk+EiIjIwAmCAG9HS3g7WmJgSw8AmpGj5OxCXIlX4kqCElfilYhKVOJWai7Scotw/EYajt9I09lPAwcLNHGz0Z6ea3z/q425TB/dMggMQkREREZIEAS42ZrDzdYcvZu5atcXqEpwPSkHUYlKXE3IxvXkbEQnZiM5u1A7Kfuvq8k6+3K1UWgeSutqBX9na/i7aL56OVhAKjHtU2wMQkRERCbEXCZFywZ2aNnATmd9Rm4RriVl43pyDmLuL9eTs5GkLERytmaJuKk7giSXStDQyRK+Tlbwc7aEj5MVfJ2s4ONkCU970whJDEJERET1gIOVXHtX7IcpC1S4mZKLG8k5uJGiWWJTc3ErLQ9FxWptaPo3mVRAAwdLNHR8sHg7WqCBgyW8HSxhZ2kcp9sYhIiIiOoxW3OZ9maODytRi4jPzMettFxNMErNw+20XNxKy8Wd9HwUlagRm6p5rTw25mZo4GAJL3sLNHCwgJe9BbwcLOBpbwFPe3M4WxnG3bQZhIiIiKgMqeTBBO3ujV10XitRi0hUFiAuLQ9x6bmIS8/D7bS8+3OQ8pCaU4TsgmJEJSgRlaAsd/9yqQRutgooSqQw803CoOAGddGtMhiEiIiIqEqkEkEzwmNvgc4BTmVezy8qwd2M+8EoMx/3MvJxLzMf9zLyEJ9ZgOTsAhSVqHEnIx+AgKz84rrvxH0MQkRERFSjLORSNL5/76LyqErUSFIW4E5aDnYdOoFO/g51XOEDDEJERERUp2RSCRo4WMLNWoZkFxHeDpZ6q4VPbyMiIqJ6i0GIiIiI6i0GISIiIqq3GISIiIio3mIQIiIionqLQYiIiIjqLQYhIiIiqreMJgilp6dj7NixsLW1hb29PaZMmYKcnLIPgXtYr169IAiCzvLyyy/XUcVERERk6Izmhopjx45FQkIC9u3bB5VKhUmTJuHFF1/Exo0bH7vdtGnTsHjxYu33lpb6u2kTERERGRajCEJRUVHYvXs3/vnnH7Rv3x4A8O2332LgwIH4/PPP4enp+chtLS0t4e7uXlelEhERkRExiiAUEREBe3t7bQgCgD59+kAikeDkyZMYNmzYI7fdsGEDfvzxR7i7u2Pw4MF47733HjsqVFhYiMLCQu33SqXmqbkqlQoqlarafSjd9kn2YejYR9PAPpoGU++jqfcPYB9rat8VEURRFGv83WvYRx99hLVr1yI6OlpnvaurK8LCwjB9+vRyt/v+++/h4+MDT09PXLhwAf/3f/+HkJAQbN++/ZHvtWjRIoSFhZVZv3HjRp5WIyIiMhJ5eXkYM2YMsrKyYGtr+8h2eh0Reuutt/DJJ588tk1UVFS19//iiy9q/9yyZUt4eHjg6aefxo0bNxAQEFDuNvPnz8fcuXO13yuVSnh7e6Nv376P/SArolKpsG/fPoSGhkImk1V7P4aMfTQN7KNpMPU+mnr/APbxSZWe0amIXoPQ66+/jokTJz62jb+/P9zd3ZGcnKyzvri4GOnp6VWa/9OxY0cAQExMzCODkEKhgEKhKLNeJpPVyEGqqf0YMvbRNLCPpsHU+2jq/QPYxyfZZ2XoNQi5uLjAxcWlwnadO3dGZmYmzpw5g3bt2gEA/vrrL6jVam24qYzIyEgAgIeHR7XqJSIiItNiFPcRCgwMRP/+/TFt2jScOnUKx44dw6xZszBq1CjtFWP37t1Ds2bNcOrUKQDAjRs38P777+PMmTO4desWfv31V4wfPx49evRAq1at9NkdIiIiMhBGEYQAzdVfzZo1w9NPP42BAweiW7du+P7777Wvq1QqREdHIy8vDwAgl8uxf/9+9O3bF82aNcPrr7+O4cOH47ffftNXF4iIiMjAGMXl8wDg6Oj42Jsn+vr64uEL4Ly9vXH48OG6KI2IiIiMlNGMCBERERHVNAYhIiIiqrcYhOjRDP9em0RERE/EaOYIkR7sfReI3ABYuQLWroCVi2axdnloneuD7yHVd8VERERVwiBEj5aTBORnaJbU6Aqbm8mt8bRgBWnKMsDGtZwA5frgq9waEIQ66AQREdGjMQjRow38DOj+OpCTDOSm3P9a+ucUzZ9Lv5YUQSjKgTVygLtJFe/bzOL+SJKL7qjSw2GpNEBZODA0ERFRrWAQokezcNAsroGPbyeKQEEWVFnxOHHgd3RuGQCzgvQHwak0LJUGKFUuUJwPZMZplopIZOWcknMuJzi5ApaOgISn6IiIqHIYhOjJCQJgYQ+YWSHduinEwIHA457xUpT7r1GmFN0Rp4dHmwqzALUKyI7XLBXWIgEsnQBbT8DeB3DwBRzuf7X3Bey9AbOyz5IjIqL6iUGI6p7cCnD00ywVKS7UDUw6Yelfp+fy0gFR/SBYJZwvZ4eCJiQ5+JYNSg6+gLUbT8MREdUjDEJk2MwUgF0DzVKRkmIgL1UTlpT3gIzbQMYtIPP+14xbgCpP85ryHnD7WDnvZ34/IPmUDUv2PoDUoka7R0RE+sUgRKZDagbYuGsWj3IerCuKQG7qQ+EoVjcsZd0Figs0V8g94io5MwtH9BDsIS34GXDy1w1Kdt6A9DGnBImIyOAwCFH9IQiaCdfWLoB3h7Kvl6g0YUhnFOmhoJSXBiE/HQ5IB6JulrN/CWDb4P5o0kPzkkpPu1k587QbEZGBYRAiKiWVPX7uUoESqtQbOHtgB9oHOEGqvPsgLGXe1owmZcVplltHy24vsyx/blLpqTi5Ve31jYiIysUgRFRZ5raAWxAS7eOg7jgQ0oevjFOrNRO2/z2KVPq98p5mflLyFc1SHitXwLkx4NwEcGkGuDQBnJtqJndzJImIqFYwCBHVBInkwfykhp3Kvl5ceP+0W2z5Yakg6/69lpLLTuKW2zwIRS73F+cmmtEk3jOJiOiJMAgR1QUzBeAUoFnKk58BpMcCqdc1E7VT7i/pN4GibODeGc3yMKmi/BEkpwDeK4mIqJIYhIgMgYUD4OUAeLXVXV9cBKTf0ISi1GtAylUg5ZrmzyWFQNIlzfIwQaqZ5/TvESTnJoDCuu76RERkBBiEiAyZmVzziJN/P+ZEXaI5rZZyPxylXnswilSUDaTFaJboP3S3s/O+H4yaakaQXJppApKlY931iYjIgDAIERkjiRRw9NcsTfs/WC+KQHbCg1CUGv0gLOWlAll3NEvMft39WbloQ5HEsRFclBlAdhvAwZsTtYnIpDEIEZkS4f4jRGw9gYDeuq/lpd8PSP8aQVLeffBYkltHIQXQBQCWfgoobMsfQbL30UwQJyIycgxCRPWFpSPg01mzPKww50EwSo2GOikKeXHnYFWUAqFQCdz9R7M8zMz8/kTtpoBrM8CtJeAeBNh6cQSJiIwKgxBRfaew1kzSvj9Ru0SlwoFduzCw79OQKW+Xnaiddl1z88jEi5rlYRYOgFsQ4N7y/tcgzSgSr2IjIgPFIERE5TNTAG4tNMvDSorvT9S+f5otOUpz5VpKtOY2ALeO6t5ZW2KmGTlyD3oQjtxaah51QkSkZwxCRFQ1UrMH90RqNvDBelWBJhglXQIS71/Wn3gRKMgEki9rFmx50N7a/aFwdH8EyamRZv9ERHWE/+IQUc2QmQOerTVLKVHU3FFbG44uar6m3wRyEoGYRN0r2MzMNbcK+PfpNXO7uu4NEdUTDEJEVHsEAbD31ixNBzxYX5ijeeZa4sWHQtJlQJULxJ/TLA+zb/hgQnZpQOKVa0RUAxiEiKjuKawB7xDNUkqt1jyLTSccXdLc9ygzTrM8fINIuY1m/pI2HLXUjCbJLeu+P0RktBiEiMgwSCQP5h61GPpgfV66ZrTo4dNryVc1d9C+c0KzlBIkgGOAbjhyDwJsPHhZPxGVi0GIiAybpSPg112zlCpRaR5QWzohuzQk5SZrLu9Puw5c3vGgvYXj/XDUSnNazbkZBLG47vtCRAaHQYiIjI9UBrg11yytRjxYn530YEJ2aThKvQbkpwOxRzQLABmAQYIMQtK3gFcbwKO1ZpK3a3PNvomo3mAQIiLTYeOmWRr1ebBOVQCkROmEIzHpIqQFWUDCOc1SSirXjBh5tn4QjlwCNQ+/JSKTxCBERKZNZg54ttEs9xUXFeHQznD0buoAs+SLQHwkkBAJFGQB8Wc1SympXDMpuzQYebTWjBwxHBGZBAYhIqp/BAF5CjeIzQcCwc9r1okikHFLE4jiIzWX8Cec19wQsvSS/jP3t2c4IjIZDEJERIDmqjJHP83SYphm3b/DUenXR4Uj1+a6p9VcWzAcERk4BiEiokd5VDjKvP3QqFHkg3CUEKlZSklkmpEjnXDUnA+hJTIgDEJERFUhCICDr2Ypvd/Rw+Ho4dGj/IxHhKPmuqfV3FowHBHpCYMQEdGTemQ4irsfjM79Kxyd1yxn12raSmSau2J7tmE4IqpjDEJERLVBEAAHH83SfIhmnU44inzwNT8dSLygWcqEo9YPRo/cghiOiGoYgxARUV15VDjKulN2ztHD4QjrNG1L5xx5tQU822q+ujQDJFL99IfIBDAIERHpkyAA9g01S/P/aNY9HI4eHj3KS3toztEaTVuZJeARDHi105xa82oLOPjpoSNExolBiIjI0DwuHN07A9w7+2DeUVE2EBehWUpZOEDq0RrNcm0gRANoGALYeuijJ0QGj0GIiMgYPByOSi/lV6s1D5i9d/9u2PfOaB5Cm58Byc2DaAoA237VtLXxvH9Krc2DrxYO+uoNkcFgECIiMlYSCeDSVLO0Hq1ZV1wEJF9GSdw/uHvqNzQ0S4WQchXIjgeuxgNXf3+wvaP/g7lGXu0A91aA3FI/fSHSEwYhIiJTYiYHPNtA7RKEyCQ3eA4cCJlYpLlcXztydBbIiAXSb2qWS9s02wrSB5fxl07IdmsBSGX67RNRLWIQIiIydXIrwKeLZimVl35/ntFZ4N45zWm1nEQg6ZJmObde006qANxbakaMSsORUyPNaBSRCWAQIiKqjywdgUZPa5ZSynjdUaP4s0BBFnDvtGYppbC9f6XaQ5fx23lr5jERGRkGISIi0rD11CyBz2i+F0XNqbOHw1HCeaBQCdw6qllKWbk8CEWlX62c9dMPoipgECIiovIJAuAUoFlaPa9ZV1IMpFx9EIzunQGSrwC5KcD1PZqllF1DwKvN/XsctdXcHVtho5euED0KgxAREVWe1AxwD9Isbcdr1qkKNJftP3xKLfUakBWnWa78cn9jQXOFm/ZKtbZ8bAjpHYMQERE9GZk54N1Bs5QqUGrugP3wDSCz7mhGk1KuAuc3atpJZJpQVTpq5NUWcG7Cx4ZQnWEQIiKimmduC/j10CylcpJ1b/5476zmmWrx5zRLKbm15kGzD59Ws2/IydhUKxiEiIiobli7Ak37axZAMxk78/aDuUbax4bkALf/1iylLJ11J2K7ttRLF8j0MAgREZF+CALg4KtZgp7VrFOXACnRupOxky4DeanA9b2aBYAMQKjcGdL8bYB3e07GpmpjECIiIsMhkQJuzTVLm3GadaoCTRi6d0YbkMTUa7AsSgWu/qpZAHAyNlWH0QShDz/8EH/88QciIyMhl8uRmZlZ4TaiKGLhwoVYtWoVMjMz0bVrV6xYsQKNGzeu/YKJiKhmyMyBBu00y33FOek4tfO/6NRQDmlCJCdjU7UZTRAqKirC888/j86dO2P16tWV2ubTTz/F0qVLsXbtWvj5+eG9995Dv379cOXKFZibm9dyxUREVGsUNki1aQ5154GQyu4/C01nMvb902qcjE0VMJogFBYWBgAIDw+vVHtRFPH111/j3XffxZAhQwAA69atg5ubG3bu3IlRo0bVVqlERKQPNTkZ27MtYO2il25Q3TKaIFRVsbGxSExMRJ8+fbTr7Ozs0LFjR0RERDwyCBUWFqKwsFD7vVKpBACoVCqoVKpq11O67ZPsw9Cxj6aBfTQNpt7HSvfP2gto6gU0Haz5Xl0CpF6DkHAOQvw5CPFnISRfgfCvydgAINo2gOjR+qElGLBwqK0ulWHqxxCo3T5Wdp+CKIpijb97LQoPD8ecOXMqnCN0/PhxdO3aFfHx8fDw8NCuHzFiBARBwJYtW8rdbtGiRdrRp4dt3LgRlpaWT1Q7EREZHom6CLb5d+CQdxP2eTfhkBcL64IECCj76zFX7opMS19kWvppFgsfFJtZ6aFqqkheXh7GjBmDrKws2NraPrKdXkeE3nrrLXzyySePbRMVFYVmzZrVUUXA/PnzMXfuXO33SqUS3t7e6Nu372M/yIqoVCrs27cPoaGhkJWezzYx7KNpYB9Ng6n3sbb7V1yYDSHxAoSEyAdLRiysipJhVZQMr8xT2raio7/uyJFbyxq5jN/UjyFQu30sPaNTEb0Goddffx0TJ058bBt/f/9q7dvd3R0AkJSUpDMilJSUhNatWz9yO4VCAYWi7KWWMpmsRg5STe3HkLGPpoF9NA2m3sda65/MEWjUS7OUys8EEs4/mHwdfw7IvA0h/SaE9JvA5e33GwqaK9M82zxY3FsC8uqdVTD1YwjUTh8ruz+9BiEXFxe4uNTOZDQ/Pz+4u7vjwIED2uCjVCpx8uRJTJ8+vVbek4iITJiFPeDfU7OUykvXDUbxkYDyLpAarVkubNa0EySAS6Dmpo+l4cgtSHNrANIro5ksHRcXh/T0dMTFxaGkpASRkZEAgEaNGsHa2hoA0KxZMyxZsgTDhg2DIAiYM2cOPvjgAzRu3Fh7+bynpyeGDh2qv44QEZHpsHQEGj2tWUrlJGsC0cMBKScRSL6sWSI3aNpJzADXQN2RI9cWgJlcL12pr4wmCC1YsABr167Vft+mTRsAwMGDB9GrVy8AQHR0NLKysrRt5s2bh9zcXLz44ovIzMxEt27dsHv3bt5DiIiIao+1K9Ckr2YppUwASm/8GH9Oc0l/XiqQeFGznF2naSeVA24tAM82ENxawTYvByhRASZ+akyfjCYIhYeHV3gPoX9fACcIAhYvXozFixfXYmVEREQVsPXQLE0HaL4XRUB571+n1c4B+RnaP5sB6A1A/PwDzRyj0lEjj9aaR4nw7tg1wmiCEBERkckQBMCugWYJvH+Po9IbQN4PQup7Z1Fy5wxkxXnA3X80SymZJeDeSve0mlMjQCLRT3+MGIMQERGRIRAEwMFXs7QYhhKVCrv++B0DOwdClnzpwWTshEjN3bHvnNAspeQ2mpEjj1aAR7AmKLk0BaQ8rfY4DEJERESGSpAAjgGAWzOg5XOadWo1kBaje0ot4TxQlA3EHdcspaQKzYRsj1aaYOTRWjMHqZqX8psiBiEiIiJjIpEALk00S/BIzbqSYiD1miYQJV4AEi5ovhYqNSNICZEPthckgFPjh8JRsGYkydJRH73ROwYhIiIiYyc1A9yaaxaM1qwTRSDjVtlwlJP04D5HF7c+2Iddw4fC0f2AZOOhOWVnwhiEiIiITJEgAI5+mqXF0Afrs5PuB6PIB+Eo4xaQFadZrv7+oK2ls244cg8GHP1NalI2gxAREVF9YuMG2IQCjUMfrCvI0tzPKOHCgxGklGjNvY5u/KVZSsmtNafStOGoFeDSzGhvBMkgREREVN+Z2wG+3TRLKVU+kHzlwahRwnkg6bLmirW4CM1SSirXTMp2f+iKNfcgQG5V932pIgYhIiIiKktmAXi10yylSoqBtOu64SjhAlCYdf/P54Fz6+83FgDnxrojRx7BBjcpm0GIiIiIKkd6//loroEPrlgrvRGkNhzdD0g5iZor2VKvAZe2PdiHbQNNIPJoBcGlOcyL0jT70BMGISIiIqq+h28E2fw/D9bnJN8PR+cfhKT0m4DyrmaJ/gNmAPoBKHFJBXrN00v5DEJERERU86xdgcZ9NEupgiwg8ZJ25EhMOA8x+SpEl2Z6K5NBiIiIiOqGuR3g21WzAChWqbD7953o7/+U3koynRsBEBERkdFRS+SAmUJv788gRERERPUWgxARERHVWwxCREREVG8xCBEREVG9xSBERERE9RaDEBEREdVbDEJERERUbzEIERERUb3FIERERET1FoMQERER1VsMQkRERFRvMQgRERFRvcUgRERERPWWmb4LMHSiKAIAlErlE+1HpVIhLy8PSqUSMpmsJkozOOyjaWAfTYOp99HU+wewj0+q9Pd26e/xR2EQqkB2djYAwNvbW8+VEBERUVVlZ2fDzs7uka8LYkVRqZ5Tq9WIj4+HjY0NBEGo9n6USiW8vb1x584d2Nra1mCFhoN9NA3so2kw9T6aev8A9vFJiaKI7OxseHp6QiJ59EwgjghVQCKRoEGDBjW2P1tbW5P9gS7FPpoG9tE0mHofTb1/APv4JB43ElSKk6WJiIio3mIQIiIionqLQaiOKBQKLFy4EAqFQt+l1Br20TSwj6bB1Pto6v0D2Me6wsnSREREVG9xRIiIiIjqLQYhIiIiqrcYhIiIiKjeYhAiIiKieotBqJqWLVsGX19fmJubo2PHjjh16tRj22/duhXNmjWDubk5WrZsiV27dum8LooiFixYAA8PD1hYWKBPnz64fv16bXahQlXp46pVq9C9e3c4ODjAwcEBffr0KdN+4sSJEARBZ+nfv39td+OxqtLH8PDwMvWbm5vrtDH249irV68yfRQEAYMGDdK2MbTjeOTIEQwePBienp4QBAE7d+6scJtDhw6hbdu2UCgUaNSoEcLDw8u0qerf8dpU1T5u374doaGhcHFxga2tLTp37ow9e/botFm0aFGZ49isWbNa7MXjVbWPhw4dKvdnNTExUaedMR/H8v6uCYKAFi1aaNsY0nFcsmQJOnToABsbG7i6umLo0KGIjo6ucDt9/35kEKqGLVu2YO7cuVi4cCHOnj2L4OBg9OvXD8nJyeW2P378OEaPHo0pU6bg3LlzGDp0KIYOHYpLly5p23z66adYunQpVq5ciZMnT8LKygr9+vVDQUFBXXVLR1X7eOjQIYwePRoHDx5EREQEvL290bdvX9y7d0+nXf/+/ZGQkKBdNm3aVBfdKVdV+who7n76cP23b9/Wed3Yj+P27dt1+nfp0iVIpVI8//zzOu0M6Tjm5uYiODgYy5Ytq1T72NhYDBo0CL1790ZkZCTmzJmDqVOn6gSF6vxs1Kaq9vHIkSMIDQ3Frl27cObMGfTu3RuDBw/GuXPndNq1aNFC5zj+/ffftVF+pVS1j6Wio6N1+uDq6qp9zdiP4zfffKPTtzt37sDR0bHM30dDOY6HDx/GzJkzceLECezbtw8qlQp9+/ZFbm7uI7cxiN+PIlVZSEiIOHPmTO33JSUloqenp7hkyZJy248YMUIcNGiQzrqOHTuKL730kiiKoqhWq0V3d3fxs88+076emZkpKhQKcdOmTbXQg4pVtY//VlxcLNrY2Ihr167VrpswYYI4ZMiQmi612qraxx9++EG0s7N75P5M8Th+9dVXoo2NjZiTk6NdZ2jH8WEAxB07djy2zbx588QWLVrorBs5cqTYr18/7fdP+rnVpsr0sTzNmzcXw8LCtN8vXLhQDA4OrrnCalBl+njw4EERgJiRkfHINqZ2HHfs2CEKgiDeunVLu86Qj2NycrIIQDx8+PAj2xjC70eOCFVRUVERzpw5gz59+mjXSSQS9OnTBxEREeVuExERodMeAPr166dtHxsbi8TERJ02dnZ26Nix4yP3WZuq08d/y8vLg0qlgqOjo876Q4cOwdXVFU2bNsX06dORlpZWo7VXVnX7mJOTAx8fH3h7e2PIkCG4fPmy9jVTPI6rV6/GqFGjYGVlpbPeUI5jdVT097EmPjdDo1arkZ2dXebv4/Xr1+Hp6Ql/f3+MHTsWcXFxeqqw+lq3bg0PDw+Ehobi2LFj2vWmeBxXr16NPn36wMfHR2e9oR7HrKwsACjzc/cwQ/j9yCBURampqSgpKYGbm5vOejc3tzLnpkslJiY+tn3p16rsszZVp4//9n//93/w9PTU+eHt378/1q1bhwMHDuCTTz7B4cOHMWDAAJSUlNRo/ZVRnT42bdoUa9aswS+//IIff/wRarUaXbp0wd27dwGY3nE8deoULl26hKlTp+qsN6TjWB2P+vuoVCqRn59fIz//hubzzz9HTk4ORowYoV3XsWNHhIeHY/fu3VixYgViY2PRvXt3ZGdn67HSyvPw8MDKlSvx888/4+eff4a3tzd69eqFs2fPAqiZf8cMSXx8PP78888yfx8N9Tiq1WrMmTMHXbt2RVBQ0CPbGcLvRz59nmrcxx9/jM2bN+PQoUM6k4lHjRql/XPLli3RqlUrBAQE4NChQ3j66af1UWqVdO7cGZ07d9Z+36VLFwQGBuK///0v3n//fT1WVjtWr16Nli1bIiQkRGe9sR/H+mbjxo0ICwvDL7/8ojN/ZsCAAdo/t2rVCh07doSPjw9++uknTJkyRR+lVknTpk3RtGlT7fddunTBjRs38NVXX2H9+vV6rKx2rF27Fvb29hg6dKjOekM9jjNnzsSlS5f0Ou+ssjgiVEXOzs6QSqVISkrSWZ+UlAR3d/dyt3F3d39s+9KvVdlnbapOH0t9/vnn+Pjjj7F37160atXqsW39/f3h7OyMmJiYJ665qp6kj6VkMhnatGmjrd+UjmNubi42b95cqX9I9Xkcq+NRfx9tbW1hYWFRIz8bhmLz5s2YOnUqfvrppzKnH/7N3t4eTZo0MZrjWJ6QkBBt/aZ0HEVRxJo1a/DCCy9ALpc/tq0hHMdZs2bh999/x8GDB9GgQYPHtjWE348MQlUkl8vRrl07HDhwQLtOrVbjwIEDOqMFD+vcubNOewDYt2+ftr2fnx/c3d112iiVSpw8efKR+6xN1ekjoJnZ//7772P37t1o3759he9z9+5dpKWlwcPDo0bqrorq9vFhJSUluHjxorZ+UzmOgOZy1sLCQowbN67C99HncayOiv4+1sTPhiHYtGkTJk2ahE2bNunc/uBRcnJycOPGDaM5juWJjIzU1m8qxxHQXI0VExNTqf+Y6PM4iqKIWbNmYceOHfjrr7/g5+dX4TYG8fuxRqZc1zObN28WFQqFGB4eLl65ckV88cUXRXt7ezExMVEURVF84YUXxLfeekvb/tixY6KZmZn4+eefi1FRUeLChQtFmUwmXrx4Udvm448/Fu3t7cVffvlFvHDhgjhkyBDRz89PzM/Pr/P+iWLV+/jxxx+Lcrlc3LZtm5iQkKBdsrOzRVEUxezsbPGNN94QIyIixNjYWHH//v1i27ZtxcaNG4sFBQVG0cewsDBxz5494o0bN8QzZ86Io0aNEs3NzcXLly9r2xj7cSzVrVs3ceTIkWXWG+JxzM7OFs+dOyeeO3dOBCB++eWX4rlz58Tbt2+LoiiKb731lvjCCy9o29+8eVO0tLQU33zzTTEqKkpctmyZKJVKxd27d2vbVPS51bWq9nHDhg2imZmZuGzZMp2/j5mZmdo2r7/+unjo0CExNjZWPHbsmNinTx/R2dlZTE5OrvP+iWLV+/jVV1+JO3fuFK9fvy5evHhRfPXVV0WJRCLu379f28bYj2OpcePGiR07dix3n4Z0HKdPny7a2dmJhw4d0vm5y8vL07YxxN+PDELV9O2334oNGzYU5XK5GBISIp44cUL7Ws+ePcUJEybotP/pp5/EJk2aiHK5XGzRooX4xx9/6LyuVqvF9957T3RzcxMVCoX49NNPi9HR0XXRlUeqSh99fHxEAGWWhQsXiqIoinl5eWLfvn1FFxcXUSaTiT4+PuK0adP09g9Sqar0cc6cOdq2bm5u4sCBA8WzZ8/q7M/Yj6MoiuLVq1dFAOLevXvL7MsQj2PpZdT/Xkr7NWHCBLFnz55ltmndurUol8tFf39/8Ycffiiz38d9bnWtqn3s2bPnY9uLouaWAR4eHqJcLhe9vLzEkSNHijExMXXbsYdUtY+ffPKJGBAQIJqbm4uOjo5ir169xL/++qvMfo35OIqi5lJxCwsL8fvvvy93n4Z0HMvrGwCdv1+G+PtRuF88ERERUb3DOUJERERUbzEIERERUb3FIERERET1FoMQERER1VsMQkRERFRvMQgRERFRvcUgRERERPUWgxARERHVWwxCREREVG8xCBGRUevVqxfmzJlT5s/6Zki1ENGjMQgRUbkiIiIglUor9eRyQ7F9+3a8//77tfoegiA8dlm0aFGd1fJve/bsqbC+vXv31mlNRIbOTN8FEJFhWr16NWbPno3Vq1cjPj4enp6e+i6pQo6OjrX+HgkJCdo/b9myBQsWLEB0dLR2nbW1dZ3V8m89evTQqS8oKAgzZszAjBkztOtcXFzqvC4iQ8YRISIqIycnB1u2bMH06dMxaNAghIeH67zeq1cvvPLKK5g3bx4cHR3h7u6uHQmpShu1Wo0lS5bAz88PFhYWCA4OxrZt27Sv7969G926dYO9vT2cnJzwzDPP4MaNG4+s+9+noypTQ3Z2NsaOHQsrKyt4eHjgq6++euxpLXd3d+1iZ2cHQRB01pUGofJqmT17NubMmQMHBwe4ublh1apVyM3NxaRJk2BjY4NGjRrhzz//rPTn828WFhbaOkpKSpCWlobu3bvr1CeVSh+5PVF9xCBERGX89NNPaNasGZo2bYpx48ZhzZo1EEVRp83atWthZWWFkydP4tNPP8XixYuxb9++KrVZsmQJ1q1bh5UrV+Ly5ct47bXXMG7cOBw+fBgAkJubi7lz5+L06dM4cOAAJBIJhg0bBrVaXem+VFTD3LlzcezYMfz666/Yt28fjh49irNnz1bnY6tULc7Ozjh16hRmz56N6dOn4/nnn0eXLl1w9uxZ9O3bFy+88ALy8vIAVPz5PM65c+cAAG3btq2VvhCZDJGI6F+6dOkifv3116IoiqJKpRKdnZ3FgwcPal/v2bOn2K1bN51tOnToIP7f//1fpdsUFBSIlpaW4vHjx3XaTJkyRRw9enS5daWkpIgAxIsXL+q8z6uvvlrmz5WpQalUijKZTNy6dav29czMTNHS0lJnP4/yww8/iHZ2duW+VlEtxcXFopWVlfjCCy9o1yUkJIgAxIiIiGp9Pg8LCwsTvb29K2xHVN9xjhAR6YiOjsapU6ewY8cOAICZmRlGjhyJ1atXo1evXtp2rVq10tnOw8MDycnJOuse1yYmJgZ5eXkIDQ3VaVNUVIQ2bdoAAK5fv44FCxbg5MmTSE1N1Y4ExcXFISgoqFL9eVwNN2/ehEqlQkhIiPZ1Ozs7NG3atFL7rqqHa5FKpXByckLLli2169zc3AAAycnJlfp8Hufs2bMcDSKqBAYhItKxevVqFBcX60yOFkURCoUC3333Hezs7AAAMplMZztBEMqcsnpcm5ycHADAH3/8AS8vL512CoUCADB48GD4+Phg1apV8PT0hFqtRlBQEIqKiirdn8rUWVfKq+XhdYIgANDMDarM5/M4Z8+exdSpU5+0ZCKTxyBERFrFxcVYt24dvvjiC/Tt21fntaFDh2LTpk14+eWXa+S9mjdvDoVCgbi4OPTs2bPM62lpaYiOjsaqVavQvXt3AMDff/9dI+9dyt/fHzKZDP/88w8aNmwIAMjKysK1a9fQo0ePGn2vqqro83mc1NRU3LlzhyNCRJXAIEREWr///jsyMjIwZcoU7chPqeHDh2P16tU1FoRsbGzwxhtv4LXXXoNarUa3bt2QlZWFY8eOwdbWFi+88AKcnJzw/fffw8PDA3FxcXjrrbdq5L0frmHChAl488034ejoCFdXVyxcuBASiUQ7OqMvFX0+EyZMeOS2pZO9GYSIKsYgRERaq1evRp8+fcqEIEAThD799FNcuHChxt7v/fffh4uLC5YsWYKbN2/C3t4ebdu2xdtvvw2JRILNmzfjlVdeQVBQEJo2bYqlS5fqzFOqCV9++SVefvllPPPMM7C1tcW8efNw584dmJub1+j7VMfjPp/HOXfuHNzc3Izi3k9E+iaI4r+uiSUiqsdyc3Ph5eWFL774AlOmTNF3OURUyzgiRET12rlz53D16lWEhIQgKysLixcvBgAMGTJEz5URUV1gECKieu/zzz9HdHQ05HI52rVrh6NHj8LZ2VnfZRFRHeCpMSIiIqq3+IgNIiIiqrcYhIiIiKjeYhAiIiKieotBiIiIiOotBiEiIiKqtxiEiIiIqN5iECIiIqJ6i0GIiIiI6i0GISIiIqq3GISIiIio3mIQIiIionrr/wFHm3FeLetm4QAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plotting costs against annealing time\n", + "labels = ['QAOA', 'FQAOA']\n", + "for i, cost_list in enumerate([qaoa_cost_list, fqaoa_cost_list]):\n", + " x, y = zip(*cost_list)\n", + " plt.plot(x, y, label=labels[i])\n", + "plt.xlabel('Annealing Time $T$')\n", + "plt.ylabel('Cost')\n", + "plt.title(r'Cost vs. Annealing Time')\n", + "plt.grid(True)\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "94ed2180-28b8-4a47-937c-90bd6ab4eadd", + "metadata": {}, + "source": [ + "## FQAOA with Fourier Parameterization\n", + "\n", + "To appreciate the benefits of the Fourier parameterization, let's compare the case $p=1$ using annealing parameterization with the case $q = 1, p=2$ using `FourierParams`. Here, we are optimising over the same total number of parameters, however the `FourierParams` ought to be capturing features of a more expressive circuit. \n", + "\n", + "Details of the Fourier parameterization are given in Ref [[3]]((https://journals.aps.org/prx/pdf/10.1103/PhysRevX.10.021067)) and in the Notebook [`05 - QAOA circuit with advanced circuit parameterizations`](https://github.com/entropicalabs/openqaoa/blob/dev/examples/05_advanced_parameterization.ipynb)." + ] + }, + { + "cell_type": "markdown", + "id": "4bb26ad2-8b2d-41a0-a8e2-b6e53da11c3e", + "metadata": {}, + "source": [ + "### Solving the problem using advanced parameterization" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b359fc8d-8f9b-4396-806e-d7f2c6e1ddc9", + "metadata": {}, + "outputs": [], + "source": [ + "# fourier parametrization\n", + "p_fourier = 3\n", + "q = 1\n", + "\n", + "fq_fourier = FQAOA()\n", + "fq_fourier.set_circuit_properties(p=p_fourier, param_type='fourier', init_type='ramp', q=q)\n", + "fq_fourier.compile(problem = problem, n_fermions=budget)\n", + "fq_fourier.optimize()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ffb9aead-90b6-4430-b1ff-2045f2c017b4", + "metadata": {}, + "outputs": [], + "source": [ + "# annealing parametrization\n", + "p_annealing = 1\n", + "\n", + "fq_annealing = FQAOA()\n", + "fq_annealing.set_circuit_properties(p=p_annealing, param_type='annealing', init_type='ramp')\n", + "fq_annealing.compile(problem = problem, n_fermions=budget)\n", + "fq_annealing.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "66b12bb6-db7f-4caf-83b3-216151cb1d88", + "metadata": {}, + "source": [ + "### Performance Evaluation of FQAOA with Fourier Parameterization" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "104e1c25-d674-4813-b706-f899b8fd23c6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHHCAYAAABTMjf2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAACQsklEQVR4nOzdd3hT1f8H8HeaJmnTvUvpYm/KKKOADAXKEAFRHKggDlQQECdflSmiggqigoOlIvBDERzI3lNWmWWPMjpoS/dKk/P7I81t0yZtWlKatu/X8+Rpc++5556chObDmTIhhAARERFRLWRX1QUgIiIiqioMhIiIiKjWYiBEREREtRYDISIiIqq1GAgRERFRrcVAiIiIiGotBkJERERUazEQIiIiolqLgRARERHVWgyEiKjGW7ZsGWQyGY4cOVJm2p49e6Jnz56VXygisgkMhIjonl2+fBljxoxB/fr14eDgAFdXV3Tt2hXz589Hdna21e+XlZWFadOmYefOnVbPuyL279+PadOmISUlpaqLQkTlZF/VBSCi6u2ff/7B448/DpVKheeeew4tW7ZEXl4e9u7di7fffhtnzpzB999/b9V7ZmVlYfr06QBg9dabzZs3l/ua/fv3Y/r06Rg1ahTc3d2tWh4iqlwMhIiowq5evYonn3wSISEh2L59O+rUqSOdGzt2LC5duoR//vmnCktYfkqlsqqLAAAQQiAnJweOjo5VXRSiGo1dY0RUYZ999hkyMjKwePFioyDIoGHDhpgwYYL0PD8/HzNnzkSDBg2gUqkQGhqK//3vf8jNzTW67siRI4iMjIS3tzccHR1Rr149jB49GgBw7do1+Pj4AACmT58OmUwGmUyGadOmlVne3NxcTJo0CT4+PnBycsLQoUNx584dozSmxggtWLAALVq0gFqthoeHB8LDw/Hrr78CAKZNm4a3334bAFCvXj2pPNeuXSvXaw4NDcXDDz+MTZs2ITw8HI6Ojvjuu+/Qo0cPhIWFmXw9TZo0QWRkZJmvm4jMY4sQEVXYX3/9hfr166NLly4WpX/xxRexfPlyPPbYY3jzzTdx6NAhzJ49G9HR0fjjjz8AAAkJCejbty98fHzw3nvvwd3dHdeuXcPatWsBAD4+Pli4cCFeffVVDB06FI8++igAoHXr1mXe//XXX4eHhwemTp2Ka9euYd68eRg3bhxWr15t9poffvgB48ePx2OPPYYJEyYgJycHJ0+exKFDh/D000/j0UcfxYULF7By5Up8+eWX8Pb2lspp6Ws2OH/+PJ566imMGTMGL730Epo0aQJnZ2e89NJLOH36NFq2bCmlPXz4MC5cuIAPPvjAoronIjMEEVEFpKamCgBi8ODBFqWPiooSAMSLL75odPytt94SAMT27duFEEL88ccfAoA4fPiw2bzu3LkjAIipU6dadO+lS5cKAKJ3795Cp9NJx9944w0hl8tFSkqKdKxHjx6iR48e0vPBgweLFi1alJr/nDlzBABx9epVo+OWvmYhhAgJCREAxMaNG43SpqSkCAcHB/Huu+8aHR8/frxwcnISGRkZpZaNiErHrjEiqpC0tDQAgIuLi0XpN2zYAACYNGmS0fE333wTAKSxRIbBxn///Tc0Go01iip5+eWXIZPJpOcPPPAAtFotrl+/bvYad3d33Lx5E4cPHy73/Sx9zQb16tUr0dXl5uaGwYMHY+XKlRBCAAC0Wi1Wr16NIUOGwMnJqdzlIqJCDISIqEJcXV0BAOnp6Ralv379Ouzs7NCwYUOj4/7+/nB3d5eCkR49emDYsGGYPn06vL29MXjwYCxdurTEmJqKCA4ONnru4eEBALh7967Za9599104OzujY8eOaNSoEcaOHYt9+/ZZdD9LX7NBvXr1TObz3HPPISYmBnv27AEAbN26FfHx8Xj22WctKgcRmcdAiIgqxNXVFQEBATh9+nS5rivaImPu/G+//YYDBw5g3LhxuHXrFkaPHo327dsjIyPjXooMuVxu8rihpcWUZs2a4fz581i1ahW6deuG33//Hd26dcPUqVMtvm9Zr9nA3AyxyMhI+Pn54ZdffgEA/PLLL/D390fv3r0tLgMRmcZAiIgq7OGHH8bly5dx4MCBMtOGhIRAp9Ph4sWLRsfj4+ORkpKCkJAQo+OdO3fGrFmzcOTIEaxYsQJnzpzBqlWrAFgeWFiLk5MTnnjiCSxduhQxMTEYOHAgZs2ahZycnFLLU97XbI5cLsfTTz+N3377DXfv3sW6devw1FNPmQ3siMhyDISIqMLeeecdODk54cUXX0R8fHyJ85cvX8b8+fMBAAMGDAAAzJs3zyjNF198AQAYOHAgAH03VfEWmjZt2gCA1D2mVqsB4L6s5JyUlGT0XKlUonnz5hBCSGOYDON0ipfH0tdsiWeffRZ3797FmDFjkJGRgWeeeaY8L4OIzOD0eSKqsAYNGuDXX3/FE088gWbNmhmtLL1//36sWbMGo0aNAgCEhYVh5MiR+P7775GSkoIePXrgv//+w/LlyzFkyBD06tULALB8+XJ8++23GDp0KBo0aID09HT88MMPcHV1lQILR0dHNG/eHKtXr0bjxo3h6emJli1bGk0vt5a+ffvC398fXbt2hZ+fH6Kjo/H1119j4MCB0kDx9u3bAwDef/99PPnkk1AoFBg0aJDFr9kSbdu2RcuWLbFmzRo0a9YM7dq1s/prJaqVqnbSGhHVBBcuXBAvvfSSCA0NFUqlUri4uIiuXbuKBQsWiJycHCmdRqMR06dPF/Xq1RMKhUIEBQWJyZMnG6U5duyYeOqpp0RwcLBQqVTC19dXPPzww+LIkSNG99y/f79o3769UCqVZU6lN0yfLz4lf8eOHQKA2LFjh3Ss+PT57777TnTv3l14eXkJlUolGjRoIN5++22RmppqlNfMmTNF3bp1hZ2dndFUektesxD66fMDBw4srZrFZ599JgCIjz/+uNR0RGQ5mRCljBIkIiKbMX/+fLzxxhu4du1aiRlwRFQxDISIiKoBIQTCwsLg5eWFHTt2VHVxiGoMjhEiIrJhmZmZ+PPPP7Fjxw6cOnUK69evr+oiEdUobBEiIrJh165dQ7169eDu7o7XXnsNs2bNquoiEdUoDISIiIio1uI6QkRERFRrMRAiIiKiWouDpcug0+lw+/ZtuLi43Pdl/YmIiKhihBBIT09HQEAA7OzMt/swECrD7du3ERQUVNXFICIiogq4ceMGAgMDzZ6vdoHQN998gzlz5iAuLg5hYWFYsGABOnbsaDLtsmXL8PzzzxsdU6lU0kaJljAsoX/jxg24urpafJ1Go8HmzZvRt29fKBQKi68j01if1sO6tC7Wp/WwLq2rttdnWloagoKCpO9xc6pVILR69WpMmjQJixYtQqdOnTBv3jxERkbi/Pnz8PX1NXmNq6srzp8/Lz0vb/eWIb2rq2u5AyG1Wg1XV9da+QG0Ntan9bAurYv1aT2sS+tifeqV9b1frQZLf/HFF3jppZfw/PPPo3nz5li0aBHUajWWLFli9hqZTAZ/f3/p4efndx9LTERERLas2gRCeXl5OHr0KHr37i0ds7OzQ+/evXHgwAGz12VkZCAkJARBQUEYPHgwzpw5cz+KS0RERNVAtekaS0xMhFarLdGi4+fnh3Pnzpm8pkmTJliyZAlat26N1NRUzJ07F126dMGZM2fMDpzKzc1Fbm6u9DwtLQ2AvolRo9FYXF5D2vJcQ+axPq2HdWldrE/rYV1aV22vT0tfd7VZWfr27duoW7cu9u/fj4iICOn4O++8g127duHQoUNl5qHRaNCsWTM89dRTmDlzpsk006ZNw/Tp00sc//XXX6FWqyv+AogqkUwmg1wur+piEBHdN1qtFqWFMFlZWXj66aeRmppa6hjfatMi5O3tDblcjvj4eKPj8fHx8Pf3tygPhUKBtm3b4tKlS2bTTJ48GZMmTZKeG0ad9+3bt9yDpbds2YI+ffrU6kFq1sL6NE0IgYSEBKnl0tJrcnJy4ODgwLWxrID1aT2sS+uqDfXp6uoKX19fk6/P0r+L1SYQUiqVaN++PbZt24YhQ4YA0C92uG3bNowbN86iPLRaLU6dOoUBAwaYTaNSqaBSqUocVygUFfoCruh1ZBrr01hsbCzS09Ph5+cHtVpt0R87nU6HjIwMODs7l7rIGFmG9Wk9rEvrqsn1KYRAVlYWEhISIJfLUadOnRJpLP2uqDaBEABMmjQJI0eORHh4ODp27Ih58+YhMzNTWivoueeeQ926dTF79mwAwIwZM9C5c2c0bNgQKSkpmDNnDq5fv44XX3yxKl8GkVVotVqkpKTA19cXXl5eFl+n0+mQl5cHBweHGvfHsSqwPq2HdWldNb0+HR0dAQAJCQnw9fWt8PCAahUIPfHEE7hz5w6mTJmCuLg4tGnTBhs3bpQGUMfExBi92Xfv3sVLL72EuLg4eHh4oH379ti/fz+aN29eVS+ByGoMAwE5do2IaivD3z+NRlM7AiEAGDdunNmusJ07dxo9//LLL/Hll1/eh1IRVZ2a2vdPRFQWa/z9q3ltZUREREQWYiBERHQf9ezZExMnTiw1zbJly+Du7l7uvPft24dWrVpBoVBIk0oqy86dOyGTyZCSklKp96nJzp8/D39/f6Snp1d1UWxOXl4eQkNDceTIkUq/FwMhIrqvRo0aBZlMVuJRdFmLGzduYPTo0QgICIBSqURISAgmTJiApKQkk3muXLkScrkcY8eONXk+OTkZEydOREhICJRKJQICAjB69GjExMSYTD979mzI5XLMmTPn3l9wMWvXrjVaxyw0NBTz5s2zSt6TJk1CmzZtcPXqVSxbtswqeZrTpUsXxMbGws3NrVLvU5NNnjwZr7/+epmbgt6LTz75BM2bN4eTkxM8PDzQu3dvi9bdq2xr165F37594eXlBZlMhqioKKPzSqUSb731Ft59991KLwsDIVumyQaqx3qXROXSr18/xMbGGj3q1asHALhy5QrCw8Nx8eJFrFy5EpcuXcKiRYuwbds2REREIDk5uUR+ixcvxjvvvIOVK1ciJyfH6FxycjI6d+6MrVu3YtGiRbh06RJWrVqFS5cuoUOHDrhy5UqJ/JYsWYJ33nmn1H0MK8rT07PSvvguX76MBx98EIGBgRVqUbKURqOBUqmEv7//PY3RyMvLs2KpSieEQH5+/n27X1liYmLw999/Y9SoUZV6nwYNGuCrr77CqVOnsHfvXoSGhqJv3764c+dOpd63LJmZmejWrRs+/fRTs2lGjBiBvXv3Vv7WWIJKlZqaKgCI1NTUcl2Xl5cn1q1bJ/Ly8ip449tCzPARYtUzFbu+hrnn+qyBsrOzxdmzZ0V2dna5rtNqteLu3btCq9VWUslKN3LkSDF48GCz5/v16ycCAwNFVlaW0fHY2FihVqvFK6+8YnT8ypUrwtHRUaSkpIhOnTqJFStWGJ1/5ZVXhJOTk4iNjTU6npWVJerWrSv69etndHznzp2ibt26Ii8vTwQEBIh9+/aV+noeffRR8eKLL0r1OWHCBAFAREdHCyGEyM3NFWq1WmzZskUIIUSPHj3EhAkTpN8BGD2EEGLp0qXCzc1NbNy4UTRt2lQ4OTmJyMhIcfv2bZNluHr1aol8li5dKr2eDh06CKVSKfz9/cW7774rNBqNdG1ISIj48ssvjfILCwsTU6dOlZ4DEN9++60YNGiQUKvVYurUqWLHjh0CgLh7966Ubs+ePaJbt27CwcFBBAYGitdff11kZGQY3WvGjBni2WefFS4uLmLkyJFG9zV8Nnv06CHGjh0rxo4dK1xdXYWXl5f44IMPhE6nk9L+9NNPon379sLZ2Vn4+fmJp556SsTHx0vnDeXbsGGDaNeunVAoFGLHjh3i0qVL4pFHHhG+vr7CyclJhIeHS+9N0XLOnDlTPPvss8LJyUkEBweL9evXi4SEBPHII48IJycn0apVK3H48GHpmmvXromHH35YuLu7C7VaLZo3by7++ecfk++XEELMmTNHhIeHGx0zvO9//PGHaNiwoVCpVKJv374iJibGbD6lMfVv3fCdtnXr1nLltXTpUhEUFCQcHR3FkCFDxNy5c4Wbm1uFylWU4bN7/Phxk+d79eolPvjgA7PXl/Z30NLvb7YI2aqoXwBtLhD9Z1WXhKqZrLx8s48cjdbqaa0pOTkZmzZtwmuvvSatEWLg7++PESNGYPXq1UbL6i9duhQDBw6Em5sbnnnmGSxevFg6p9PpsGrVKowYMaLECvSOjo547bXXsGnTJqNWpsWLF+Opp56CQqHAU089ZZSfKT169MDevXul57t27YK3t7c0i/Xw4cPQaDTo0qVLiWvXrl2LwMBAzJgxQ2oZM8jKysLcuXPx888/Y/fu3YiJicFbb71lsgxBQUGIjY2Fq6sr5s2bh9jYWDzxxBO4desWBgwYgA4dOuDEiRNYuHAhFi9ejI8++qjU12TKtGnTMHToUJw6dQqjR48ucf7y5cvo168fhg0bhpMnT2L16tXYu3dviVm+c+fORVhYGI4fP44PP/zQ7P2WL18Oe3t7/Pfff5g/fz6++OIL/Pjjj9J5jUaDmTNn4sSJE1i3bh2uXbtmsnXlvffewyeffILo6Gi0bt0aGRkZGDBgALZt24bjx4+jX79+GDRoUIlu0i+//BJdu3bF8ePHMXDgQDz77LN47rnn8Mwzz+DYsWNo0KABnnvuOemzOHbsWOTm5mL37t04deoUPv30Uzg7O5t9fXv27EF4eHiJ41lZWZg1axZ++ukn7Nu3DykpKXjyySeNrnN2di71sWLFCpP3zMvLw/fffw83NzeEhYWZLVtxhw4dwgsvvIBx48YhKioKvXr1KvEZupdylaZjx47Ys2dPua8rj2o3fb7WUFZenzHVbM2nbDJ7rlcTHyweWfjHt/3MrcguFvAYdKrnidVjCvf16/bpDiRnluzKuPbJwHKX8e+//zb6kujfvz/WrFmDixcvQgiBZs2ambyuWbNmuHv3Lu7cuQNfX1/odDosW7YMCxYsAAA8+eSTePPNN3H16lXUq1cPd+7cQUpKSqn5CSFw6dIldOzYEWlpafjtt99w4MABAMAzzzyDBx54APPnzzf7pdajRw9MnDgRd+7cgVKpxNmzZ/Hhhx9i586deOWVV7Bz50506NDB5HpPnp6ekMvlcHFxKRGoaTQaLFq0CA0aNACgXzpkxowZJssgl8ulbio3Nzcpr2+//RZBQUH4+uuvIZPJ0LRpU9y+fRvvvvsupkyZUq5F9p5++mlp8VoAJboUZ8+ejREjRkgDwRs1aoSvvvoKPXr0wMKFC+Hg4AAAePDBB/Hmm2+Web+goCB8+eWXkMlkaNKkCU6dOoUvv/wSL730EgAYBWP169fHV199hQ4dOkgrKRvMmDEDffr0kZ57enoaBQEzZ87EH3/8gT///NMoaBswYADGjBkDAJgyZQoWLlyIDh064PHHHwcAvPvuu4iIiJC2eYqJicGwYcPQqlUrqUyluX79uslASKPR4Ouvv0anTp0A6APCZs2a4b///kPHjh0RHh5eYjxNccU3J//777/x9NNPIysrC3Xq1MGWLVvg7e1dah5FzZ8/H/369cM777wDAGjcuDH279+PjRs3SmkqUi5LBAQE4Pr16+W+rjwYCNkqZx/9z9AHqrYcRJWgV69eWLhwofTcycnJ6LwoY2ycUqkEAGzZsgWZmZnStjne3t7o06cPlixZYjQguaz8DFauXIkGDRpIX5Rt2rRBSEgIVq9ejRdeeMHkNS1btoSHhwd27doFBwcHtG3bFg8//DC++eYbAPoWop49e1p0/6LUarUUBAFAnTp1kJCQUK48oqOjERERYTSOp2vXrsjIyMDNmzcRHBxscV6mvrSLOnHiBE6ePGn0v34hBHQ6Ha5evSoFo2XlY9C5c2ejckdERODzzz+HVquFXC7H0aNHMW3aNJw4cQJ3796FTqcDoB97U3TR3OL3y8jIwLRp0/DPP/8gNjYW+fn5yM7OLtEi1Lp1a+l3wxe4IcgpeiwhIQH+/v4YP348Xn31VWzevBm9e/fGsGHDjPIoLjs7WwoOi7K3t0eHDh2k502bNoW7uzuio6PRsWNHODo6omHDhuYrzoRevXohKioKiYmJ+OGHHzB8+HAcOnQIvr6+Fl0fHR2NoUOHGh2LiIgwCoQqUi5LODo6Iisry+r5FsVAyFZpsvU/FY6lpyMq5uyMSLPn7IoNbD36YW+L0+59t9e9FawIJycnk380GzZsCJlMZvIPL6D/g+zj4yMNBF68eDGSk5ONutF0Oh1OnjyJ6dOnS2mjo6NNliM6OhoymUwqy+LFi3HmzBnY29sb5bdkyRKzgZBMJkOXLl2kQKhnz55o3bo1cnNzcfr0aezfv99sl1Zpiu+TJJPJLA7oysPOzq5EvoZVy4sqHqwWl5GRgTFjxmD8+PElzhUNuMrKxxKZmZmIjIxEZGQkVqxYAR8fH8TExCAyMrLEAOzi93vrrbewZcsWzJ07Fw0bNoSjoyMee+yxEtcVrX9DQGbqmCEAe/HFFxEZGYl//vkHmzdvxuzZs/H555/j9ddfN/kavL29cffu3XK/9j179qB///6lpvnuu+8wYsQI6bnh31vDhg3RuXNnNGrUCIsXL8bkyZPLfX9rlssSycnJ8PHxuZeilYmBkK0yBEL2Jf/HQFQatbL0f9aGP9yWpC1Pvtbg5eWFPn364Ntvv8Ubb7xhFODExcVhxYoV0hT5pKQkrF+/HqtWrUKLFi2kdFqtFt26dcPmzZvRr18/DB8+HCtWrMCMGTOMup+ys7Px7bffIjIyEp6enjh16hSOHDmCnTt3wtPTU0qXnJyMnj174ty5c2jatKnJcnft2hW//PILVCoVZs2aBTs7O3Tv3h1z5sxBbm4uunbtavY1K5VKaLWmuyfvVbNmzfD7779DCCF9ce/btw8uLi4IDAwEAPj4+BiNTUpLS8PVq1fLfa927drh7NmzVmsVKD7F++DBg2jUqBHkcjnOnTuHpKQkfPLJJwgKCgIAi9eb2bdvH0aNGiUF2hkZGbh27ZpVyhwUFIRXXnkFr7zyCiZPnowffvjBbCDUtm1bnD17tsTx/Px8HDlyBB07dgSgX2uoaPeuNbqgdDodcnNzLXhFes2aNTP5fhRVWV1jp0+fRtu2bct9XXlwsLStciqIgKP/BGJPVG1ZiO6jr7/+Grm5uYiMjMTu3btx48YNbNy4EX369EHjxo0xZcoUAMDPP/8MLy8vDB8+HC1btpQeYWFhGDBggDTI+eOPP4a/vz/69OmDf//9Fzdu3MDu3bsRGRkJjUYjdWEtXrwYHTt2RPfu3Y3y6969Ozp06FDqoOmuXbvi7NmzOHPmDLp16wZAv3DiihUrEB4eXmorSGhoKHbv3o1bt24hMTHRWtUIAHjttddw48YNvP766zh37hzWr1+PqVOnYtKkSdL4oAcffBA///wz9uzZg1OnTmHkyJEV2rPp3Xffxf79+6UBtRcvXsT69evNbolUlpiYGEyaNAnnz5/HypUrsWDBAkyYMAGAvoVJqVRiwYIFuHLlCv7880+jrtDSNGrUCGvXrkVUVBROnDiBp59+2ug/BxU1ceJEbNq0CVevXsWxY8ewY8cOs2PTACAyMhIHDhwoEQQrFAq8/vrrOHToEI4ePYpRo0ahc+fOUmBk6IIq7WFYniEzMxMzZszAwYMHcf36dRw9ehSjR4/GrVu3pLFOlhg/fjw2btyIuXPn4uLFi/j666+NusXKWy5A/x+MqKgoKRg8f/48oqKiEBcXZ5Tvnj170LdvX4vLWhEMhGxViyGAT8E/ouzyN58SVVeNGjXC4cOHUb9+fQwfPhwhISHo378/GjdujH379kkDYZcsWYKhQ4eaXMdm2LBh+PPPP5GYmAgvLy8cPHgQvXr1wpgxY9CgQQMMHz4cDRo0kO6Tl5eHX375BcOGDTNZpmHDhuGnn34y2WUEAC1atIC7uzvatGkjla9nz57QarVljg+aMWMGrl27hgYNGli9C6Bu3brYsGED/vvvP4SFheGVV17BCy+8gA8++EBKM3nyZPTo0QMPP/wwBg4ciCFDhhiNTbJU69atsWvXLly4cAEPPPAA2rZtiylTpiAgIKBCZX/uueeQnZ2Njh07YuzYsZgwYQJefvllAPpWrGXLlmHNmjVo3rw5PvnkE8ydO9eifL/44gt4eHigS5cuGDRoECIjI9GuXbsKlbEorVaLsWPHolmzZujXrx8aN26Mb7/91mz6/v37w97eHlu3bjU6rlar8e677+Lpp59G165d4ezsjNWrV1eoTHK5HBcvXsTjjz+Oxo0bY9CgQUhKSsKePXuMWlF79uxZ6npGnTt3xg8//ID58+cjLCwMmzdvNvoMVcSff/6Jtm3bYuBA/WSLJ598Em3btsWiRYukNAcOHEBqaioee+yxe7pXmSyY5l+rVdk6QkII8WNfIaa6CnFmXcXzqCG4jlBJ1XUdoYqYMmWKcHZ2FgcOHKjqopRQHevTVhVdR8iw1lJN9vXXX4u+fftKzw3rCFmLpZ/N4OBgad0pS1m7rKYMHz5czJo1q9Q01lhHiGOEbJmju/5nTmqVFoOoqk2fPh2hoaE4ePAgOnbsWK5p30S2asyYMUhJSUF6enqlbrNRmjNnzsDNzQ3PPfdcldzfnLy8PLRq1QpvvPFGpd+LgZCt+vc94EJBH2x2SpUWhcgWFF3DhqgmsLe3x/vvv1+lZWjRogVOnjxZpWUwRalU3nP3m6X43ypblV1kPyW2CBFRLbJ9+3arbURbnYwaNQopKSlVXQyLVKeyloWBkK3SFFlAKielyopBRERUkzEQslWaIjtou/ibT0dEREQVxjFCtiq/IBB6bAnQ0vSUXiIiIro3bBGyVYauMXtusUFERFRZGAjZKkPXGPcaIyIiqjQMhGyVdyN9a9DPQ4BvI6q6NERERDUSAyFbNXw5MLZgk7vk8m+ASES2qWfPnpg4cWKpaZYtWwZ3d/f7Up6qIpPJsG7dOgDAtWvXIJPJyty0kyz37LPPYvbs2VVahkWLFmHQoEFVWgZLMBCyZQ5u+p/52UC+5TsFE9myUaNGQSaTlXhcunRJSnPjxg2MHj0aAQEBUCqVCAkJwYQJE5CUlGQyz5UrV0Iul0s70xeXnJyMiRMnIiQkBEqlEgEBARg9ejRiYmJMpp89ezbkcjnmzJlz7y+4mLVr1xptEBoaGlor18wpKigoCLGxsWjZsmVVF6VGOHHiBDZs2IDXX3+90u6Rk5ODUaNGoVWrVrC3t8eQIUNKpBk9ejSOHTuGPXv2VFo5rIGBkC1TuQIo2FCSiypSDdKvXz/ExsYaPerVqwcAuHLlCsLDw3Hx4kWsXLkSly5dwqJFi7Bt2zZEREQgOTm5RH6LFy/GO++8g5UrVyInJ8foXHJyMjp37oytW7di0aJFuHTpElatWoVLly6hQ4cOuHLlSon8lixZgnfeeQdLliyx+mv39PSssu0UbJVcLoe/vz/s7avnROa8vLyqLoKRBQsW4PHHH5c2AK4MWq0Wjo6OGD9+PHr37m0yjVKpxNNPP42vvvqq0sphDQyEbJFOB3zeDPi6PQChP8ZAiGoQlUoFf39/o4dcLgcAjB07FkqlEps3b0aPHj0QHByM/v37Y+vWrbh161aJLQmuXr2K/fv347333kPjxo2xdu1ao/Pvv/8+bt++ja1bt6J///4IDg5G9+7dsWnTJigUihKtSLt27UJ2djZmzJiBtLQ07N+/v9TX8vjjj+Ptt9+Wnk+cOBEymQznzp0DoP+SdHJyknYZL9o11rNnT1y/fh1vvPGG1DJW1KZNm9CsWTM4OztLwaM5Wq0WL7zwAurVqwdHR0c0adIE8+fPN0ozatQoDBkyBHPnzkWdOnXg5eWFsWPHQqPRSGlCQ0Px8ccfY/To0XBxcUFwcDC+//57o3xu3LiB4cOHw93dHZ6enhg8eDCuXbsmnT98+DD69OkDb29vuLm5oUePHjh27JjZshfvGtu5cydkMhm2bduG8PBwqNVqdOnSBefPnze67qOPPoKvry9cXFzw4osv4r333kObNm3M3seQ7z///IPWrVvDwcEBnTt3xunTp6U0SUlJeOqpp1C3bl2o1Wq0atUKK1euNMqnZ8+eGDduHCZOnAhvb29ERkYC0O9s36pVKzg5OSEoKAivvfYaMjIypOsMXZ5///03mjRpArVajcceewxZWVlYvnw5QkND4eHhgfHjx0Or1UrXffvtt2jUqBEcHBzg5+dX6m7sWq0Wv/32W4kuqdDQUMycORNPPfUUnJycULduXXzzzTdm8ymLk5MTFi5ciJdeegn+/ubXuhs0aBD+/PNPZGdnV/helY2BkC3KzwbSbwPJVwC1t/4Y9xsjS+Vlmn9ocsqRNtuytFaUnJyMTZs24bXXXoOjo/GMSX9/f4wYMQKrV6+GEEI6vnTpUgwcOBBubm545plnsHjxYumcTqfDqlWrMGLEiBJ/rB0dHfHaa69h06ZNRq1MixcvxlNPPQWFQoGnnnrKKD9TevTogb1790rPd+3aBW9vb+zcuROAPijQaDTo0qVLiWvXrl2LwMBAzJgxQ2oZM8jKysLcuXPx888/Y/fu3YiJicFbb71lthw6nQ6BgYFYs2YNzp49iylTpuB///sf/u///s8o3Y4dO3D58mXs2LEDy5cvx7Jly7Bs2TKjNJ9//jnCw8Nx/PhxvPbaa3j11VelIESj0SAyMhIuLi7Ys2cP9u3bJwVqhpaR9PR0jBw5Env37sXBgwfRqFEjDBgwAOnp6aXWZXHvv/8+Pv/8cxw5cgT29vYYPXq0dG7FihWYNWsWPv30Uxw9ehTBwcFYuHChRfm+/fbb+Pzzz3H48GH4+Phg0KBBUjCYk5OD9u3b459//sHp06fx8ssv49lnn8V///1nlMfy5cuhVCqxb98+LFq0CABgZ2eHr776CmfOnMHy5cuxfft2vPPOO0bXZWVl4auvvsKqVauwceNG7Ny5E0OHDsWGDRuwYcMG/Pzzz/juu+/w22+/AQCOHDmC8ePHY8aMGTh//jw2btyI7t27m31tJ0+eRGpqKsLDw0ucmzNnDsLCwnD8+HG89957mDBhArZs2SKd79+/P5ydnc0+WrRoYVH9FhUeHo78/HwcOnSo3NfeN6XuTU8iNTVVABCpqanlui4vL0+sW7dO5OXllf+mGYlCTHXVP77tov95YUv586lB7qk+a6js7Gxx9uxZkZ2dbXzC8Nkx9fjlMaHVasXdu3eFVqsV4iN/82mXDDDO99N6ptOV08iRI4VcLhdOTk7S47HHHhNCCHHw4EEBQPzxxx8mr/3iiy8EABEfHy+EEEKr1YqgoCCxbt06IYQQd+7cEUqlUly5ckUIIURcXJwAIL788kuT+a1du1YAEIcOHRJC6P+9Ozo6iqioKCGEEMePHxfOzs4iPT3d7OuJiooSMplMxMXFieTkZKFUKsXMmTPFE088IYQQ4qOPPhJdunSR0vfo0UNMmDBBeh4SElKifEuXLhUAxKVLl6Rj33zzjfDz8zNbDlPGjh0rhg0bJj0fOXKkCAkJEfn5+dKxxx9/XCqroTzPPPOM9Fyn0wlfX1+xcOFCIYQQP//8s2jSpInQ6XRSmtzcXOHo6Cg2bdpkshxarVa4uLiIv/76SzpW9H2+evWqACCOHj0q7t69K7Zt2yYAiK1bt0rp//nnHwFA+rx36tRJjB071ug+Xbt2FWFhYWbrY8eOHQKAWLVqlXQsKSlJODo6itWrV5u9buDAgeLNN9+Unvfo0UO0bdvWbHqDNWvWCC8vL+m5qfd1zJgxQq1WG33GIiMjxZgxY4QQQvz+++/C1dVVpKWllXk/IYT4448/hFwuFzqdzujfekhIiOjXr59R2ieeeEL0799fen7z5k1x8eJFs49r166ZvOfIkSPF4MGDzZbJw8NDLFu2zKLyl5fZv4PC8u/v6tkhW9MZFlOUqwDfZoDMDpArqrZMRFbUq1cvo/+9Ozk5GZ0XRVp8TFEqlQCALVu2IDMzEwMGDAAAeHt7o0+fPliyZInRgOSy8jNYuXIlGjRogLCwMABAmzZtEBISgtWrV+OFF14weU3Lli3h4eGBXbt2wcHBAW3btsXDDz8sdTvs2rULPXv2tOj+RanVajRo0EB6XqdOHSQkJJR6zTfffIMlS5YgJiYG2dnZyMvLK9FV1KJFC6kb0pDvqVOnjNK0bt1a+l0mk8Hf31+694kTJ3Dp0qUS45xycnJw+fJlAEB8fDw++OAD7Ny5EwkJCdBqtcjKyjI7ON2couWoU6cOACAhIQHBwcE4f/48XnvtNaP0HTt2xPbt28vMNyKicEkST09PNGnSBNHR0QD0XUsff/wx/u///g+3bt1CXl4ecnNzoVarjfJo3759iXy3bt2K2bNn49y5c0hLS0N+fj5ycnKQlZUlXV/8ffXz80NoaKjReB4/Pz+pvvv06YOQkBDUr18f/fr1Q79+/TB06NAS5THIzs6GSqWCTCYr8bkv+roNz4sO1K9bt67ZOrsXjo6OyMrKKjthFWEgZIsM22soHIBhP1ZtWaj6+d9t8+dkcuPnb18ynQ7QB+BFTTxlOl0FODk5oWHDhiWON2zYEDKZDNHR0Rg6dGiJ89HR0fDx8ZGmli9evBjJyclG3Wg6nQ4nT57E9OnTpbSGLzlT+clkMqksixcvxpkzZ4wG7ep0OixZssRsICSTydClSxcpEOrZsydat26N3NxcnD59Gvv37y+1S8schcL4Pz+mvtiKWrVqFd566y18/vnniIiIgIuLC+bMmVOiS8JUvjqdzuI0GRkZaN++PVasWFGiDD4+PgCAkSNHIikpCfPnz0dISAhUKhUiIiLKPai4aDkM46eKl9Xa5syZg/nz52PevHnSeJ+JEyeWKHvx4P3atWt4+OGH8eqrr2LWrFnw9PTE3r178cILLyAvL08KXEzVbWn17eLigmPHjmHnzp3YvHkzpkyZgmnTpuHw4cMml1jw9vZGVlYW8vLyyj34vH///qXO8AoJCcGZM2fKlSeg7/I2fDZsEQMhW2QYm6EwHfETlUrpVPr5ol8kZaUtT75W4OXlhT59+uDbb7/FG2+8YRTgxMXFYcWKFdLg5qSkJKxfvx6rVq0yGrug1WrRrVs3bN68Gf369cPw4cOxYsUKzJgxw2icUHZ2Nr799ltERkbC09MTp06dwpEjR7Bz5054enpK6ZKTk9GzZ0+cO3cOTZs2NVnurl274pdffoFKpcKsWbNgZ2eH7t27Y86cOcjNzUXXrl3NvmalUmk0MLai9u3bhy5duhi1khhaaKypXbt2WL16NXx9feHq6mq2LN9++63UUnfjxg0kJiZatRxNmjTB4cOH8dxzz0nHDh8+bNG1Bw8eRHBwMADg7t27uHDhApo1ayaVffDgwXjmmWcA6AOvCxcuoHnz5qXmefToUeh0Onz++eews9P/J6L4+KyKsre3R+/evdG7d29MnToV7u7u2L59Ox599NESaQ0tgGfPnjVqUQP0r7v4c8PrBoAff/yx1EHNxQM2S1y+fBk5OTlo27Ztua+9XzhY2hYZAiF7h6otB1EV+Prrr5Gbm4vIyEjs3r0bN27cwMaNG9GnTx80btwYU6ZMAQD8/PPP8PLywvDhw9GyZUvpERYWhgEDBkiDnD/++GP4+/ujT58++Pfff3Hjxg3s3r0bkZGR0Gg0UhfW4sWL0bFjR3Tv3t0ov+7du6NDhw6lDpru2rUrzp49izNnzqBbt24A9DOLVqxYgfDw8BKtB0WFhoZi9+7duHXr1j0FC40aNcKRI0ewadMmXLhwAR9++KHFgUF5jBgxAt7e3hg8eDD27NmDq1evYufOnRg/fjxu3rwpleXnn39GdHQ0Dh06hBEjRpQY/H6vXn/9dSxevBjLly/HxYsX8dFHH+HkyZMlZt6ZMmPGDGzbtg2nT5/GqFGj4O3tLa2D06hRI2zZsgX79+9HdHQ0xowZg/j4+DLzbNiwITQaDRYsWIArV67g559/lgZR34u///4bX331FaKionD9+nX89NNP0Ol0aNKkicn0Pj4+aNeundEAfoN9+/bhs88+w4ULF/DNN99gzZo1mDBhgnS+bt26aNiwodlHSEiIUX5nz55FVFQUkpOTkZqaiqioqBKLYu7Zswf169c36g60NQyEbJHcHvBtrt9m4+hy4Ku2wKb3y76OqAZo1KgRDh8+jPr162P48OEICQlB//790bhxY2mGEqBf62fo0KEmv/iGDRuGP//8E4mJifDy8sLBgwfRq1cvjBkzBg0aNMDw4cPRoEED6T55eXn45ZdfMGzYMJNlGjZsGH766SejaeZFtWjRAu7u7mjTpo1Uvp49e0Kr1ZY5PmjGjBm4du0aGjRocE/dB2PGjMGjjz6KJ554Ap06dUJSUlKJMTTWoFarsXv3bgQHB+PRRx9Fs2bN8MILLyAnJ0dqIVq8eDHu3r2Ldu3a4dlnn8X48ePh6+tr1XKMGDECkydPxltvvYV27drh6tWrGDVqFBwcyv4P5CeffIIJEyagffv2iIuLw19//SWNO/vggw/Qrl07REZGomfPnvD39ze5WGBxYWFh+OKLL/Dpp5+iZcuWWLFihVVWdnZ3d8fatWvx4IMPolmzZli0aBFWrlxZ6gyuF1980WTX5ZtvvokjR46gbdu2+Oijj/DFF19IU/8rYsCAAWjbti3++usv7Ny5E23bti3R8rNy5Uq89NJLFb7HfVEZo7hrkiqZNVbUwUX6mTmrn7u3fKo5zhorqbTZEqUxmjVWTUyZMkU4OzuLAwcOVHVRSqiO9Wmr7rUue/fubTTjrTjDrLG7d+9WsITVQ1ZWlggKChJ79+41mjVmbvZkZTl9+rTw9fUVKSkplXYPzhqrDRzc9T+5oCLVYtOnT0doaCgOHjyIjh07SmMwqPbKysrCokWLEBkZCblcjpUrV2Lr1q1G6+LUVo6Ojvjpp5+sPi6rvGJjY/HTTz/Bzc2tSstRFgZCts6w31hOSpUWg6iqPf/881VdBLIhMpkMGzZswKxZs5CTk4MmTZrg999/N7vdQ23Ts2dP6HQ6pKWlVVkZqst7wUDIFp1YDeyeAzSOBJo+rD/GFiEiIomjo6O0bYmlevbsafGaUjVN0S1QyBjbl21RZgKQdBHIvFPYIsQtNoiIiKyOgZAtMuwHZe8AOLrrf89JBWrp/2SodLX1f7hERNb4+8euMVtk2GJD4ahvEXIP0f/Mz9EfI0Lh4mZZWVlWX6OFiKg6MGzdUZHFHg0YCNkiaYsNR/1qvhNPVm15yCbJ5XK4u7tLexKp1WqLFpPT6XTIy8tDTk4OZ19ZAevTeliX1lWT61MIgaysLCQkJMDd3d1o/7zyYiBkiwwtQvb8Xz6VzrBlRFmbcRYlhEB2djYcHR0tCpyodKxP62FdWldtqE93d3ejrXMqgoGQLdIUaREiKoVMJkOdOnXg6+trdtXj4jQaDXbv3o3u3bvfU3My6bE+rYd1aV01vT4VCsU9tQQZMBCyRY4egHswoC7Y+HHda0DMAWDAXKDhQ1VbNrJJcrnc4j8Icrkc+fn5cHBwqJF/HO831qf1sC6ti/VpGQZCtqj/J/qHQXoskHwFyLC8+4OIiIjKVrNGT9VU0urSXFSRiIjImhgIVQfcb4yIiKhSMBCyRaufAb7vCdw6qn/O/caIiIgqBccI2aL4s0DyZSA/T/+cXWNERESVgi1CtkiTrf9pmD7P/caIiIgqBQMhW5RfLBBy9tNvs+HkVXVlIiIiqoHYNWaLircINXtY/yAiIiKrYouQrdHpCvca4xYbRERElYqBkK0xBEEAt9ggIiKqZAyEbI02D3ANBBw9CwOhnFTg+17AV+0AnbZqy0dERFSDcIyQrXF0ByadMT5m7wjcPqb/PTdNvxcZERER3TO2CFUH9kpAodb/zrWEiIiIrIaBUHXBtYSIiIisjoGQrYk7DfzwEPD7i8bHubo0ERGR1XGMkK3JSgJuHQHyMo2Pc+NVIiIiq2OLkK2RFlN0MD7OFiEiIiKrYyBka6TtNdTGx10DAPdgwI6NeERERNbCb1VbY2gRsi/WIjRo3n0vChERUU3HFiFbU3yfMSIiIqo0DIRsDQMhIiKi+4aBkK2xk+sHRqtcjI9f3KrfZuPP8VVTLiIiohqIY4RsTedX9Y/iNFn6bTbkivtfJiIiohqKLULVhaO7/ienzxMREVkNA6HqgusIERERWR0DIVuzew6w/BHg7Hrj49xrjIiIyOqqXSD0zTffIDQ0FA4ODujUqRP++++/UtOvWbMGTZs2hYODA1q1aoUNGzbcp5JWUNxp4OouID3e+LghEMrPBvJz73+5iIiIaqBqFQitXr0akyZNwtSpU3Hs2DGEhYUhMjISCQkJJtPv378fTz31FF544QUcP34cQ4YMwZAhQ3D69On7XPJyyM/R/yy+xYbKDYBM/3tO2n0tEhERUU1VrQKhL774Ai+99BKef/55NG/eHIsWLYJarcaSJUtMpp8/fz769euHt99+G82aNcPMmTPRrl07fP311/e55OWgydL/LL7Fhp0d4Bak32bDsA0HERER3ZNqM30+Ly8PR48exeTJk6VjdnZ26N27Nw4cOGDymgMHDmDSpElGxyIjI7Fu3Tqz98nNzUVubmHXU1qavvVFo9FAo9FYXF5D2vJcAwDyvGzYAciXKSCKXzvuWNEblCvf6q6i9UklsS6ti/VpPaxL66rt9Wnp6642gVBiYiK0Wi38/PyMjvv5+eHcuXMmr4mLizOZPi4uzux9Zs+ejenTp5c4vnnzZqjVahNXlG7Lli3lSt8zOR5uAP47fhJ3Lpf7djVeeeuTzGNdWhfr03pYl9ZVW+szKyvLonTVJhC6XyZPnmzUipSWloagoCD07dsXrq6uFuej0WiwZcsW9OnTBwqF5Ysg2l+fDmQDHbv0gAiOKFfZa7KK1ieVxLq0Ltan9bAurau216ehR6cs1SYQ8vb2hlwuR3y88Wyq+Ph4+Pv7m7zG39+/XOkBQKVSQaVSlTiuUCgq9EEq93V2ckAmh72DM1D8up2fABc2ARFjgVaPlbssNUFF3wcqiXVpXaxP62FdWldtrU9LX3O1GSytVCrRvn17bNu2TTqm0+mwbds2RESYbjmJiIgwSg/omwjNpbcJ4w4DU5OBgLYlz6XE6LfZSIm5/+UiIiKqgapNixAATJo0CSNHjkR4eDg6duyIefPmITMzE88//zwA4LnnnkPdunUxe/ZsAMCECRPQo0cPfP755xg4cCBWrVqFI0eO4Pvvv6/Kl2EZmazkMQd3/c+clPtZEiIiohqrWgVCTzzxBO7cuYMpU6YgLi4Obdq0wcaNG6UB0TExMbCzK2zk6tKlC3799Vd88MEH+N///odGjRph3bp1aNmyZVW9hHvDbTaIiIisqloFQgAwbtw4jBs3zuS5nTt3ljj2+OOP4/HHH6/kUllJfi6w8kn9GkLDfgQUjsbnGQgRERFZVbULhGo0TRZwebv+dzsTbw13oCciIrKqajNYulbQFKwYbWcPyE2MdufGq0RERFbFQMiWGAIhe0fT5x099MGQ0un+lYmIiKgGY9eYLTEEQsXHBhkEdwbe49R5IiIia2GLkC0xt/M8ERERVQoGQrbE3M7zREREVCkYCNmS/DwAMsC+lBahFY8D3/cC0s1vHEtERESW4RghW9KoNzD1LqDVmE9z6yiQlQRkJQMu5vdMIyIiorKxRcjWyGSAvdL8eS6qSEREZDUMhKob7jdGRERkNQyEbMm5f4DVzwKHfzSfhi1CREREVsMxQrYkIRqI/rMw2DGFgRAREZHVsEXIlkjrCJlZUBHgfmNERERWxEDIlkhbbJQyfd6wzQYRERHdM3aN2RJpi41SFlTsPU3/ICIionvGFiFbwi02iIiI7isGQrbEsMWGvSN2nE9A4w/+xaPf7qvaMhEREdVgDIRsiaZwsPS/p2KRl6/DsZgU4zRxp4CfhgC/v3S/S0dERFTjcIyQLXlyhb57zM4emefPSIeFEJDJZPon+bnAlR2AW3AVFZKIiKjmYIuQLbGTA0onwF6F7DytdDg3X1eYhusIERERWQ0DIRuVmZsv/Z6jKQyKpEAoNw3Q6UBEREQVx0DIlmz8H7DuNSDpMlKzC3egz8ozEQhBALlsFSIiIroXDIRsydn1QNQKICcFiRl50uHsoi1C9irAvmDlaXaPERER3RMGQrYkX7+gok7uiOTMXOlw0fFCADhOiIiIyEo4a8yWFJk+v+aVCBy+dhd9mvsh0KPY3mNqT33QZEhPREREFcJAyFYIIS2oaKd0RPsQT7QP8TSd9tX9gGE6PREREVUYAyFboc0DIPS/l7b7PGBxELQ+6haOx6SgV1Nf9Gjsc2/lIyIiqoEYCNkKw4arAM4labD/6lVEx6bBy1mF/i39ERbkXu4s919KwuojN+DjomIgREREZAIDIVthCIRkdvjvejpm/H1WOhXsqTYOhI79BJxeCzR/BAgfbTbLrILZZip7joknIiIyhd+QtsLZD3gvBph0DomZGqNTRtPnAeDudf02G/FnUZq/T94GAHz0T7RVi0pERFRTMBCyFXZ2+mnxLn5Iysg1OpWdl2+c1tFd/7OM6fNCWLF8RERENRADIRuUVLCYokKuHxSdVcF1hOztOLOMiIioNBwjZCvunAf2LwA8QpGU2QUAEOihxtXEzJJdY5YGQnIZ8nVsFiIiIjKHLUK2IiUGOP4zEP2n1CJkWEix5MrS7vqfOSmlZqmwK3x78/K5QSsREVFxDIRsRcFiilCokVgwRijQQw3AxGBpC1uEtkzqIf2elqMpJSUREVHtxK4xW2HYLsPeActGd0Riei6a1XHFs51D4OWsNE5rCITKGA3t7+YAF5U90nPzkZqtgbezqhIKTkREVH0xELIVBRuuQqFGu2CP0tN6hAJTkgE7eZnZdm3ojTytjgOniYiITGAgZCsMCyoqHMpOK5MBstKDoByNFp9uPIcGvk54o3dj2MvZC0pERFQcAyFbURAIpWkV+G3vVdT3cUJDX2esO34Lzip7jOpar1zZZeTmY+m+awCAN/s0sXZpiYiIagQ2E9iKfP0YoTvZMsz4+yx+3HMVt1NyMHfzBSw/cL1k+n/eAn4aAiSYXjXaMNPMUSGHnZ0MgqsrEhERlcBAyFZ0nQBMisa+oJcBAF7OSqiV+u6vEtPnASDmoH6bjbTbJrMzLMKYrdGi2YcbsXDX5copNxERUTXGrjFboXQClE64laefEu/lpIKDQh8IZRXfYgMoMoU+xWR2mUWuydZokZrN6fNERETFsUXIxhgWUyzaIpSjMbEYYhn7jRVvRUrLNhFMERER1XJsEbIVR5cDCdHwSGwCwBfezko4FrQI5Wl1yNfqjGd+GVqEslNMZld8f7I0tggRERGVwBYhW3FhI3BoIdwy9WN5vJxUcFQWTpEv7+rSxbvTuLI0ERFRSQyEbEXBFhtJufq3xMtZCZW9HWQF6yCa32/MdCDUu5kftk7qgSkPNwcAjhEiIiIygV1jtqJgi42R3Zuhq1c4Gvg6QyaT4bdXIqCyl8NdbWabjfxck9k5qezR0NcZKVn6MUfsGiMiIiqJgZCtKGgRCvX3RmgjP+lw+xBP0+k7vAB0fKnMbTa8nVUID/FA3YKd7ImIiKgQAyFbUbCgIhQWBixyRamnd5xLwLGYu+hUzwu/vdrlHgtHRERUM3GMkK0o2GJj/ZlkbDkbLx1eH3UL3+y4hOtJmeXKbteFO1iw/RL2X060ajGJiIhqErYI2YqCQOibvbfheO0S+jTXd48t2XcNJ26koImfC0K8nArTp8UCG98DIIDhP5XIzjC4Wq0se4d6IiKi2ootQrZizC5s6PEnrgl/eDsVDox2VOjfoqzi0+eFDji7Dji3ATCxj5ghvaPSHgPm70GzDzciOjat0opPRERUHTEQshVugbiKusiDAl7OhYGQWqlvtMspMX2+YNaYTiMNtC4qu2AdIbVSjpx8LbI1Ws4cIyIiKoaBkA0p3F5DJR1zNLffmNIJkBV0e5lYSyirSNeYq4N+YDXXEiIiIjLGQMgW5GUBm95Hp2vfwg46eBXtGjPsQF98vzGZrHC/scySA6IzpUDIHm6O+kAoLYf7jRERERXFQMgW5KYBB75Gn6QV0EEGbxMtQtmmdqD3b6X/eXl7iVNFu8ZcHdkiREREZAoDIVtQMGMsF0oAsmJjhAwtQtqS1zUfrP95dn2JU989G471Y7uidaAb3Bz144wYCBERERnj9HlbUBAIKRycsPiJcLQIcJNOPdkxGL2a+qKuu4mFFps+DGx4B1A567foUDhIp+p5F061N4wR4mBpIiIiYwyEbEG+PhCyV6nxUDM/o1P1vJ2Mghojzr7AO5cLZ5CZUc/bCeEhHgjkNhtERERGGAjZgoIWIYu31yjKRBCk0wnM33YRaqUcI7uE4vHwIDweHnSPhSQiIqp5OEbIFhTsPJ+Ya4cNp2KNTl1LzMRPB66VOF5Cerw0jT4nX4v52y5i9r/noNWVXGyRiIiI9BgI2YKCBRGvpeowd/N5o1NnbqdhyvozWLb/mvnrN7wNfN4EOLEaQOEaQkDhrDMiIiIqiYGQLWjQC7t6/4V3NC/D20lldMpRqX+LckzNGjPwCAUgpNljhn3GHBR2sLOT4UJ8Ojp9vBW95u6shMITERFVXwyEbIHKBdflwbgiAoymzgOAo0I/jCur+BYbRTV7RP/z+j4gI6HIqtL6a1X2dohPy0V8Wo71y05ERFSNMRCyEYnS9hrFAiHDOkKlBULuQUBAOwACiP5L2o7D0C1mWFk6K08LjVZnLhciIqJah4GQLbi6B2GXFqKnXRS8inWNlbqgYlFFFlfMLrLPGAA4qwonB3ItISIiokIMhGzBtT14KH4JHrI7Bu8SXWNmNl0trnlB99i1vdCk3wEAqAsCIHu5nRQMcXVpIiKiQlxHyBYUrCOUDRUCnYsPltYHQjkaHXQ6ATs7mek8POsD/q2BuJPokHsQf7w2BAp5YZzr5qhARm4+N14lIiIqgoGQLcjXD2Lu0zoETiEeRqfcHBVYPDJcCohK9cAkIC8L6ib90VZtnI+LA1uEiIiIimMgZAsK1hGqV8cbcHUwOqWQ25XYdsOsFkPNnwpwg5PKHg727A0lIiIyqDbfisnJyRgxYgRcXV3h7u6OF154ARkZGaVe07NnT8hkMqPHK6+8cp9KXA4FK0vD3jp7gR2+lowfdl/BwStJ0rHPh4fh91e7oFN9L6vcg4iIqCaoNoHQiBEjcObMGWzZsgV///03du/ejZdffrnM61566SXExsZKj88+++w+lLZ8NLmZAICo+FyT5/8+eRs/H7yO5My8sjPLTETGjnnQbJ6KjafjrFlMIiKiGqdadI1FR0dj48aNOHz4MMLDwwEACxYswIABAzB37lwEBASYvVatVsPf3/9+FbVCcrMzoQCw9lQS2gwpef7jf6JxOzUHreu6wdNJWTJBURkJ6HV9PrrI7bHQbkJlFJeIiKjGqBaB0IEDB+Du7i4FQQDQu3dv2NnZ4dChQxg61PzYmBUrVuCXX36Bv78/Bg0ahA8//BBqtdps+tzcXOTmFrbMpKWlAQA0Gg00GssHGhvSWnLNqdbv47PLD0HpXc9keoeCKfTp2bll5+fREHeVwfDNi0GTtP3QaMIAACv+u4GFO68gsoUfPhzY1OLXYSvKU59UOtaldbE+rYd1aV21vT4tfd3VIhCKi4uDr6+v0TF7e3t4enoiLs5898/TTz+NkJAQBAQE4OTJk3j33Xdx/vx5rF271uw1s2fPxvTp00sc37x5c6kBlDlbtmwpM83hOzIcF43QOE+HDRs2lDifly0HIMPu/YeQFF32bvJ5oi0eRwwCrq/Hhg0hAICTsTLEp8tx8uI1bNhwpdyvw1ZYUp9kGdaldbE+rYd1aV21tT6zsrIsSlelgdB7772HTz/9tNQ00dHRFc6/6BiiVq1aoU6dOnjooYdw+fJlNGjQwOQ1kydPxqRJk6TnaWlpCAoKQt++feHq6mrxvTUaDbZs2YI+ffpAoVCUmjZu3zXg0gU0CQnAgAGtS5z/+fZ/uJmZgpZhbdG/ZdndfB/FZeLx2PVooTmF5r27A0pn5EXdxtprp+Hk7oMBA9pb/DpsRXnqk0rHurQu1qf1sC6tq7bXp6FHpyxVGgi9+eabGDVqVKlp6tevD39/fyQkJBgdz8/PR3JycrnG/3Tq1AkAcOnSJbOBkEqlgkqlKnFcoVBU6INkyXX1r67Ei/I4ODo8bTKtWqU/lqeTWVSGi7J6uKbzQyjigWs7gRZD4eGkn5afkZtfrf9BVPR9oJJYl9bF+rQe1qV11db6tPQ1V2kg5OPjAx8fnzLTRUREICUlBUePHkX79vrWjO3bt0On00nBjSWioqIAAHXq1KlQeStL51tL0UeRjFWKQSbPqxUW7jdWIEujw0ZdR7xi9xdwdj3QYijc1PoPBBdUJCIiKlQtps83a9YM/fr1w0svvYT//vsP+/btw7hx4/Dkk09KM8Zu3bqFpk2b4r///gMAXL58GTNnzsTRo0dx7do1/Pnnn3juuefQvXt3tG5dsvupKtlr9esIubg4mzxfuAO9ZdtjfDy0FTo/PBpCrgLs9AGQq4P+J7fYICIiKlShQGjGjBkmByFlZ2djxowZ91woU1asWIGmTZvioYcewoABA9CtWzd8//330nmNRoPz589L5VIqldi6dSv69u2Lpk2b4s0338SwYcPw119/VUr57oWDTL8+ULuGdU2eH9UlFD88F45+LSxryWoe4Io2nXpB9s5lYNgPAPRbdQD63eeFKHvANRERUW1Qoa6x6dOn45VXXikxiyorKwvTp0/HlClTrFK4ojw9PfHrr7+aPR8aGmr0BR8UFIRdu3ZZvRxWp9VAptO30tTxdDeZJCzI9PFSyWSAykV66uaoQCNfZ7g6KpCbr5Om5BMREdVmFQqEhBCQyUrugn7ixAl4enrec6FqlYKd5wEAivJPzzdl2b6rUNrLMbhNAJxU9oA2H45Ke2yZ1MMq+RMREdUU5QqEPDw8pD27GjdubBQMabVaZGRk2OZeXjYsJzsDDgAEZBB2SpN9lZfvZODkzRTUcXNE5zL2CtPpBKb9dRYA0N8rAU5/jgQUDsDrR61feCIiomquXIHQvHnzIITA6NGjMX36dLi5uUnnlEolQkNDERERYfVC1mRJd9NQF0C2UMLRrmQrGwDsvnAH0/86i4db1ykzEMrJL5xZpnLzAdJuAnb6ViHIq8X6mURERPdNub4ZR44cCQCoV68eunbtCnt7frHeqwS4YXzuVNRxtsfXJrobAcDRMH0+r+zp81lF0jh4BgL2DkB+DpAag9c3peK/q0n45NHW6NXUt5RciIiIaocKzRpzcXExWvF5/fr1GDJkCP73v/8hL8+CHdJJkphjh6OiCW64tTObRpo+b8E6QoZgyUFhBzu5HPCopz+RfAWp2RrEp+UiyZJd7ImIiGqBCgVCY8aMwYULFwAAV65cwRNPPAG1Wo01a9bgnXfesWoBa7qkDP0Gr17OJVezNjC0CGWVo0VIrSxorfOsr/+ZfBWuDvpjaVxUkYiICEAFA6ELFy6gTZs2AIA1a9agR48e+PXXX7Fs2TL8/vvv1ixfjadJvILR8n/RU7vfbBpDUJNjQYtQVsGii+qCViR4FQRCSZeltYS4ujQREZFehQIhIQR0Oh0AYOvWrRgwYAAA/do9iYmJ1itdLaBIOospip/x4N01ZtM4KvVvU/lahAoCIalF6ApcDYsq5jAQIiIiAiq4jlB4eDg++ugj9O7dG7t27cLChQsBAFevXoWfn59VC1jT5RSshC1TOJpN46jQv02WjBFqXscVy0d3hEJeMPDatzkQHAH4t4SbPVuEiIiIiqpQIDRv3jyMGDEC69atw/vvv4+GDRsCAH777Td06dLFqgWs6QY0dQNiAQ83V7Np6no44ssnwqT9wkrj4aREj8ZFNrIN7gyM3ggAcD0UAwBIy+Z+Y0REREAFA6HWrVvj1KlTJY7PmTMHcjm3bigPXwf9tiBqtekNVwH99hhD2wbe87383VRo5OuMOm4O95wXERFRTXBPCwEdPXpUmkbfvHlztGtnfgo4maEp2LzWSttrnLmdilM3U9HIzxntQ4psd5Kfiwfru+BBbrNBREQkqVAglJCQgCeeeAK7du2Cu7s7ACAlJQW9evXCqlWr4OPjU3oGBEC/Hcaxy7cRDkArV8FcW5oQAlvOxiNbo0VkC/9SN0zdef4O5mw6j+HhgYWB0PqxQNSvwIA5QIcXrf46iIiIqqsKzRp7/fXXkZGRgTNnziA5ORnJyck4ffo00tLSMH78eGuXscZKzdbgyMXb+ieltAjJZDK8tuIYJqyKwt2s0hdDLJw+XyTGVbkBQgckX73nMhMREdUkFWoR2rhxI7Zu3YpmzZpJx5o3b45vvvkGffv2tVrharqkzFys1PbCCfvWWNj+kVLTOirlSM/JL3ObDcP0ecNq1AAAT/3q0vl3LmLAl7uQlp2PPe/2gkJeoTiYiIioxqhQIKTT6aBQlJzBpFAopPWFqGyJGXm4Lvwhd2kA+DYtNa26IBAqay0hQ6CkLtp9VrCWkDzlGi7EZwAA0nPy4emkvIfSExERVX8VahJ48MEHMWHCBNy+fVs6duvWLbzxxht46KGHrFa4mi4pQ9/N5e1kfnsNA8M2G2WtLm2yRcirAQBAdvcqXFX6t5xrCREREVUwEPr666+RlpaG0NBQNGjQAA0aNEC9evWQlpaGBQsWWLuMNVZSZi562R3HIN1WIOlyqWkdC8b8lNUiVGKvMQBwDQTsFIA2Dw0d0gBwvzEiIiKggl1jQUFBOHbsGLZu3Ypz584BAJo1a4bevXtbtXA1XWJGHp6Xb0T3hFPAjYZSy40pjgp9zFrW6tLZmmJ7jQGA3B7wCAGSLqGRfQKOwZktQkRERChnILR9+3aMGzcOBw8ehKurK/r06YM+ffoAAFJTU9GiRQssWrQIDzzwQKUUtqZJysiFSlYQkChKX+TQ0MJT1mDpSX2aYESnHIQFuRufaDIAyLwD2S03ANxvjIiICChnIDRv3jy89NJLcHUtuR2Em5sbxowZgy+++IKBkIVeeqA+PG8ogGSUuaDiCw/Uw9C2ddEu2KPUdO1DzJzvOxMAkPTTEQDxbBEiIiJCOccInThxAv369TN7vm/fvjh69Og9F6q2CPV2gqu8ICCxL71FqFcTXwxrH4hgr3tbgTrEU41Gvs7S4GsiIqLarFwtQvHx8SanzUuZ2dvjzp0791yoWkWTrf9ppS021kfdgtxOhp5NfOGsKvb2ajX4oJszPni4uVXuRUREVN2Vq0Wobt26OH36tNnzJ0+eRJ06de65ULXFkr1XkZ2dqX9Sxhihq4mZ2BYdj3NxaaWmm7z2FMb9ehxJGbnGJ1JigI/8gK87AFzriYiICEA5A6EBAwbgww8/RE5OTolz2dnZmDp1Kh5++GGrFa4my8vXYcbfZ6HJKQiE7B1LTf9/R27gheVHsPrwDbNphBDSrDKjdYQAwCUAkMmA/BwgPfaeyk5ERFRTlKtr7IMPPsDatWvRuHFjjBs3Dk2aNAEAnDt3Dt988w20Wi3ef//9SiloTWPYM2xC/ngsfqo57FxLb0lTW7CgYo5GByEK0iuLvbVye8A9GEi+gre+/wP5QV0x78m2FX8BRERENUC5AiE/Pz/s378fr776KiZPngxR8K0rk8kQGRmJb775Bn5+fpVS0JomsaDr6rS6I+xalr3+kqGFp7QFFQ0brgIwPRjasz6QfAXylGu46NimfAUmIiKqgcq9oGJISAg2bNiAu3fv4tKlSxBCoFGjRvDwKH1aNxkzbK/hZeF+X4ZAqLR1hAxBkoPCDnI7WckEng0AbEWoLA7/5eSXPE9ERFTLVGhlaQDw8PBAhw4drFmWWiUpMxcOyMVQ2THgTBLQYmip6Q0tPKWtLG1ye42iCjZfDZXFcR0hIiIiVHCvMbp3SRl58JalYczducAfr5aZXm1Ri5C+lcfsGkFSIBSPtGyN1LVJRERUW1W4RYjuTWJGHhxQMMW9jKnzAOCgKHuMUKiXE75+ui3sTXWLAYBvU+S3eAzro+yRrxPIytPCqfhaQ0RERLUIvwWryIhOwejt3gjYhDKnzgNAYz8XzBjcAr4uKrNpPJyUeLh1gPlM3IMhf+xH/Hj8XwACaTkaBkJERFSr8VuwigR5qhEUUBAAKcoOhALcHfFcROg931cmk6GhrzO0OoG8fC6sSEREtRsDoaqkydL/tCAQssTVxExEx6YhyEONVoFuphNp87HxuUD93mauTla5LxERUXXFwdJVKb9ghW4LAqG8fB0OXknCjvMJZtPsOJeA11Ycw3e7L5vPaON7wFdtgf9+KG9piYiIahy2CFUlw4arZew8DwCZufl48vuDAIBLs/rDXl4yhjVMrVcX316jqIKZY0i+Ur6yEhER1UAMhKpSUCfgsSWAY9mLURbdOyxbo4WLiUDIMH3e7DpCgBQIXTx3EseP3MDw8KByFpqIiKjmYCBUldyD9A8LqOztIJMBQhQEQg6KEmkMU+tLbLhaVEEgVEd7G//czSp/mYmIiGoQjhGqJmQyWeHq0mbWEjIcV5tbUBEAPEIgIIOzLAfa9DtWLycREVF1wkCoKsWdBs6sA+LPWJRcWl3azDYb0hYbpa0NZK9CusofAKBMu2ZxUYmIiGoiBkJV6dQaYM1I4PgKi5KXtbp04RihUlqEAGQ6BwMAnDKvW1pSIiKiGoljhKqSNH2+7FljQGGAk2MmEBrdrR56N/NDh3qepeaTEDII/xcfgFsi2PKyEhER1UAMhKpSORdUfLl7A6TnaBDibXohxC4NvNGlQdn5pDZ9El/ur4+m+S6WlpSIiKhGYiBUlTQFLUIW7DUGAI+1D7TKbT2dlPB1UcHLWWmV/IiIiKorBkJVKb9gQUUrbbGx64J+Fli7YHeT0+sNWtZxwX+vNdIvqigEIDOzWz0REVENx8HSVUlTvkAoJikL/11Nxq2UbJPn3/y/KIxc8h9u3jV9XqLNA+aHAT8PAbKSylFgIiKimoWBUFXSWL7XGADM33YRw787gL9O3DZ5Xpo+X8asMSgcANe6+t+51QYREdViDISq0gNvAIO+AgLaWpRcWkfIxKwxIYS0vlCpK0sXOJvrDQBIiom2tLREREQ1DscIVaWGvcuVvLQFFXM0OghhSFf223pR44PmAPITL5WrDERERDUJW4SqEYdSttgwLKYIQNqKozR3lPoZaLLkq1YqHRERUfXDFqGqdHGLfsZWUGdA5VxmckOLkKmVpQ3HVPZ2kNuVPQvsrmMgkMNtNoiIqHZji1BV+u0F4JdhQHqsRckNY39yTHSNGbrLyhwoXSBDrV9VWp1+DVKfGhERUS3DFqGqVM51hBylvcbyS5zzcVbh46GtYEFjEAAg1yUEX+cPRoumbdGLawkREVEtxUCoqui0+vV8AItXlm4V6Ia3+jZGfZ+S3WgeTko83cnyvcMcnZwxN/8JvOreAL3s2DBIRES1EwOhqqIpsuihhS1CTf1d0dTf1Sq393ZWwddFBaWcQRAREdVeDISqStFAyN6y3edLE5eag4sJ6fBzdUBjv7I3Ux3bqyHGdnQH4s8AtzMtXsuIiIioJmFzQFUxjA+ydwAs7JrK0Whx5nYqTt1MLXFu14UEPLv4P3zy7znLy3DsJ+CnR4CDCy2/hoiIqAZhi1BVkXaet7w16FpSJgZ+tRdeTkoc/bCP0TnD9HlLVpWWeNbX/7y0FVg1ApDZ6R8qZ2DwN4XphACEDrArR95ERETVAAOhquLkDTw8r1yztdQK/dtlamVpaZ8xCxZTBIAL8en4ans+vgb0G6+e+7vwpKNHYSCUegv4eyJQtz3Q8z2Ly0pERFQdMBCqKmpPIPz5cl3ioNR3oWVrtBBCQFYkiDKsNu2ksuwt1QmBv285Ilc9HT8MdNe3+AgdoNMBckVhwhsHgYubgcs7gGaPAH7Ny1VmIiIiW8ZAqBox7CEmBJCbr5O23ACAzIK1hSztGnNz1Ac7O3IaQ7TrbxRUGWnxKHDqN+D8BmD9WOCFLYCcHxsiIqoZOFi6qqTH61tZ4k5bfEnRPcSKb7ORXc6uMVcHfSCUrxMmu9okMhkw8AtA5QbcPgYc/Nbi8hIREdk6BkJV5fpe4OchwEbLx93I7WRQ2hd2jxVV3sHSaqUc9gXLUKdma0ym0eoEdp5PwPdRWYjt/IH+4I5ZQNJli8tMRERky9jHUVU0RabPl4OjQo68fB2yi22z8Wi7umhV1w2d6nlZlI9MJoOrowLJmXlIy85HHTfj8zvOJeDTjedwNysP8Wm5cBzcF8/W+xu4ugv483Vg5N8WT/snIiKyVQyEqoqmfPuMGbzcvT60OgE3R6XR8Z5NfNGziW+58nIrCISKtwhl5ubjg3WncSulcNHHpMw84JGvgG8j9JvEZsQBrgHluh8REZGtYSBUVSoYCI3t1dBqRfBxUSEzNx/5Op3R8S+2XMCtlGwEejiiXwt//Lj3KpIy8gCPxsAzvwN12gBKtdXKQUREVFUYCFWV/IIFFcsZCJkTdSMFQgg09nOxeAr9/42JKHHsxI0ULN13FQDw0ZCWuJaYCQBIyszVJwjpYpXyEhER2QIO8qgqmiz9Twt3njeIS83Bubg0pGTlGR0fv/I4hn67H+fi0iteJK0O7609BZ0ABrcJQM8mvvByVgEAEjOM7wedFjjwrX5qPRERUTXFQKiqaCrWIvT2byfQb94ebD+XYHQ8q2DwtLo8W2wUs3jvVUTHpsFdrcCHD+sXTvRy1o9FSsrINU4c9SuwaTKw4S0gI6F4VkRERNVCtQmEZs2ahS5dukCtVsPd3d2ia4QQmDJlCurUqQNHR0f07t0bFy9erNyCWqrpQKDvR0CjPmWnLcKwllDxdYSkLTbKEQitOXIDjy3cj0W7LkMIgQOXkwAAHwxsDu+CliDDz6TMYi1CYU8C/q2A7Lv6YEgIID0OuHnUON2f44HP6gNzGgHf9wRWPwP8+x6wfwFwem1hQAjoV7U2PIQofBAREVWSajNGKC8vD48//jgiIiKwePFii6757LPP8NVXX2H58uWoV68ePvzwQ0RGRuLs2bNwcCjftHWrq/eA/lFOhkAnp8g6QkIULopYnk1X72Tk4sj1uwj1doJMJsPSUR2w7VwCejcrnH0W5KHG0uc7wNtJZXyxXKHfj+z7XsDZ9cDHAfruPpkd8H48YF8wqy0/V7+XGQBkJgC3jxvn835c4e//96zxnmdFKADIw34oPPDvu0D03/r7yJX68sjsDBUCPL8BULnon+/8BDj7Z+G10iraMkAG4Jm1gHPBa96/ADi1xnylPfEL4B6s//2/H4Djv5hPO+xHwLuR/vfjvwCHfzSf9pEF+sAS0Hc3HvjafNr+c4CgDvrfo/8C9nxuPm2fmYWfs4tbgR0fQS4ElF4vmr+GiKgWqTaB0PTp0wEAy5Ytsyi9EALz5s3DBx98gMGDBwMAfvrpJ/j5+WHdunV48sknK6uolcoQ6BRtEcrR6KSGE8M2HJYwbLNhmD5vZydDn+Z+Je7Xy9y0/DphwANvArs/KwyC3AKBrMTCqfUPvAlEjNXvY5Z2S7+Ja+oN/e+5GeXqGtTKiuyBlnkHSLtpPrGuSItZ2i0g4UwpaYusyZR6C4g9YT5tfpGWsfRYIDbKfFpN4fIDSI8rGQQWlZdZ+HuGiYCxqNy0wt+zkkpPm5Na+Hv2XeD2cdgBsPMqZTVxIqJapNoEQuV19epVxMXFoXfv3tIxNzc3dOrUCQcOHKi+gZCJHeiziiyu6GjhFhtA4TYbW87GIyM3H84WzjYz0ut/QON+gKM74BZU2BJk4NO48PeANqXnNXQRoNUU6w7T/67JywV2Hyk8/NBUfYCl1QDavIIAxXCdDFA6FabtPFa/Z1qxPKX7OHoWngp/HmjYu2RaQ3rXOoXPw54Cgjqbfz0eoYW/txgK+Lc2n9a7SD01HQB4lbJMQp2wwt8bPAQ8XUoLVtG0oV2Bp9cgX5uPvHOZ5q8hIqpFamwgFBen73Lx8zNu4fDz85POmZKbm4vc3MKBwWlp+v99azQaaDSmt6IwxZC2PNdYQlUQ52TkFJYnLUtfXgeFHXTafKPGkNI4KQs3Wh234ih+eLadyXTbziXgSmImHmrii/o+TiUT+BV8wQsA9/J67Rz0DxM0smL16Rygf5ijA6ArSOvRQP8ojSFf9/r6hyVp3UL1D0vSugbrH5akda6rf1iS1skfqOdvWVpHH6BeL2g0GugubLH6Z7O2qqx/67UR69K6ant9Wvq6qzQQeu+99/Dpp5+WmiY6OhpNmza9TyUCZs+eLXXDFbV582ao1eVfRHDLli3WKJYk5qYMgBwXr1zDhg1XAABZ+cAjwTIIaLFhwwaL87qRARg+Au2UcWav/fqMHS6m2SH28jmE+1Tt4GVr12dtxrq0Ltan9bAurau21mdWVpZF6ao0EHrzzTcxatSoUtPUr1/G/87N8PfX/y85Pj4edeoUdmfEx8ejTZs2Zq+bPHkyJk2aJD1PS0tDUFAQ+vbtC1dXV4vvr9FosGXLFvTp0wcKhaLsCyzkeSUZdS4mIizQDf1aFLZ2PVaBvIQQuOt6AY18nTGsnfkWiM0ZJ3HxVByCGjXHgC4hFbjTvaus+qyNWJfWxfq0HtalddX2+jT06JSlSgMhHx8f+Pj4VEre9erVg7+/P7Zt2yYFPmlpaTh06BBeffVVs9epVCqoVKoSxxUKRYU+SBW9zpwHmvjhgSZ+ZSe00IeDWpaZxsdF3111Nzu/yv8xWbs+azPWpXWxPq2HdWldtbU+LX3N1WYdoZiYGERFRSEmJgZarRZRUVGIiopCRkaGlKZp06b4448/AOh3V584cSI++ugj/Pnnnzh16hSee+45BAQEYMiQIVX0KirH3cw8nLiRghvJljUDlpd3waKKycVXlyYiIqrmqs1g6SlTpmD58uXS87Zt2wIAduzYgZ49ewIAzp8/j9TUwunC77zzDjIzM/Hyyy8jJSUF3bp1w8aNG6t+DaF7kKPR4k66fnB0kKd+zNLeS4l4feVxdK7viVUvl9w/7F55SYsq5paRkoiIqHqpNoHQsmXLylxDSBRbhVgmk2HGjBmYMWNGJZbs/vrvajKeW/Ifmvq7YOPE7gCAbGlV6cp5O72c9C1CJfYbIyIiquaqTdcY6ZlaWTqzYB2h8qwqXR5sESIiopqq2rQIkZ6Dib3GpH3GyrGYYnk09nPGsuc7wNel+nYpEhERmcJAqJoxtAgVXVk6uwIbrpaHi4MCPc1ts0FERFSNsWusmjGMA8o20SLkWEljhO5VWo4Gefm6qi4GERFRCQyEqhnDXmL5OgGNVh9cZGv0Y4Qqq0UIAP49FYvvdl3Gzbvlm6KflqPBA5/uwGOL9ldSyYiIiCrONpsQyKyiA6Kz8rRwc7TDQ0394O2sQsd6nqVceW8W7rqMkzdT0dDXGYEelm81EhWTgtRsDU7eTEVqlgZu6tq3qBcREdkuBkLVjEIuw4hOwXBUyCG302+a2ru5H3o3t95q06YYptAnlXMKfVpO4aZ3qdkMhIiIyLYwEKpmZDIZZg1tdd/va5hCn1jOKfSxKTkAgEfCAhDsVf5Na4mIiCoTA6Ea4PKdDAgB1HV3rLy1hJwqts3G7dRsAEAdN069JyIi28PB0tVQapYGt1KypUUVx644ht5f7MKR68mVdk+vgv3GkjLLFwgZWoTquDkYLQJJRERkCxgIVUNPfH8AXT/ZjsPX9IFPViWvIwQAXk4FXWMZ5esa69HEBx1DPTHtr7Po9un2yigaERFRhTEQqoYM3V+GtYSkdYQUldfTKbUIlbNr7KmOwVg+uiNkMv1eZeUNpIiIiCoTxwhVQ4a1hAyrS2fnVf46QmGB7lg+uiP8Xcs/1sdRKUewpxrXk7JwIT4d3gUDr4mIiKoaW4SqIXWRFiEhBLIKAiK1qvICIQ8nJXo09kETfxeLr8nRaHEtMRM5Gi0a++mvuxCXXllFJCIiKjcGQtVQ0Y1XczQ6CKE/rraxLTZO3UpFz7k70ffL3Wjs5wwAuJCQUcWlIiIiKsRAqBoquvFqVkG3GFDYZVZZ1kfdwqJdl3En3bJxPrdTCqfOs0WIiIhskW01IZBFpDFCeVoo7O0wtlcD5OXrpJWmK8u8rRdxNTET7YI94ONS9jif2FT91PkAd0cpEDofnw4hBGSyyi0rERGRJRgIVUPtQz2Rp9WhVaAbXB0UeDuy6X25r5eTElcTM5Fk4cyv2IIWIX83B9T3cUL7EA808nVGbr5O6t4jIiKqSgyEqqFHwgLwSFjAfb+vYQp9ooWLKt42tAi5OUBlL8fvr3aptLIRERFVBMcIVXOZufm4cifjvqzP41mwqKLFLULS9hqOlVYmIiKie8FAqBrK1+qQmq1BSlYeDl5JwoOf78ILyw5X+n29y7moorS9hnvh2kM5Gq0UIBEREVU1BkLV0Pqo2wibvhnjV0UVripdiYspGhg2Xk2yYAd6IQRGdA7BY+0DEeiu33V+5/kENJ+yEWN+Plqp5SQiIrIUxwhVQ4VbbORL0+fvxxpCXs6G/cbKbhGSyWSY1Kex0bEgTzV0ArgYnwGdTsCukme5ERERlYWBUDXkaLSO0P1rEepU3xPLR3dEXffyb7MBACGeaijldsjWaHHzbjaCvdRWLiEREVH5MBCqhhyLrCwt7Tx/H6aj+7o4wNfFsiDoTnouMnPz4e/mIE2Vt5fboYGvM6Jj03AhPp2BEBERVTmOEaqGDCtL5+RppR3oK3PD1Yr4vyM30HPuTnyw7rTRccNWG+fjucI0ERFVPQZC1ZDUIlSka0ytuj+Ne78fvYmFOy8jNVtTarqi22sUJW21wUCIiIhsALvGqiHHIrvPd6znCZ0QCA/xuC/3nv1vNBIz8tCjsQ/cHBVm0xm21yi+hlBhIMTNV4mIqOoxEKqGXB0VGBQWALVCjsgWfujX0v++3dvLSYXEjLwyp9BLLULFBla3CHDF0LZ10SLAtdLKSEREZCkGQtWQq4MCC55qWyX39nJWAvFlL6oobbharEUowN0RXz7RprKKR0REVC4MhKq5hLQcQAa4OyqhtK/8IV+GtYSSStlvLCsvXxpDVLxFiIiIyJZwsHQ1pdUJpOdo8NLPR9Fx1jbsOJ9wX+4rrS5dyn5jhtYgZ5U9XB1KjiPK1+pwKSEDlxI4YJqIiKoWW4SqqY6ztiIpMw/ygtWZ79f0eUv2G3NW2WP8gw2RrxMmzy/ddw2zNkRjQCt/fDuifaWUk4iIyBIMhKopwyKF2oJg434FQoVdY+ZbhPxcHTCpbxOz5xv7c+YYERHZBgZC1VTxLTUcFffnrezR2Ac/v9ARdd0dy05sRpOCKfRXEzORm6+Fyt62FoMkIqLag4FQNVW8Beh+tQgFuDsioIwg6MqdDNjJZKjj7mAyyPFzVcHFwR7pOfm4mpiJpv6cSk9ERFWDg6WrKQdF1QRClpj+11n0nLsT647fMnleJpNJrULn4zhgmoiIqg4DoWqqeOBzP3afB/Rjkv7v8A18u/MScjRak2liUw3ba5hvOSocJ8RAiIiIqg67xqopQyAkkwHD2gVCrbw/b6WdDPhg/Wnk5eswqHUAgjxL7iAfm1KwmGIpawg19tVvvsoB00REVJUYCFVT7YI9IIMMj7UPRK+mvvftvjKZDN5OStxOzUFSZl6JQCg9R4P03HwApbcIdW7ghYm9G6Ft8P3ZI42IiMgUBkLV1IsP1K+ye3s5q3A7NQfJJqbQxxUspujqYA8nlfmPV1N/Vw6SJiKiKscxQtVYbr4WiRm5yM4zPVansngVLKqYaGJRxduGPcbuYXo9ERHR/cJAqBo7cu0uwj/aikFf772v9/VyKlhU0UQgFGvYdd6t7D3G4lJzsC06Hufi0qxbQCIiIgsxEKqmfth9BSN+PATg/k+dL9xmo2TXWIsAN4x/qBEebh1QZj4Ld17CC8uPYO0x09PsiYiIKhvHCFVTRXead1Tc30DI0DVmagf6VoFuaBXoZlE+nEJPRERVjYFQNVU0+LnfLUL9WtRBszquCDYxdb48DIsqXuCiikREVEUYCFVTRRdQVJcyO6syBHupEexlOgg6FnMXHmolAj0coZCX3vPaqCAQup2ag/QcDVwcFFYvKxERUWk4RqiaMmoRus9dY+YIIfDMj4fQa+5O3EjOKjO9m6MC/q76QdVcWJGIiKoCA6Fqqmh32P3uGsvN12L14Rh8s+MShBDS8bTsfGQVTOUvbTHFojhOiIiIqhIDoWrKoUjw0ybY/b7f/93fT2HOpvNIy86Xjt0u2GPMQ62weO+zwq02GAgREdH9xzFC1ZS3kwo9GvvAz1WFoW0D7+u9VfZyuDjYIz0nH4mZuXBT68f2GFaVtrQ1CAAeaROAlnXdEBbkXhlFJSIiKhUDoWoq2EuN5aM7Vtn9vZyUSM/JR1JGHhr46I8ZWoRK22y1uNaB7mgd6F4JJSQiIiobu8aqsYzcfORotEbjdO4XL2fD6tKFiyoadp0vT4sQERFRVWIgVI298vNRNP1wI9ZF3f+Vmb2cCvYbK7KooqFFyN+C7TWKOno9GUv2XsXpW6nWKyAREZEF2DVWTWl1AnsvJQIA8vJ19/3+plqEBrUOQKCHGp3re5Urr7XHbmHFoRj0bOKDZc9XXXcfERHVPmwRqqbkdjLpdxlkpaSsHIX7jRW2CPVq6otJfRqjfYhHufJ6uXt92MmAnefv4MxttgoREdH9w0CoJrj/cRCGtK2LX17ohJe717/nvEK8nDCwYJPWhTsv33N+RERElmIgVAOEVcGsqwY+zujWyBtBBfuNZedpsf9SIq4lZlYov1d7NAAAbDgVW+E8iIiIyouBUDW2551e+O2VCDQpWJ25Kl2+k4GnfzyExxYdqND1zQNc0auJD3QC+G73FSuXjoiIyDQGQtVYkKca4aGeVXLvjNx8rPovBj8UBC2xBYsplmcNoeJe69UQAPD70ZuIT8u590ISERGVgbPGqEJyNVq8t/YUAOD5rqGILZg6X6ecU+eL6hDqiQcaeaOetxNkVTDuiYiIah8GQlQh7mol7GSATgDJWXm4baXFFH8a3REyRkFERHSfsGuMKkRuJ4OHunAKvTVahAAwCCIiovuKgRBVmFeRtYSk7TXcrbO9RtSNFExaHYWsvPyyExMREVUQAyGqMC+ngtWlM3MLN1y9xxYhQL9q9viVx7H2+C2sPnzjnvMjIiIyh4EQVZihRSgxIw8THmqECQ81Qj1vp3vOV24nkxZq/GH3lSrZQoSIiGoHBkJUYd5F9ht7PDwIb/RpLO1Bdq8eax8IHxcVbqfm4M8Tt62SJxERUXEMhKjCnu4UjBUvdsIznUOsnreDQo4XutUDACzadRk6nbD6PYiIiBgIUYU19nNB14beEAD2X0rEzbtZVs1/RKdguDjY41JCBraeS7Bq3kRERAADIbKCzWfi8PSPh/Dxhmir5uvioMBzEfrWpu92X4VgoxAREVlZtQmEZs2ahS5dukCtVsPd3d2ia0aNGgWZTGb06NevX+UWtBZJycrDyv9i8Mm/5wDc+2KKpjzftR7q+zjhkbA6YBxERETWVm1Wls7Ly8Pjjz+OiIgILF682OLr+vXrh6VLl0rPVSrrDOYlICVLg8kF22wA976Yoinezipsm9QD+fn52LDhjNXzJyKi2q3aBELTp08HACxbtqxc16lUKvj7+1dCicgwfd6gMlqEAOPVpg9dTcat1Fw80SG4Uu5FRES1S7UJhCpq586d8PX1hYeHBx588EF89NFH8PLyMps+NzcXubm50vO0tDQAgEajgUajsfi+hrTluaa6UdkJKO3tpHV+fJztK+31ajQapOYBU1ZGITU7Hxfi0vB238aQ292fLTmEENgSnQB7uR0ebOJzX+5ZWWrDZ/N+Yn1aD+vSump7fVr6umVCVK8hqMuWLcPEiRORkpJSZtpVq1ZBrVajXr16uHz5Mv73v//B2dkZBw4cgFwuN3nNtGnTpNanon799Veo1ep7LX6NM+WIHKkafTAyvV0+3Cux51EIYONNGTbe1L93LTx0eK6RDg6m30qr0Qpg7VU77I3XD6kb21yLxm7V6p8NEVGtk5WVhaeffhqpqalwdXU1m65KA6H33nsPn376aalpoqOj0bRpU+l5eQKh4q5cuYIGDRpg69ateOihh0ymMdUiFBQUhMTExFIrsjiNRoMtW7agT58+UCgU5S5rdRHx6U4kZuQBAM5N71NpLTRF63PzuSS8u/Y0cvN1aOLnjO+eaYu6VtrjrLjM3Hy8seYkdpxPlI7VdXfA3+O6wFlVPRtUa8tn835hfVoP69K6ant9pqWlwdvbu8xAqEr/kr/55psYNWpUqWnq169vtfvVr18f3t7euHTpktlASKVSmRxQrVAoKvRBquh11UWIlxMSM/LQ2M8ZDipl2RfcI4VCgSHtghDq44KXfjqC8/EZeOy7Q/ju2XC0D/Gw+v2uxGZg76UkqOzt8PHQVvhy6wXcvJuNuVsuYdbQVla/3/1U0z+b9xvr03pYl9ZVW+vT0tdcpYGQj48PfHzu33iLmzdvIikpCXXq1Llv96zpQr2ccPT6XQxtG3hf79smyB3rx3bFi8uP4GxsGlb9F1MpgVC7YA98MbwNAtwd0T7EA3XcHTDix0PQCQGdTsDuPo1RIiKiylFt2vZjYmKQnJyMmJgYaLVaREVFAQAaNmwIZ2dnAEDTpk0xe/ZsDB06FBkZGZg+fTqGDRsGf39/XL58Ge+88w4aNmyIyMjIKnwlNcvobqEY1q4uGvg63/d7B7g7Ys0rEVi48zLGPdhQOr75TBy0OoFeTX3hoCj/AKJ9lxLh46JCYz8XAMCgsADpXJcG3tjyRg80rILXS0RE1ldtAqEpU6Zg+fLl0vO2bdsCAHbs2IGePXsCAM6fP4/U1FQAgFwux8mTJ7F8+XKkpKQgICAAffv2xcyZM7mWkBW1CHCr0vs7qezxVmQTo2Nfbr2I6Ng0OKvs0beFHx4JC0DXht5QyMteP3TNkRuYvPYU/FwdsG5sV/i4lPysFA2ChBBG0/uJiKh6qTaB0LJly8pcQ6jouG9HR0ds2rSpkktFtiZfq0P3xt5Iy9bgVko21h67hbXHbsHTSYleTXzRvbE3BrepCwDQaHVYdfgGHOzt4KCQ4/StVHy3+woAoF2IB1wcSv/ncTslG++tPYWXHqiHBxpV7yn1RES1VbUJhIgsYS+3w+T+zfBuZFMci7mL9VG3seFULJIy8/D7sZtQKeykQCgjJx8frjtdIo+xvRrgzT5Nyhz/s2TvVey+cAeX4tOx6Y3ucHGofYMRiYiqOwZCVCPZ2ckQHuqJ8FBPTB3UHPsuJyEqJgUt6xZOoZTJgMgWfsjR6JCj0UII4MmOQXi0nWUDvyf1bYzNZ+MRk5yFjzdEY/ajrSvr5RARUSVhIEQ1nr3cDj0a+6BHY+PuK3e1Et89G17hfNVKe3z2WGs8+f1BrPzvBvq1rFPiHkREZNuqze7zRLaoc30vjOoSCgB47/eTSMupnUvZExFVVwyEiO7RO/2aIMRLjdjUHMz6O7qqi0NEROXAQIjoHqmV9pjzWBhkMuDI9WRk5OZXdZGIiMhCHCNEZAUd63ni+2fD8UAjb2kRxwOXk7D/ciKGhwchyJMb9hIR2SIGQkRW0qe5n9Hz5fuvYeOZOCzYfgndGnrjiQ5B6NvCDyr78q92TURElYOBEFElGdwmAJl5+dhzMRF7L+kfHmoFHgkLwOC2ddEu2Pp7oxERUfkwECKqJP1b1UH/VnVwIzkLa47cwP8duYm4tBwsP3AdR67fxT/jH6jqIhIR1XoMhIgqWZCnGpP6NsGE3o2x++Id/Bl1G+2C3aXz6TkaPL/0MPq19McjYQHwdXWQzuVotLC3k8G+YJ+0G8lZuJSQgcb+Lghwc+A+Z0RE94iBENF9IreToVcTX/Rq4mt0fNOZeBy5fhdHrt/FxxuiEeihRmZuPtJz8pGn1WHFi53QtaE3AGD3xTt4/w/9tiDezkq0DnRHWKA7Wge5ISzQHZ5OygqVLTVLgxt3s3ArJRuxKdlIy8lHeKgHOoR6WrRZLRFRdcVAiKiK9Wrig+mPtMC6qFs4HpOCmOQso/PpRRZpDPRQo4mfCy7dyUBiRh62n0vA9nMJ0vllz3dAz4JAa8vZeOy+cAeAfjsRQ9uRTgicv2yHLlka+Ljp90f7cusFLNt/rUTZ3BwV6NXEB2/3a4q67o5WfNVERLaBgRBRFfNyVmFkl1CM7BKKG8lZiE3NgYuDPVwdFXBxsIezsvCfqWGrkByNFmdup+HkzRScvJmKEzdTcOVOJprVKdxL7XjMXfx88LqZu9rh5t1s+Ljpp/UHejjCx0WFAHdHBLg5QGlvhz0XE5GcmYe/T8Zi+uCW0pWnb6XCx0UFvyJdeERE1RUDISIbEuSptmjNIQeFHO1DPNA+pHDmWVqOBq4OCul514be0tgiCAGh/wGtTouYK5fg5VzYjfZCt3p48YH6RvfQ6gSOxdzF+bh0uDkW5vvButOIupGCQA9HBHuqEeKlRrCnE4I91ajn7YTmAa4gIqouGAgR1RBFgyBAHwgZxhYVpdFosGHDRdRxK2zRMTXoWm4nQ4dQT3QI9ZSO5eXrILeTQSYDbt7Nxs272dh/OUk639jPGZvf6CE9f/+PU9AJwLegBUn66aqCl5OyMFADcC0xE1l5WuTka5FT8FNuZwcnpRzuaiUa+jpLaYUQUplzNFokZeYhKSMXSRl5UMjt0K1R4etefTgGdjIZQrycEOKlhq+LioPMiUjCQIiILKa0t8Pvr3ZBcmYeLt/JQExSFq4nZ+FGchauJ2WigY+zUfo/o24j3cyWI10bemHFi52l54O+3ov0HNNpW9Z1xd+vFy430GvuTtxJzwUAZOZpjdKGBbkbBULztl5EbGqO9NxRIUewpxrBXmqEBbph3IONpHOL915FjqYwP/3YKhns7WTwdVVhcJu60rl/T8UiW6OF0OkQdUcGzYlYyOV2EAJwVtmjbwt/Ke2mM3FIy9ZAJpNBBsDOTp+vTKbfoqXoYpybz8QhMSMPWiGg0wlodQI6IWBvJ4OTyh6PhwdJaU/cSEFmbj6U9nawl9vp85bp87WTyYxa527ezUJWnhaygteFgvsbrgn1dpLSxqflICM3H0IAgCj4WaiBjzPs7PTBZEJ6DjJzi+ZrLNBDDXlB2sSMXGQUeY8N6WUFI9i8nQoXG03JyjP7eQAAP1d9Fy4ApGZrpLF0xcsKAL6uKmkh07QcDVKzzG+O7OOiklaHz8jNR0pWntm03s6FaTNz83G3lLSeTkqoC7q5s/LykZxpPq2HWgknlT5tdp4WSZm5ZtO6q5VwLkibo9EiMaMwbX5+PpJzgVsp2bC318DVUSH9hyk3Xyv9GzLFxUEhtQTn5euQkJ5jPq1KATe1Pm2+Voe4NPNpnZT28CiY1KHVCcSmZhu93qrAQIiIys3TSQlPJ+PWouKEEPjfwGaIT8tBQnouEtJyEJ+Wi4T0HNxJz0WORmeU3sdZ/2XlqLSDg70cDgo5tDqBzLx8BLgZD9TOyM03CoAUchm8nFTwclaioY9xy9FDzXxxLTEL15IycTslG9kaLc7Hp+N8fDpSszRGgdC3Oy4hycwXVOtAN6NA6KN/onErJbvgmRw/XzolnWvo62wUCM3ZdB6XEjJM5lvX3dEoEPp6xyWcvJlqMq2HWmEUCH28IRqHriabTOuokCN6Zj/p+ft/nMaugsHzplz7ZKD0+7Q/z+Df03Fm00bP6AdHpT4A+GTDOaw9fsts2mMf9pFmM87begG/HIwxm3bnm4XB7rc7L+P73VfMpt3yRnc08nMBoA9gv9p20Wza9WO7IizIHQDw66EYfPLvObNpV73cGZ3rewEA1h67iSnrz5hNu3RUB/Rqqp+c8M+pWLzz20mzab95uh0Gtq4DANh+LgHjfj1uNu3cx8PwWPtAAMCBK4kYveyI2bQzB7fAsxGhAIBjMXfx9A+HiqWwx/RjewAAk/s3xZgeDQAA0bHpGPLNPrP5TuzdCBN7NwYAXEvKRN8vd5tNO6Z7fUwe0AwAEJuagwc+22E27bOdQzBziH7M4d2sPHT7dAfmPdEGQ9rWNXtNZWMgRESVQiaT4amOwSbPaXWixOa029/qaXHeGyd2R2ZuPrQ6AS9nFVwd7E12d8lkMnw0pJX0PC9fh1sp2bielImY5CyjsU8AMLhNXWTm5kM/okpPJwCdTiDQwzgY61TfE3fSc6HV6pCYlAhvb2/I7fQtFMVn2HWs54lAD0cIgYKxWqLgdwFvZ5VxvvU84efqALlMBrmdDHZ2MshlgEYn4Kgw3p4l0EON5Mw85ObroNUJfb7Qt4oYAhUDFwd7eDopjdIYfi9OrbSX6tTQamSoX323ZGFalUIOZ5W9lJfhlKn3Q2Uvh4vKXrqnKGi+MTwveoW9nazE69WnFQX5Fx5T2MngoDC/zINdkcTm8jWVVm4mX0OrU9EyyGWll6HoKhRymQwqe8vSyspIa2iZM5S9eFqdVgs7ubwg38K0MqDUfO3LkbZovigrX3nJtHZ2JpoS7yOZEKYaEskgLS0Nbm5uSE1Nhaur5YNA9eMwNmDAgAFQKBRlX0ClYn1aD+vSulif1sO6tK7aXp+Wfn9zpTQiIiKqtRgIERERUa3FQIiIiIhqLQZCREREVGsxECIiIqJai4EQERER1VoMhIiIiKjWYiBEREREtRYDISIiIqq1GAgRERFRrcVAiIiIiGotBkJERERUazEQIiIiolqLgRARERHVWvZVXQBbJ4QAAKSlpZXrOo1Gg6ysLKSlpUGhUFRG0WoV1qf1sC6ti/VpPaxL66rt9Wn43jZ8j5vDQKgM6enpAICgoKAqLgkRERGVV3p6Otzc3Myel4myQqVaTqfT4fbt23BxcYFMJrP4urS0NAQFBeHGjRtwdXWtxBLWDqxP62FdWhfr03pYl9ZV2+tTCIH09HQEBATAzs78SCC2CJXBzs4OgYGBFb7e1dW1Vn4AKwvr03pYl9bF+rQe1qV11eb6LK0lyICDpYmIiKjWYiBEREREtRYDoUqiUqkwdepUqFSqqi5KjcD6tB7WpXWxPq2HdWldrE/LcLA0ERER1VpsESIiIqJai4EQERER1VoMhIiIiKjWYiBEREREtRYDoUryzTffIDQ0FA4ODujUqRP++++/qi6Szdu9ezcGDRqEgIAAyGQyrFu3zui8EAJTpkxBnTp14OjoiN69e+PixYtVU1gbN3v2bHTo0AEuLi7w9fXFkCFDcP78eaM0OTk5GDt2LLy8vODs7Ixhw4YhPj6+ikps2xYuXIjWrVtLC9NFRETg33//lc6zLivuk08+gUwmw8SJE6VjrE/LTZs2DTKZzOjRtGlT6TzrsmwMhCrB6tWrMWnSJEydOhXHjh1DWFgYIiMjkZCQUNVFs2mZmZkICwvDN998Y/L8Z599hq+++gqLFi3CoUOH4OTkhMjISOTk5Nznktq+Xbt2YezYsTh48CC2bNkCjUaDvn37IjMzU0rzxhtv4K+//sKaNWuwa9cu3L59G48++mgVltp2BQYG4pNPPsHRo0dx5MgRPPjggxg8eDDOnDkDgHVZUYcPH8Z3332H1q1bGx1nfZZPixYtEBsbKz327t0rnWNdWkCQ1XXs2FGMHTtWeq7VakVAQICYPXt2FZaqegEg/vjjD+m5TqcT/v7+Ys6cOdKxlJQUoVKpxMqVK6ughNVLQkKCACB27dolhNDXnUKhEGvWrJHSREdHCwDiwIEDVVXMasXDw0P8+OOPrMsKSk9PF40aNRJbtmwRPXr0EBMmTBBC8LNZXlOnThVhYWEmz7EuLcMWISvLy8vD0aNH0bt3b+mYnZ0devfujQMHDlRhyaq3q1evIi4uzqhe3dzc0KlTJ9arBVJTUwEAnp6eAICjR49Co9EY1WfTpk0RHBzM+iyDVqvFqlWrkJmZiYiICNZlBY0dOxYDBw40qjeAn82KuHjxIgICAlC/fn2MGDECMTExAFiXluKmq1aWmJgIrVYLPz8/o+N+fn44d+5cFZWq+ouLiwMAk/VqOEem6XQ6TJw4EV27dkXLli0B6OtTqVTC3d3dKC3r07xTp04hIiICOTk5cHZ2xh9//IHmzZsjKiqKdVlOq1atwrFjx3D48OES5/jZLJ9OnTph2bJlaNKkCWJjYzF9+nQ88MADOH36NOvSQgyEiGq4sWPH4vTp00bjBqj8mjRpgqioKKSmpuK3337DyJEjsWvXrqouVrVz48YNTJgwAVu2bIGDg0NVF6fa69+/v/R769at0alTJ4SEhOD//u//4OjoWIUlqz7YNWZl3t7ekMvlJUblx8fHw9/fv4pKVf0Z6o71Wj7jxo3D33//jR07diAwMFA67u/vj7y8PKSkpBilZ32ap1Qq0bBhQ7Rv3x6zZ89GWFgY5s+fz7osp6NHjyIhIQHt2rWDvb097O3tsWvXLnz11Vewt7eHn58f6/MeuLu7o3Hjxrh06RI/mxZiIGRlSqUS7du3x7Zt26RjOp0O27ZtQ0RERBWWrHqrV68e/P39jeo1LS0Nhw4dYr2aIITAuHHj8Mcff2D79u2oV6+e0fn27dtDoVAY1ef58+cRExPD+rSQTqdDbm4u67KcHnroIZw6dQpRUVHSIzw8HCNGjJB+Z31WXEZGBi5fvow6derws2mpqh6tXROtWrVKqFQqsWzZMnH27Fnx8ssvC3d3dxEXF1fVRbNp6enp4vjx4+L48eMCgPjiiy/E8ePHxfXr14UQQnzyySfC3d1drF+/Xpw8eVIMHjxY1KtXT2RnZ1dxyW3Pq6++Ktzc3MTOnTtFbGys9MjKypLSvPLKKyI4OFhs375dHDlyRERERIiIiIgqLLXteu+998SuXbvE1atXxcmTJ8V7770nZDKZ2Lx5sxCCdXmvis4aE4L1WR5vvvmm2Llzp7h69arYt2+f6N27t/D29hYJCQlCCNalJRgIVZIFCxaI4OBgoVQqRceOHcXBgwerukg2b8eOHQJAicfIkSOFEPop9B9++KHw8/MTKpVKPPTQQ+L8+fNVW2gbZaoeAYilS5dKabKzs8Vrr70mPDw8hFqtFkOHDhWxsbFVV2gbNnr0aBESEiKUSqXw8fERDz30kBQECcG6vFfFAyHWp+WeeOIJUadOHaFUKkXdunXFE088IS5duiSdZ12WTSaEEFXTFkVERERUtThGiIiIiGotBkJERERUazEQIiIiolqLgRARERHVWgyEiIiIqNZiIERERES1FgMhIiIiqrUYCBHVINeuXYNMJkNUVFRVF0Vy7tw5dO7cGQ4ODmjTpo3JNEIIvPzyy/D09Kzy8ttiHVbUzp07IZPJSuw1VRmmTZtm9v0lsmUMhIisaNSoUZDJZPjkk0+Mjq9btw4ymayKSlW1pk6dCicnJ5w/f95oz6OiNm7ciGXLluHvv/9GbGwsWrZseV/KNmrUKAwZMsToWFBQ0H0tQ3Ukk8mwbt06o2NvvfWW2feXyJYxECKyMgcHB3z66ae4e/duVRfFavLy8ip87eXLl9GtWzeEhITAy8vLbJo6deqgS5cu8Pf3h729fYXvd6/kcnmVl6E6cnZ2Nvv+EtkyBkJEVta7d2/4+/tj9uzZZtOY6kaYN28eQkNDpeeG1oqPP/4Yfn5+cHd3x4wZM5Cfn4+3334bnp6eCAwMxNKlS0vkf+7cOXTp0gUODg5o2bIldu3aZXT+9OnT6N+/P5ydneHn54dnn30WiYmJ0vmePXti3LhxmDhxIry9vREZGWnydeh0OsyYMQOBgYFQqVRo06YNNm7cKJ2XyWQ4evQoZsyYAZlMhmnTppXIY9SoUXj99dcRExMDmUwm1UFoaCjmzZtnlLZNmzZGechkMvz4448YOnQo1Go1GjVqhD///NPomjNnzuDhhx+Gq6srXFxc8MADD+Dy5cuYNm0ali9fjvXr10Mmk0Emk2Hnzp0mu8Z27dqFjh07QqVSoU6dOnjvvfeQn59vVF/jx4/HO++8A09PT/j7+5t8rcX9+OOPaNasGRwcHNC0aVN8++230rkuXbrg3XffNUp/584dKBQK7N69GwDw888/Izw8HC4uLvD398fTTz+NhIQEs/ez5HN3+PBh9OnTB97e3nBzc0OPHj1w7Ngx6bwh7dChQ43er+J5l/XZMNTz2rVr0atXL6jVaoSFheHAgQNSmuvXr2PQoEHw8PCAk5MTWrRogQ0bNpRap0TlxUCIyMrkcjk+/vhjLFiwADdv3rynvLZv347bt29j9+7d+OKLLzB16lQ8/PDD8PDwwKFDh/DKK69gzJgxJe7z9ttv480338Tx48cRERGBQYMGISkpCQCQkpKCBx98EG3btsWRI0ewceNGxMfHY/jw4UZ5LF++HEqlEvv27cOiRYtMlm/+/Pn4/PPPMXfuXJw8eRKRkZF45JFHcPHiRQBAbGwsWrRogTfffBOxsbF46623TOZh+MKMjY3F4cOHy1VH06dPx/Dhw3Hy5EkMGDAAI0aMQHJyMgDg1q1b6N69O1QqFbZv346jR49i9OjRyM/Px1tvvYXhw4ejX79+iI2NRWxsLLp06VIi/1u3bmHAgAHo0KEDTpw4gYULF2Lx4sX46KOPStSXk5MTDh06hM8++wwzZszAli1bzJZ7xYoVmDJlCmbNmoXo6Gh8/PHH+PDDD7F8+XIAwIgRI7Bq1SoU3Q5y9erVCAgIwAMPPAAA0Gg0mDlzJk6cOIF169bh2rVrGDVqVLnqr7j09HSMHDkSe/fuxcGDB9GoUSMMGDAA6enpACC9P0uXLi31/Srrs2Hw/vvv46233kJUVBQaN26Mp556Sgoyx44di9zcXOzevRunTp3Cp59+Cmdn53t6fUQlVO2er0Q1y8iRI8XgwYOFEEJ07txZjB49WgghxB9//CGK/nObOnWqCAsLM7r2yy+/FCEhIUZ5hYSECK1WKx1r0qSJeOCBB6Tn+fn5wsnJSaxcuVIIIcTVq1cFAPHJJ59IaTQajQgMDBSffvqpEEKImTNnir59+xrd+8aNGwKAOH/+vBBCvxt427Zty3y9AQEBYtasWUbHOnToIF577TXpeVhYmJg6dWqp+RR/7UIIERISIr788kujY8XzAiA++OAD6XlGRoYAIP79918hhBCTJ08W9erVE3l5eSbvW/T9MjDU4fHjx4UQQvzvf/8TTZo0ETqdTkrzzTffCGdnZ+m96dGjh+jWrZtRPh06dBDvvvuu2dfcoEED8euvvxodmzlzpoiIiBBCCJGQkCDs7e3F7t27pfMRERGl5nn48GEBQKSnpwshhNixY4cAIO7evSuEsOxzV5xWqxUuLi7ir7/+ko4BEH/88YdRuuJ5l/XZMNTzjz/+KJ0/c+aMACCio6OFEEK0atVKTJs2zWzZiKyBLUJEleTTTz/F8uXLER0dXeE8WrRoATu7wn+mfn5+aNWqlfRcLpfDy8urRHdIRESE9Lu9vT3Cw8Olcpw4cQI7duyAs7Oz9GjatCkA/Vgdg/bt25datrS0NNy+fRtdu3Y1Ot61a9d7es3l1bp1a+l3JycnuLq6SvURFRWFBx54AAqFosL5R0dHIyIiwmiwe9euXZGRkWHUEle0HABQp04ds91UmZmZuHz5Ml544QWj9+Gjjz6S3gMfHx/07dsXK1asAABcvXoVBw4cwIgRI6R8jh49ikGDBiE4OBguLi7o0aMHACAmJqbCrzc+Ph4vvfQSGjVqBDc3N7i6uiIjI6NceZbns1G03urUqQMAUr2NHz8eH330Ebp27YqpU6fi5MmTFX1ZRGYxECKqJN27d0dkZCQmT55c4pydnZ1Rlweg7+YorvgXuEwmM3lMp9NZXK6MjAwMGjQIUVFRRo+LFy+ie/fuUjonJyeL86wM91JHhvpwdHSsvAKWoxzFZWRkAAB++OEHo/fg9OnTOHjwoJRuxIgR+O2336DRaPDrr7+iVatWUiCcmZmJyMhIuLq6YsWKFTh8+DD++OMPAOYHt1tSpyNHjkRUVBTmz5+P/fv3IyoqCl5eXvc0YL40RevNEGwa6u3FF1/ElStX8Oyzz+LUqVMIDw/HggULKqUcVHsxECKqRJ988gn++usvowGggP5/+3FxcUZfStZct6bol2l+fj6OHj2KZs2aAcD/t3N/oex2ARzAv2+KC5FiFLbI39Es41aTCwlll9aS/CsZoy1FkXBhhAuaG0StlBTZBVvCxSwZCUs2MhcuKH9WcuFmve+F3tV+vH722u/q+X5qFzt7ds7OnlP77pzzPFAoFDg/P0daWhoyMzODHqGEn9jYWCQnJ8PhcASVOxwO5OXl/bgPIpEId3d3gecvLy+4ubkJqY6CggLY7fZPAxQAREZGwu/3f1mHVCrF/v5+0LlyOByIiYlBampqSJ/nX0lJSUhOTobX6/1wDtLT0wPH1dTU4O3tDVarFUtLS0GzQW63G09PTzAajSgpKUFubu6XG6WB7407h8MBnU6HyspK5OfnIyoqKmgjPfAeXr763sI5NsRiMVpbW7G6ugqDwYDZ2dmQ3k/0OwxCRH+QTCaDRqPB1NRUUHlpaSkeHh4wNjaG6+trmEwmbG5uhq1dk8mEtbU1uN1uaLVa+Hw+NDY2AnjfgPr8/Ay1Wo3Dw0NcX1/DZrOhoaHht6HgV93d3RgdHcXy8jI8Hg96enpwcnKCzs7OH/ehrKwMZrMZdrsdLpcL9fX1iIiICKmO9vZ2vLy8oLa2FkdHR7i6uoLZbIbH4wHwfgXU2dkZPB4PHh8fPw1MbW1tuL29RUdHB9xuN9bX1zEwMAC9Xh+0bBmqwcFBjIyMYGpqCpeXl3C5XFhYWMDk5GTgmOjoaKhUKvT39+Pi4gJqtTrwmkQiQWRkJKanp+H1emGxWDA8PPxlm98Zd1lZWTCbzbi4uMDBwQE0Gs2HmbW0tDRsb2/j/v7+P28TEY6x0dXVBZvNhpubGxwfH2N3dzcQ6InChUGI6A8bGhr6sEQilUoxMzMDk8kEuVwOp9P56RVV/5fRaITRaIRcLsfe3h4sFgsSEhIAIPBP3e/3o7y8HDKZDF1dXYiLiwv5h12n00Gv18NgMEAmk8FqtcJisSArK+vHfejt7YVSqUR1dTWqqqqgUqmQkZERUh3x8fHY2dnB6+srlEolioqKMDs7G1iOaWlpQU5ODoqLiyESiT7MYABASkoKNjY24HQ6IZfL0draiqamJvT19f2of83NzZibm8PCwgJkMhmUSiUWFxeDZoSA9+Wx09NTlJSUQCKRBMpFIhEWFxexsrKCvLw8GI1GjI+Pf9nmd8bd/Pw8fD4fFAoF6urqoNPpkJiYGHTMxMQEtra2IBaLUVhY+Glb4Rgbfr8fWq0WUqkUFRUVyM7ODrrFAFE4/PX3rwvGRERERALBGSEiIiISLAYhIiIiEiwGISIiIhIsBiEiIiISLAYhIiIiEiwGISIiIhIsBiEiIiISLAYhIiIiEiwGISIiIhIsBiEiIiISLAYhIiIiEiwGISIiIhKsfwBHt9mYnkV2LAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = [f'FQAOA with fourier params (p={p_fourier}, q={q})',\n", + " f'FQAOA with annealing params (p={p_annealing})',]\n", + "\n", + "# plot cost history\n", + "fig, ax = plt.subplots()\n", + "for i, result in enumerate([fq_fourier.result, fq_annealing.result]):\n", + " result.plot_cost(ax=ax, color=f'C{i}', label=labels[i])\n", + "plt.grid(True)\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8106c098-1fe7-43ad-b276-8aeeb57f0e7d", + "metadata": {}, + "source": [ + "## References\n", + "[1] T. Yoshioka, K. Sasada, Y. Nakano, and K. Fujii, [Phys. Rev. Research 5, 023071 (2023).](https://journals.aps.org/prresearch/pdf/10.1103/PhysRevResearch.5.023071), [arXiv:2301.10756 [quant-ph]](https://arxiv.org/pdf/2301.10756).\\\n", + "[2] E. Farhi, J. Goldston, S. Gutmann, and M. Sipser, [arXiv:quant-ph/0001106](https://arxiv.org/pdf/quant-ph/0001106).\\\n", + "[3] L. Zhou, S. Wang, S. Choi, H. Pichler, and M. D. Lukin, [Phys. Rev. X 10, 021067 (2020).](https://journals.aps.org/prx/pdf/10.1103/PhysRevX.10.021067), [arXiv:1812.01041v2 [quant-ph]](https://arxiv.org/pdf/1812.01041)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/openqaoa-core/openqaoa/__init__.py b/src/openqaoa-core/openqaoa/__init__.py index 4db56ed3..e1612f08 100644 --- a/src/openqaoa-core/openqaoa/__init__.py +++ b/src/openqaoa-core/openqaoa/__init__.py @@ -1,3 +1,3 @@ -from .algorithms import QAOA, RQAOA, QAOABenchmark +from .algorithms import QAOA, RQAOA, FQAOA, QAOABenchmark from .problems import QUBO from .backends import create_device diff --git a/src/openqaoa-core/openqaoa/algorithms/__init__.py b/src/openqaoa-core/openqaoa/algorithms/__init__.py index 8c3953e1..2c853b1b 100644 --- a/src/openqaoa-core/openqaoa/algorithms/__init__.py +++ b/src/openqaoa-core/openqaoa/algorithms/__init__.py @@ -1,2 +1,3 @@ from .qaoa import QAOA, QAOAResult, QAOABenchmark from .rqaoa import RQAOA, RQAOAResult +from .fqaoa import FQAOA diff --git a/src/openqaoa-core/openqaoa/algorithms/fqaoa/__init__.py b/src/openqaoa-core/openqaoa/algorithms/fqaoa/__init__.py new file mode 100644 index 00000000..695ad6a7 --- /dev/null +++ b/src/openqaoa-core/openqaoa/algorithms/fqaoa/__init__.py @@ -0,0 +1,2 @@ +from .fqaoa_workflow import FQAOA, FermiCircuitProperties, GivensRotationGateMap +from .fqaoa_utils import * diff --git a/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_utils.py b/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_utils.py new file mode 100644 index 00000000..4449cbf5 --- /dev/null +++ b/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_utils.py @@ -0,0 +1,402 @@ +from typing import List, Optional +from itertools import combinations +from scipy import linalg +import numpy as np + +ALLOWED_LATTICE = ["cyclic", "chain"] + +def get_givens_rotation_angle(orbitals: np.ndarray) -> List[float]: + """ + Compute the Givens rotation angles for transforming the orbital matrix `orbitals`. + + The function calculates the Givens rotation angles required to convert + the input orbitals matrix `orbitals` to a left-aligned diagonal matrix. + The angles are calculated based on the matrix entries and are returned + as a list of angles in radians. + + Parameters + ---------- + orbitals : np.ndarray + A 2D NumPy array representing the matrix of orbitals. The matrix should have a shape of + (n_fermions, n_qubits), where `n_fermions` is the number of rows and `n_qubits` is + the number of columns. + + Returns + ------- + list of float + A list of Givens rotation angles in radians. + + Raises + ------ + ValueError + If the number of rows (n_fermions) is greater than the number of columns (n_qubits). + + Notes + ----- + The function modifies the `orbitals` matrix in-place during the calculation of the + Givens rotation angles. + """ + + n_fermions, n_qubits = orbitals.shape + + if n_fermions > n_qubits: + raise ValueError(f"n_fermions ({n_fermions}) cannot be greater than n_qubits ({n_qubits}).") + + # perform unitary transformations to reduce the number of Givens rotation operations + orbitals = _unitary_sparsification(orbitals) + + # Calculate the Givens rotation angles + gtheta = [] + for irow in range(n_fermions): + for icol in range(n_qubits - n_fermions + irow, irow, -1): + if orbitals[irow][icol - 1] != 0.0: + rate = orbitals[irow][icol] / orbitals[irow][icol - 1] + uk = 1.0 / np.sqrt(1 + rate ** 2) + vk = -rate / np.sqrt(1 + rate ** 2) + angle = np.arccos(uk) if vk >= 0 else -np.arccos(uk) + else: + angle = np.pi/2.0 + gtheta.append(angle) + + # Apply the computed rotation angle to the matrix + for ik in range(n_fermions): + temp = orbitals[ik][icol - 1] + orbitals[ik][icol - 1] = temp * np.cos(angle) - orbitals[ik][icol] * np.sin(angle) + orbitals[ik][icol] = temp * np.sin(angle) + orbitals[ik][icol] * np.cos(angle) + + return gtheta + +def get_statevector(orbitals: np.ndarray) -> np.ndarray: + """ + Compute the statevector from fermionic orbitals. + + Parameters + ---------- + orbitals : np.ndarray + A 2D NumPy array representing the matrix of orbitals. The matrix should have a shape of + (n_fermions, n_qubits), where `n_fermions` is the number of rows and `n_qubits` is + the number of columns. + + Returns + ------- + np.ndarray + A 1D NumPy array of complex numbers representing the statevector of the quantum system. + The length of this array is `2**n_qubits`, corresponding to all possible basis states. + + Raises + ------ + ValueError + If the number of rows (n_fermions) is greater than the number of columns (n_qubits). + + Notes + ----- + The statevector is calculated by iterating over all possible combinations of `n_fermions` + occupied orbitals and computing the determinant of the corresponding submatrix of `orbitals`. + """ + + n_fermions, n_qubits = orbitals.shape + + if n_fermions > n_qubits: + raise ValueError(f"n_fermions ({n_fermions}) cannot be greater than n_qubits ({n_qubits}).") + + statevector = np.zeros(2**n_qubits, dtype = np.complex128) + cof = np.zeros((n_fermions, n_fermions), dtype = np.complex128) + # Generate all combinations of bit positions with the number of fermions being 1 + for bits in combinations(range(n_qubits), n_fermions): + indices = list(bits) + inum = sum(1 << i for i in indices) + # Update the cof matrix based on the current combination of bit positions + for i, j in enumerate(indices): + cof[:, i] = orbitals[:, j] + # Calculate the determinant and store it in the statevector + statevector[inum] = linalg.det(cof) + + return statevector + +def get_analytical_fermi_orbitals( + n_qubits: int, + n_fermions: int, + lattice: str, + hopping: float, +) -> np.ndarray: + """ + Compute fermionic orbitals from the analytical solution. + + This function computes a matrix representing fermionic orbitals based on an analytical + solution for a free fermions on a cyclic lattice. + The specific orbitals are found in Eq. (11) of T. Yoshoka et al. arXiv:2312.04710v1 [quant-ph]. + + Parameters + ---------- + n_qubits : int + The number of qubits, which determines the size of the system. + n_fermions : int + The number of fermions, which determines how many rows of the + orbital matrix are considered in the computation. + lattice : str + The type of lattice configuration. Only 'cyclic' lattice configurations + is supported. + hopping : float + The hopping parameter, which must be greater than 0. This value represents + the amplitude of hopping between lattice sites. + + Returns + ------- + np.ndarray + A 2D NumPy array of shape (n_fermions, n_qubits) representing the fermionic orbitals. + + Raises + ------ + ValueError + If `n_fermions` is greater than `n_qubits`. + ValueError + If the `lattice` is not 'cyclic'. + ValueError + If `hopping` is less than or equal to 0. + + Notes + ----- + The orbitals computed by this function are based on the analytical solutions described in: + - Eq. (11) in arXiv:2312.04710v1 [quant-ph], https://arxiv.org/pdf/2312.04710 + + The function currently only supports systems with a cyclic lattice configuration. + """ + + if n_fermions > n_qubits: + raise ValueError(f"n_fermions ({n_fermions}) cannot be greater than n_qubits ({n_qubits}).") + + if lattice not in ['cyclic']: + raise ValueError("analytical solutions support only 'cyclic'") + + if hopping <= 0.0: raise ValueError("analytical solutions support hopping > 0") + + orbitals = np.zeros((n_fermions, n_qubits), dtype = np.float64) + if n_fermions % 2 == 0: + for jw in range(n_qubits): + for k in range(int(n_fermions/2.0)): + k2 = k + int((n_fermions)/2.0) + angle = jw * 2.0 * np.pi * ((k+0.5) / n_qubits) + orbitals[ k][jw] = np.sin( angle) * np.sqrt(2.0/n_qubits) + orbitals[k2][jw] = np.cos(-angle) * np.sqrt(2.0/n_qubits) + else: + for jw in range(n_qubits): + orbitals[0][jw] = np.sqrt(1.0/n_qubits) + for k in range(int((n_fermions-1)/2.0)): + k2 = k + int((n_fermions-1)/2.0) + angle = jw * 2.0 * np.pi * ((k+1) / n_qubits) + orbitals[ k+1][jw] = np.sin( angle) * np.sqrt(2.0/n_qubits) + orbitals[k2+1][jw] = np.cos(-angle) * np.sqrt(2.0/n_qubits) + + return orbitals + +def get_fermi_orbitals( + n_qubits: int, + n_fermions: int, + lattice: str, + hopping: float +) -> np.ndarray: + """ + Compute fermionic orbitals from the Hamiltonian eigenvectors. + + This function generates a matrix representing fermionic orbitals by computing the eigenvectors + of a given fermionic mixer Hamiltonian. + + Parameters + ---------- + n_qubits : int + The number of qubits, which corresponds to the number of sites or modes in the system. + n_fermions : int + The number of fermions, which determines the number of occupied states in the system. + lattice : str + The type of lattice configuration. Only specific lattice types defined in `cyclic` and `chain` + are supported. + hopping : float + The hopping parameter, which defines the amplitude of hopping between adjacent lattice sites. + It must be non-zero. + + Returns + ------- + np.ndarray + A 2D NumPy array of shape (n_fermions, n_qubits) representing the fermionic orbitals. + Each row corresponds to an orbital and each column corresponds to a qubit (site). + + Raises + ------ + ValueError + If `n_fermions` is greater than `n_qubits`. + ValueError + If the `lattice` type is not recognized (i.e., not in [`cyclic`, `chain`]). + ValueError + If `hopping` is zero. + + Returns + ------- + np.ndarray + matrix representation of Fermionic orbitals. + + Notes + ----- + The orbitals are derived from the eigenvectors of the Hamiltonian corresponding to the given + system parameters. The specific eigenvector calculation is handled by the `_get_free_eigen` + function, which depends on the system's configuration. + """ + + if n_fermions > n_qubits: + raise ValueError(f"n_fermions ({n_fermions}) cannot be greater than n_qubits ({n_qubits}).") + + if lattice not in ALLOWED_LATTICE: + raise ValueError(f"In FQAOA, lattice {lattice} is not recognized. Please use {ALLOWED_LATTICE}") + + if hopping == 0.0: raise ValueError("In FQAOA, hopping = 0 is not recgnized. Please use hopping != 0") + + orbitals = np.zeros((n_fermions, n_qubits), dtype = np.float64) + eig = _get_free_eigen(n_qubits, n_fermions, lattice, hopping) + for jw in range(n_qubits): + for k in range(n_fermions): + orbitals[k][jw] = eig[jw][k] + + return orbitals + +def generate_random_portfolio_data( + num_assets: int, + num_days: int, + seed: Optional[int] = None, +) -> tuple[list[float], list[list[float]], np.ndarray]: + """ + Generates random portfolio data including mean returns, covariance matrix, + and historical price movements for a given number of assets and days. + + Parameters + ---------- + num_assets : int + The number of assets in the portfolio. + num_days : int + The number of days over which the historical data is generated. + seed : Optional[int], optional + An optional random seed for reproducibility, by default None. + + Returns + ------- + mu : List[float] + The mean returns for each asset. + sigma : List[List[float]] + The covariance matrix of the asset returns. + hist_exp : np.ndarray + The generated historical price movements for the assets. + + Notes + ----- + The function simulates historical price movements by generating random data + influenced by a time trend and random fluctuations, suitable for use in + portfolio optimization and risk analysis. + """ + + # If a seed is provided, set the random seed + if seed is not None: + np.random.seed(seed) + + # Generate historical-like data for multiple assets over a number of days + random_asset_factors = (1 - 2 * np.random.rand(num_assets)).reshape(-1, 1) + day_indices = np.array([np.arange(num_days) for i in range(num_assets)]) + np.random.randint(10) + random_fluctuations = 1 - 2 * np.random.rand(num_assets, num_days) + + # The resulting matrix hist_exp represents the daily returns or price levels of the assets + hist_exp = random_asset_factors * day_indices + random_fluctuations + + # Calculate the mean returns (mu) for each asset + # and the covariance matrix (sigma) of the asset returns + mu = hist_exp.mean(axis=1).tolist() + sigma = np.cov(hist_exp).tolist() + + return mu, sigma, hist_exp + +def _get_free_eigen( + n_qubits: int, + n_fermions: int, + lattice: str, + hopping: float, +) -> np.ndarray: + """ + Compute the eigenvectors of the fermionic mixer Hamiltonian for a given lattice and hopping parameter. + + This function constructs the Hamiltonian for a system with a specified number of qubits + and fermions, based on the lattice configuration and hopping amplitude. It then computes the + eigenvectors of this Hamiltonian matrix. + + Parameters + ---------- + n_qubits : int + The number of qubits, corresponding to the size of the Hamiltonian matrix (n_qubits x n_qubits). + n_fermions : int + The number of fermions in the system, which affects the phase of the cyclic boundary condition + if the lattice is cyclic. + lattice : str + The type of lattice configuration. Currently, only 'cyclic' is supported. + hopping : float + The hopping parameter that scales the Hamiltonian matrix elements. It defines the amplitude + of hopping between adjacent lattice sites. + + Returns + ------- + np.ndarray + A 2D NumPy array of shape (n_qubits, n_qubits) containing the eigenvectors of the Hamiltonian matrix. + Each column of the array represents an eigenvector. + + Notes + ----- + The Hamiltonian matrix is constructed with nearest-neighbor interactions and optional cyclic boundary + conditions, depending on the lattice type. The eigenvectors are computed using `scipy.linalg.eigh`, + which returns them in columns. + """ + + fermi_hamiltonian = np.zeros((n_qubits, n_qubits), dtype = np.float64) + for jw in range(1, n_qubits): + fermi_hamiltonian[jw, jw-1] = -1.0 + if lattice == 'cyclic': + fermi_hamiltonian[n_qubits-1, 0] = (-1.0)**n_fermions + fermi_hamiltonian = fermi_hamiltonian*hopping + eig = linalg.eigh(fermi_hamiltonian) + + return eig[1] + +def _unitary_sparsification(orbitals: np.ndarray) -> np.ndarray: + """ + Perform a unitary transformation to sparsify a matrix `orbitals` + by setting the elements in the upper triangular region to zero. + + This method applies a series of Givens rotations to eliminate the upper + triangular elements of the input matrix `orbitals`. The transformation is + carried out in-place, modifying `orbitals` directly. + + Parameters + ---------- + orbitals : numpy.ndarray + A 2D NumPy array representing the non-square matrix to be transformed. + The matrix shape is expected to be `(n_fermions, n_qubits)` where + `n_fermions <= n_qubits`. + + Returns + ------- + np.ndarray + The modified matrix `orbitals` with its upper triangular elements set to zero. + """ + + n_fermions, n_qubits = orbitals.shape + + for it in range(n_fermions - 1): + icol = n_qubits - 1 - it + for irot in range(n_fermions - 1 - it): + if orbitals[irot + 1][icol] == 0.0: + # Swap rows if necessary + orbitals[irot], orbitals[irot + 1] = orbitals[irot + 1], orbitals[irot] + else: + # Apply Givens rotation + rate = orbitals[irot][icol] / orbitals[irot + 1][icol] + factor = np.sqrt(1 + rate ** 2) + for jw in range(n_qubits): + orbitals[irot][jw], orbitals[irot + 1][jw] = ( + (orbitals[irot][jw] - rate * orbitals[irot + 1][jw]) / factor, + (orbitals[irot + 1][jw] + rate * orbitals[irot][jw]) / factor, + ) + + return orbitals diff --git a/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_workflow.py b/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_workflow.py new file mode 100644 index 00000000..9a570f2a --- /dev/null +++ b/src/openqaoa-core/openqaoa/algorithms/fqaoa/fqaoa_workflow.py @@ -0,0 +1,907 @@ +from typing import Callable, Optional, Tuple, List, Union, Dict +from copy import deepcopy +import numpy as np + +from .fqaoa_utils import ( + get_fermi_orbitals, + get_statevector, + get_givens_rotation_angle, +) + +from ..workflow_properties import WorkflowProperties +from ..baseworkflow import Workflow, check_compiled + +from ...backends.devices_core import DeviceBase, DeviceLocal +from ...backends.qaoa_backend import get_qaoa_backend +from ...backends.basebackend import QuantumCircuitBase +from ...problems import QUBO +from ...qaoa_components.ansatz_constructor.gatemap import GateMap +from ...qaoa_components.ansatz_constructor.gatemaplabel import GateMapType, GateMapLabel +from ...qaoa_components.ansatz_constructor.gates import RotationAngle, X, RY, RZ, CX +from ...qaoa_components import ( + Hamiltonian, + QAOADescriptor, + create_qaoa_variational_params, +) +from ...qaoa_components.variational_parameters.variational_baseparams import ( + QAOAVariationalBaseParams, +) +from ...utilities import ( + get_mixer_hamiltonian, + generate_timestamp, +) +from ...optimizers.qaoa_optimizer import get_optimizer +from ...backends.wrapper import SPAMTwirlingWrapper,ZNEWrapper + +ALLOWED_PARAM_TYPES = [ + "standard", + "standard_w_bias", + "extended", + "fourier", + "fourier_extended", + "fourier_w_bias", + "annealing", +] +ALLOWED_INIT_TYPES = ["rand", "ramp", "custom"] +ALLOWED_MIXERS = ["xy"] +ALLOWED_LATTICE = ["cyclic", "chain"] +ALLOWED_LOCAL_SIMUALTORS = [ + "vectorized", + "pyquil.statevector_simulator", + 'qiskit.qasm_simulator', + 'qiskit.shot_simulator', + 'qiskit.statevector_simulator', +] +NOT_ALLOWED_LOCAL_SIMULATORS = ["analytical_simulator"] + +class FQAOA(Workflow): + """ + A class implementing a FQAOA workflow end to end. + + It's basic usage consists of + 1. Initialization + 2. Compilation + 3. Optimization + + .. note:: + The attributes of the FQAOA class should be initialized using the set methods of FQAOA. + For example, to set the circuit's depth to 10 you should run `set_circuit_properties(p=10)` + + Attributes + ---------- + device: `DeviceBase` + Device to be used by the optimizer + circuit_properties: `FermiCircuitProperties` + The circuit properties of the FQAOA workflow. Use to set depth `p`, + choice of parameterization, parameter initialisation strategies, mixer hamiltonians. + For a complete list of its parameters and usage please see the method `set_circuit_properties` + backend_properties: `FermiBackendProperties` + The backend properties of the FQAOA workflow. Use to set the backend properties + such as the number of shots and the cvar values. + For a complete list of its parameters and usage please see the method `set_backend_properties` + classical_optimizer: `ClassicalOptimizer` + The classical optimiser properties of the QAOA workflow. Use to set the + classical optimiser needed for the classical optimisation part of the QAOA routine. + For a complete list of its parameters and usage please see the method `set_classical_optimizer` + local_simulators: `list[str]` + A list containing the available local simulators + cloud_provider: `list[str]` + A list containing the available cloud providers + mixer_hamil: Hamiltonian + The desired mixer hamiltonian + cost_hamil: Hamiltonian + The desired mixer hamiltonian + qaoa_descriptor: QAOADescriptor + the abstract and backend-agnostic representation of the underlying QAOA parameters + variate_params: QAOAVariationalBaseParams + The variational parameters. These are the parameters to be optimised by the classical optimiser + backend: VQABaseBackend + The openQAOA representation of the backend to be used to execute the quantum circuit + optimizer: OptimizeVQA + The classical optimiser + result: `Result` + Contains the logs of the optimisation process + compiled: `Bool` + A boolean flag to check whether the QAOA object has been correctly compiled at least once + + Examples + -------- + Basic usage with default settings: + + Initialize FQAOA with default parameters. This approach is straightforward and uses standard configurations. + This is suitable when the problem's requirements align with the default settings of FQAOA. + + >>> fqaoa = FQAOA() + >>> fqaoa.compile(problem, n_fermions) + >>> fqaoa.optimize() + + Where `problem` is an instance of `openqaoa.problems.problem.QUBO` + with hamming weight constant constraint, where `n_fermions` is a constraint value. + + Custom usage with non-default parameters: + + If your problem requires specific configurations or optimization strategies, you can customize the FQAOA instance + by setting circuit properties, choosing a different device, adjusting backend settings, or selecting a different classical optimizer. + + >>> fqaoa_custom = FQAOA() + >>> fqaoa_custom.set_circuit_properties( + p=10, + param_type='extended', + init_type='ramp', + ) + >>> device = create_device( + location='aws', + name='arn:aws:braket:::device/quantum-simulator/amazon/sv1', aws_region='us-east-1', + ) + >>> fqaoa_custom.set_device(device) + >>> fqaoa_custom.set_backend_properties(n_shots=200) + >>> fqaoa_custom.set_classical_optimizer(method='nelder-mead', maxiter=2) + >>> fqaoa_custom.compile(problem, n_fermions) + >>> fqaoa_custom.optimize() + """ + + def __init__(self, device=DeviceLocal("vectorized")): + """ + Initialize the QAOA class. + + Parameters + ---------- + device: `DeviceBase` + Device to be used by the optimizer. Default is using the local 'vectorized' simulator. + """ + super().__init__(device) + self.circuit_properties = FermiCircuitProperties() + self.backend_properties = FermiBackendProperties() + + # Exception handling in FQAOA + if device.device_name in NOT_ALLOWED_LOCAL_SIMULATORS: + raise ValueError(f"FQAOA does not support {NOT_ALLOWED_LOCAL_SIMULATORS}.") + + # change header algorithm to fqaoa + self.header["algorithm"] = "fqaoa" + + @check_compiled + def set_device(self, device: DeviceBase): + """ + Override set_device to add a check for unsupported devices in FQAOA. + + Parameters + ---------- + device: `DeviceBase` + Device to be used by the optimizer. + """ + + # Exception handling in FQAOA + if device.device_name in NOT_ALLOWED_LOCAL_SIMULATORS: + raise ValueError(f"FQAOA does not support {NOT_ALLOWED_LOCAL_SIMULATORS}.") + + # Call the parent class's set_device method to handle the rest + super().set_device(device) + + @check_compiled + def set_backend_properties(self, **kwargs): + """ + Specify the backend properties to construct FQAOA circuit + + Parameters + ---------- + device: DeviceBase + The device to use for the backend. + prepend_state: Union[openqaoa.basebackend.QuantumCircuitBase,np.ndarray(complex)] + The initial state for FQAOA is specified within the `FQAOA.compile` method, and therefore + `prepend_state` should not be set here. Providing a value for this property will + raise a ValueError. + append_state: Union[QuantumCircuitBase,np.ndarray(complex)] + The state appended to the circuit. + init_hadamard: bool + Specifies whether to apply the Hadamard gate during initialization. + This is always set to `False` in FQAOA and will raise a ValueError if set to `True`. + n_shots: int + The number of shots to be used for the shot-based computation. + cvar_alpha: float + The value of the CVaR parameter. + noise_model: NoiseModel + The `qiskit` noise model to be used for the shot-based simulator. + initial_qubit_mapping: Union[List[int], np.ndarray] + Mapping from physical to logical qubit indices, used to eventually + construct the quantum circuit. For example, for a system composed by 3 qubits + `qubit_layout=[1,3,2]`, maps `1<->0`, `3<->1`, `2<->2`, where the left hand side is the physical qubit + and the right hand side is the logical qubits + qiskit_simulation_method: str + Specify the simulation method to use with the `qiskit.AerSimulator` + qiskit_optimization_level: int, optional + Specify the qiskit.transpile optimization level. Choose from 0,1,2,3 + seed_simulator: int + Specify a seed for `qiskit` simulators + active_reset: bool + To use the active_reset functionality on Rigetti backends through QCS + rewiring: str + Specify the rewiring strategy for compilation for Rigetti QPUs through QCS + disable_qubit_rewiring: bool + enable/disable qubit rewiring when accessing QPUs via the AWS `braket` + """ + + for key, value in kwargs.items(): + if hasattr(self.backend_properties, key): + pass # setattr(self.backend_properties, key, value) + else: + raise ValueError( + f"Specified argument `{value}` for `{key}` in set_backend_properties is not supported" + ) + self.backend_properties = FermiBackendProperties(**kwargs) + + return None + + @check_compiled + def set_circuit_properties(self, **kwargs): + """ + Specify the circuit properties to construct FQAOA circuit + + Parameters + ---------- + qubit_register: `list` + Select the desired qubits to run the FQAOA program. Meant to be used as a qubit + selector for qubits on a QPU. Defaults to a list from 0 to n-1 (n = number of qubits) + p: `int` + Depth `p` of the FQAOA circuit + q: `int` + Analogue of `p` of the FQAOA circuit in the Fourier parameterization + param_type: `str` + Choose the FQAOA circuit parameterization. Currently supported parameterizations include: + `'standard'`: Standard FQAOA parameterization + `'standard_w_bias'`: Standard FQAOA parameterization with a separate parameter for single-qubit terms. + `'extended'`: Individual parameter for each qubit and each term in the Hamiltonian. + `'fourier'`: Fourier circuit parameterization + `'fourier_extended'`: Fourier circuit parameterization with individual parameter + for each qubit and term in Hamiltonian. + `'fourier_w_bias'`: Fourier circuit parameterization with a separate + parameter for single-qubit terms + init_type: `str` + Initialisation strategy for the FQAOA circuit parameters. Allowed init_types: + `'rand'`: Randomly initialise circuit parameters + `'ramp'`: Linear ramp from Hamiltonian initialisation of circuit + parameters (inspired from Quantum Annealing) + `'custom'`: User specified initial circuit parameters + mixer_hamiltonian: `str` + Allowed mixer hamiltonian: + `'xy'`: xy-mixer + mixer_qubit_connectivity: `[Union[List[list],List[tuple], str]]` By default set to 'cyclic' + The connectivity of the qubits in the mixer Hamiltonian. Use only if + `mixer_hamiltonian = xy`. The user can specify the connectivity as a list of lists, + a list of tuples, or a string chosen from ['cyclic', 'chain']. + mixer_coeffs: `list` + The coefficients of the mixer Hamiltonian. By default all set to -1 + annealing_time: `float` + Total time to run the FQAOA program in the Annealing parameterization (digitised annealing) + linear_ramp_time: `float` + The slope(rate) of linear ramp initialisation of FQAOA parameters. + variational_params_dict: `dict` + Dictionary object specifying the initial value of each circuit parameter for + the chosen parameterization, if the `init_type` is selected as `'custom'`. + For example, for standard params set {'betas': [0.1, 0.2, 0.3], 'gammas': [0.1, 0.2, 0.3]} + """ + + for key, value in kwargs.items(): + if hasattr(self.circuit_properties, key): + pass + else: + raise ValueError("Specified argument is not supported by the circuit") + self.circuit_properties = FermiCircuitProperties(**kwargs) + + return None + + def compile( + self, + problem: QUBO = None, + n_fermions: int = None, + hopping: float = 1.0, + verbose: bool = False, + routing_function: Optional[Callable] = None, + ): + """ + Initialise the trainable parameters for FQAOA according to the specified + strategies and by passing the problem statement + + .. note:: + Compilation is necessary because it is the moment where the problem statement and + the FQAOA instructions are used to build the actual FQAOA circuit. + + .. tip:: + Set Verbose to false if you are running batch computations! + + Parameters + ---------- + problem: `QUBO` + QUBO problem to be solved by FQAOA + n_fermions: `int` + Number of fermions corresponding to the value of the constraint + hopping: `float`, optional + the coefficient of the fermionic mixer Hamiltonian + verbose: bool + Set True to have a summary of FQAOA to displayed after compilation + """ + + # connect to the QPU specified + self.device.check_connection() + # we compile the method of the parent class to generate the id and + # check the problem is a QUBO object and save it + super().compile(problem=problem) + + # check the n_fermions is given and save it + if n_fermions is None: + raise ValueError("In FQAOA, the 'n_fermions' argument must be specified") + + self.n_fermions = n_fermions + self.hopping = hopping + + self.cost_hamil = Hamiltonian.classical_hamiltonian( + terms=problem.terms, coeffs=problem.weights, constant=problem.constant + ) + + self.n_qubits = self.cost_hamil.n_qubits + + # Determine the coefficients of the mixer hamiltonian + if self.circuit_properties.mixer_qubit_connectivity == "cyclic": + self.circuit_properties.mixer_coeffs = [-0.5*hopping] * 2 * self.n_qubits + elif self.circuit_properties.mixer_qubit_connectivity == "chain": + self.circuit_properties.mixer_coeffs = [-0.5*hopping] * 2 * (self.n_qubits-1) + + self.mixer_hamil = get_mixer_hamiltonian( + n_qubits=self.n_qubits, + mixer_type=self.circuit_properties.mixer_hamiltonian, + qubit_connectivity=self.circuit_properties.mixer_qubit_connectivity, + coeffs=self.circuit_properties.mixer_coeffs, + ) + + self.qaoa_descriptor = QAOADescriptor( + self.cost_hamil, + self.mixer_hamil, + p=self.circuit_properties.p, + routing_function=routing_function, + device=self.device, + ) + + self.variate_params = create_qaoa_variational_params( + qaoa_descriptor=self.qaoa_descriptor, + params_type=self.circuit_properties.param_type, + init_type=self.circuit_properties.init_type, + variational_params_dict=self.circuit_properties.variational_params_dict, + linear_ramp_time=self.circuit_properties.linear_ramp_time, + q=self.circuit_properties.q, + seed=self.circuit_properties.seed, + total_annealing_time=self.circuit_properties.annealing_time, + ) + + # Backend configuration required for initial state preparation in FQAOA. + lattice = self.circuit_properties.mixer_qubit_connectivity + + # initial statevector or circuit + orbitals = get_fermi_orbitals(self.n_qubits, self.n_fermions, lattice, hopping) + if self.device.device_name in 'vectorized': + self.backend_properties.prepend_state = get_statevector(orbitals) + else: + gate_applicator = self._gate_applicator() + self.backend_properties.prepend_state = self._fermi_initial_circuit(orbitals, gate_applicator) + + backend_dict = self.backend_properties.__dict__.copy() + + self.backend = get_qaoa_backend( + qaoa_descriptor=self.qaoa_descriptor, + device=self.device, + **backend_dict, + ) + + # Implementing SPAM Twirling and MITIQs error mitigation requires wrapping the backend. + # However, the BaseWrapper can have many more use cases. + if ( + self.error_mitigation_properties.error_mitigation_technique + == "spam_twirling" + ): + self.backend = SPAMTwirlingWrapper( + backend=self.backend, + n_batches=self.error_mitigation_properties.n_batches, + calibration_data_location=self.error_mitigation_properties.calibration_data_location, + ) + elif( + self.error_mitigation_properties.error_mitigation_technique + == "mitiq_zne" + ): + self.backend = ZNEWrapper( + backend=self.backend, + factory=self.error_mitigation_properties.factory, + scaling=self.error_mitigation_properties.scaling, + scale_factors=self.error_mitigation_properties.scale_factors, + order=self.error_mitigation_properties.order, + steps=self.error_mitigation_properties.steps + ) + + self.optimizer = get_optimizer( + vqa_object=self.backend, + variational_params=self.variate_params, + optimizer_dict=self.classical_optimizer.asdict(), + ) + + # Set the header properties + self.header["target"] = self.device.device_name + self.header["cloud"] = self.device.device_location + + metadata = { + "p": self.circuit_properties.p, + "param_type": self.circuit_properties.param_type, + "init_type": self.circuit_properties.init_type, + "optimizer_method": self.classical_optimizer.method, + } + + self.set_exp_tags(tags=metadata) + + self.compiled = True + + if verbose: + print("\t \033[1m ### Summary ###\033[0m") + print("OpenQAOA has been compiled with the following properties") + print( + f"Solving FQAOA with \033[1m {self.device.device_name} \033[0m on" + f"\033[1m{self.device.device_location}\033[0m" + ) + print( + f"Using p={self.circuit_properties.p} with {self.circuit_properties.param_type}" + f"parameters initialized as {self.circuit_properties.init_type}" + ) + + if hasattr(self.backend, "n_shots"): + print( + f"OpenQAOA will optimize using \033[1m{self.classical_optimizer.method}" + f"\033[0m, with up to \033[1m{self.classical_optimizer.maxiter}" + f"\033[0m maximum iterations. Each iteration will contain" + f"\033[1m{self.backend_properties.n_shots} shots\033[0m" + ) + else: + print( + f"OpenQAOA will optimize using \033[1m{self.classical_optimizer.method}\033[0m," + "with up to \033[1m{self.classical_optimizer.maxiter}\033[0m maximum iterations" + ) + + return None + + def optimize(self, verbose=False): + """ + A method running the classical optimisation loop + """ + + if self.compiled is False: + raise ValueError("Please compile the FQAOA before optimizing it !") + + # timestamp for the start of the optimization + self.header["execution_time_start"] = generate_timestamp() + + self.optimizer.optimize() + # TODO: result and qaoa_result will differ + self.result = self.optimizer.qaoa_result + + # timestamp for the end of the optimization + self.header["execution_time_end"] = generate_timestamp() + + if verbose: + print("Optimization completed.") + return + + def evaluate_circuit( + self, + params: Union[List[float], Dict[str, List[float]], QAOAVariationalBaseParams], + ): + """ + A method to evaluate the QAOA circuit at a given set of parameters + + Parameters + ---------- + params: list or dict or QAOAVariationalBaseParams or None + List of parameters or dictionary of parameters. Which will be used to evaluate the QAOA circuit. + If None, the variational parameters of the QAOA object will be used. + + Returns + ------- + result: dict + A dictionary containing the results of the evaluation: + - "cost": the expectation value of the cost Hamiltonian + - "uncertainty": the uncertainty of the expectation value of the cost Hamiltonian + - "measurement_results": either the state of the QAOA circuit output (if the QAOA circuit is + evaluated on a state simulator) or the counts of the QAOA circuit output + (if the QAOA circuit is evaluated on a QPU or shot-based simulator) + """ + # before evaluating the circuit we check that the QAOA object has been compiled + if self.compiled is False: + raise ValueError("Please compile the FQAOA before optimizing it!") + + # Check the type of the input parameters and save them as a + # QAOAVariationalBaseParams object at the variable `params_obj` + + # if the parameters are passed as a dictionary we copy and update the variational parameters of the QAOA object + if isinstance(params, dict): + params_obj = deepcopy(self.variate_params) + # we check that the dictionary contains all the parameters of the QAOA object that are not empty + for key, value in params_obj.asdict().items(): + if value.size > 0: + assert ( + key in params.keys() + ), f"The parameter `{key}` is missing from the input dictionary" + params_obj.update_from_dict(params) + + # if the parameters are passed as a list we copy and update the variational parameters of the QAOA object + elif isinstance(params, list) or isinstance(params, np.ndarray): + assert len(params) == len( + self.variate_params + ), "The number of parameters does not match the number of parameters in the QAOA circuit" + params_obj = deepcopy(self.variate_params) + params_obj.update_from_raw(params) + + # if the parameters are passed as a QAOAVariationalBaseParams object we just take it as it is + elif isinstance(params, QAOAVariationalBaseParams): + # check whether the input params object is supported for circuit evaluation + assert ( + len(self.variate_params.mixer_1q_angles) == len(params.mixer_1q_angles) + and len(self.variate_params.mixer_2q_angles) + == len(self.variate_params.mixer_2q_angles) + and len(self.variate_params.cost_1q_angles) + == len(self.variate_params.cost_1q_angles) + and len(self.variate_params.cost_2q_angles) + == len(self.variate_params.cost_2q_angles) + ), "Specify a supported params object" + params_obj = params + + # if the parameters are passed in a different format, we raise an error + else: + raise TypeError( + f"The input params must be a list or a dictionary. Instead, received {type(params)}" + ) + + # Evaluate the QAOA circuit and return the results + output_dict = { + "cost": None, + "uncertainty": None, + "measurement_results": None, + } + # if the workflow implements SPAM Twirling, + # we just return the expectation value of the cost Hamiltonian and the measurement outcomes + if isinstance(self.backend, SPAMTwirlingWrapper): + cost = self.backend.expectation(params_obj) + measurement_results = ( + self.backend.measurement_outcomes + if isinstance(self.backend.measurement_outcomes, dict) + else self.backend.measurement_outcomes.tolist() + ) + output_dict.update( + { + "cost": cost, + "measurement_results": measurement_results, + } + ) + # in all other cases, we return the expectation value of the cost Hamiltonian, + # the associated uncertainty and the measurement outcomes + else: + cost, uncertainty = self.backend.expectation_w_uncertainty(params_obj) + measurement_results = ( + self.backend.measurement_outcomes + if isinstance(self.backend.measurement_outcomes, dict) + else self.backend.measurement_outcomes.tolist() + ) + output_dict.update( + { + "cost": cost, + "uncertainty": uncertainty, + "measurement_results": measurement_results, + } + ) + return output_dict + + def _serializable_dict( + self, complex_to_string: bool = False, intermediate_measurements: bool = True + ): + """ + Returns all values and attributes of the object that we want to return in + `asdict` and `dump(s)` methods in a dictionary. + + Parameters + ---------- + complex_to_string: bool + If True, complex numbers are converted to strings. + This is useful for JSON serialization. + + Returns + ------- + serializable_dict: dict + A dictionary containing all the values and attributes of the object + that we want to return in `asdict` and `dump(s)` methods. + intermediate_measurements: bool + If True, intermediate measurements are included in the dump. + If False, intermediate measurements are not included in the dump. + Default is True. + """ + + # we call the _serializable_dict method of the parent class, + # specifying the keys to delete from the results dictionary + serializable_dict = super()._serializable_dict( + complex_to_string, intermediate_measurements + ) + + # we add the keys of the QAOA object that we want to return + serializable_dict["data"]["input_parameters"]["circuit_properties"] = dict( + self.circuit_properties + ) + + # include parameters in the header metadata + serializable_dict["header"]["metadata"]["param_type"] = serializable_dict[ + "data" + ]["input_parameters"]["circuit_properties"]["param_type"] + serializable_dict["header"]["metadata"]["init_type"] = serializable_dict[ + "data" + ]["input_parameters"]["circuit_properties"]["init_type"] + serializable_dict["header"]["metadata"]["p"] = serializable_dict["data"][ + "input_parameters" + ]["circuit_properties"]["p"] + + if ( + serializable_dict["data"]["input_parameters"]["circuit_properties"]["q"] + is not None + ): + serializable_dict["header"]["metadata"]["q"] = serializable_dict["data"][ + "input_parameters" + ]["circuit_properties"]["q"] + + return serializable_dict + + def _fermi_initial_circuit(self, orbitals: np.ndarray, gate_applicator: object) -> object: + """ + Constructs the initial quantum circuit for the FQAOA. + + This method initializes a quantum circuit for a system with a specified number of fermions and + qubits. The method applies X gates to excite the number of fermions and then applies a series + of Givens rotation gates according to the provided orbital data. + + Parameters + ---------- + orbitals : np.ndarray + A numpy array containing the orbital information needed to compute the Givens rotation angles. + gate_applicator : object + An object responsible for applying quantum gates to the circuit. + + Returns + ------- + object + A quantum circuit object initialized with the fermions and Givens rotations. + """ + + initial_circuit = gate_applicator.create_quantum_circuit(self.n_qubits) + + # excites `n_fermions` number of fermion + for i in range(self.n_fermions): + gate = X(gate_applicator, i) + gate.apply_gate(initial_circuit) + + # apply `givens rotation gate` + gtheta = get_givens_rotation_angle(orbitals) + i = (self.n_qubits-self.n_fermions)*self.n_fermions + for irow in range(self.n_fermions-1, -1, -1): + for icol in range(irow+1, self.n_qubits-self.n_fermions+irow+1): + i -= 1 + angle = gtheta[i] + for each_tuple in GivensRotationGateMap(icol, icol-1, angle).decomposition('standard'): + gate = each_tuple[0](gate_applicator, *each_tuple[1]) + gate.apply_gate(initial_circuit) + + return initial_circuit + + def _gate_applicator(self) -> object: + """ + Set up and return the gate applicator for the specified device. + + This method temporarily sets the backend by calling the appropriate + gate applicator based on the specified device properties. + + Returns + ------- + object + The gate applicator object associated with the specified device. + """ + + device_name = self.device.device_name + backend_dict = self.backend_properties.__dict__.copy() + self.backend = get_qaoa_backend( + qaoa_descriptor=self.qaoa_descriptor, + device = self.device, + **backend_dict,) + gate_applicator = self.backend.gate_applicator + + return(gate_applicator) + +class GivensRotationGateMap(GateMap): + """ + Returns the gate applicator object for the specified quantum backend. + + This method configures the quantum backend based on the device. + It then retrieves and returns the gate applicator, which is used to apply quantum gates in the + circuit construction process. + + Returns + ------- + object + An object representing the gate applicator for the current backend. + """ + + def __init__(self, qubit_1: int, qubit_2: int, angle: float): + super().__init__(qubit_1) + self.qubit_2 = qubit_2 + self.angle = angle + self.gate_label = GateMapLabel(n_qubits=2, gatemap_type=GateMapType.FIXED) + + @property + def _decomposition_standard(self) -> List[Tuple]: + return[ + (RZ, [self.qubit_2, RotationAngle(lambda x: x, self.gate_label, np.pi / 2)]), + (RZ, [self.qubit_1, RotationAngle(lambda x: x, self.gate_label, np.pi / 2)]), + (RY, [self.qubit_1, RotationAngle(lambda x: x, self.gate_label, np.pi / 2)]), + (X, [self.qubit_1]), + (CX, [self.qubit_1, self.qubit_2]), + (RY, [self.qubit_2, RotationAngle(lambda x: x, self.gate_label, self.angle)]), + (RY, [self.qubit_1, RotationAngle(lambda x: x, self.gate_label, self.angle)]), + (CX, [self.qubit_1, self.qubit_2]), + (RY, [self.qubit_1, RotationAngle(lambda x: x, self.gate_label, np.pi / 2)]), + (X, [self.qubit_1]), + (RZ, [self.qubit_2, RotationAngle(lambda x: x, self.gate_label, -np.pi / 2)]), + (RZ, [self.qubit_1, RotationAngle(lambda x: x, self.gate_label, -np.pi / 2)]), + ] + +class FermiBackendProperties(WorkflowProperties): + """ + Tunable backend properties for FQAOA circuit to be specified by the user. + + The only difference with `BackendProperties` is that `init_hadamard` and `prepend_state` + are constrained in FQAOA. Because the initial state for FQAOA is specified within the `FQAOA.compile` method, + and therefore `init_hadamard` and `prepend_state` should not be set here. + Providing a value for this property will raise a ValueError. + """ + + def __init__( + self, + prepend_state: Optional[ + Union[QuantumCircuitBase, List[complex], np.ndarray] + ] = None, + append_state: Optional[ + Union[QuantumCircuitBase, np.ndarray] + ] = None, + init_hadamard: bool = False, + n_shots: int = 100, + cvar_alpha: float = 1, + noise_model=None, + initial_qubit_mapping: Optional[Union[List[int], np.ndarray]] = None, + qiskit_simulation_method: Optional[str] = None, + qiskit_optimization_level: Optional[int] = None, + seed_simulator: Optional[int] = None, + active_reset: Optional[bool] = None, + rewiring: Optional[str] = None, + disable_qubit_rewiring: Optional[bool] = None, + ): + if init_hadamard: + raise ValueError("In FQAOA, init_hadamard is not recognized.") + if prepend_state is not None: + raise ValueError("In FQAOA, prepend_state is not recognized.") + self.init_hadamard = False + self.prepend_state = None + self.append_state = append_state + self.n_shots = n_shots + self.cvar_alpha = cvar_alpha + self.noise_model = noise_model + self.initial_qubit_mapping = initial_qubit_mapping + self.seed_simulator = seed_simulator + self.qiskit_simulation_method = qiskit_simulation_method + self.qiskit_optimization_level = qiskit_optimization_level + self.active_reset = active_reset + self.rewiring = rewiring + self.disable_qubit_rewiring = disable_qubit_rewiring + +class FermiCircuitProperties(WorkflowProperties): + """ + Tunable circuit properties of the FQAOA circuit to be specified by the user + + The only difference with `CircuitProperties` is that mixer_hamiltonian is limited to "xy" + and mixer_qubit connetivity is limited to "cyclic" or "chain". + """ + + def __init__( + self, + param_type: str = "standard", + init_type: str = "ramp", + qubit_register: List = [], + p: int = 1, + q: Optional[int] = 1, + annealing_time: Optional[float] = None, + linear_ramp_time: Optional[float] = None, + variational_params_dict: Optional[dict] = {}, + mixer_hamiltonian: Optional[str] = "xy", + mixer_qubit_connectivity: Optional[str] = "cyclic", + mixer_coeffs: Optional[float] = None, + seed: Optional[int] = None, + ): + self.param_type = param_type + self.init_type = init_type + self.qubit_register = qubit_register + self.p = p + self.q = ( + q + if param_type.lower() in ["fourier", "fourier_extended", "fourier_w_bias"] + else None + ) + self.variational_params_dict = variational_params_dict + self.annealing_time = ( + annealing_time if annealing_time is not None else 0.7 * self.p + ) + self.linear_ramp_time = ( + linear_ramp_time if linear_ramp_time is not None else 0.7 * self.p + ) + if mixer_hamiltonian.lower() not in ALLOWED_MIXERS: + raise ValueError(f"In FQAOA, mixer_hamiltonian {mixer_hamiltonian.lower()} is not recognized.") + if mixer_qubit_connectivity not in ALLOWED_LATTICE: + raise ValueError(f"In FQAOA, mixer_qubit_connectivity {mixer_qubit_connectivity} is not recognized.") + self.mixer_hamiltonian = mixer_hamiltonian + self.mixer_qubit_connectivity = mixer_qubit_connectivity + self.mixer_coeffs = mixer_coeffs + self.seed = seed + + @property + def param_type(self): + return self._param_type + + @param_type.setter + def param_type(self, value): + if value not in ALLOWED_PARAM_TYPES: + raise ValueError( + f"param_type {value} is not recognized. Please use {ALLOWED_PARAM_TYPES}" + ) + self._param_type = value + + @property + def init_type(self): + return self._init_type + + @init_type.setter + def init_type(self, value): + if value not in ALLOWED_INIT_TYPES: + raise ValueError( + f"init_type {value} is not recognized. Please use {ALLOWED_INIT_TYPES}" + ) + self._init_type = value + + @property + def mixer_hamiltonian(self): + return self._mixer_hamiltonian + + @mixer_hamiltonian.setter + def mixer_hamiltonian(self, value): + if value not in ALLOWED_MIXERS: + raise ValueError( + f"mixer_hamiltonian {value} is not recognized. Please use {ALLOWED_MIXERS}" + ) + self._mixer_hamiltonian = value + + @property + def p(self): + return self._p + + @p.setter + def p(self, value): + if value <= 0: + raise ValueError( + f"Number of layers `p` cannot be smaller or equal to zero. Value {value} was provided" + ) + self._p = value + + @property + def annealing_time(self): + return self._annealing_time + + @annealing_time.setter + def annealing_time(self, value): + if value <= 0: + raise ValueError( + f"The annealing time `annealing_time` cannot be smaller or equal to zero. Value {value} was provided" + ) + self._annealing_time = value diff --git a/src/openqaoa-core/openqaoa/problems/portfoliooptimization.py b/src/openqaoa-core/openqaoa/problems/portfoliooptimization.py index 41e9f27b..9dac80a1 100644 --- a/src/openqaoa-core/openqaoa/problems/portfoliooptimization.py +++ b/src/openqaoa-core/openqaoa/problems/portfoliooptimization.py @@ -104,7 +104,7 @@ def docplex_model(self): # Specific the objective of the # portfolio optimization function - objective_function = -np.array(self.mu) @ x + x.T @ np.array(self.sigma) @ x + objective_function = -np.array(self.mu) @ x + self.risk_factor * x.T @ np.array(self.sigma) @ x # For this problem it aims to maximize the profit # of those assets minimizing the risk of the investment diff --git a/src/openqaoa-core/openqaoa/utilities.py b/src/openqaoa-core/openqaoa/utilities.py index 145a3748..bfeee5a3 100644 --- a/src/openqaoa-core/openqaoa/utilities.py +++ b/src/openqaoa-core/openqaoa/utilities.py @@ -1984,3 +1984,43 @@ def to_bin(number, n_qubits): ) / np.sqrt(len(wavefn_locs)) return wavefunction + +def is_close_statevector(statevector1: np.ndarray, statevector2: np.ndarray) -> bool: + """ + Checks if statevector1 can be expressed as e^(i*theta) * statevector2. + Both statevector1 and statevector2 must be numpy arrays of the same size. + """ + + # Check for size consistency + if statevector1.shape != statevector2.shape: + raise ValueError("The statevectors must have the same shape.") + + # Threshold for considering a value to be zero + tolerance = 1e-10 + + # Check if statevector1 is approximately zero where statevector2 is approximately zero + zero_mask_0 = np.isclose(statevector1, 0, atol=tolerance) + zero_mask_1 = np.isclose(statevector2, 0, atol=tolerance) + + if np.all(zero_mask_1 == zero_mask_0): + # Create a mask to avoid division by zero + non_zero_mask = ~np.isclose(statevector2, 0, atol=tolerance) + + # Compute the ratio with the mask applied + ratio = statevector1[non_zero_mask] / statevector2[non_zero_mask] + + # Verify if all ratios have the same phase angle + theta_calculated = np.angle(ratio) + theta_adjusted = (theta_calculated + np.pi) % (2 * np.pi) - np.pi + consistent_phase = np.allclose(theta_adjusted, theta_adjusted[0]) + + # Verify if all absolute values are same + absolute_ratio = np.abs(statevector1[non_zero_mask] / statevector2[non_zero_mask]) + consistent_magnitude = np.allclose(absolute_ratio, absolute_ratio[0]) + + if consistent_phase and consistent_magnitude: + return True + + return False + return False + diff --git a/src/openqaoa-core/tests/test_analytical_simulator.py b/src/openqaoa-core/tests/test_analytical_simulator.py index f990653c..7cf42917 100644 --- a/src/openqaoa-core/tests/test_analytical_simulator.py +++ b/src/openqaoa-core/tests/test_analytical_simulator.py @@ -2,7 +2,7 @@ import numpy as np from openqaoa.backends.qaoa_analytical_sim import QAOABackendAnalyticalSimulator -from openqaoa.algorithms import QAOA, RQAOA +from openqaoa.algorithms import QAOA, RQAOA, FQAOA from openqaoa.problems import MaximumCut from openqaoa.backends.qaoa_device import create_device from openqaoa.utilities import ( @@ -185,6 +185,28 @@ def test_end_to_end_rqaoa(self): assert opt_solution_string == "01011010" + def test_fqaoa_raises_value_error_with_analytical_simulator(self): + """ + Test to ensure that initializing FQAOA with the 'analytical_simulator' + raises a ValueError. + """ + # Create a 3-regular weighted graph and a qubo problem + g = random_k_regular_graph( + degree=3, nodes=range(8), seed=2642, weighted=True, biases=False + ) + maxcut_qubo = MaximumCut(g).qubo + + # Define the device to be the analytical simulator + device = create_device(location="local", name="analytical_simulator") + + # Define the FQAOA object and set its params + with self.assertRaises(ValueError): + fqaoa = FQAOA(device) + + with self.assertRaises(ValueError): + fqaoa = FQAOA() + fqaoa.set_device(device) + def test_exact_solution(self): """ Testing the exact solution which is a property of every backend. diff --git a/src/openqaoa-core/tests/test_fqaoa.py b/src/openqaoa-core/tests/test_fqaoa.py new file mode 100644 index 00000000..b4e7b9a0 --- /dev/null +++ b/src/openqaoa-core/tests/test_fqaoa.py @@ -0,0 +1,170 @@ +import unittest +import copy +import numpy as np + +from openqaoa.utilities import is_close_statevector + +from openqaoa.algorithms.fqaoa.fqaoa_utils import ( + get_analytical_fermi_orbitals, + get_fermi_orbitals, + get_givens_rotation_angle, + get_statevector, +) + +""" +Unittest based testing of current implementation of the FQAOA Algorithm +""" + +class TestFQAOAUtils(unittest.TestCase): + + def setUp(self): + self.valid_params = [ + {"n_qubits": 5, "n_fermions": 3, "lattice": "cyclic", "hopping": 1.0}, # cyclic lattice with hopping > 0 and odd n_femrimons + {"n_qubits": 5, "n_fermions": 3, "lattice": "cyclic", "hopping": -1.0}, # cyclic lattice with hopping < 0 and odd n_femrimons + {"n_qubits": 5, "n_fermions": 2, "lattice": "cyclic", "hopping": 1.0}, # cyclic lattice with hopping > 0 and even n_femrimons + {"n_qubits": 5, "n_fermions": 2, "lattice": "cyclic", "hopping": -1.0}, # cyclic lattice with hopping < 0 and even n_femrimons + {"n_qubits": 5, "n_fermions": 3, "lattice": "chain", "hopping": 1.0}, # chain lattice with hopping > 0 and odd n_femrimons + {"n_qubits": 5, "n_fermions": 3, "lattice": "chain", "hopping": -1.0}, # chain lattice with hopping < 0 and odd n_femrimons + {"n_qubits": 5, "n_fermions": 2, "lattice": "chain", "hopping": 1.0}, # chain lattice with hopping > 0 and even n_femrimons + {"n_qubits": 5, "n_fermions": 2, "lattice": "chain", "hopping": -1.0}, # chain lattice with hopping < 0 and even n_femrimons + ] + + self.invalid_params = [ + {"n_qubits": 5, "n_fermions": 2, "lattice": "cyclic", "hopping": 0.0}, # hopping: 0.0 + {"n_qubits": 5, "n_fermions": 2, "lattice": "cyclic", "hopping": 0.0}, # hopping: 0.0 + {"n_qubits": 3, "n_fermions": 5, "lattice": "cyclic", "hopping": 1.0}, # n_fermions > n_qubits + {"n_qubits": 3, "n_fermions": 5, "lattice": "chain", "hopping": 1.0}, # n_fermions > n_qubits + {"n_qubits": 5, "n_fermions": 2, "lattice": "star", "hopping": 1.0}, # unsupported lattice + {"n_qubits": 5, "n_fermions": 2, "lattice": "full", "hopping": 1.0}, # unsupported lattice + ] + + def test_fermi_orbitals_equivalence_to_statevector(self): + """ + Test that get_analytical_fermi_orbitals and get_fermi_orbitals are equivalent by outputting statevector. + + The test consists of generating analytical and numerical free fermi orbitals + and checking that the respective state vectors are numerically consistent. + """ + + for n_qubits, n_fermions, lattice, hopping in [ + (5, 3, "cyclic", 1.0), + (5, 2, "cyclic", 1.0) + ]: + analytical_fermi_orbitals = get_analytical_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + fermi_orbitals = get_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + self.assertTrue(is_close_statevector(get_statevector(analytical_fermi_orbitals), get_statevector(fermi_orbitals)), + "statevector[0] cannot be expressed as e^(i*theta) * statevector[1].") + + def test_givens_rotation_angle_length(self): + """ + Test with various valid inputs for get_givens_rotation_angle. + + This test consists of checking that the fermi orbit is diagonalised, + and the length of the givens rotation angle generated by this diagonalisation. + """ + + for params in self.valid_params: + n_qubits = params["n_qubits"] + n_fermions = params["n_fermions"] + lattice = params["lattice"] + hopping = params["hopping"] + + if lattice == "cyclic" and hopping > 0.0: + orbitals = get_analytical_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + else: + orbitals = get_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + + gtheta = get_givens_rotation_angle(orbitals) + + # Check the diagonal matrix + self.assertTrue(self.is_left_aligned_diagonal_matrix(orbitals), "orbitals are not diagonalized") + + # Check the length of the returned angles + expected_length = n_fermions * (n_qubits - n_fermions) + self.assertEqual(len(gtheta), expected_length, "Incorrect number of Givens rotation angles") + + def test_givens_rotation_to_statevector(self): + """ + Test get_givens_rotation_angle by outputting statevector. + + The test consists of generating a givens rotation angle by diagonalising the Fermi orbitals + and confirming that the original Fermi orbit is recovered by applying a givens inverse rotation to the diagonalised Fermi orbit. + """ + + for params in self.valid_params: + n_qubits = params["n_qubits"] + n_fermions = params["n_fermions"] + lattice = params["lattice"] + hopping = params["hopping"] + + if lattice == "cyclic" and hopping > 0.0: + orbitals0 = get_analytical_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + else: + orbitals0 = get_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + + orbitals1 = copy.deepcopy(orbitals0) + gtheta = get_givens_rotation_angle(orbitals1) + + # Set an localized n_fermions state. + matrix = np.zeros((n_fermions, n_qubits), dtype=float) + for i in range(min(n_fermions, n_qubits)): + matrix[i, i] = 1 + + # perform givens rotations + it = (n_qubits-n_fermions)*n_fermions + for irow in range(n_fermions-1, -1, -1): + for icol in range(irow+1, n_qubits-n_fermions+irow+1): + it = it-1 + angle = gtheta[it] + for ik in range(n_fermions): + temp = matrix[ik][icol - 1] + matrix[ik][icol - 1] = temp * np.cos(-angle) - matrix[ik][icol] * np.sin(-angle) + matrix[ik][icol] = temp * np.sin(-angle) + matrix[ik][icol] * np.cos(-angle) + self.assertTrue(is_close_statevector(get_statevector(orbitals0), get_statevector(matrix)), + "statevector[0] cannot be expressed as e^(i*theta) * statevector[1].") + + # exception handling + def test_fermi_analytical_orbital_invalid_input(self): + """Test with various invalid inputs for get_fermi_analytical_orbitals.""" + + for n_qubits, n_fermions, lattice, hopping in [ + (3, 5, "cyclic", 1.0), # n_fermions > n_qubits + (5, 3, "cyclic", -1.0), # hopping < 0.0 + (5, 3, "cyclic", 0.0), # hopping = 0.0 + (5, 3, "cyc", 1.0), # lattice = "cyc" + ]: + with self.assertRaises(ValueError): + get_analytical_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + + def test_fermi_orbital_invalid_input(self): + """Test with various invalid inputs for get_fermi_orbitals.""" + + for params in self.invalid_params: + n_qubits = params["n_qubits"] + n_fermions = params["n_fermions"] + lattice = params["lattice"] + hopping = params["hopping"] + + with self.assertRaises(ValueError): + get_fermi_orbitals(n_qubits, n_fermions, lattice, hopping) + + @staticmethod + def is_left_aligned_diagonal_matrix(matrix: np.ndarray) -> bool: + """ + Checks if the matrix has the most left-aligned diagonal elements as either 1 or -1, + and all other elements as 0. + """ + + rows, cols = matrix.shape + for i in range(rows): + for j in range(cols): + if i == j: # For diagonal elements + if not np.isclose(abs(matrix[i, j]), 1): + return False + else: # For non-diagonal elements + if not np.isclose(matrix[i, j], 0): + return False + return True + +if __name__ == '__main__': + unittest.main() diff --git a/src/openqaoa-core/tests/test_portfolio_optimization.py b/src/openqaoa-core/tests/test_portfolio_optimization.py index b0116ada..e7c4c138 100644 --- a/src/openqaoa-core/tests/test_portfolio_optimization.py +++ b/src/openqaoa-core/tests/test_portfolio_optimization.py @@ -43,22 +43,28 @@ def test_portfoliooptimization_terms_weights_constant(self): """Test terms,weights,constant of QUBO generated by PortfolioOptimization class""" po_terms = [[0, 1], [0, 2], [1, 2], [0], [1], [2]] - po_weights = [0.505, 0.505, 0.505, 0.535, 0.535, 0.535] - po_constant = 0.8799999999999999 - mu = [0.1, 0.1, 0.1] - sigma = [[0.01, 0.01, 0.01], [0.01, 0.01, 0.01], [0.01, 0.01, 0.01]] + mu0 = 0.1 + sigma0 = 0.01 + mu = [mu0] * 3 + sigma = [[sigma0] * 3] * 3 risk_factor = 0.1 budget = 2 penalty = 1 + # w1, w2, po_constant are derived analytically + w1 = risk_factor*sigma0/2.0 + penalty/2.0 + w2 = (mu0 - risk_factor*sigma0*3)/2.0 + (penalty*budget - penalty*3/2.0) + po_weights = [w1]*3 + [w2]*3 + po_constant = (risk_factor*sigma0*12.0/4.0 - mu0*3/2.0) + (penalty*12/4.0 - penalty*budget*3 + penalty*budget**2) + qubo = PortfolioOptimization(mu, sigma, risk_factor, budget, penalty).qubo terms, weights = qubo.terms, qubo.weights constant = qubo.constant - - self.assertEqual(weights, po_weights) + for w, pw in zip(weights, po_weights): + self.assertAlmostEqual(w, pw, places=10) self.assertEqual(terms, po_terms) - self.assertEqual(po_constant, constant) + self.assertAlmostEqual(po_constant, constant, places=10) def test_portfoliooptimization_random_instance(self): """Test random instance method of PortfolioOptimization problem class""" diff --git a/src/openqaoa-core/tests/test_workflows.py b/src/openqaoa-core/tests/test_workflows.py index 661a5cab..12fe6b4b 100644 --- a/src/openqaoa-core/tests/test_workflows.py +++ b/src/openqaoa-core/tests/test_workflows.py @@ -7,7 +7,7 @@ import datetime from copy import deepcopy -from openqaoa import QAOA, RQAOA +from openqaoa import QAOA, RQAOA, FQAOA from openqaoa.problems import NumberPartition from openqaoa.algorithms import QAOAResult, RQAOAResult from openqaoa.algorithms.baseworkflow import Workflow @@ -16,6 +16,7 @@ XY_mixer_hamiltonian, is_valid_uuid, ground_state_hamiltonian, + is_close_statevector, ) from openqaoa.algorithms.workflow_properties import ( BackendProperties, @@ -23,6 +24,10 @@ CircuitProperties, ) from openqaoa.algorithms.rqaoa.rqaoa_workflow_properties import RqaoaParameters +from openqaoa.algorithms.fqaoa.fqaoa_utils import ( + get_analytical_fermi_orbitals, + get_statevector, +) from openqaoa.backends import create_device, DeviceLocal from openqaoa.backends.cost_function import cost_function @@ -39,7 +44,7 @@ ) from openqaoa.backends import QAOAvectorizedBackendSimulator from openqaoa.backends.basebackend import QAOABaseBackendStatevector -from openqaoa.problems import MinimumVertexCover, QUBO, MaximumCut +from openqaoa.problems import MinimumVertexCover, PortfolioOptimization, QUBO, MaximumCut from openqaoa.optimizers.qaoa_optimizer import available_optimizers from openqaoa.optimizers.training_vqa import ( ScipyOptimizer, @@ -51,7 +56,6 @@ PARAMS_CLASSES_MAPPER, ) - def _compare_qaoa_results(dict_old, dict_new): for key in dict_old.keys(): if key == "cost_hamiltonian": # CHECK WHAT DO WITH THIS @@ -110,7 +114,6 @@ def _test_keys_in_dict(obj, expected_keys): for item in obj: _test_keys_in_dict(item, expected_keys) - class TestingVanillaQAOA(unittest.TestCase): """ Unit test based testing of the QAOA workflow class @@ -340,7 +343,7 @@ def test_set_circuit_properties_qaoa_descriptor_mixer_xy(self): def test_set_circuit_properties_variate_params(self): """ - Ensure that the Varitional Parameter Object created based on the input string , param_type, is correct. + Ensure that the Variational Parameter Object created based on the input string , param_type, is correct. TODO: Check if q=None is the appropriate default. """ @@ -377,7 +380,7 @@ def test_set_circuit_properties_variate_params(self): def test_set_circuit_properties_change(self): """ - Ensure that once a property has beefn changed via set_circuit_properties. + Ensure that once a property has been changed via set_circuit_properties. The attribute has been appropriately updated. Updating all attributes at the same time. """ @@ -1129,7 +1132,7 @@ def test_qaoa_from_dict_and_load(self): nw.generators.fast_gnp_random_graph(n=6, p=0.6, seed=42) ).qubo - # run rqaoa with different devices, and save the objcets in a list + # run rqaoa with different devices, and save the objects in a list qaoas = [] for device in [ create_device(location="local", name="vectorized"), @@ -1401,28 +1404,28 @@ def test_qaoa_evaluate_circuit_shot(self): q.set_device(device) q.set_circuit_properties(p=3) - # try to evaluate the circuit before compiling - error = False - try: - q.evaluate_circuit() - except Exception: - error = True - assert ( - error - ), f"param_type={param_type}. `evaluate_circuit` should raise an error if the circuit is not compiled" + with self.assertRaises(ValueError) as cm: + q.evaluate_circuit([1, 2, 1, 2, 1, 2]) + self.assertIn( + "Please compile the QAOA before optimizing it!", + str(cm.exception), + ) # compile and evaluate the circuit, and check that the result is correct q.compile(problem) result = q.evaluate_circuit([1, 2, 1, 2, 1, 2]) - assert isinstance( - result["measurement_results"], dict - ), "When using a shot-based simulator, `evaluate_circuit` should return a dict of counts" - assert ( - abs(result["cost"]) >= 0 - ), "When using a shot-based simulator, `evaluate_circuit` should return a cost" - assert ( - abs(result["uncertainty"]) > 0 - ), "When using a shot-based simulator, `evaluate_circuit` should return an uncertainty" + self.assertIsInstance( + result["measurement_results"], dict, + "When using a shot-based simulator, evaluate_circuit should return a dict of counts" + ) + self.assertTrue( + abs(result["cost"]) >= 0, + "When using a shot-based simulator, evaluate_circuit should return a cost" + ) + self.assertTrue( + abs(result["uncertainty"]) > 0, + "When using a shot-based simulator, evaluate_circuit should return an uncertainty" + ) cost = cost_function( result["measurement_results"], @@ -1435,12 +1438,14 @@ def test_qaoa_evaluate_circuit_shot(self): q.backend.cvar_alpha, ) uncertainty = np.sqrt(cost_sq - cost**2) - assert ( - np.round(cost, 12) == result["cost"] - ), "When using a shot-based simulator, `evaluate_circuit` not returning the correct cost" - assert ( - np.round(uncertainty, 12) == result["uncertainty"] - ), "When using a shot-based simulator, `evaluate_circuit` not returning the correct uncertainty" + self.assertTrue( + np.round(cost, 12) == result["cost"], + "When using a shot-based simulator, evaluate_circuit not returning the correct cost" + ) + self.assertTrue( + np.round(uncertainty, 12) == result["uncertainty"], + "When using a shot-based simulator, evaluate_circuit not returning the correct uncertainty" + ) def test_qaoa_evaluate_circuit_analytical_sim(self): # problem @@ -1726,6 +1731,590 @@ def test_change_properties_after_compilation(self): with self.assertRaises(ValueError): r.set_rqaoa_parameters(rqaoa_type="adaptive", n_cutoff=3, n_steps=3) +class TestingFQAOA(unittest.TestCase): + """ + Unit test based testing of the QAOA workflow class + """ + + def test_vanilla_fqaoa_default_values(self): + fqaoa = FQAOA() + assert fqaoa.circuit_properties.p == 1 + assert fqaoa.circuit_properties.param_type == "standard" + assert fqaoa.circuit_properties.init_type == "ramp" + assert fqaoa.circuit_properties.mixer_hamiltonian == "xy" + assert fqaoa.circuit_properties.mixer_qubit_connectivity == "cyclic" + assert fqaoa.device.device_location == "local" + assert fqaoa.device.device_name == "vectorized" + + def test_end_to_end_vectorized(self): + num_assets, budget = 4, 2 + po = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + fqaoa = FQAOA() + fqaoa.set_classical_optimizer(optimization_progress=True) + fqaoa.compile(po, budget) + fqaoa.optimize() + result = fqaoa.result.most_probable_states["solutions_bitstrings"][0] + assert "1010" == result + + def test_set_device_local(self): + """ " + Check that all local devices are correctly initialised + """ + fqaoa = FQAOA() + for d in fqaoa.local_simulators: + if d == 'analytical_simulator': + continue # Skip this + fqaoa.set_device(create_device(location="local", name=d)) + assert type(fqaoa.device) == DeviceLocal + assert fqaoa.device.device_name == d + assert fqaoa.device.device_location == "local" + + def test_compile_before_optimise(self): + """ + Assert that compilation has to be called before optimisation + """ + + fqaoa = FQAOA() + fqaoa.set_classical_optimizer(optimization_progress=True) + + self.assertRaises(ValueError, lambda: fqaoa.optimize()) + + def test_cost_hamil(self): + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + test_hamil = Hamiltonian.classical_hamiltonian( + terms=problem.terms, + coeffs=problem.weights, + constant=problem.constant, + ) + + fqaoa = FQAOA() + + fqaoa.compile(problem=problem, n_fermions=budget) + + self.assertEqual(fqaoa.cost_hamil.expression, test_hamil.expression) + self.assertEqual( + fqaoa.qaoa_descriptor.cost_hamiltonian.expression, test_hamil.expression + ) + + def test_set_circuit_properties_fourier_q(self): + """ + The value of q should be None if the param_type used is not fourier. + Else if param_type is fourier, fourier_extended or fourier_w_bias, it + should be the value of q, if it is provided. + """ + + fourier_param_types = ["fourier", "fourier_extended", "fourier_w_bias"] + + fqaoa = FQAOA() + + for each_param_type in fourier_param_types: + fqaoa.set_circuit_properties(param_type=each_param_type, q=1) + self.assertEqual(fqaoa.circuit_properties.q, 1) + + fqaoa.set_circuit_properties(param_type="standard", q=1) + + self.assertEqual(fqaoa.circuit_properties.q, None) + + def test_set_circuit_properties_annealing_time_linear_ramp_time(self): + """ + Check that linear_ramp_time and annealing_time are updated appropriately + as the value of p is changed. + """ + + fqaoa = FQAOA() + + fqaoa.set_circuit_properties(p=3) + + self.assertEqual(fqaoa.circuit_properties.annealing_time, 0.7 * 3) + self.assertEqual(fqaoa.circuit_properties.linear_ramp_time, 0.7 * 3) + + fqaoa.set_circuit_properties(p=2) + + self.assertEqual(fqaoa.circuit_properties.annealing_time, 0.7 * 2) + self.assertEqual(fqaoa.circuit_properties.linear_ramp_time, 0.7 * 2) + + def test_set_circuit_properties_fqaoa_descriptor_mixer_x(self): + """ + Checks if setting the incorrect mixer_hamiltonian 'x' raises a ValueError. + """ + + nodes = 6 + edge_probability = 0.6 + g = nw.generators.fast_gnp_random_graph(n=nodes, p=edge_probability) + problem = MinimumVertexCover(g, field=1.0, penalty=10) + + fqaoa = FQAOA() + self.assertRaises( + ValueError, lambda: fqaoa.set_circuit_properties(mixer_hamiltonian="x") + ) + + def test_set_circuit_properties_fqaoa_descriptor_mixer_xy(self): + """ + Checks if the XY mixer created by the XY_mixer_hamiltonian method + and the automated methods in workflows do the same thing. + + Depending on the qubit connectivity selected. (chain, full or star) + For each pair of connected qubits, there should be 1 RXXGateMap and RYYGateMap per layer of p. + """ + + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + qubit_connectivity_name = ["cyclic", "chain"] + + for i in range(2): + fqaoa = FQAOA() + fqaoa.set_circuit_properties( + mixer_hamiltonian="xy", + mixer_qubit_connectivity=qubit_connectivity_name[i], + p=2, + ) + + fqaoa.compile(problem, budget, hopping = -1.0) + + self.assertEqual(type(fqaoa.qaoa_descriptor), QAOADescriptor) + self.assertEqual(fqaoa.qaoa_descriptor.p, 2) + + mixer_hamil = XY_mixer_hamiltonian( + n_qubits=num_assets, qubit_connectivity=qubit_connectivity_name[i] + ) + + self.assertEqual(fqaoa.mixer_hamil.expression, mixer_hamil.expression) + + self.assertEqual(len(fqaoa.qaoa_descriptor.mixer_qubits_singles), 0) + for i in range(0, len(fqaoa.qaoa_descriptor.mixer_qubits_pairs), 2): + self.assertEqual(fqaoa.qaoa_descriptor.mixer_qubits_pairs[i], "RXXGateMap") + self.assertEqual( + fqaoa.qaoa_descriptor.mixer_qubits_pairs[i + 1], "RYYGateMap" + ) + + def test_set_circuit_properties_variate_params(self): + """ + Ensure that the Variational Parameter Object created based on the input string , param_type, is correct. + + TODO: Check if q=None is the appropriate default. + """ + + param_type_names = [ + "standard", + "standard_w_bias", + "extended", + "fourier", + "fourier_extended", + "fourier_w_bias", + ] + object_types = [ + QAOAVariationalStandardParams, + QAOAVariationalStandardWithBiasParams, + QAOAVariationalExtendedParams, + QAOAVariationalFourierParams, + QAOAVariationalFourierExtendedParams, + QAOAVariationalFourierWithBiasParams, + ] + + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + for i in range(len(object_types)): + fqaoa = FQAOA() + fqaoa.set_circuit_properties(param_type=param_type_names[i], q=1) + + fqaoa.compile(problem=problem, n_fermions=budget) + + self.assertEqual(type(fqaoa.variate_params), object_types[i]) + + def test_set_circuit_properties_change(self): + """ + Ensure that once a property has been changed via set_circuit_properties. + The attribute has been appropriately updated. + Updating all attributes at the same time. + """ + + fqaoa = FQAOA() + + update_pairings = { + "param_type": "fourier", + "init_type": "rand", + "qubit_register": [0, 1], + "p": 2, + "q": 2, + "annealing_time": 1.0, + "linear_ramp_time": 1.0, + "variational_params_dict": {"key": "value"}, + "mixer_hamiltonian": "xy", + "mixer_qubit_connectivity": "chain", + "mixer_coeffs": [0.1, 0.2], + "seed": 45, + } + + fqaoa.set_circuit_properties(**update_pairings) + + for each_key, each_value in update_pairings.items(): + self.assertEqual(getattr(fqaoa.circuit_properties, each_key), each_value) + + def test_set_circuit_properties_rejected_values(self): + """ + Some properties of CircuitProperties Object return a ValueError + if the specified property has not been whitelisted in the code. + This checks that the ValueError is raised if the argument is not whitelisted. + """ + + fqaoa = FQAOA() + + self.assertRaises( + ValueError, lambda: fqaoa.set_circuit_properties(param_type="wrong name") + ) + self.assertRaises( + ValueError, lambda: fqaoa.set_circuit_properties(init_type="wrong name") + ) + self.assertRaises( + ValueError, lambda: fqaoa.set_circuit_properties(mixer_hamiltonian="wrong name") + ) + self.assertRaises(ValueError, lambda: fqaoa.set_circuit_properties(p=-1)) + + def test_set_backend_properties_change(self): + """ + Ensure that once a property has been changed via set_backend_properties. + The attribute has been appropriately updated. + Updating all attributes at the same time. + """ + + default_pairings = { + "n_shots": 100, + "cvar_alpha": 1.0, + } + + fqaoa = FQAOA() + + for each_key, each_value in default_pairings.items(): + self.assertEqual(getattr(fqaoa.backend_properties, each_key), each_value) + + update_pairings = { + "n_shots": 10, + "cvar_alpha": 0.5, + } + + fqaoa.set_backend_properties(**update_pairings) + + for each_key, each_value in update_pairings.items(): + self.assertEqual(getattr(fqaoa.backend_properties, each_key), each_value) + + def test_set_backend_init_hadamard_change(self): + """ + Ensure that an error occurs if the `init_hadmard` in the backend properties is set True. + """ + + fqaoa = FQAOA() + + self.assertFalse(fqaoa.backend_properties.init_hadamard) + + with self.assertRaises(ValueError): + fqaoa.set_backend_properties(init_hadamard=True) + + def test_set_backend_init_prepend_state_change(self): + """ + Ensure that an error occurs if the `prepend_state` is set by the set_backend method. + """ + + fqaoa = FQAOA() + + self.assertIsNone(fqaoa.backend_properties.prepend_state) + prepend_state_rand = np.random.rand(2**2) + with self.assertRaises(ValueError): + fqaoa.set_backend_properties(prepend_state=prepend_state_rand) + + def test_set_backend_properties_check_backend_vectorized(self): + """ + Check if the backend returned by set_backend_properties is correct + Based on the input device. + Also Checks if defaults from workflows are used in the backend. + """ + + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + fqaoa = FQAOA() + fqaoa.set_device(create_device(location="local", name="vectorized")) + fqaoa.compile(problem=problem, n_fermions=3) + + orbitals = get_analytical_fermi_orbitals(n_qubits=num_assets, n_fermions=budget, lattice="cyclic", hopping=1.0) + initial_state = get_statevector(orbitals) + + self.assertEqual(type(fqaoa.backend), QAOAvectorizedBackendSimulator) + + self.assertEqual(fqaoa.backend.init_hadamard, False) + + self.assertTrue(is_close_statevector(fqaoa.backend.prepend_state, initial_state), + f"statevector[0] cannot be expressed as e^(i*theta) * statevector[1].") + + self.assertEqual(fqaoa.backend.append_state, None) + self.assertEqual(fqaoa.backend.cvar_alpha, 1) + self.assertRaises(AttributeError, lambda: fqaoa.backend.n_shots) + + def test_set_backend_properties_check_backend_vectorized_w_custom(self): + """ + Check if the backend returned by set_backend_properties is correct + Based on the input device. + Uses custom values for attributes in backend_properties and checks if the + backend object responds appropriately. + """ + + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + fqaoa = FQAOA() + fqaoa.set_device(create_device(location="local", name="vectorized")) + + update_pairings = { + "n_shots": 10, + "cvar_alpha": 1, + } + + fqaoa.set_backend_properties(**update_pairings) + + fqaoa.compile(problem=problem, n_fermions=budget) + + orbitals = get_analytical_fermi_orbitals(n_qubits=num_assets, n_fermions=budget, lattice="cyclic", hopping=1.0) + initial_state = get_statevector(orbitals) + + self.assertEqual(type(fqaoa.backend), QAOAvectorizedBackendSimulator) + + self.assertTrue(is_close_statevector(fqaoa.backend.prepend_state, initial_state), + f"statevector[0] cannot be expressed as e^(i*theta) * statevector[1].") + + self.assertEqual(fqaoa.backend.cvar_alpha, 1) + + self.assertRaises(AttributeError, lambda: fqaoa.backend.n_shots) + + def test_fqaoa_evaluate_circuit(self): + """ + test the evaluate_circuit method + """ + + # problem + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + # run fqaoa with different param_type, and save the objects in a list + fqaoas = [] + for param_type in PARAMS_CLASSES_MAPPER.keys(): + fqaoa = FQAOA() + fqaoa.set_circuit_properties( + p=3, param_type=param_type, init_type="rand", seed=0 + ) + fqaoa.compile(problem=problem, n_fermions=budget) + fqaoas.append(fqaoa) + + # for each fqaoa object, test the evaluate_circuit method + for fqaoa in fqaoas: + # evaluate the circuit with random dict of params + params = { + k: np.random.rand(*v.shape) + for k, v in fqaoa.variate_params.asdict().items() + } + result = fqaoa.evaluate_circuit(params) + assert ( + abs(result["cost"]) >= 0 + ), f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` \ + should return a cost, here cost is {result['cost']}" + assert ( + abs(result["uncertainty"]) > 0 + ), f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return an uncertanty, \ + here uncertainty is {result['uncertainty']}" + assert ( + len(result["measurement_results"]) > 0 + ), f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return \ + a wavefunction when using a state-based simulator" + + # evaluate the circuit with a list of params, taking the params from the dict, + # so we should get the same result + params2 = [] + for value in params.values(): + params2 += value.flatten().tolist() + result2 = fqaoa.evaluate_circuit(params2) + for res, res2 in zip(result, result2): + self.assertAlmostEqual( + res, + res, + places=15, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return the same result \ + when passing a dict or a list of params" + ) + + # evaluate the circuit with np.ndarray of params, taking the params from the dict, + # so we should get the same result + result2 = fqaoa.evaluate_circuit(np.array(params2)) + for res, res2 in zip(result, result2): + self.assertAlmostEqual( + res, + res2, + places=15, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return the same result \ + when passing a dict or a list of params", + ) + + # evaluate the circuit with the params as a QAOAVariationalBaseParams object, + # so we should get the same result + params_obj = deepcopy(fqaoa.variate_params) + params_obj.update_from_raw(params2) + result3 = fqaoa.evaluate_circuit(params_obj) + for res, res3 in zip(result, result3): + self.assertAlmostEqual( + res, + res3, + places=15, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return the same result \ + when passing a dict or a list of params", + ) + + # run the circuit with the params manually, we should get the same result + result4 = {} + ( + result4["cost"], + result4["uncertainty"], + ) = fqaoa.backend.expectation_w_uncertainty(params_obj) + result4["measurement_results"] = fqaoa.backend.wavefunction(params_obj) + for res, res4 in zip(result, result4): + self.assertAlmostEqual( + res, + res4, + places=15, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should return the same result \ + when passing the optimized params manually", + ) + + # evaluate the circuit with a wrong input, it should raise an error + with self.assertRaises( + TypeError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error when \ + passing a wrong input" + ): + fqaoa.evaluate_circuit(1) + + # evaluate the circuit with a list longer than it should, it should raise an error + with self.assertRaises( + AssertionError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error when \ + passing a list longer than it should" + ): + fqaoa.evaluate_circuit(params2 + [1]) + + # evaluate the circuit with a list shorter than it should, it should raise an error + with self.assertRaises( + AssertionError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error when \ + passing a list shorter than it should" + ): + fqaoa.evaluate_circuit(params2[:-1]) + + # evaluate the circuit with a dict with a wrong key, it should raise an error + with self.assertRaises( + KeyError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error \ + when passing a dict with a wrong key" + ): + fqaoa.evaluate_circuit({**params, "wrong_key": 1}) + + # evaluate the circuit with a dict with a value longer than it should, it should raise an error + with self.assertRaises( + ValueError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error when \ + passing a dict with a value longer than it should" + ): + fqaoa.evaluate_circuit( + {**params, list(params.keys())[0]: np.random.rand(40)} + ) + + # evaluate the circuit without passing any param, it should raise an error + with self.assertRaises( + TypeError, + msg=f"param_type={fqaoa.circuit_properties.param_type}. `evaluate_circuit` should raise an error when \ + not passing any param", + ): + fqaoa.evaluate_circuit() + + def test_fqaoa_evaluate_circuit_shot(self): + # problem + num_assets, budget = 5, 3 + problem = PortfolioOptimization.random_instance(num_assets=num_assets, budget=budget).qubo + + if "qiskit.qasm_simulator" not in SUPPORTED_LOCAL_SIMULATORS: + self.skipTest( + reason="Qiskit QASM Simulator is not available. Please install the qiskit plugin: openqaoa-qiskit." + ) + else: + # check that it works with shots + fqaoa = FQAOA() + device = create_device(location="local", name="qiskit.qasm_simulator") + fqaoa.set_device(device) + fqaoa.set_circuit_properties(p=3) + + with self.assertRaises(ValueError) as cm: + fqaoa.evaluate_circuit([1, 2, 1, 2, 1, 2]) + self.assertIn( + "Please compile the FQAOA before optimizing it!", + str(cm.exception), + ) + + # compile and evaluate the circuit, and check that the result is correct + fqaoa.compile(problem, budget) + result = fqaoa.evaluate_circuit([1, 2, 1, 2, 1, 2]) + self.assertIsInstance( + result["measurement_results"], dict, + "When using a shot-based simulator, evaluate_circuit should return a dict of counts" + ) + self.assertTrue( + abs(result["cost"]) >= 0, + "When using a shot-based simulator, evaluate_circuit should return a cost" + ) + self.assertTrue( + abs(result["uncertainty"]) > 0, + "When using a shot-based simulator, evaluate_circuit should return an uncertainty" + ) + + cost = cost_function( + result["measurement_results"], + fqaoa.backend.qaoa_descriptor.cost_hamiltonian, + fqaoa.backend.cvar_alpha, + ) + cost_sq = cost_function( + result["measurement_results"], + fqaoa.backend.qaoa_descriptor.cost_hamiltonian.hamiltonian_squared, + fqaoa.backend.cvar_alpha, + ) + uncertainty = np.sqrt(cost_sq - cost**2) + self.assertTrue( + np.round(cost, 12) == result["cost"], + "When using a shot-based simulator, evaluate_circuit not returning the correct cost" + ) + self.assertTrue( + np.round(uncertainty, 12) == result["uncertainty"], + "When using a shot-based simulator, evaluate_circuit not returning the correct uncertainty" + ) + + def test_change_properties_after_compilation(self): + device = create_device(location="local", name="vectorized") + fqaoa = FQAOA() + fqaoa.compile(QUBO.random_instance(4), 2) + state_rand = np.random.rand(2**2) + + with self.assertRaises(ValueError): + fqaoa.set_device(device) + with self.assertRaises(ValueError): + fqaoa.set_circuit_properties( + p=1, param_type="standard", init_type="rand", + ) + with self.assertRaises(ValueError): + fqaoa.set_backend_properties(append_state=state_rand) + with self.assertRaises(ValueError): + fqaoa.set_backend_properties(prepend_state=state_rand) + with self.assertRaises(ValueError): + fqaoa.set_classical_optimizer( + maxiter=100, method="vgd", jac="finite_difference" + ) if __name__ == "__main__": unittest.main()