diff --git a/.github/workflows/functional_tests.yaml b/.github/workflows/functional_tests.yaml index cde7e8ba..9b913d09 100644 --- a/.github/workflows/functional_tests.yaml +++ b/.github/workflows/functional_tests.yaml @@ -4,7 +4,7 @@ name: Python package on: - push: + push: pull_request: jobs: @@ -17,16 +17,16 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - python -m pip install flake8 pytest qiskit-aer qiskit_ibm_runtime + python -m pip install flake8 pytest - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4495c0e7..6af4cfc2 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -14,9 +14,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Update pip and install lint utilities diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 5d423f1f..35746edd 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -9,8 +9,8 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: pre-commit/action@v2.0.3 diff --git a/README.md b/README.md index e653af8c..a74a5a22 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Simulate quantum computations on classical hardware using PyTorch. It supports s Researchers on quantum algorithm design, parameterized quantum circuit training, quantum optimal control, quantum machine learning, quantum neural networks. #### Differences from Qiskit/Pennylane -Dynamic computation graph, automatic gradient computation, fast GPU support, batch model tersorized processing. +Dynamic computation graph, automatic gradient computation, fast GPU support, batch model tensorized processing. ## News - v0.1.8 Available! diff --git a/examples/PauliSumOp/pauli_sum_op_noise.py b/examples/PauliSumOp/pauli_sum_op_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/QCBM/README.md b/examples/QCBM/README.md new file mode 100644 index 00000000..cf61c65c --- /dev/null +++ b/examples/QCBM/README.md @@ -0,0 +1,42 @@ +# Quantum Circuit Born Machine +(Implementation by: [Gopal Ramesh Dahale](https://github.com/Gopal-Dahale)) + +Quantum Circuit Born Machine (QCBM) [1] is a generative modeling algorithm which uses Born rule from quantum mechanics to sample from a quantum state $|\psi \rangle$ learned by training an ansatz $U(\theta)$ [1][2]. In this tutorial we show how `torchquantum` can be used to model a Gaussian mixture with QCBM. + +## Setup + +Below is the usage of `qcbm_gaussian_mixture.py` which can be obtained by running `python qcbm_gaussian_mixture.py -h`. + +``` +usage: qcbm_gaussian_mixture.py [-h] [--n_wires N_WIRES] [--epochs EPOCHS] [--n_blocks N_BLOCKS] [--n_layers_per_block N_LAYERS_PER_BLOCK] [--plot] [--optimizer OPTIMIZER] [--lr LR] + +options: + -h, --help show this help message and exit + --n_wires N_WIRES Number of wires used in the circuit + --epochs EPOCHS Number of training epochs + --n_blocks N_BLOCKS Number of blocks in ansatz + --n_layers_per_block N_LAYERS_PER_BLOCK + Number of layers per block in ansatz + --plot Visualize the predicted probability distribution + --optimizer OPTIMIZER + optimizer class from torch.optim + --lr LR +``` + +For example: + +``` +python qcbm_gaussian_mixture.py --plot --epochs 100 --optimizer RMSprop --lr 0.01 --n_blocks 6 --n_layers_per_block 2 --n_wires 6 +``` + +Using the command above gives an output similar to the plot below. + +

+sample output of QCBM +

+ + +## References + +1. Liu, Jin-Guo, and Lei Wang. “Differentiable learning of quantum circuit born machines.” Physical Review A 98.6 (2018): 062324. +2. Gili, Kaitlin, et al. "Do quantum circuit born machines generalize?." Quantum Science and Technology 8.3 (2023): 035021. \ No newline at end of file diff --git a/examples/QCBM/assets/sample_output.png b/examples/QCBM/assets/sample_output.png new file mode 100644 index 00000000..c1626a4e Binary files /dev/null and b/examples/QCBM/assets/sample_output.png differ diff --git a/examples/QCBM/qcbm_gaussian_mixture.ipynb b/examples/QCBM/qcbm_gaussian_mixture.ipynb new file mode 100644 index 00000000..849f7cdc --- /dev/null +++ b/examples/QCBM/qcbm_gaussian_mixture.ipynb @@ -0,0 +1,255 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1cfe7a69-13c6-48ce-bc02-62e2047eef22", + "metadata": {}, + "source": [ + "# Learning Gaussian Mixture with Quantum Circuit Born Machine\n", + "\n", + "A QCBM is a generative model that represents the probability distribution of a classical dataset as a quantum pure state. It is a quantum circuit that generates samples via projective measurements on qubits. Given a target distribution $\\pi(x)$, we can generate samples closer to it using a QCBM.\n", + "\n", + "The Kerneled MMD loss is used to measure the difference between the generated samples and the target distribution.\n", + "\n", + "$$\n", + "\\mathcal{L} = \\underset{x, y \\sim p_\\boldsymbol{\\theta}}{\\mathbb{E}}[{K(x,y)}]-2\\underset{x\\sim p_\\boldsymbol{\\theta},y\\sim \\pi}{\\mathbb{E}}[K(x,y)]+\\underset{x, y \\sim \\pi}{\\mathbb{E}}[K(x, y)]\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4d440b94-63d4-4f6d-882e-45827d54cb4d", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from torchquantum.algorithm import QCBM, MMDLoss\n", + "import torchquantum as tq\n", + "from qcbm_gaussian_mixture import gaussian_mixture_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "2d14c9f7-4e5d-4fe1-98b4-83962d949519", + "metadata": {}, + "source": [ + "We use the function `gaussian_mixture_pdf` to generate a gaussian mixture which will be the target distribution $\\pi(x)$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5483ab05-1a08-4bdc-8799-0e67433131af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGdCAYAAADqsoKGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABjIklEQVR4nO3de1yT1/0H8E8SSMItQe6gKKgoXhAUBFHrlYqta2tnW3V2Oudq29XWlm6tdq3a9WIvPzvX6urc1su2Wq1ba621rIhWa0UR8Ib3G4JiuIgkEEjI5fn9AYmlogICTy6f9+uVV2ty8uSbx5jnm3O+5xyJIAgCiIiIiJycVOwAiIiIiDoCkxoiIiJyCUxqiIiIyCUwqSEiIiKXwKSGiIiIXAKTGiIiInIJTGqIiIjIJTCpISIiIpfgIXYAXcVqtaK0tBR+fn6QSCRih0NEREStIAgCampqEBERAan05n0xbpPUlJaWIjIyUuwwiIiIqB1KSkrQo0ePm7Zxm6TGz88PQONJUalUIkdDREREraHT6RAZGWm/jt+M2yQ1tiEnlUrFpIaIiMjJtKZ0hIXCRERE5BKY1BAREZFLYFJDRERELoFJDREREbkEJjVERETkEpjUEBERkUtgUkNEREQugUkNERERuQQmNUREROQSmNQQERGRS2BSQ0RERC6BSQ0RERG5BCY1RERE5BKY1JDbK68xNPvz378/h//730noDCaRIiIiovZgUkNuq77Bgne+PYnRb+7A9hNlAIDqugas3HYaq3acwc6TFSJHSNSyPWcq8cD7e7Bm51mxQyFyKB5iB0DU1QRBwNYjGrz29TGUaht7ab49WoYJsaFQe3lixUPx2HL4MqbEhdufc75Sj54B3pBJJWKFTYTCS1q8mXkC35+uRDdvT/x9eJL9MUEQIJHw80nujUkNuZUz5bV4aVMhcs5dAQB09/fCi1MGYPLgMACARCJB+qAwpA8Ksz9HbzTjwTU5CPKVY83DiYgK8hEldnJfRZV6/N+3J7Hl8GUAgKdMgnviI2C2CgAaE5p5H+dhYLgKvx3fB95yfrWTe+Inn9yGts6EGWtzUFnbAIWHFI+N7YPHxvaBl1x20+ed0OhgNFtwQlODZzcewsZHUyFljw11gfIaA97LPoNPc4vtCcx9CRF49s7+6BnobW+391wVtp8ox+4zlXgoKRI9A/nVTu6Jn3xyG3/adgqVtQ3oHeSDj3+djMgA71s/CUBirwBkPj0Gk97ZifwLV/FZXglmJPfs5GjJ3ZVW12PKu9/jal1jwfrYfsF4bnJ/DIpQX9d2RO8ArHl4GDRaQ7Nkh8jdsFCY3MKxUh3+mVMEAPjjfYNbndDYdPf3wjN39gMALP/mBCprjR0dIlEzSzcfxdU6E/qG+OLTR0bg418nt5jQAI3DppMHh+NXo6Lt9xVe0uJfTZ95InfBpIZcniAIWLq5EFYBuDsuDKNjgtp1nF+NjMLAcBW09Sa8vvV4B0dJdM3/jmqQdawMHlIJ/jJrGFL7BLbp+WfKa3Hvqt1Y9tUxnNTUdFKURI6HSQ25vC8PlmJ/0VV4ecrw4pSB7T6Oh0yK138eB4kE+LzgEvacrezAKIka1RrNWLb5KADgkTG90S/Ur83H6Bvii0kDw2CxCnjpy0IIgtDRYRI5JCY15NJqDCa81tSrsmBCX0T4e93W8RIi/fFwSi8AwIubCmE0W247RqIf27C/BJe1BkQGeOGpCTHtPs5L9wyE0lOK3PNV2HyotAMjJHJcTGrIpZ0ur4XZYkV0kA9+c0f0rZ/QCr9L749gPwXOVeixdue5Djkmkc3ckVF4/f44vH5/3C1n5t1Md38vLBjfFwDw2tfHUcMVsskNtCupWb16NaKioqBUKpGSkoLc3Nybtt+4cSNiY2OhVCoRFxeHrVu3Nnt82bJliI2NhY+PD7p164a0tDTs27evWZuqqirMmjULKpUK/v7+mDdvHmpra9sTPrmRYT27YcfvxuEvs4ZB4dH+C8SPqb088dLPGoex3ttxBkWV+g45LhEASKUS/CKlJ+6ICb7tYz0ypjeiAr1RXmPEu9mnOyA6IsfW5qRmw4YNyMjIwNKlS1FQUID4+Hikp6ejvLy8xfZ79uzBzJkzMW/ePBw4cABTp07F1KlTUVhYaG/Tr18/rFq1CkeOHMHu3bsRFRWFSZMmoaLi2jL1s2bNwtGjR5GVlYUtW7Zg165dmD9/fjveMrkbf285BoSrOvSY9wwJxx0xQWgwW1mzQB0ir6gKdQ3mDj2mwkOGpfcOAgB8+EMRTpWxaJhcm0Ro47dxSkoKhg8fjlWrVgEArFYrIiMj8eSTT2LRokXXtZ8+fTr0ej22bNliv2/EiBFISEjAmjVrWnwNnU4HtVqNbdu2YeLEiTh+/DgGDhyI/fv3IympcVnwzMxM3H333bh48SIiIiJuGbftmFqtFipVx17gyPFkHy+D0WzFXYPDOm3p+KJKPSat3IUGsxX//HUyxvS7/V/W5J7KdAZMXLETai9PrJ8/os1LDtzKI//MQ9axMozoHYBPHxnB7RTIqbTl+t2mnpqGhgbk5+cjLS3t2gGkUqSlpSEnJ6fF5+Tk5DRrDwDp6ek3bN/Q0IC1a9dCrVYjPj7efgx/f397QgMAaWlpkEql1w1T2RiNRuh0umY3cg96oxl/+KIQv/2kABvzL3ba60QF+WDxXbH40/R43NHOaeJEAKDRGqD28kSwn+K2i9lbsuRnA6HwkGLvuSp81bTVApEralNSU1lZCYvFgtDQ0Gb3h4aGQqPRtPgcjUbTqvZbtmyBr68vlEol/vSnPyErKwtBQUH2Y4SEhDRr7+HhgYCAgBu+7vLly6FWq+23yMjItrxVcmIyqQTTh0eib4gv7o2/dS/e7Zg7Khr3D+3BX750W+Ij/ZGVMQarZw3rlE1TIwO88YS9aPgYao0dO8xF5CgcZvbT+PHjcfDgQezZsweTJ0/GQw89dMM6ndZYvHgxtFqt/VZSUtKB0ZIjU3rK8Myd/ZC58A4oPTumOLg1zBYrzBZrl70euRZvuQe6d0Ivjc38Mb3RK9AbZToj3mPRMLmoNiU1QUFBkMlkKCsra3Z/WVkZwsLCWnxOWFhYq9r7+Pigb9++GDFiBP7xj3/Aw8MD//jHP+zH+GmCYzabUVVVdcPXVSgUUKlUzW7kXjxkXZezf/jDeYx+cwe2Frbcc0jUkm+PavDZ/pIOLxBuidJThqX3NM7a+2hPEa5wqw9yQW361pfL5UhMTER2drb9PqvViuzsbKSmprb4nNTU1GbtASArK+uG7X98XKPRaD9GdXU18vPz7Y9v374dVqsVKSkpbXkL5MIEQcDLXx1F1rEymLq4x6S6zgSNzoAtXOSM2uDd7afx3H8P49PcrulJnhAbiicn9MUXvx2FQF9Fl7wmUVdq8y7dGRkZmDNnDpKSkpCcnIyVK1dCr9dj7ty5AIDZs2eje/fuWL58OQBg4cKFGDt2LFasWIEpU6Zg/fr1yMvLw9q1awEAer0er732Gu69916Eh4ejsrISq1evxqVLl/Dggw8CAAYMGIDJkyfjkUcewZo1a2AymbBgwQLMmDGjVTOfyD0cLdXhwx+K8Mm+Yux/IQ1q767rqZmRHImYUF/cOTD01o2J0LjhZOElHeQyKe4f2r3LXvfZSf277LWIulqbk5rp06ejoqICS5YsgUajQUJCAjIzM+3FwMXFxZBKr11MRo4ciXXr1uHFF1/ECy+8gJiYGGzatAmDBw8GAMhkMpw4cQIff/wxKisrERgYiOHDh+P777/HoEGD7Mf55JNPsGDBAkycOBFSqRTTpk3Du+++e7vvn1zIxrzGX7vpg8Kg9vbs0tcOV3vhZ0M6rx6CXM+G/U2f18FhCPCRixKDIAgscieX0uZ1apwV16lxbUazBSmvZ6O6zoSPf52MsSKuGWO1Nv6TknbCLBZyDfUNFiS/vg01BjM++U0KRvXt2iUBLlzR473tZ6A3mvH+w4ld+tpEbdVp69QQOars4+WorjMhTKXE6C6+QPzYP3OKMO7/vsOOk+2fuUeub+uRy6gxmNEzwBupvQNFieE/+ReReVQDjdYgyusTdYY2Dz8ROaL/NC2y9/Nh3TtlnY/WKr5Sh+KqOqzfX4KJA1hfQy1bv78YADB9eKQoPXq9An3w/ORYDI/qhlAVC4bJdbCnhpxeuc6Anaca9wl7ILGHqLFMH964yOP2E+Uo1/EXMF3vTHkt9hddhUwqEfXz+vi4PkiKCmBNDbkUJjXk9L44cAkWq4DEXt3QO9hX1FhiQv2Q2KsbLFYB/ynovC0ayHltaOqlGd8/BKEqpcjRELkWJjXk1ARBsA89id1LY2Prrdmwv4S7d1MzDWYr/ltwCQAwY7j4W7ecr9TjxU1H8OqWY2KHQtQhmNSQUzt0UYvT5bVQekoxZUi42OEAAKbEhcNX4YELV+qw91yV2OGQA8k6VoYqfQNCVQqM6y/+ru7lOgP+vbcYn+YWd8mqxkSdjUkNObX/5Deu9TF5UBhUyq5dm+ZGfBQeuKdpI03bUAMRcK1A+MHEyC7dxuNGkqMD0CvQG/oGC745wi0+yPmJ/6+KqJ0MJgs2H2zcluCBRPG78n/MNrSwtVADbZ1J5GjIEegMJhwqqQZwbYhSbBKJBA8Maxy23ZjPTX/J+TGpIad1+KIWdQ0WRKiVGNlHnLU+bmRIDzViw/zQYLZi08FLYodDDkCl9MTeFybiw7nDERngLXY4dtMSe0AiAfaeq0LxlTqxwyG6LUxqyGklRwdg3wsT8d4vhjnc6r0SicTeW/NpbjELhgkA4C33wPj+IWKH0UyEv5d9wUrO2CNnx6SGnFqgrwKJvbqJHUaLpg7tDrmHFCc0NThySSt2OCSiGoPJoRNb28zB/+ZftG/zQeSMmNSQU3KGmRr+3nLcNTgMALB+P+sV3FnGZ4eQvnIX9p67InYoLUofFAY/pQcuVdcjx0FjJGoNJjXkdARBwD3v7cZDf83B+Uq92OHc1IOJkegd5IMINRdZc1d6oxl7z17BqbJaBIq0G/etKD1l9hl7tnWfiJwRkxpyOic0NThboceRi1oE+zn2vjWj+gZi++/GYcGEGLFDIZH4KDzww+IJ+MusYYgJ9RM7nBt6sGkI6pvCy9AZOGOPnBOTGnI6A8JV2LNoAv76y0T4Khx7T1buq0NA48ynu+McY3HIG0mI9EffEF8YTFZ8ffiy2OEQtQuTGnJKEf5eGNNP/BVZW8totjhsPQUR0JiAT2tasyazkAvxkXNiUkPUyQwmC5Jfy8aMtXtx4Ypj1wBRx/riwEVMXf0DNuY5R6F42oDG6eY6g4mzoMgpMakhp/LlwUv41Ye52HrEebrHlZ4yDIpQIcRPgZKqerHDoS707dEyHCypRkmVcyxq1zfEF3sXT8QXvx3lcGs/EbWGYxckEP3Et8fK8N3JCgzprnb4GoUfW/WLYfD38uSFwo0YzRbsOlUBAEgbGCpyNK0jkUgQxpl65MTYU0NOw2Sx2i8S42Ida1XWWwnwkTOhcTN7z1VB32BBiJ8CgyPUYofTZvUNFodeMJCoJUxqyGkUXLiKGoMZAT5yxPfwFzucdrFYBW5w6Sa2HSsDAEwcEOpUCa0gCHj0X3mI/+O3OFVWK3Y4RG3CpIacxo6Tjb00Y/sFQ+ZEFwmbrUcuI+X1bXjpy0KxQ6FOJggCth1vTGruHOhcvYoSiQRGsxUNZityzlaKHQ5Rm7CmhpzGdyfLAQDj+jvPVO4fC1UpUFnbgO9OlsNkscJTxt8UrupoqQ6XtQZ4ecowsk+Q2OG02e8m9ccf7h6AviG+YodC1Cb8ViWnUFpdjxOaGkglwJgY50xqEiK7IcBHDp3BjP1FVWKHQ50o+3hjAn5HTBCUnjKRo2m7wd3ViAn14+KR5HSY1JBT2NHUSzO0Zzd0c9D9c25FJpVgQlOBs+2iR67JNvTkLLOeiFwFkxpyCjtONNbTjHfSoScb2+Jm246XcWaJi7qsrceRS1pIJLAnsc7o8MVqPLGuAMs2HxU7FKJWY1JDDs9otuCHM40Fi+Od+CIBAHfEBEMuk+LClTqcreDMEldk64UbGumPIF/H3nD1ZmqNZnx9+DK2HC7l6sLkNJjUkMPLPV+FelPjeh8Dw1Vih3NbfBQeSO0TCADYxiEol+QqQ09JvQLgq/BAZW0DjlzSih0OUaswqSGHd23oKcQlChftQ1BN65iQ6zBbrDhWqgMA3DnAuZMauYcUd8Q0ztzafoIJODkHJjXk8K4NPTl3PY3NhKaLXUHxVVypNYocDXUkD5kUexZNwH8fT3WJ6dDj+zcm4LblFIgcHZMacnhfPDESf5+dhNFOOpX7p7r7e2FguApW4dqCguQ6PGRSJPYKcIlexXFNPyQOXdSiooYJODk+JjXk8LzlHkgbGApfheusFWkbgso+ziEoVyEIgsvNaAvxUyKue+O+VeytIWfApIZIBBObhqB2naqA0WwRORrqCAXFVzHqje14+38nxA6lQ9mWUfiOvYrkBJjUkMOqazDjvtU/4P/+d9LlLvxx3dUIVSkQHeyDMi279V3BtuPlKNUacPFqvdihdCjbMgq7TlXAZLGKHA3RzblOfz65nJyzV3CopBpXao14dlI/scPpUFKpBNnPjnOpITV3t3BiDJJ6dUOgE69N05IhPfwR4CNHlb4B+ReuYkTvQLFDIrohfqOSw0rs1Q3vPBQPs1VwiaLLn2JC41qUnjL7sKIrkUklGNcvGJ8fuIQdJ8qZ1JBD4/ATOSx/bzl+PqwHHkqKFDuUTlXfYIHeaBY7DKIbsg1Bcb0acnRMaohE9OqWY4h/+Vt8XnBR7FDoNizbfBRvfHMCF6/WiR1KpxgTEwyZVIKzFbUo1xnEDofohtj/TQ7p26MaFF3RY0JsqEssYnYjai9PNFisKLykEzsUaieDyYJPc4thNFvx82HdxQ6nU6i9PfGPOUn2+hoiR8WkhhzSZ3kl2Ha8HIIAl05qpg+PxM/iIxAV6C12KNROBcVXYTRbEeynQIwLf1bH9XfuzWTJPTCpIYdjsQrIPV8FAC5flBiiUoodAt0m2zYeo/sGuWRBO5EzYU0NOZzjl3XQGczwVXhgUIRz78pNru+HM1cAACP7uHYCDgD/2H0ev/jbXhRy125yUO1KalavXo2oqCgolUqkpKQgNzf3pu03btyI2NhYKJVKxMXFYevWrfbHTCYTnn/+ecTFxcHHxwcRERGYPXs2SktLmx0jKioKEomk2e2NN95oT/jk4Paea7xIJEV1g4fM9fPuExodHv93PhasKxA7FGojbb0Jhy9WAwBG9Q0SN5gukHO2EnvOXkHO2Stih0LUojZfMTZs2ICMjAwsXboUBQUFiI+PR3p6OsrLW57qt2fPHsycORPz5s3DgQMHMHXqVEydOhWFhYUAgLq6OhQUFOCll15CQUEBPv/8c5w8eRL33nvvdcf64x//iMuXL9tvTz75ZFvDJyewz02GnmykEgm+KdRg2/EyGEyutXKyq9t77gqsAtA72AcR/l5ih9PpfpHSE69OHYzJg8PEDoWoRW2uqXnnnXfwyCOPYO7cuQCANWvW4Ouvv8YHH3yARYsWXdf+z3/+MyZPnozf//73AIBXXnkFWVlZWLVqFdasWQO1Wo2srKxmz1m1ahWSk5NRXFyMnj172u/38/NDWBj/MbkyqxvV09jEhPgi2E+BihojCoqvYmQf1//F7yr2/Kiexh1MiHW9xQXJtbSpp6ahoQH5+flIS0u7dgCpFGlpacjJyWnxOTk5Oc3aA0B6evoN2wOAVquFRCKBv79/s/vfeOMNBAYGYujQoXj77bdhNnPBMldzXKODtt4EH7kMg92knkYikdjrMfacYbe+M9ndlNQwESVyDG3qqamsrITFYkFoaPNsPTQ0FCdOtLwzrUajabG9RqNpsb3BYMDzzz+PmTNnQqW6dlF76qmnMGzYMAQEBGDPnj1YvHgxLl++jHfeeafF4xiNRhiN1zYK1Om4Dogz2HeusZcmKSrALeppbEb1DcKXB0vxw9lK/A79xQ6HWkGjNeBshR5SCZDqJr2KAFBSVYedpyoQ4a9kzw05HIea0m0ymfDQQw9BEAS8//77zR7LyMiw//+QIUMgl8vx6KOPYvny5VAort9Abvny5Xj55Zc7PWbqWLYiYXcZerKxFZkevqiFzmCCSukpckR0K7ap3HE9/KH2dp+/r/8d1eDVr49jYmwIkxpyOG36KRwUFASZTIaysrJm95eVld2w1iUsLKxV7W0JzYULF5CVldWsl6YlKSkpMJvNKCoqavHxxYsXQ6vV2m8lJSW3eHckNqtVQG5RY09NSu8AkaPpWt39vRAV6A2LVbD3VpFju7Y+jXsl4LYfHLnnq2CxCiJHQ9Rcm5IauVyOxMREZGdn2++zWq3Izs5Gampqi89JTU1t1h4AsrKymrW3JTSnT5/Gtm3bEBh46y+JgwcPQiqVIiSk5VUuFQoFVCpVsxs5tpNlNaiuM8FbLkNcd7XY4XQ5W2+N7WJJjksQBHs9zSg3q6cZEK6Cn9IDNUYzjpVyWJ8cS5uHnzIyMjBnzhwkJSUhOTkZK1euhF6vt8+Gmj17Nrp3747ly5cDABYuXIixY8dixYoVmDJlCtavX4+8vDysXbsWQGNC88ADD6CgoABbtmyBxWKx19sEBARALpcjJycH+/btw/jx4+Hn54ecnBw888wzePjhh9GtW7eOOhcksmvr0wTA043qaWxG9Q3CJ/uKsecskxpn8Pc5Sdh9phLDernXd5BMKkFyVACyT5Rj77kriOvhfj9AyHG1OamZPn06KioqsGTJEmg0GiQkJCAzM9NeDFxcXAyp9NoFaeTIkVi3bh1efPFFvPDCC4iJicGmTZswePBgAMClS5ewefNmAEBCQkKz19qxYwfGjRsHhUKB9evXY9myZTAajYiOjsYzzzzTrM6GnF9ir26YP6Y3YsP8xA5FFKm9AyGRAKfKGndC5hYKjksikWBID38M6eEvdiiiGNE70J7UPDKmt9jhENlJBEFwi0FRnU4HtVoNrVbLoShyWD9773sUXtJh5fQETB3qmjs+k/M7clGLe1bthp/CAweXToJMyj2vqPO05frtfn38RA7MVp/BuhrHZbZY8YcvjuDLg5dgsljFDkcUAyNU8FOwroYcD5Macgj5F65i16kK6I3uvaDiyKZi4T3cW8dhHb6kxSf7irHky6OQuumu3DKpBMnRjTMUbbVwRI6ASQ05hH/sPofZH+Tiwx/Oix2KqJKjAvCXWcOw5cnRYodCNxDgLcf8Mb0xIznSrYddbFO7mdSQI3GoxffIfYWrvdDd3wupfdxrzY+f8pLLcHdcuNhh0E1EBfnghbsHiB2G6H66Xo07J3jkOJjUkEN46WcD8dLPBsJN6taJnN5P62o4tZscAYefyKFI3LRG4cf0RjP+vO00fv3Rfli5YqtDOVNei12nKlDfYBE7FNGxroYcEZMaEt3Fq3W8eP+IwkOKv31/DttPlOPYZc4scSQb80ow+4NcLN1cKHYoDuHuuHDMSumJIeylIQfB4ScSlSAIuG/VDzBZrPjv4yMRE+qeC+/9mIdMiifG94XKywPhai7A50h+aFrt2balhbubltgD0xJ7iB0GkR2TGhLVmfJaXNE3QOEhRc9Ab7HDcRiPj+sjdgj0E1X6BhxtWpNlpJvt90TkLJjUkKj2nm/ckTqxVzcoPGQiR0N0Yzlnr0AQgP6hfgj2U4gdjsNoMFtx5FI1pBIJhvZ0r32wyPGwpoZEZSswtE0PpWvOVtTiXzlFOF1WI3YoBA493cg/c4ow7f0crNp+RuxQiJjUkHgEQcA+JjU39E7WKbz05VFkFmrEDoXQ2FMDAKP68rP6YynRgQjwkSPARy52KERMakg8Zyv0qKxtrKeJj+TsiZ8a0TRddl/TEB2JR6M14HylHlIJMLzp74UaDe6uQt4f0vD2g/Fih0LEpIbEYxt6GtaT9TQtsfVe5V2oQoPZPTdOdBT7zjd+Vgd3V0Ol9BQ5GscikUgg5WrC5CCY1JBo9hc19kAk85dvi/qG+CLQRw6DyYrDF6vFDsetsfbr1gRBQHmNQewwyM0xqSHR5BVdBQAMj2JS0xKJRIKU3hyCcgR7zzWe/xG9+VltSUlVHVKXb8fEFTu5kCaJikkNiUKjNeBSdT2kEiChp7/Y4Tgs7oQsvh/X0yQxAW9RuFoJncGEGoMZp8trxQ6H3BiTGhJF/oXGXpoB4Sr4Krhc0o2kRDfV1RRdhcnCuhox2OppBkWwnuZGPGRSJET6A2isASMSC5MaEoXtiy+pFxfrupmYEF8E+MhRb7Lg8EWt2OG4JZWXJ+6ICcL4/sFih+LQbP+W85uGlYnEwJ/IJIoqfQMkEiCR3fk3JZVKkBwVgMyjGuw9dwWJTAK73Pj+IRjfP0TsMBye7d/yfvbUkIjYU0Oi+POMoTi0dBLuHBAqdigObwSLhckJDO3pD4kEKKmqR7mOs6BIHExqSDQqpSe85Fyf5lZG9LHV1VSxrqaLXbxax2nKraRSeiI2TAUAyLvAISgSB5Ma6nKCwCmfbdEvxA/+3p6oa7DgyCXW1XSl1TvOIPm1bKzewX2NWsNWV5PHuhoSCZMa6nKP/isfD7y/B/kce28VqVSCacN64JcjekGlZBlcV7LVfsWG+YkdilNIimoqFua/bRIJvyGpS1mtAnLOXUGNwcytEdrgpZ8NFDsEt/TXXyahuq4BSk9+VlvDVsh+tFSH+gYLh5epyzGpoS4lkQCbF4xGXlEVf/2SU/D35u7TrdXd3wthKiU0OgMOllQjtQ+3laCuxeEn6lISiQTRQT54MCkSHjJ+/NqiwWxFXlEVSqvrxQ7FLXC5/7aTSCRI5BAUiYhXFSIn8fSGA3hgTQ42HyoVOxS3cNefv8dDa3JwroLL/rfF8KYhqKIrdSJHQu6Iw0/UpV7aVIioIB88kNgDai8uOd8Ww3p2w75zVbCwB6HTabQGnCyrgVQCBPoqxA7HqUwd2h13x4UjRKUUOxRyQ0xqqMuU1xjwr70XIJEADyb1EDscp/PL1F6YNzoaEolE7FBc3o/3e2Ly3TasQSIxMamhLlPQtCBX/1A/bgzYDpwt1nVsu6LbVnMmIufAmhrqMrYFubh/0e0RBAE1BpPYYbi0vecai1xH9ObsnfbYe+4KZn+Qi5c2FYodCrkZJjXUZWxLp9sW6KK2236iDCmvZ2Ph+oNih+KyNFoDzlfqIZUASdxwtV0azFbsOlWB706Vix0KuRkOP1GXMJgsOFrauMR/Ui9eKNorxE+J8hoj9p9vLBiWSVlf09FYT3P7hvXqhmX3DGRSSF2OPTXUJQ6VVMNkERDip0CPbl5ih+O0BoSr4Kf0QI3RjGOlOrHDcUmsp7l9vgoP/GpUNAZ3V4sdCrkZJjXUJX489MTZO+0nk0qQ3PTr13bxpY7Fehoi58WkhrpEflNSM6wn62lul+1iy6Sm47GepuNo601Yn1uMldtOiR0KuREmNdTprFbBntTwQnH7UpqGRXKLuBBfR2M9TcepNZqx6PMjWLX9DOoazGKHQ26CSQ11unOVtdDWm6D0lGJQhErscJzewHAV/BQeqDGYcfwy62o6EutpOk53fy+Eq5UwWwUcLKkWOxxyE0xqqNPZ1qeJ7+EPT25ieds8ZFIMj2ZdTWc4UFwNgPU0HcW2JlV+03cAUWfjFYY6Hden6Xi2noScs0xqOtKXC0bhs0dTkcKkpkMkNSU1tu8Aos7GdWqo090bHwGV0hMTYkPEDsVlpPYOAgDkcr2aDqXwkCE5mkNPHcVWQ1dQfBVWqwApP6fUyZjUUKcb0y8YY/oFix2GSxkY0bRejcGMo6VaDOnhL3ZIRNeJDfODt1yGGoMZp8prEBvGmjrqXO0aflq9ejWioqKgVCqRkpKC3Nzcm7bfuHEjYmNjoVQqERcXh61bt9ofM5lMeP755xEXFwcfHx9ERERg9uzZKC0tbXaMqqoqzJo1CyqVCv7+/pg3bx5qa2vbEz6R05NJJUhhXU2HmvNBLpZ8WYiKGqPYobgMD5kUQ3v6A7hWW0fUmdqc1GzYsAEZGRlYunQpCgoKEB8fj/T0dJSXt7zHx549ezBz5kzMmzcPBw4cwNSpUzF16lQUFjZudFZXV4eCggK89NJLKCgowOeff46TJ0/i3nvvbXacWbNm4ejRo8jKysKWLVuwa9cuzJ8/vx1vmbrSD2cqsedMJad0dgJbMev5Sr3IkTi/i1frsPNUBT7ZVwwvOXdD70iJTdui5BVViRwJuQOJIAhtWugiJSUFw4cPx6pVqwAAVqsVkZGRePLJJ7Fo0aLr2k+fPh16vR5btmyx3zdixAgkJCRgzZo1Lb7G/v37kZycjAsXLqBnz544fvw4Bg4ciP379yMpKQkAkJmZibvvvhsXL15ERETELePW6XRQq9XQarVQqdgF2lUeeH8P8i5cxf89GI8HEnuIHY5LuapvQIPFilCVUuxQnF5dgxm7TlXgwpU6PDq2j9jhuJRdpyow+4Nc9Ojmhd3PTxA7HHJCbbl+t6mnpqGhAfn5+UhLS7t2AKkUaWlpyMnJafE5OTk5zdoDQHp6+g3bA4BWq4VEIoG/v7/9GP7+/vaEBgDS0tIglUqxb9++Fo9hNBqh0+ma3ajr9Qz0RoRaaZ/aSR2nm4+cCU0H8ZZ7YPLgcCY0nWBoT39IJcDFq/Uo0xnEDodcXJuSmsrKSlgsFoSGhja7PzQ0FBqNpsXnaDSaNrU3GAx4/vnnMXPmTHtGptFoEBLSfOaMh4cHAgICbnic5cuXQ61W22+RkZGteo/Usd55KAF7Fk9EVKC32KEQkQj8lJ7o31QgXMCp3dTJHGqdGpPJhIceegiCIOD999+/rWMtXrwYWq3WfispKemgKKk9uIll5zh8sRqzP8jFbz/JFzsUp1VaXY+V205hP2s+Ok1iL38A1/aAI+osbZrSHRQUBJlMhrKysmb3l5WVISwsrMXnhIWFtaq9LaG5cOECtm/f3mzcLCws7LpCZLPZjKqqqhu+rkKhgEKhaPV7o45XXmNAsK+CCU0n8pBKsetUBbzlMpgsVq7Y3A7fn67Aym2nsft0Jf7z+Eixw3FJE2JDYLEKGNU3SOxQyMW16RtQLpcjMTER2dnZ9vusViuys7ORmpra4nNSU1ObtQeArKysZu1tCc3p06exbds2BAYGXneM6upq5Odf+zW6fft2WK1WpKSktOUtUBcRBAH3vLcbQ1/JwklNjdjhuKzYMD+8fO8g/PfxkfDgwmbtYluVObUPVxHuLBNiQ7H850MwngtwUidr8+J7GRkZmDNnDpKSkpCcnIyVK1dCr9dj7ty5AIDZs2eje/fuWL58OQBg4cKFGDt2LFasWIEpU6Zg/fr1yMvLw9q1awE0JjQPPPAACgoKsGXLFlgsFnudTEBAAORyOQYMGIDJkyfjkUcewZo1a2AymbBgwQLMmDGjVTOfqOuVag0o0xnhIZWgZwDraTqLVCrBnJFRYofhtARBQE7TOj+p3BqByOm1OamZPn06KioqsGTJEmg0GiQkJCAzM9NeDFxcXAyp9FoH0MiRI7Fu3Tq8+OKLeOGFFxATE4NNmzZh8ODBAIBLly5h8+bNAICEhIRmr7Vjxw6MGzcOAPDJJ59gwYIFmDhxIqRSKaZNm4Z33323Pe+ZuoBt7HxghIrrfpDDOl+pR5nOCLlMimGcodepTBYrjpXqIJVIENdDLXY45KLavE6Ns+I6NV1r2eaj+GhPEX41MgrL7h0kdjguzWSx4ouCS8gtqsLr98dB7sG6mtb6ZN8F/OGLQqREB2DDoy0PoVPHWLvrLF7fegLpg0Lx118m3foJRE06bZ0aotYqKG7sqeGv384nk0jwRuYJ/Cf/Ig5frBY7HKfCepquk9irG9RenvBVeIodCrkwJjXU4eobLDhW2rjYIRfd63xSqQQjenMfqLYSBAF7zzVO42Y9TecbGtkNB166Eyseihc7FHJhTGqowx2+WA2zVUCoSoEINVe87Qq2faBymNS02pnyWlTWGqHwkCKhadNF6jxSqQRSztCjTsakhjpcftPQU2KvblyjpovYehryL1yF0WwRORrnYEsAk6K6QeHBYvaupDdyg1vqHExqqMMVXKgGAAzryaGnrtI3xBdBvnIYTFYcKtGKHY5TsNfTcOipyxwsqcaoN7bjgTU33vuP6HYwqaEOJQgCi4RFIJFIkNJ0cWZdza1ZrYL9PLFIuOuEq5W4VF2PkxodatlbQ52ASQ11qKIrdajSN0DuIcWgCE6d70r2upqzTGpu5WRZDa7WmeAtl2FID3+xw3EboSolenTzglUADpVUix0OuSAmNdShbLvwDumuZp1CF7MNoxQUX4XBxLqam1F7eWLhxBj8ckQv7pfVxWwzIrm5JXUG/mumDlV0RQ+AQ09i6BPsg2A/BYxmKw7yV/BNRfh74Zk7+2Hx3QPEDsXt2GrtmNRQZ2BSQx3q2Un9cXDJnXjkjt5ih+J2JBKJfQiKdTXkqGw9NQXFV2G1usWC9tSFmNRQh/P3liPYTyF2GG7Jtggf62purKSqDpmFGlTXNYgdiluKDfODl6cMNQYzzlTUih0OuRgmNUQuxFZXc6CkmnU1N5BZqMFj/85HxmeHxA7FLXnIpEiI9AfAISjqeExqqMOs3XUWM9fuxdeHL4sdituKDvLB3FFReHNanNihOCxvhQx9Q3wxklO5RcNiYeosHmIHQK5j95kryDl3BXfFhYkdituSSCRYeg93Rb+ZWSm9MCulFwSB9RxisdfVMKmhDsakhjrMkp8NRF5RFUb2CRI7FKJb4hYe4hnatNfWuUo9qvQNCPCRixsQuQwOP1GH6RviixnJPdEz0FvsUNxe4SUt/vLdGVzVsxj2xy5erePeWA7A31uOviG+ANhbQx2LSQ2RC/rdxkN4K/Mkvj9TKXYoDuXJTw8g4eUsfHeyXOxQ3F5i03o1xy/rRI6EXAmHn6hDrNtXDLPVijsHhiJc7SV2OG4vfVAYenTzQiC79e20dSYcKqmGVQD6hfqJHY7bWzChLxamxSDCn98X1HGY1FCH+Pv353CuUo8e3byY1DiAZ+7sJ3YIDueHs5WwCo3DpLyQii8ygMPU1PE4/ES3rUrfgHOVjdsjDI3k9gjkmHadqgAAjIkJFjkSIuosTGroth0obiz06xPsg24c7nAoJVV1OKFhzYIgCNeSmn6cnecoth65jN98vB+f5ZWIHQq5CCY1dNsKmpIa20Z15BjW7SvGHW/twPKtJ8QORXRnK2pRqjVA7iFFSjQX3XMU5ypqse14uT3hJLpdrKmh22ZbFTSRO3M7FNvfx77zV2AwWaD0lIkckXh2nmqcBZYcFQAvufueB0eTNjAUSk+ZfSNWotvFpIZui8lixcGSagDAMCY1DqVfqC9C/BQorzEir+gqRse477DL96c59OSIYsNUiA1TiR0GuRAOP9FtOVaqg8FkhUrpgb7BvmKHQz8ikUhwR1NRrO2i7o4MJgv2nmvctXxMPxYJE7kyJjV0W/Kahp6SogIglXLZeUdj65nYddp9F+HLK7oKg8mKUJUC/bk+jcMprzHgv/kXsflQqdihkAtgUkO3Jf9CFQDW0ziq0X0bk5rjl3UorzGIHI04djX1Ut0RE8z9nhxQ7vkqPLvxEP6686zYoZALYFJD7SYIAvKKmnpqmNQ4pEBfBQZ3b6xZ2O2mvTW2mTV3uHFNkSNL6hUAoDHxrjWaRY6GnB2TGmq3i1frUV5jhKdMgvhIf7HDoRsYY6+rcb+kxmi2wN/bE3IPqb2+iBxLmFqJHt28YBWurXlF1F5Maqjd9hc1Dj0N7q526+nCju7HxcJWqyByNF1L4SHD+vmpOLx0EgK4MKTDGh7V2Ftj6/klai8mNdRufYJ9MSe1F+4ZEiF2KHQTib26wVsuQ2VtA4676erCTLodm60mL6+pRo+ovbhODbVbfKQ/h52cgNxDitTegcg+UY7vT1diUIRa7JC6hNUqoLrexB4aJ2DrqTlQXA2zxQoPGX9vU/vwk0PkBmxFsu60HP3RUh0SX83CzLV7IQjuNezmbGJCfKFSeqCuwYLjl2vEDoecGJMaapeiSj32F1XBYLKIHQq1wph+wYhQK9E3xNdtLvCHL1VDEABfpQencjs4qVTCISjqEBx+onbZmF+C1TvO4sHEHnj7wXixw6FbiA7ywQ+LJrjVxX1WSi/cOSAUNZwm7BSSogKw42QF8oquYu6oaLHDISfFpIbaxUMqRZCvgovuOQl3SmZ+LESlRIjYQVCrJP2op0YQBLf9zNLtYVJD7fLMnf3wdFoMLG42RdjZWawCjl/WYXB39ygWJucRH+kPT5kEZTojLl6tR2SAt9ghkRNiTQ21m0Qi4SwFJ2IwWZD82jb87L3dKK2uFzucTvXqlmN4+O/78MMZ91tw0FkpPWX2ZJt1NdRevCJRm+mNZrcpNnUlSk8ZegZ6w0/pgXMVerHD6VRZx8uw+0wl6hpYyO5M7hochmnDeqBHN/bSUPtw+Ina7NnPDiHvwlW8OnUwJg8OEzscaoM1Dyci0Efu0j1sF67oceFKHTykEqT2CRQ7HGqD+WP6iB0COTkmNdQmgiAg78JVVNYaEejLRc2cTahKKXYIne7bo2UAGhd081XwK47InbjuzzXqFMVVdaisNUIukyKOxaZOSxAE1BhMYofRKb4pvAwAuDuOvYjOyGIVcLRUi5KqOrFDISfEpIbaZH/ThnNxPbiJpbPKPl6G0W/uwAtfFIodSoe7rK1HQXE1JBIgfRCTGmf00peFmPLubqzLLRY7FHJC7UpqVq9ejaioKCiVSqSkpCA3N/em7Tdu3IjY2FgolUrExcVh69atzR7//PPPMWnSJAQGBkIikeDgwYPXHWPcuHGQSCTNbo899lh7wqfbkN80KyGJ69M4rQAfOS5V12P78TKXWxE6s1ADoPHzGeIGQ22uKKGHP3wVHlwugtqlzUnNhg0bkJGRgaVLl6KgoADx8fFIT09HeXl5i+337NmDmTNnYt68eThw4ACmTp2KqVOnorDw2q9EvV6P0aNH480337zpaz/yyCO4fPmy/fbWW2+1NXy6TXlNPTVcdM95xffwR7haCX2DBbtPu9aU52+akprJg8NFjoTa676hETi0dBJeuHuA2KGQE2pzUvPOO+/gkUcewdy5czFw4ECsWbMG3t7e+OCDD1ps/+c//xmTJ0/G73//ewwYMACvvPIKhg0bhlWrVtnb/PKXv8SSJUuQlpZ209f29vZGWFiY/aZSqdoaPt2G6roGnC6vBcCkxplJpRL70IwtCXAF5TUG7C9q7EnkrDznpfCQQSblasLUPm1KahoaGpCfn98s+ZBKpUhLS0NOTk6Lz8nJybkuWUlPT79h+5v55JNPEBQUhMGDB2Px4sWoq7txIZnRaIROp2t2o9uTf6Gxl6Z3sA8CfRUiR0O3466mi/6242UwWawiR9Mx/ne0DIIAJET6o7u/l9jhUAdoMLvGZ5O6TpuSmsrKSlgsFoSGhja7PzQ0FBpNy7/4NBpNm9rfyC9+8Qv8+9//xo4dO7B48WL861//wsMPP3zD9suXL4darbbfIiMj2/R6dL28pqSG9TTOLykqAEG+cmjrTcg5e0XscDpEZtOsp7vYS+P0dpwsx9i3d+C3nxSIHQo5GadZxGH+/Pn2/4+Li0N4eDgmTpyIs2fPok+f6xdsWrx4MTIyMux/1ul0TGxuU36RLakJEDkSul0yqQSTBoVh3b5ifFOowZh+wWKHdFuu1Bqx91zj0NNdrKdxeiqlJy5cqYOu3sTNLalN2tRTExQUBJlMhrKysmb3l5WVISys5V9HYWFhbWrfWikpKQCAM2fOtPi4QqGASqVqdqP2M5otOHixGgCQGMWeGldg69HIOqZx+pkmJzQ1UHhIMShChZ6BXGLf2Q3uroLCQ4qrdSacdfEtPahjtSmpkcvlSExMRHZ2tv0+q9WK7OxspKamtvic1NTUZu0BICsr64btW8s27Ts8nL/KukLhJR0azFYE+MjRO8hH7HCoA4zoHQi1lycqaxvsBbbOalTfIBS8dCdW/WKY2KFQB1B4yBDfwx/AtWUkiFqjzbOfMjIy8Le//Q0ff/wxjh8/jscffxx6vR5z584FAMyePRuLFy+2t1+4cCEyMzOxYsUKnDhxAsuWLUNeXh4WLFhgb1NVVYWDBw/i2LFjAICTJ0/i4MGD9rqbs2fP4pVXXkF+fj6KioqwefNmzJ49G2PGjMGQIUNu6wRQ65zQNBZaJ/bqxq5gF+EpkyJtQGO9W6YLzIJSesoQzYTbZSQ19QjbFvwkao0219RMnz4dFRUVWLJkCTQaDRISEpCZmWkvBi4uLoZUei1XGjlyJNatW4cXX3wRL7zwAmJiYrBp0yYMHjzY3mbz5s32pAgAZsyYAQBYunQpli1bBrlcjm3btmHlypXQ6/WIjIzEtGnT8OKLL7b7jVPbzErphUkDw6A3msUOhTrQXYPD8N+Ci8gs1GDJzwZC6oRTaesazPCWO015ILWSLamxzbokag2JIAjOPZjeSjqdDmq1GlqtlvU1RE0MJgsSX8mCvsGCz387EsN6Ol+91G8+3o8LV+rw8n2DMLJPkNjhUAfR1pkQ/8dvAQD7/5CGYD8uI+Gu2nL95t5PRG5M6SnDhAGh6B3sA129821waTRbsPdcFU6X1yKIaye5FLW3J/qF+gJgXQ21Hvts6ZY25pXgy4OleDCpB+5L6C52ONTB3po2BEpPqVPWSik8ZPhh0QTsOVOJmBBfscOhDpYcHYBTZbXYd76KW19Qq7Cnhm5p56kK7D5TiaLKG6/gTM7LSy5zyoTGRu3libviwp36PVDLRvQOBAD7GkREt8KeGrqlp9P6ITk6ACnRgWKHQp3IYLLgUnU9+gQ7R48HF2VzfbbvnBMaHarrGuDvLRc5InJ07KmhW+ob4ovZqVHoH+YndijUSfYXVSHxlSw8+q98sUNptW8KNbhv1W58tr9E7FCokwT7KdA3xBeCAOw7z94aujUmNUSE/mF+aLBYoTeaUaVvEDucVtl65DIOXdTibEWt2KFQJxrRu3FbliMXtSJHQs6Aw090U//KKYJMKsWdA0M5pdKFqZSeyHx6DKIDfZxirRqDyYIdJ8oBAHfFsYDUlc2/ow8euaM3egZw+wu6NSY1dEOCIGDVjjMo0xkRHeTDpMbFOUstDQB8d7Ic+gYLItRKxPdQix0OdSLu5UVtweEnuqELV+pQpjNCLpNiaE9/scOhLmIwWXBZWy92GDf1773FAIB7EiJYLExEdkxq6Ib2nrsCAEiI9IfSUyZyNNQVtp8oQ+rybLzw+RGxQ7mh02U12H2mElIJ8MsRvcQOh7rAnrOVePRfeVi57ZTYoZCDY1JDN2SbbWAr1CPXFx3ki6t1Jnx3qgJFlXqxw2nRh3uKAACTBoahRzcOTbiDK7UN+N/RMnx7tEzsUMjBMamhFgmCYO+pSenN9WncRXSQD8b1D4YgAP/MuSB2ONfR1pnwecFFAMCvRkWJGwx1mdQ+gXhucn+8dv/gWzcmt8akhlpUXFWHy1oDPGUSp9zkkNrvVyOjADRuj+Fou7JvyCuGwWRFbJgfUqLZg+gugnwV+O24vhjK7yK6BSY11KJ9TcuSx/fwh5ec9TTuZExMMKKDfFBjNNt7RRyBxSrg4z2NvUe/HhXNAmEiug6TGmqRbehpBIee3I5UKsHs1MYC3I9zLkAQBJEjapR1rAyXquvRzdsT9yZEiB0OdTG90YwvD17Cu9mnxQ6FHBiTGrqOIAj2IuEUFgm7pQcSe8BHLsOZ8lr8cOaK2OEAAD7acx4A8IuUnpyN54ZqjWYsXH8Qf9p2Cto6k9jhkINiUkPXuXi1Hpeq6+EhlSCxF8ew3ZGf0hMPJPYAAHzUNNtITBargKE9uyHIV4GHOY3bLYWqlOgd5ANBaNyrjKglTGroOrahpyE91PCWc9FpdzW7qWA4+0QZSqrqRI1FJpXg+cmx2Lt4AsLVXqLGQuKxzcS0fUcR/RSTGrrO3nO2oSfW07izPsG+uCMmqGl6d5HY4QAAPGT8ynJntjWz9p5nUkMt4zcEXefuuDDMTO6JCbEhYodCIpvbtBbMhv0lqGsQZ3r31iOXsfNUBaxWxyhYJvHYJi4cLdVBW8+6Groekxq6zsQBoVj+8zgMj2KRsLsb1y8EvQK9oTOY8eXB0i5/fZPFile2HMOcD3KxtfByl78+OZZmdTXnWVdD12NSQ0Q3JJVKkHFnP7xy3yDcG9/106gNJgsmDw5DVKA37hwY2uWvT47HNiOTdTXUElaBUjM7TpTD39sTg7ur4cn6BQJwX0J30V7bT+mJpfcMwktTBkIq5WJ71DgE9WluCetqqEW8alEzSzYX4v6/7MHuM5Vih0IOqL7BIsrrMqEhm5ToxrqaY6yroRYwqSE7g8mCAWEqBPnKkcT1aegndp6qwPj/+w5Zxzp/p2SrVcBz/zmE/Ausm6DmwtRKRAf5wCoAeVyvhn6CSQ3ZKT1lWDs7Cfv/kAY/pafY4ZCD2XOmEhqdAX/bda7Tt074cE8RPsu7iDkf7OevcbrOCNbV0A2wpoauw40CqSXP3NkPfkoPzBvdu1M/I6fLavBm5gkAwKK7YqH2YoJNzdnras6xp4aaY1JDdhev1qG7vxeTGmqR0lOGBRNiOvU1GsxWPL3hIBrMVozrH4xZKT079fXIOY3oHYh74yMwsg8XCKXmOPxEAIDL2nqMfnMH7nhrBxrMVrHDIQdntQr48IfzKNMZOvS472afxtFSHbp5e+KtaUOYYFOLQlVKvDtzKGYkM+ml5pjUEADg+9ONs50CfRWQe/BjQTf38ldH8fJXx/Dcfw53WH1N/oUq/OW7MwCA1++PQ4hK2SHHJSL3wasXAQB2naoAAIyNCRI5EnIGv0ztBbmHFDtPVWD9/pLbPp7eaEbGZ4dgFYCfD+2Ou+LCOyBKcmWCIOBMeQ0+y7v9zx+5DiY1BItVsK9LM6ZfsMjRkDPoG+KH59L7AwBe3XLstnfxfvXr47hwpQ4RaiWW3TeoI0IkF6erN2PSn3bhuf8cxmVtvdjhkINgUkMovKRFdZ0JfgoPxEf6ix0OOYlfj4pGcnQA9A0WPLvxULs3nNx+ogyf5hYDAP7voXiouJwAtYLa2xPDowIwum8QdPXibLZKjodJDdmHnkb2DeTWCNRqUqkE//dAPLzlMuSer8I/dp9v8zGu1Brx3H+OAADmjY7GyD4c/qTWWz9/BP79mxT0D/MTOxRyELyCEXadbkxqOPREbdUz0BsvThkIAHht63HM+2g/jlzUtvr5j/wzD5W1RsSE+OL3TcNZRK3F2XH0U0xq3FyNwYSC4moAwJgYJjXUdjOTI/GrkVGQSoDsE+W4Z9Vu/ObjPBReuj65qW+wwGy5tmTAPfERCPKV488zhkLpKevKsMmFlOsMqKw1ih0GOQAmNW5uz9krsFgFRAf5IDLAW+xwyAlJJBIsu3cQtmWMxf1Du0MqAbYdL8PP3tuNJz4psNfa/GvvBYx+czu+PnLZ/tyZyT2x67nxGBihEit8cnKvfX0Mya9n4997L4gdCjkAJjVuzlZPM4ZTuek29Q72xZ+mJ+DbZ8bivoQISCSAwlNq32FbW9eAK/oGbDpwyf4cpacM3nIubE7t1yfYF8C17zJyb/w2cWOCILCehjpc3xBf/HnGUCwY3xfeimtfMbNHRiFc7YV7EyJEjI5cje2762BJNbR1Jqi9OXvOnbGnxo1duFKHkqp6eMokGNGbe6hQx4oJ9UN3fy/7n1VKT0xL7MEZdtShIvy90DfEF1YB+OFspdjhkMj47eLGynQG9Ar0RmKvbvBRsNOOiJyTbZIDh6CIVzI3ltI7EDt/Px56IxeuIiLnNaZfED744Tx2naqAIAic6u3G2tVTs3r1akRFRUGpVCIlJQW5ubk3bb9x40bExsZCqVQiLi4OW7dubfb4559/jkmTJiEwMBASiQQHDx687hgGgwFPPPEEAgMD4evri2nTpqGsrKw94dNPsJeGiJxZSnQg5B5SlGoNOFNeK3Y4JKI2JzUbNmxARkYGli5dioKCAsTHxyM9PR3l5eUttt+zZw9mzpyJefPm4cCBA5g6dSqmTp2KwsJCexu9Xo/Ro0fjzTffvOHrPvPMM/jqq6+wceNG7Ny5E6Wlpfj5z3/e1vCpSY3BBNOP1gshInJWXnIZUqIDAAA7OQTl1iSCILRpw5aUlBQMHz4cq1atAgBYrVZERkbiySefxKJFi65rP336dOj1emzZssV+34gRI5CQkIA1a9Y0a1tUVITo6GgcOHAACQkJ9vu1Wi2Cg4Oxbt06PPDAAwCAEydOYMCAAcjJycGIESNuGbdOp4NarYZWq4VKxTUx3so8gX/mXMDTaTH4zR29xQ6HiOi2/G3XOby29TjG9AvGP3+dLHY41IHacv1uU09NQ0MD8vPzkZaWdu0AUinS0tKQk5PT4nNycnKatQeA9PT0G7ZvSX5+PkwmU7PjxMbGomfPnm06Dl1z6GI1ao1mBPjIxQ6FiOi22aZ27zt3BQaTReRoSCxtKqaorKyExWJBaGhos/tDQ0Nx4sSJFp+j0WhabK/RaFr9uhqNBnK5HP7+/q0+jtFohNF4bdlsnU7X6tdzB//6dQqOlurQk6sIE5EL6BfqizCVEhqdAbnnq7j2lpty2Sndy5cvh1qttt8iIyPFDsmhSKUSxPVQc6EqInIJEokEdzStjM6p3e6rTUlNUFAQZDLZdbOOysrKEBYW1uJzwsLC2tT+RsdoaGhAdXV1q4+zePFiaLVa+62kpKTVr0dERM5nTL9gBPnK4SXn5qjuqk1JjVwuR2JiIrKzs+33Wa1WZGdnIzU1tcXnpKamNmsPAFlZWTds35LExER4eno2O87JkydRXFx8w+MoFAqoVKpmN2rcGuGe93bjdxsP4Qp3tSUiF3LX4DDkvpCGZyf1FzsUEkmbFyjJyMjAnDlzkJSUhOTkZKxcuRJ6vR5z584FAMyePRvdu3fH8uXLAQALFy7E2LFjsWLFCkyZMgXr169HXl4e1q5daz9mVVUViouLUVpaCqAxYQEae2jCwsKgVqsxb948ZGRkICAgACqVCk8++SRSU1NbNfOJrjl+uQZHLmlxprwWr90/WOxwiIg6jAe34HB7bU5qpk+fjoqKCixZsgQajQYJCQnIzMy0FwMXFxdDKr32wRo5ciTWrVuHF198ES+88AJiYmKwadMmDB587YK6efNme1IEADNmzAAALF26FMuWLQMA/OlPf4JUKsW0adNgNBqRnp6Ov/zlL+160+7MtoHliN4BUHiwi5aIXI8gCCjVGprtPUbuoc3r1DgrrlPTaNbf9+KHM1ew9J6BmDsqWuxwiIg61IUrejywJgcNZisKXroTMim3THB2nbZODTm3ugYz9p+/CgCc7khELqm7vxcMJgsazFYUXdGLHQ51MW7640b2nruCBosV3f290DvIR+xwiIg6nIdMiv88NhJRQd4cYndDTGrcyLdHG6fWj48N5i62ROSy+of5iR0CiYTDT27CYhXw7bHGpGbyoHCRoyEi6nyCIMDMjXvdCpMaN7G/qApV+gaovTyR0jtA7HCIiDrV+txiTFixE//MuSB2KNSFmNS4if8dbdwjK21AKDy5lgMRubh6kwXnK/XIPNr6fQbJ+fHq5gYEQcD/Chv/YacPCr1FayIi55c+qHELnf1FVaio4erp7oJJjRs4ckmLUq0BXp4yTuUmIrcQ4e+F+B5qCAKw7XjZrZ9ALoFJjRsouNC4Ns342GAoPTnFkYjcQ/rgxt6abwo5BOUuOKXbDfxqVDQmDQpDvckidihERF1m8qAwvJV5EnvOVEJbb4Lay1PskKiTsafGTUT4e6FPsK/YYRARdZnewb7oF+oLs1XA9hMcgnIHTGpcnNXqFlt7ERG1aHJTwXAmh6DcApMaF/fAmj2Y80EuzpTXih0KEVGXs9XV7DxVgboGs8jRUGdjUuPCLmvrUVBcje9PV8Dfm2PJROR+BoarEBngBYPJil2nKsQOhzoZkxoXFq72wraMsXj7gXgE+SrEDoeIqMtJJBIOQbkRJjUurm+IL6Yl9hA7DCIi0UxuGoLKPl6OBjP3gnJlTGqIiMilDY3shmA/BdTenii5Wid2ONSJuE6Ni/riwEVkHSvDjOE9uYowEbk1qVSCrxaMRqhKAYlEInY41InYU+OivjxYiq1HNDhySSt2KEREogtTK5nQuAEmNS5IZzBhz5krALiBJRHRjzWYrdDWmcQOgzoJkxoXtONEORosVvQJ9kHfED+xwyEicgif5hYj6dUs/GnbKbFDoU7CpMYF/e9o47RFW8U/EREBQb4K6AxmFBRfFTsU6iQsFHYxBpMFO040LjCVPohJDRGRzR0xQfjs0VQk9uomdijUSZjUuJjvT1ei3mRBhFqJuO5qscMhInIYSk8ZkqMDxA6DOhGHn1zMpgOXADTud8JKfyKiljWYrRAEbvjrapjUuJCr+gZkHSsDADzAVYSJiFr0p6xTSF2ejX3nq8QOhToYkxoX8uXBS2iwWDEwXIVBERx6IiJqSXmNAVf0Dfgsr0TsUKiDMalxIRvzLwIAHkpiLw0R0Y08mBQJANh65DJqDFyzxpUwqXERR0u1OFqqg1wmxX0J3cUOh4jIYQ2N9EffEF8YTFZsOXxZ7HCoAzGpcRFHL+ngKZPgzoGh6OYjFzscIiKHJZFI7D3aHIJyLZzS7SIeGh6JtIGh0BvNYodCROTw7h/aA29mnsSB4mqcLqtBTChXX3cF7KlxIQE+ckQGeIsdBhGRwwv2U2BCbAiAa/WI5PyY1LiAcp1B7BCIiJzOQ00Fw58XXITJYhU5GuoITGqcnEZrQOob2/Hgmj0wmCxih0NE5DTG9Q9GkK8ClbUN2HGiXOxwqAMwqXFyuUVV9lUxlZ4ykaMhInIenjIppg1rnC36WR6HoFwBC4Wd3L3xEUiOCkCVvkHsUIiInM6DST3w113nsONkOcprDAjxU4odEt0G9tS4gDC1EgMjVGKHQUTkdPqG+GFYT39YrAK+KLgkdjh0m9hT48R0BhNUSk+xwyAicmoPJUWi3mRFmJq9NM6OSY2T0hvNGLl8OxIi/bHqF0Ph780F94iI2uOhpEjMSO4pdhjUATj85KS+PnIZtUYzLlXXQ+3F3hoiovaSSiVih0AdhEmNk/pPU6X+A4k9IJHwHyQR0e2qNZqxYX8x6hq4Mruz4vCTEzpfqUduURWkEmDaMO7ITUTUER5ck4Pjl3XwkEoxLZHfrc6ISY0Tsm3ANqZfMAvbiIg6yJS4MBjNFsg9OIjhrNr1N7d69WpERUVBqVQiJSUFubm5N22/ceNGxMbGQqlUIi4uDlu3bm32uCAIWLJkCcLDw+Hl5YW0tDScPn26WZuoqChIJJJmtzfeeKM94Tu1WqMZ6/YVAwBmDI8UORoiItcxf0wfZGeMxT3xEWKHQu3U5qRmw4YNyMjIwNKlS1FQUID4+Hikp6ejvLzlJab37NmDmTNnYt68eThw4ACmTp2KqVOnorCw0N7mrbfewrvvvos1a9Zg37598PHxQXp6OgyG5nsa/fGPf8Tly5fttyeffLKt4Tu99bnF0Nab0DvIB3cODBM7HCIilyH3kLJG0clJBNsa+62UkpKC4cOHY9WqVQAAq9WKyMhIPPnkk1i0aNF17adPnw69Xo8tW7bY7xsxYgQSEhKwZs0aCIKAiIgIPPvss/jd734HANBqtQgNDcVHH32EGTNmAGjsqXn66afx9NNPt+uN6nQ6qNVqaLVaqFTOuVCd0WzBmLd2oExnxJvT4jB9OKcgEhF1NKPZgs8LLiG1dyCignzEDsftteX63aaemoaGBuTn5yMtLe3aAaRSpKWlIScnp8Xn5OTkNGsPAOnp6fb258+fh0ajadZGrVYjJSXlumO+8cYbCAwMxNChQ/H222/DbL5xhbrRaIROp2t2c3ZfHihFmc6IMJUSU4d2FzscIiKXtPi/R7D48yP4666zYodCbdSmpKayshIWiwWhoaHN7g8NDYVGo2nxORqN5qbtbf+91TGfeuoprF+/Hjt27MCjjz6K119/Hc8999wNY12+fDnUarX9Fhnp3PUnFquANU3/wH5zRzQUHty8koioM/wipbEX/L/5l1CmM9yiNTkSpynxzsjIwLhx4zBkyBA89thjWLFiBd577z0YjcYW2y9evBhardZ+Kykp6eKIO9a3RzU4V6GH2suTK18SEXWipKgAJEcFoMFixd+/Pyd2ONQGbUpqgoKCIJPJUFZW1uz+srIyhIW1XLQaFhZ20/a2/7blmEBjbY/ZbEZRUVGLjysUCqhUqmY3ZyUIAt7f2dhLMye1F3wVnIlPRNSZHh/XBwDwyb5iVNc1iBwNtVabkhq5XI7ExERkZ2fb77NarcjOzkZqamqLz0lNTW3WHgCysrLs7aOjoxEWFtasjU6nw759+254TAA4ePAgpFIpQkJC2vIWnJJEIsHr98fhvoQI/GpUtNjhEBG5vHH9gzEgXIW6Bgv+mXNB7HColdr8kz8jIwNz5sxBUlISkpOTsXLlSuj1esydOxcAMHv2bHTv3h3Lly8HACxcuBBjx47FihUrMGXKFKxfvx55eXlYu3YtgMYL9tNPP41XX30VMTExiI6OxksvvYSIiAhMnToVQGOx8b59+zB+/Hj4+fkhJycHzzzzDB5++GF069atg06FYxvcXY0/zxgqdhhERG5BIpHg8XF98NSnB/DhD+fxmzui4S1nL7mja/Pf0PTp01FRUYElS5ZAo9EgISEBmZmZ9kLf4uJiSKXXOoBGjhyJdevW4cUXX8QLL7yAmJgYbNq0CYMHD7a3ee6556DX6zF//nxUV1dj9OjRyMzMhFLZuFquQqHA+vXrsWzZMhiNRkRHR+OZZ55BRkbG7b5/hycIAtdNICISwd2Dw7Ai0BsXrtRhfW4Jfj2aPeWOrs3r1DgrZ12n5un1B+Ct8MCC8X0R4e8ldjhERG7lk30X8IcvChGuVmLn78dzCwURdNo6NdS1Sqrq8OWhUqzbV4xaI3eNJSLqatOG9UCwnwKXtQZ8efCS2OHQLTCpcWA9unlhw/xUPHtnP/QL9RM7HCIit6P0lOE3TcNOa3aehdXqFoMbTotJjQOTSCRIjg7AkxNjxA6FiMht/SKlJ1RKD5yt0OPbYy0vNEuOgUmNg6oxmMQOgYiIAPgpPTE7NQp+Sg9creN3syNjobADKqrU4573dmPOyCg8nRYDDxlzTyIiMekMJkjQmOBQ12rL9ZuT7h2MIAh4+aujqDGacehiNWRSTucmIhKbismMU2AXgIPZdrwcO05WwFMmwcv3DuIaNUREDmbHiXJ8sPu82GFQC9hT40AMJgv+uOUoAOA3d/RG72BfkSMiIqIfKyi+irkf7YenTIIx/YLRN4Tf046ESY0DWbPzLEqq6hGuVmLB+L5ih0NERD8xNNIfkwaGIirIB2Fqpdjh0E8wqXEQJVV1eP+7xp24/zBlAHy4EzcRkcORSCRY83AipKx3dEisqXEQL391DEazFSP7BGJKXLjY4RAR0Q38OKGxWgU0mK0iRkM/xqTGAew4UY5tx8vgIZXgj/exOJiIyBkcv6zDg3/Nwcptp8QOhZowqRGZ0WzBy181Fgf/enQ0+oZwOwQiImdQUlWH/AtX8bfvz+FsRa3Y4RCY1Ijub7vOoehKHUL8FHiK2yEQETmNOweGYnz/YJgsApZtPgo3WcvWoTGpEdHFq3VYteMMgMbiYF8WBxMROQ2JRIKl9wyCXCbF96crkVnIfaHExqRGRLp6M3oF+CAlOgD3xkeIHQ4REbVRVJAPHhvbGwDwypZj0BvNIkfk3pjUiGhghApfPzUaq34xjMXBRERO6vFxfdGjmxdKtQYs/vwIh6FExKRGBHUN1zJ5D5kUwX4KEaMhIqLb4SWX4Z2HEiCTSrD5UCk+/KFI7JDcFpOaLlZRY0Taip1Ytf00rFZm80REriA5OgAv3D0AAPD61uPIPV8lckTuiUlNF9t8qBSlWgM2HSyFwWwROxwiIuogvx4VhXvjI2C2CnhiXQHKdQaxQ3I7nG7TxeaNjoaf0gPDenaDt5ynn4jIVUgkErwxLQ6nympwQlOD335SgHWPjIDcg/0HXYVnWgQPJUVyZ1ciIhfkLffAmocT4af0QN6Fq/hnTpHYIbkVJjVd4Ex5DR77Vz6q9A1ih0JERJ0sKsgHK6cnYHZqL8xOjRI7HLfC8Y9OVms049F/5eNshR5echn+ND1B7JCIiKiTTRwQiokDQsUOw+2wp6YTWawCfr/xEM5W6BGmUtor44mIyH2YLVas3nEG1XXsre9sTGo6icliRcZnB/FNoQaeMgn+8vAwrkdDROSGFn1+BG//7yR+9eF+LuXRyZjUdAKj2YLfflKALw+WwkMqwZ+mJ2BYz25ih0VERCKYP6Y3wtVKLBjfF1IpV4/vTKyp6WB1DY01NN+froTcQ4o1Dw/DhFiOqxIRuat+oX7Y8btxUHrK7PcJgsDtcToBe2o6kM5gwux/5OL705Xwlsvw0a+GM6EhIqJmCU3xlTrct/oHnCqrETEi18SkpoNU6Rsw62/7kHfhKlRKD/z7NykY2TdI7LCIiMjBvPL1MRy+qMVDf83B4YvVYofjUpjUdIBynQHT/5qDI5e0CPSR49P5I1hDQ0RELXr7gSFIiPRHdZ0Jv/jbPuw7d0XskFwGk5oOsOjzIzhdXoswlRIbHk3FoAi12CEREZGD8veW49+/SUFq70DUGs2Y/UEuthwuFTssl8CkpgO8dv9gjOobiI2PpXL7AyIiuiVfhQc+nDscE2JDYDRbsWDdATz16QGuZXObJIIguMWkeZ1OB7VaDa1WC5VKJXY4REREMFmseC/7NFZ/dxYWq4AQPwXenDYE42NDxA7NYbTl+s2eGiIiIpF4yqTImNQf/318JPoE+6C8xoi5H+3Hov8eRo3BJHZ4TodJDRERkcgSIv3x9VN34DejoyGRAOv3l2Dyyu+x52yl2KE5FSY1REREDkDpKcOLPxuITx8ZgR7dvHCpuh7fn2ZS0xZcUZiIiMiBjOgdiMynx+Dv35/D4+P62O/PK6qCl1zGGbY3wZ4aIiIiB+Or8MDTaf2g8GhciVgQBLy4qRBT3t2NTQcuiRyd42JSQ0RE5OBqjWb0C/WDSumBcf2D7fcXVephMFlEjMyxcPiJiIjIwfkpPfHuzKHQGUxQKT3t9y/4tADnKvSYEBuCuwaHY1z/YPgo3PfS7r7vnIiIyMn8OKGpMZhwVW9CXYMFWw5fxpbDl6HwkGJsv2DcHReO8bEhUHt53uRoroeL7xERETkpQRBw6KIW3xRexjdHNCiuqrM/JpEAfYN9kRDpj4Se/hga2Q39Qn3hIXOuypNOX3xv9erViIqKglKpREpKCnJzc2/afuPGjYiNjYVSqURcXBy2bt3a7HFBELBkyRKEh4fDy8sLaWlpOH36dLM2VVVVmDVrFlQqFfz9/TFv3jzU1ta2J3wiIiKXIJFIkBDpj8V3DcDO34/D10+NxpMT+qJPsA8EAThdXouN+Rfxhy8Kcfe73yNu2bco0xnszz9fqUdJVR0sVtfo32hzUrNhwwZkZGRg6dKlKCgoQHx8PNLT01FeXt5i+z179mDmzJmYN28eDhw4gKlTp2Lq1KkoLCy0t3nrrbfw7rvvYs2aNdi3bx98fHyQnp4Og+HaiZ81axaOHj2KrKwsbNmyBbt27cL8+fPb8ZaJiIhcj0QiwaAINZ6d1B/Zz47D/j+k4e+zk/DE+D4Y1TcQvgoPeMllCPFT2J/z2tfHcMdbO7Aut9h+3+myGqzafhqf5hbj26MaFBRfRfGVOtQ1mMV4W23S5uGnlJQUDB8+HKtWrQIAWK1WREZG4sknn8SiRYuuaz99+nTo9Xps2bLFft+IESOQkJCANWvWQBAERERE4Nlnn8Xvfvc7AIBWq0VoaCg++ugjzJgxA8ePH8fAgQOxf/9+JCUlAQAyMzNx99134+LFi4iIiLhl3Bx+IiIid2a1CtDoDIjw97LfN++j/fj+dCX+8ask3BHTOKvqv/kX8ezGQy0eQ+kphUrpCV+lB3wV127+3p5464H4Tom704afGhoakJ+fj7S0tGsHkEqRlpaGnJycFp+Tk5PTrD0ApKen29ufP38eGo2mWRu1Wo2UlBR7m5ycHPj7+9sTGgBIS0uDVCrFvn37Wnxdo9EInU7X7EZEROSupFJJs4QGAP7xq+E4/spkpPYOtN8XGeCN6UmRmBgbgvhIf3T394LCozFdMJisKK8x4lyFHocvarHn7BV8e6wM/zta1qXv5UbaNPupsrISFosFoaGhze4PDQ3FiRMnWnyORqNpsb1Go7E/brvvZm1CQprvWOrh4YGAgAB7m59avnw5Xn755Va+MyIiIvckk0oASOx/To4OQHJ0QLM2giBA32BBVW0Daowm1BrM0DeYUWMwo9boOMNSLjule/HixcjIyLD/WafTITIyUsSIiIiInJNEIrEPNTmyNg0/BQUFQSaToayseTdTWVkZwsLCWnxOWFjYTdvb/nurNj8tRDabzaiqqrrh6yoUCqhUqmY3IiIicl1tSmrkcjkSExORnZ1tv89qtSI7OxupqaktPic1NbVZewDIysqyt4+OjkZYWFizNjqdDvv27bO3SU1NRXV1NfLz8+1ttm/fDqvVipSUlLa8BSIiInJRbe5HysjIwJw5c5CUlITk5GSsXLkSer0ec+fOBQDMnj0b3bt3x/LlywEACxcuxNixY7FixQpMmTIF69evR15eHtauXQugsUvr6aefxquvvoqYmBhER0fjpZdeQkREBKZOnQoAGDBgACZPnoxHHnkEa9asgclkwoIFCzBjxoxWzXwiIiIi19fmpGb69OmoqKjAkiVLoNFokJCQgMzMTHuhb3FxMaTSax1AI0eOxLp16/Diiy/ihRdeQExMDDZt2oTBgwfb2zz33HPQ6/WYP38+qqurMXr0aGRmZkKpVNrbfPLJJ1iwYAEmTpwIqVSKadOm4d13372d905EREQuhNskEBERkcPq9G0SiIiIiBwNkxoiIiJyCUxqiIiIyCUwqSEiIiKXwKSGiIiIXAKTGiIiInIJTGqIiIjIJTCpISIiIpfg2NttdiDbGoM6nU7kSIiIiKi1bNft1qwV7DZJTU1NDQAgMjJS5EiIiIiorWpqaqBWq2/axm22SbBarSgtLYWfnx8kEkmHHlun0yEyMhIlJSXcguEneG5ujufnxnhubo7n5+Z4fm7M2c6NIAioqalBREREs70lW+I2PTVSqRQ9evTo1NdQqVRO8QERA8/NzfH83BjPzc3x/Nwcz8+NOdO5uVUPjQ0LhYmIiMglMKkhIiIil8CkpgMoFAosXboUCoVC7FAcDs/NzfH83BjPzc3x/Nwcz8+NufK5cZtCYSIiInJt7KkhIiIil8CkhoiIiFwCkxoiIiJyCUxqiIiIyCUwqblNq1evRlRUFJRKJVJSUpCbmyt2SKLYtWsX7rnnHkREREAikWDTpk3NHhcEAUuWLEF4eDi8vLyQlpaG06dPixNsF1u+fDmGDx8OPz8/hISEYOrUqTh58mSzNgaDAU888QQCAwPh6+uLadOmoaysTKSIu9b777+PIUOG2BcCS01NxTfffGN/3J3PzU+98cYbkEgkePrpp+33ufP5WbZsGSQSSbNbbGys/XF3PjcAcOnSJTz88MMIDAyEl5cX4uLikJeXZ3/cFb+XmdTchg0bNiAjIwNLly5FQUEB4uPjkZ6ejvLycrFD63J6vR7x8fFYvXp1i4+/9dZbePfdd7FmzRrs27cPPj4+SE9Ph8Fg6OJIu97OnTvxxBNPYO/evcjKyoLJZMKkSZOg1+vtbZ555hl89dVX2LhxI3bu3InS0lL8/Oc/FzHqrtOjRw+88cYbyM/PR15eHiZMmID77rsPR48eBeDe5+bH9u/fj7/+9a8YMmRIs/vd/fwMGjQIly9ftt92795tf8ydz83Vq1cxatQoeHp64ptvvsGxY8ewYsUKdOvWzd7GJb+XBWq35ORk4YknnrD/2WKxCBEREcLy5ctFjEp8AIQvvvjC/mer1SqEhYUJb7/9tv2+6upqQaFQCJ9++qkIEYqrvLxcACDs3LlTEITGc+Hp6Sls3LjR3ub48eMCACEnJ0esMEXVrVs34e9//zvPTZOamhohJiZGyMrKEsaOHSssXLhQEAR+dpYuXSrEx8e3+Ji7n5vnn39eGD169A0fd9XvZfbUtFNDQwPy8/ORlpZmv08qlSItLQ05OTkiRuZ4zp8/D41G0+xcqdVqpKSkuOW50mq1AICAgAAAQH5+PkwmU7PzExsbi549e7rd+bFYLFi/fj30ej1SU1N5bpo88cQTmDJlSrPzAPCzAwCnT59GREQEevfujVmzZqG4uBgAz83mzZuRlJSEBx98ECEhIRg6dCj+9re/2R931e9lJjXtVFlZCYvFgtDQ0Gb3h4aGQqPRiBSVY7KdD56rxt3in376aYwaNQqDBw8G0Hh+5HI5/P39m7V1p/Nz5MgR+Pr6QqFQ4LHHHsMXX3yBgQMH8twAWL9+PQoKCrB8+fLrHnP385OSkoKPPvoImZmZeP/993H+/HnccccdqKmpcftzc+7cObz//vuIiYnB//73Pzz++ON46qmn8PHHHwNw3e9lt9mlm8gRPPHEEygsLGw27k9A//79cfDgQWi1WvznP//BnDlzsHPnTrHDEl1JSQkWLlyIrKwsKJVKscNxOHfddZf9/4cMGYKUlBT06tULn332Gby8vESMTHxWqxVJSUl4/fXXAQBDhw5FYWEh1qxZgzlz5ogcXedhT007BQUFQSaTXVdJX1ZWhrCwMJGicky28+Hu52rBggXYsmULduzYgR49etjvDwsLQ0NDA6qrq5u1d6fzI5fL0bdvXyQmJmL58uWIj4/Hn//8Z7c/N/n5+SgvL8ewYcPg4eEBDw8P7Ny5E++++y48PDwQGhrq1ufnp/z9/dGvXz+cOXPG7T874eHhGDhwYLP7BgwYYB+ec9XvZSY17SSXy5GYmIjs7Gz7fVarFdnZ2UhNTRUxMscTHR2NsLCwZudKp9Nh3759bnGuBEHAggUL8MUXX2D79u2Ijo5u9nhiYiI8PT2bnZ+TJ0+iuLjYLc5PS6xWK4xGo9ufm4kTJ+LIkSM4ePCg/ZaUlIRZs2bZ/9+dz89P1dbW4uzZswgPD3f7z86oUaOuWzri1KlT6NWrFwAX/l4Wu1LZma1fv15QKBTCRx99JBw7dkyYP3++4O/vL2g0GrFD63I1NTXCgQMHhAMHDggAhHfeeUc4cOCAcOHCBUEQBOGNN94Q/P39hS+//FI4fPiwcN999wnR0dFCfX29yJF3vscff1xQq9XCd999J1y+fNl+q6urs7d57LHHhJ49ewrbt28X8vLyhNTUVCE1NVXEqLvOokWLhJ07dwrnz58XDh8+LCxatEiQSCTCt99+KwiCe5+blvx49pMguPf5efbZZ4XvvvtOOH/+vPDDDz8IaWlpQlBQkFBeXi4Ignufm9zcXMHDw0N47bXXhNOnTwuffPKJ4O3tLfz73/+2t3HF72UmNbfpvffeE3r27CnI5XIhOTlZ2Lt3r9ghiWLHjh0CgOtuc+bMEQShcfrgSy+9JISGhgoKhUKYOHGicPLkSXGD7iItnRcAwocffmhvU19fL/z2t78VunXrJnh7ewv333+/cPnyZfGC7kK//vWvhV69eglyuVwIDg4WJk6caE9oBMG9z01LfprUuPP5mT59uhAeHi7I5XKhe/fuwvTp04UzZ87YH3fncyMIgvDVV18JgwcPFhQKhRAbGyusXbu22eOu+L0sEQRBEKePiIiIiKjjsKaGiIiIXAKTGiIiInIJTGqIiIjIJTCpISIiIpfApIaIiIhcApMaIiIicglMaoiIiMglMKkhIiIil8CkhoiIiFwCkxoiIiJyCUxqiIiIyCUwqSEiIiKX8P/GRD+I1MXD4AAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "n_wires = 6\n", + "x_max = 2**n_wires\n", + "x_input = np.arange(x_max)\n", + "mus = [(2 / 8) * x_max, (5 / 8) * x_max]\n", + "sigmas = [x_max / 10] * 2\n", + "data = gaussian_mixture_pdf(x_input, mus, sigmas)\n", + "\n", + "# This is the target distribution that the QCBM will learn\n", + "target_probs = torch.tensor(data, dtype=torch.float32)\n", + "\n", + "plt.plot(x_input, target_probs, linestyle=\"-.\", label=r\"$\\pi(x)$\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b1bb110-e81c-455e-86a6-6b722f3a4433", + "metadata": {}, + "source": [ + "Using `torchquantum`, we can create a parameterized quantum circuit which will be used to generate a probability distribution. The gradient-based learning algorithm is used to update the circuit parameters $\\theta$ iteratively." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8347fa01-d519-40e3-a7ea-67fabca8ed56", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/gopald/Documents/tq-env/lib/python3.10/site-packages/qiskit/visualization/circuit/matplotlib.py:266: FutureWarning: The default matplotlib drawer scheme will be changed to \"iqp\" in a following release. To silence this warning, specify the current default explicitly as style=\"clifford\", or the new default as style=\"iqp\".\n", + " self._style, def_font_ratio = load_style(self._style)\n" + ] + }, + { + "data": { + "image/png": "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wTXkF+aycssCAAZ3G82YCx8D4NF/XYzDYaeopJBhPW/hyoET/Fklj3DmGCi1fNMckhO70aJh+7LXzBwLiohYmc0GbRsbf07mwb7SsaFM40IAuwNCbNCxqRELNU+A5IbGIx7NzNkEcE3GQtQ3BhZnxgarUlXeMDoilvtGvc5jb10GOIiNqs1jYz7ydpXcZnM4HE5c2yO+VlIIy6a7t+6RrP38/d+jKSjKI8QWQu3YBoy7fBptmnYHyh/oB4/+ynNzxpJ9+njZl6FVYhcA0o7v5an3r8FuL8GBg8SEZO658mUaJySdtc+hEyHUg9O71KT+/uLJNrB6/b3piQVGkFM7Gp4a5e/SiIj4nlnOg1bvCz1R/zMHvuNj6pW72K+yHz6//5ELRkz4/Nw7OJmbQYgthJuH/ZXzul5z1v4CKR48diKVm55uTmJCMtGRccBvF4SC8/V3JR4Gc8RDZjkHiIh4i1nOgzWNBVzpC6vr65dumM3spVMIsYVgd9i58YI/cUGPm87aZyDFAv4SSPGgq/HQfTMHMKLfnQz/3wWwpZyNBUGxkIiIGZjlPOhuP1hVzstTYyHV9Y2e6g99OTZU1XLVbedMVo8H3al/bm4utWrVAiAnJ4fYWPees6qEf4CywkFcFbPVHwLrR50/mOFHHZgnoBER8RaznAet3hdavf6gNvAWs5wDRES8xSznQfWDagOr199bzHIOEBHxFrOcB83YD5YKpIS/r1k9HvRnwj/ErbVERERERERERERERERERETEr5TwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETCvN3AaRiIeEwdKK/S+G8kHDPb89M9QfPtoHV6y8iImL1vtDq9S/dntXbQERErEv9oNrA6vUXERFrM2M/WMpT/aEZ28Dq8aA/YyEl/AOUzQahEf4uhf+o/tauv4iIiNX7QqvXH9QGIiJibeoH1QZWr7+IiFib+kG1AagNXKEp/UVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVERERERERERERERERERExICX8RERERERERERERERERERETUsJfRERERERERERERERERETEhJTwFxERERERERERERERERERMSEl/EVEREREREREREREREREREwozN8FkIo5HGAv8ncpnBcSDjab57ZntvqD59rAjHUv5enjQKzLjN8DnQc92wZWr7+IiIjV+0Kr1x/UBiIiYm3qB9UGVq+/iIgrlPAPUPYiWDbd36Vw3tCJEBrhue2Zrf7guTYwY91Lefo4EOsy4/dA50HPtoHV6y8iImL1vtDq9Qe1gYiIWJv6QbWB1esvIuIKTekvIiIiIiIiIiIiIiIiIiJiQkr4i4iIiIiIiIiIiIiIiIiImJAS/iIiIiIiIiIiIiIiIiIiIiakhL+IiIiIiIiIiIiIiIiIiIgJKeEvIiIiIiIiIiIiIiIiIiJiQmH+LoCIiLcUFMHuo3AwEw4eh6xcOJVvvJdTAAt/huYJ0LoR1Inxb1lFREREPM3hgENZkJIBqZlw+MQZsVA+fLAKmteDFvWgZX0Isfm1uCIiIiIel5MPe0rHhjJ/i4VO5cPrS41xoWYJ0KYRxEb6t6wiIiIi7lLCX0SCTtoJWLUL1u6FguKKlymxw/JfjH/bbNCxCQxqB+ckarBbREREzK2gCNalwKqdRpK/IiUOY5l1Kcb/69WCgW2hbzLUivJNOUVERES8weEwLnhcuRM2HjDGgH7P7oBf0ow/AGEh0L2lMTbUsp4xViQiIiJiFkr4i0jQyC8y7tr/aY9r6zkcsO2Q8adFPbipPzSu7Z0yioiIiHjTpgMwd61xN5srjufApxvgyy1wRQ8Y0FYXQYqIiIj5nDgNH6+G7YddW6/YDuv2GX86NYXr+kHtaO+UUURERMTTlPAPIpv2LOeh14eWey0qIpZmDdoxrOctXDXwXkJDg/sjt3obWLn+O9Phwx+NH3Y1ceA4TPsCLu0GQzvoim6zsfJ3oJTV28Dq9RcR6zpdCHPXwIb9NdtOYTHMW2tcOHBTf6gb65nyiW+oH1QbWL3+ImJt6/YZcUx+Uc22s+0QPLsIru0DPZM8UjTxIav3hVavP6gN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zL2TrftWUGIvpkPL/ky4asZZgbKnjwNPOl0AWaeNge6wECPZXyfGeoPaTywwfuDWjoanRvm7NJWz8jmglCfa4MwfOvEx9cp+6MBv54W2TXty09PNSUxIJjoyDvjtYom043t56v1rsNtLcOAgMSGZe658mcYJSRXuz5Nt4Kv61+S8+HuBfA48k1nOAyLiWSV24wLIgiLj/xFhUL8WhIVWvV6wMdM5sKZ9oTP9YKvGnavt69OO7+X5uXdwMjeDEFsINw/7K+d1vabCfQZaLOBrgRgP+poZ4iEznQdExLOy84w/xXZjbKh2tDVv/jDLebCm/eCxE6mVjnfAb/FQ6ybd+Pu/R1NQlEeILYTasQ0Yd/m0cjO/OjMuCoEXC3hqvLiUMwl/xYOKhUTENbm5udSqZUwtlJOTQ2xsrFvbUcI/QJmtMwu0YMYf/J3wDwRmCGisziwBnRm/BzoP6kedWc6BZjkPiIh4g5nOgVbvC61ef1AbeIuZzgMiIt5glvOg+kG1gdXr7y1mOQeIWIWnEv56MqOIiIiIiIiIiIiIiIiIiIgJKeEvIiIiIiIiIiIiIiIiIiJiQkr4i4iIiIiIiIiIiIiIiIiImJAS/iIiIiIiIiIiIiIiIiIiIiYU5u8CSMVCwmHoRH+Xwnkh4Z7fnpnqD55rAzPWvZSnjwOxLjN+D3Qe9GwbWL3+IiIiVu8LrV7/0u1ZvQ1ERMS61A+qDaxefxERVyjhH6BsNgiN8Hcp/MfK9bdy3UVK6XugNrB6/UVERKzeF1q9/qA2EBERa1M/qDawev1FRFyhKf1FRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEhJfxFRERERERERERERERERERMSAl/ERERERERERERERERERERE1LCX0RERERERERERERERERExISU8BcRERERERERERERERERETEh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unxNnZaYQaAGtBWx+nnA6vX3JrN8D3QMiIh4h/oB71HC3xx9ob4D3mOGz19ExCz9AFi7LzBj3UsFen8YqN8BM3/m1Qn0Y0L8y1MJf03iLCIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImFObvAkh5IeHG8zzMKiTcc9sxWzt4qu6l27Jy/UVE5wGr1190DIiIWJ36AbWB1enzFxERK/cFZqx7KfWH7jHzZ14dHRPiC0r4BxibDUIj/F0K/7N6O1i9/iKi84DV6y86BkRErE79gNrA6vT5i4iIlfsCK9fdqvSZi9SMpvQXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETEgJfxERERERERERERERERERERNSwl9ERERERERERERERERERMSElPAXERERERERERERERERERExISX8RURERERERERERERERERETCjM3wWQijkcYC/ydymcFxIONpvntme2+oNn28Dq9RcR0XlQrH4MWL3+IiKivsDKzPjZl/LUMaA2EBERM/YF6gM8x4yffykrx0P6DviPEv4Byl4Ey6b7uxTOGzoRQiM8tz2z1R882wZWr7+IiM6DYvVjwOr1FxER9QVWZsbPvpSnjgG1gYiImLEvUB/gOWb8/EtZOR7Sd8B/NKW/iIiIiIiIiIiIiIiIiIiICSnhLyIiIiIiIiIiIiIiIiIiYkJK+IuIiIiIiIiIiIiIiIiIiJiQEv4iIiIiIiIiIiIiIiIiIiImpIS/iIhYgt0ODofxb4fjt3+LiIiIWEVBMdjPiIdERERErKREY0MiIhKkwvxdABEREW8oscPWVNiVDgcz4fAJKCox3svOhz/NheYJ0CwBOjaF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AAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADAhEv4AAAAAAAAAAAAAAJgQCX8AAAAAAAAAAAAAAEyIhD8AAAAAAAAAAAAAACZEwh8AAAAAAAAAAAAAABMi4Q8AAAAAAAAAAAAAgAmR8AcAAAAAAAAAAAAAwIRI+AMAAAAAAAAAAAAAYEIk/AEAAAAAAAAAAAAAMCES/gAAAAAAAAAAAAAAmBAJfwAAAAAAAAAAAAAATIiEPwAAAAAAAAAAAAAAJkTCHwAAAAAAAAAAAAAAEyLhDwAAAAAAAAAAAACACZHwBwAAAAAAAAAAAADAhEj4AwAAAAAAAAAAAABgQiT8AQAAAAAAAAAAAAAwIRL+AAAAAAAAAAAAAACYEAl/AAAAAAAAAAAAAABMiIQ/AAAAAAAAAAAAAAAmRMIfAAAAAAAAAAAAAAATIuEPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCEQn1dAQAAUJXdLtlKfV0L11jDJIul/ucxYx+4q+0VzNYH7m4/ACB4mW0MPFYwx0IVgrkPiIcAAAAA+AIJfwAA/IytVFr8jK9r4Zph06SQ8Pqfx4x94K62VzBbH7i7/QCA4GW2MfBYwRwLVQjmPiAeAgAAAOALLOkPAAAAAAAAAAAAAIAJkfAHAAAAAAAAAAAAAMCESPgDAAAAAAAAAAAAAGBCJPwBAAAAAAAAAAAAADChUF9XAAAATyosltJzpPRsKb9IKrNJoSFSYrSUkiS1SJTCGQ0BAECAKrdJ+w9Le7Klg0ek0nLJapEiwqSURCMeSoiSLBZf1xQAAMAzjhw1YqH0HOloiWSzS6FWqVGc1DJJSk4w5ooAADArUhwAgIBTXCat3in9sNVI9NfGapE6NJUGd5S6tpBCWPsGAACYnN0u7TokLd8ird8tlZTXvn9itDSgvfGTEOWdOgIAAHhSfpH0yw5pxTbjosfahFqlrinSkI5S+yZcCAkAMB8S/gCAgFFukxZtkr7bJBWVOnaMzS79kWn8NIiWxqRJvdrw5Q4AAJjTrizpw5XGXWyOyimUvlwvfb1B6tNWGtdLionwXB0BAAA8pbhU+mydtGKrscqjI8ps0rrdxk9ygnRuH6lDsmfrCQCAO5HwBwAEhH050rs/1X1Hf21yC6X//Cit3S2d30+KN9kdbuu2L9GtLw6r8lpkeIxSGnfUyF4TdfbgGxQSEthDf7D3QbC3HwCCWWm59NV6adFm4w5/V9jsxp1wv2dIE/pJ3VLcW0dvCPaxMNjbL9EHABDMtmZKc3+SsgtcP0fmYem576QhHaQxJxqPQQIAwN/xDQcAYHq/pUtvLHP8yu26bEg37o67boTUrIF7zulNw9IuUr9Oo2WXXTlHMvXtr2/pxU9v0e4Dm3XzeS/5unpeEex9EOztB4BgU1gszVlixC/ukHdUeuV76Yzu0undzbnyUbCPhcHefok+AIBgs/QP6aNV7jvf8q3S9gPStSN45BEAwP/xpGIAgKn9li69ttR9yf4KeUXS7G+ljFz3ntcbOrTopZG9L9WpvSdqwtDb9MwNP6lxQoq+/OUV5eYf9HX1vCLY+yDY2w8AweRoifT8d+5L9h/rqw3SF+vcf15vCPaxMNjbL9EHABBMlvzu3mR/hYzD0rPfSkeOuv/cAAC4Ewl/AIBppWcbd/bbXFy2ti6FJdKLi6SCYs+c31uiwmPUqfUA2e127Tu03dfV8Ylg74Ngbz8ABCqb3YiF0nM8V8a3G6Wftnnu/N4S7GNhsLdfog8AIFCt3yMt+NVz5z94RHppiVTu5htNAABwJ5b0BwCYUlm59O4K5+7sv+UMKT7KWKZ21leOHXP4qHGV+MTBrtXTX2T8b1IzPjrJxzXxnWDvg2BvPwAEoh+3Sn9kOneMK/HQx79KHZOlpFjn6+hPgn0sDPb2S/QBAASa/CLp/V+cO8aVWGhPtrRwo/GoIwAA/BEJfwCAKX27UdqX69wx8VFSg2jny/p1p5TWSure0vljfaGotFCHC7JktxvPK/10xYvatneNOrXsp5TGHX1dPa8I9j4I9vYDQDA4lC99ssb541yJh4rLpPd+lq4bLlkszpfpC8E+FgZ7+yX6AACCwfxVRtLfGa7ODX3zm9Q9RWqe6PyxAAB4WlAk/LOysjRz5kx99NFHSk9PV+PGjTV+/Hg9/PDDmjZtml577TXNnj1bU6dO9XVVPSLriHHnx7YDUnGpFBEmtW8iDeogNYrzde0AwHmFJdKiTd4t84t1UrcUc0xyv/XNfXrrm/uqvDak23jdcM5zPqqR9wV7HwR7+/+utFxau0ta9aexaofVYtylOqCd1KW5ZOUhVwBM6LuNUkmZ98rbkiltPyC1b+q9Musj2MfCYG+/RB/8XXq29MNW4y7V0jIpKlzq0kIa2E6Ki/J17QDAeRm50ppd3iuv3CZ9vUG64mTvlQkAgKMCPuG/du1ajRo1SpmZmYqJiVGXLl20b98+PfPMM9q+fbuys7MlSWlpab6tqAcUlUpzf5LW75b+/njr3YekxZulHq2kiwZIkWE+qSIAuGTlDiOB500Zh6UdB6V2TbxbrivO7D9ZJ/c4X2W2Uv2ZsUHzljymrMPpCg+LrNynpKxY1z/VS8NOvFiXjLir8vWZ701Sbv5+PXzVl76outs40gcPvX2hbHab7pn4fuVreYXZuvqJrpp81hMa0esSX1TdLRxp/4Ydy/SPV0cdd2xZeYlstnJ9PdPLHzIPWblDWrBaKiiu+vq+XOm3dCkxxoiFOib7pHoA4JKjJdKqnd4vd/kW8yT8gz0eCvZYSCIeqpBbKP3nB+OCnb/bmWUkrwZ1kM7uJYVwESQAE/lhi/fL3JBu/F11ZYUAAAA8KaBD+aysLI0ZM0aZmZmaPn26MjIytHr1amVmZuqxxx7T559/rpUrV8pisahHjx6+rq5bFZVKzy2U1lWT7K9gl7H9uYXG/gBgBna7cWeKL/jiy6QrWjTqoF4dR6pfp1G6YNgMPXjFp/ojfaWenn9t5T7hoRGaceFbeu+7h7V93zpJ0g+/LdBPmz/VLee/6ququ40jfXDD+Oe1cecPWrRmbuVrsz+eoq5th5h+gtuR9ndPPUmfPpRf5ef1GVsUH9NIl5/+oA9r7z5L/5DeWXF8sv9YOQXSnMXSxnTv1QsA6mvVn969u7/C+j3G827NINjjoWCPhSTiIcmIc576uvpkf4Vym7TsD+n1Zcb/A4AZFJdKK//0frk2u7Rim/fLBQCgLgGd8J82bZrS09M1depUPfHEE4qL+2v9+hkzZqhnz54qKytTmzZtFB8f78Oaut+8n41l2hyxJ9vYHwDMILdQOpDnm7K3ZBoXHJhN1zaDNLLXRC1ZN08bd/5Y+XrHlN4675RbNfO9y3QwN11PfThZN5zznBolNPdhbT2juj6Ij07S9PNf1bMLpirr8D4tXf+h1m9fopvGv+jj2rpfTe+BY5WUFeuBt8arW5shunjEP7xcQ/fbfkD6eJVj+5bbpDeWS9n5nq0TALjLH5m+Kddml7bt903Z9RXs8VCwx0JS8MVDdrv06vfG9ydH/JZuPJ8aAMxg1yGp2AcXP0rG3BAAAP4mYBP+mzdv1rx589SoUSM98sgj1e7Tu3dvSVLPnj2rvP7nn39q7NixiouLU2Jioi677DIdOnTI43V2l+x8ae1u545Zu9u48hsA/J2jFzN5Qn6x4xNm/uaSkffIag3RoH+O2gAAvZpJREFUm1/f+7fX71aINVTXPXWierYfpmFpF/qohp5XXR/07XSGTukxQY/NvVSzP7pet5z/iuJjGvqwlp5T03ugwtPzr1VJaZFuu+AN71bMQ77/veZVjqpTWu671UMAwFnpPoyHfFl2fQV7PBTssZAUXPHQ1v1Seo5zxyz7wzerhwCAs/b4cKp+b7ZkY0UUAICfCdiE/9y5c2Wz2XTJJZcoNja22n2ioqIkVU34HzlyRMOGDVN6errmzp2rl156ScuWLdNZZ50lm5Mjud1uV0FBgQoKCmT34i2hP25z/g5Uu136kUluACbg60lmX15wUB8tGrXXsJ4Xas2277Rhx7LK10NDwtSlzSAdLsjS6X2u8GENPa+mPpg85gntPbRNfTuNUv/OZ/qwhp5VU/sl6ePlz+jnzZ/pgUkLFBlu/ocR5hQYz1Z01k/bjcQ/APizI0W+vQDRrLGQRDwU7LGQFFzxkCuPIysscf4GEgDwBV/GIyXl0oEjvisfAIDqhPq6Ap6yaNEiSdKwYcNq3Cc93ZgJPjbh/9JLL2nv3r1aunSpWrVqJUlKSUnRoEGD9Mknn+jss892uA6FhYWVFxs0a9ZMVqt3rq8Yeu18NWrb3+nj3v30J11z1nkeqBEAuE+v8Y8ptX/1zxS95QwpPqrmY+Mj//rv/efUXk7eUWnWV8e/PvWmO7Tj57cdrK1rwkOj9NJU91+FddGIu7R47Vy9+c29euLaxZKkDTuW6ZtVb2jc4Kl6/pMb9WK7tYoIq6UT69ChYweVlNX/4b7e7IOo8Bg1S0pV2+Tu9Tq3u9pewRN9UF37125brFc+v10PX/WlkpPauHxud7e/PlJ6nKUBlzi/HHFBsZQ24FQdztjsgVoBgHskJHfWqTd/W+22umIhyfF4qKZYaNX6P5QyaYSDtXWNp+IAyfPxUDDHQhXM2AeBGA+ddfcaRcY1dvq4h56eq1/n3+aBGgGA+5xyzYdqnDqg2m3emBsaNWaCDu6o/vEwAAC4Kjk5WatWOfiM0r8J2IT/rl27JEmtW7eudntZWZl++OEHSVUT/p999pmGDBlSmeyXpIEDByo1NVWffvqpUwn/Y2VkZLh0nCtslnCXjrNbw7V371431wYA3KtLcWmN2+KjpAYO3IxjtTq2X3WOFBz1+N/KyDDXKtez3VB9+3jNS7y0btpZX8/86/blo8X5enzeJF056lGNGXidpr94il778h+6buyTLpUvSRn79qmotP63HXqrD9zJXW2v4EofONv+zOyd+tfbE3T1WY+rZ7uhrlSzkrvbXx8J7UpcPjbncKEyiIcA+LHS8OQatzkaC0mux0N2u9VvYyHJ9/FQMMdCFczWB4EaD4W42H8l5RbmhgD4vbLymv/Oe2NuKOdwHn8rAQB+JWAT/gUFxgPpjx6t/srqefPmKSsrS3FxcWrbtm3l65s2bdL5559/3P5du3bVpk2bXK6PN+/wt9iKXTyuRC1atHBzbQDAvSIjwmrcllfHzTTxkcYXOptNyiuqfd+azhUXE+Xxv5Xhoa7fYe+MOZ9OV3JSW40ddL0sFotum/CGrn0qTYO7naMeqSe7dM5mzZu77Y4us3FX2yt4ug+KSgp13xtna2CXsTp78NR6n8/d7a+P2Oia/07UJTE+WlbiIQB+LD6pQY3b6oqFJMfjoZrOZVF5wMRCkvvjoWCOhSqYqQ8COR4qK8lXWGSM08eFhdiYGwLg90JrmWb3xtxQYkK8yvhbCQBws+Tkmi/wr0vAJvyTk5OVk5Oj1atXa+DAgVW2ZWRk6LbbjOXJevToIYvFUrktJydHDRo0OO58SUlJ+uOPP1yuz9atWxUT4/wXLVd8tV76aoPzx008e4DeuceFB94CgBfV9jeuumXWjnX/OcbV23lF0v0fu1b+808/qm4pj7p2sIPKS6TFz3i0CP3y+5dasm6eXrplfeU42LxRO1056lE9Me8KzZm+XlHhzo9bW7dsVYhrC81U4Y0+cDd3tb2Cp/tg2Yb52pGxTnuztmjJunnHbX/11k1qktiqmiOr5+7210feUeMzbqv5po9qxUdK63/5ViHeuUYTAFxSUCzd9WH12+qKhaT6x0P90jrp9XTPfm/0VhzgiXgomGOhCmbqg0COh/7zg/TrTuePu//mi9Xr6YvdXh8AcKe3lkurd1W/zRtzQ1999r6axrt2LAAAnhCwCf+RI0dq8+bNeuyxx3TqqaeqY8eOkqSVK1dq4sSJysrKkiSlpaX5sJaeMbC99M1vzk1yWy3SwHaeqxMAuEtKkm/Lb+nj8t2lX6dRWvBg7nGvjxs8ReMGT/F+hXzs39ct8XUVvO7U3hN1au+Jvq6GR8RHST1bSWtqmACqycAOItkPwO/FREiJMVJOgW/K93Us5k7EQ38JxlhICux4aHAH5xP+sZFSj5YeqQ4AuFXLhjUn/D0tIlRqHOebsgEAqEnATmnOmDFDDRs21J49e9S1a1d1795dHTp0UL9+/ZSamqrhw4dLknr27FnluMTEROXm5h53vuzsbCUlmWNmIyFa6tO27v2O1TfVOA4A/J0vE+7xkfytBMxiaCfjgkZHRYYZF00CgBn4Mh5q2dB3ZQNwXNvGxo8zhnaSQkM8Ux8AcCdfXoCYkuTcd00AALwhYBP+KSkpWrZsmc4880xFRkZq586dSkpK0pw5c/T5559ry5Ytko5P+Hfu3FmbNm067nybNm1S586dvVJ3dzivr9SuiWP7tmsindvHs/UBAHdJiJaaNfBN2Sc09025AJzXupF04QDJkXmYsBDpypONZR0BwAw6+ygmCbFK7R38ngnAtywW6f9Ocvwu1D5tpeFdPFsnAHCX1g2lKB89QqVTM9+UCwBAbQI24S8ZyfvPPvtMR44c0ZEjR/Tzzz9r8uTJKigo0M6dO2W1WtWtW7cqx5x11llavny50o95JuHPP/+s7du3a8yYMd5ugsvCQ6Vrhhl3qtW0NG2I1dh+zTBjfwAwiyEdgqtcAK7plyr938nG0tc1aZYgTR0pdUj2Xr0AoL56tTFWJvG2E1sbS34DMIe4KOnG06RuKTVfBBkRKp3eXbp4IHesAjCP8FDj+563hVilAawMBwDwQ0GZ5t24caPsdrs6duyo6Oiqt3JNnjxZs2fP1rhx4/TAAw+oqKhIM2bMUL9+/TRu3Dgf1dg14aHSBf2l0T2kn7ZL2w5I2/ZL5TbjC93d46Q4JmsAmFDvttIna6TiMu+V2TJJasUStoDpdG8pdW0hbd4nrfxT2pBuxEJhIdJ1w42lbi1MbgMwmYj/TXIv/cO75Q7p6N3yANRfbKR01SlS1hHpx23S978bsVCoVTq7t3Fnvy8uIAKA+hrcwfib5k1prZhPBwD4p4C+w78mGzZskHT8cv6SFB8fr0WLFqlZs2a68MILddVVV2nQoEH67LPPZLWas7vioqRTuxmT2rERxmuRYQQnAMwrMkw6rVvd+7nTWWkkBQGzslqlrinSpJP+ioWiw6XUJnyuAZjXiC7eXcq2W4qxfC4Ac2oUJ4098a9YKCbCuIiHZD8As2oSLw1o573ywkKk07p7rzwAAJwRlHf415bwl6R27drps88+82aVAABOGtpZWrdH2n3I82UNbC+dwDPaYBIlpUV66J0LtWv/JkWERalBbBNNG/+CWjSquu7gyj++1iuf317579yCA0qKS9YLN632dpUBAC5IiJbO6S29u8LzZUWHS+f34yIp+LfnFkzTik2faH/OLr1w0xq1b5FW4752u10z5ozQ1r2rteDBXEnS0eJ8PfDWudqa/qvKbWWVrwMA/Ne4XtLvGVJuoefLGt1Tahrv+XIAAHAFCX8AgCmFWI3nTM76SipxcGn/vKNV/+uIRrHGF0gzSD+4VY/Pu1yHC7IUE5mg2y54Q22Sux6336UPt1FYaITCQ6MkSRcNv1ND0y5waPvtL52mnCOZslisio6M05Rxz6h9ixO90DrnOTrpW1JWrDmfTteqLV8rPDRS7Zr11B0Xv125va7+8kej+09Wv06jZLFYtOCHZzXrg6v07+uWVNmn7wmnq+8Jp1f+++7XzlLPdsO8XFMAQH30bStt2GM8rsRRrsRD5/WVEqKcq5svOBoL1TX213QeRy+q8yVH+6DCVytf17/f/z/df/nHGtzt7MrXa4v5/DU2OqnHeZowdIZufn5InfvOX/qkmjVsp617/7rQMSQkTBcMu11xUUm69cWhHqwpAMBdosKliwZILy6W7HbHjnElFmrXRDrlBOfrBwCAtwRlwn/RokW+rgIAwA2SE4znUb60WCqz1b3/rK+cO398lHTtcPMsc/n0/Gs0uv9knd53kpau/1CPz5uk525cWe2+d10yr9a7nmrafs/E9xUb1UCStHzDx3p83iTNuWWdG2rvfo5O+r76xR2yWCx6Y8YWWSwWZedlHrdPXf3lT8LDItW/8+jKf3duNUAffv9ErcdkHd6nNVu/0/QJr3m6egAAN7JYpImDpRcXSTsOOnaMs/HQ2BOlXm2crppPOBoL1TX213YeRy6q8yVn4sHM7J368ueX1bnVgOO21RXz+WNs1CP1ZIf225m5UT9uXKBbJ7yupes/qHw9PDRCJ7YfrszsnR6qIQDAE05oJl3QX3rvJ8f2dzYWat5AuvJk41FxAAD4K4YpAICpdUyWrhkmRbj5EraGsdK0U41nXZpBTv4BbUlfpZG9LpUkndT9XB3M3aO9WdvcWk7FxK8kFRQdluS/a/v2SD1ZjRuk1LrP0ZICffXLq7rijIdk+d86xUnxyd6ontd8vPxpDew6rtZ9vln1hvp1Gq3E2CZeqhUAwF3CQ41Y6AQPDF/n9JaGd3H/eT3B0ViorrG/tvNUXFRXcVznVgO0P2enF1rnGGfiQZvNplkfXKUpZ89WWGjEcdvNFPM5o6y8VE9+eLVuPHeOrNYQX1cHAOAmA9oZq0Ba3TxctW4oTRkpRR8/VAIA4FeC8g5/AEBg6ZAs3TZaeu9nadv++p+vX6p0dm/jebVmcTB3j5LimykkxBjaLRaLmiS20oHc3dUuMzvzvctkl12dWvbTlaMfVYPYxg5vf2zuZVq3fbEk6aErv/BgqzwvI2u74qKTNHfRw1q9daEiwqI08dT71avDiCr71dVf/urd7x7WvqxtmnnNdzXuY7fb9fXK13T9uGe8WDMAgDtFhEmTh0mLN0tfrpfKHVj5qDYNY6WLB0jtmrqnft7gaCxU19jvTEzlyEV13uRM3ecvnaWubQarY0rvGs9XW8xn1tjoP98+oCHdxqt1087cyQ8AAaZfqrES5LsrpMzD9TuX1SKN6CKd3l0K5fowAIAJkPAH/p+9+46Pqsr/P/6eNFIggdACJJQQeu9SVKKoFLFgF1nx54oNUUFxd13b111UxAY2rOi6iyiia0cRUcAG0hHpAQIJENJ7mfn9cZdAJCEzyczcuTOv5+ORR2Duufd8zs2duZ85595zAfiFZo2k286VftgpfbXFtWexHRcXY0xb272N++Orr2nzhupgxs5ql71093qXtvX0rd+rRZO2Kq8o05tf/l2zF12vWSd14ta2/L5r3pYkfbX2Lb36+X1VlnlLbfujReMEp7ZTYS/X4ax9ateiu/489nHtOrhe971ynl67Z6uaNDJGOWrbH77q/RVztGrLEs2eskzhYZE1ltu05zuVlhdrYJcLvBgdAMDdgoOkUT2knvHSkrXSjlOfUFOrsBBpWJI0po/7Z0+qL3flQs6c+53hzEV17uaufbA3fYtWbv5AT9/2/WnL1ZTzWTU3koy850jWfv33h+dVYS9XYUmurpvVXs9PW2OZixYAADVr21S6Z4z09Vbpu9+l4jLXt9GhmXTpQGNbAABYhY99hQcAoO6CbNKIztLQJGlzqrR6h/E829Pd5dYgROrW2livYwvjWbi+aO4dP552eWhIA2XmpqmiolzBwSFyOBw6krVfLRq3PaVsiybGayHBoZpw5l26YXZnl5Yfd/7A6/XcB7cot+CYoqO8+024tv3hrBZN2irIFqRz+k+UJCW16ae42A7am7a5stPf2f3hSxZ/97S+3bBQT0xZVmVK3up88cvrOn/gZAUzrS0A+IW4GOMiyPQc40LI9fukvOKay9sktWoiDe0oDewgRfjoDEfuyoVqO/c3b5xQ63acvajO3dy1D7bsWanDWSma/EQnSVJmXrqeXTxFmblpGj/s1lO2+8ecz4q50XHP3Lay8t/pmSm65Zm+eudvKeYFBABwu5BgaUxv47FE61KkH3dJBzIlh6PmdSLDpD5tpeGdpPhYr4UKAIDbMOAPAPA7wUFS37bGT3mFlJYjpWZKH/0qlZQbg/xXDJYSYqXm0e5/xpsZmjRsoaQ2/bVs3Tu6YNBkrdz8gZo1jj9l+tai0gJVVJRVDgJ/u36hklr3c2p5flG2iksL1SymtSRp9ZaPFB3VVI0irfttOCaqmfomnau125dqSLexSsvcq/TMvWrbspuk2veXLzqanar5n85Qq9hE3fNysiQpLKSB5k37WQuWPqim0a01fugtkqSCohyt3rxEr8zYbGbIAAAPiIuRJgyULh0g5RRJB45JR3KNmZBKyqXwUOmmkVKbJsa/rc7ZXKi2c39t23Hlojpvc3YfjB92a5WB/RkvjdSEM+/S8J6XSDp9zufLudGzi2/Wz79/psy8dP31tQsU2aCR3vrLLj31/p81tPtFGtbjolq3MeWp3sopOKrCklxd84949emYrL9c8y8vRA8AcLcGIcYNIUOTpNJy6WCW8fPJ+v/lQiHSNUONAf7YKN+9AQQAAGcw4A8A8GshwcbAfkKs9OWmEx3cAzuYHZn73XXZfD25aLIWLp+lyPBo3Xvlm5XLjnd0dojrqUfevkx2e4UccqhVbKJmXv12ZbnsvMM1Li8oztGj/7pCJWVFCrIFKSaquR694VPZfPRbcU2dvpKqdPzeddnLeur9G/Xa5/cpyBakuy6br2YxxnMdTrc/fFXzxvH6+snqb12YfMH/Vfl/VESMPplV4I2wAAAmsdmkxpHGjyR9v/3EBZAdW5gbm7s5kwvVdu4/3XZOd1Gdr3B2H5zO6XI+X86N7rp8frWvz7jitWpfj4ttr48eza7y2iszNrk7LACADwgLkTo0N36+/t/Fjw1Cjbv6AQDwBzaH43ST2aA+CgoK1LBhQ0lSfn6+oqKiTI5IemiJcXdHTIT0yASzowEA77LKZ2BFqfTtXLOjqJvkaVKwG6YCtuI+cFfbj7PaPnB3+z3FKp8DAOApVvgctNo58GSBnAsdF8j7wAr5kBU+AwDAk/gcBAD4oyCzAwAAAAAAAAAAAAAAAK5jwB8AAAAAAAAAAAAAAAtiwB8AAAAAAAAAAAAAAAsKMTsAAABQVVCo8fxPKwoKdd92rLYP3NX2k7dnpX3g7vYDAAKX1c6BJwvkXOi4QN4H5EMAAAAAzMCAPwAAPsZmk4LDzI7CXOwD9gEAIHBxDmQfSOwDAAAAAHAWU/oDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBDPgDAAAAAAAAAAAAAGBBIWYHAOBUDodkLzM7CtcEhUo2m/u2F+j7INDbDwBAoJ8LA739kvX2AbkQAAAAAAAwAwP+gA+yl0nfzjU7CtckT5OCw9y3vUDfB4HefgAAAv1cGOjtl6y3D8iFAAAAAACAGZjSHwAAAAAAAAAAAAAAC2LAHwAAAAAAAAAAAAAAC2LAHwAAAAAAAAAAAAAAC2LAHwAAAAAAAAAAAAAACwoxOwB4R2GplFckVdiN/zsc5sYDAADgTRV2KavgRC5kt0t2hxRkMzcuAAAAb8kvPpELVdil0nIpjJ5BAAAAwPJI6/1Uabm0bp+0PU06cEzKyK+6PLdY+sd/pYSmUpdWUv92fMkDAAD+w+GQ9h+Tfk0xfh/MksoqTizPK5H+9r6UECu1ayYNSpRaRpsWLgAAgNsVlEhr9kq7DkupmVJ24Yll+SXSfe8Z+U9CrNQz3vgJZi5QAAAAwHIY4vUzOYXS8m3SL3ukotLTl83IN37W75P+u04a1EE6t7sUE+mdWAEAANzN7pDW7pVWbpcOZJ6+bHGZtPOw8bNsq9Q5ThrZVerexjuxAgAAeEJ6jrT8N6O/5+QLHv/I4TDKpucYFwbEREhDk6SR3aTwUO/FCwAAAKB+GPD3Ew6H8eXsw19rH+ivTlGp9P12YxuXDjAG/21McWs5G3ev0D0vJ1d5LTwsSvHNO2tU/0m6ZPgdCg7237d9oLdfYh8ACGwZedLCn6TdR+q2/o5046dfO+mygVLDcPfGB8/jPMg+CPT2AwhsFXZjoP/LzSem7ndFTpGx7k+7pavPkLq2cn+MAAAAANyPng4/UFIu/Wu1tCW1/tsqKpX+86O06YA0abjUgCPEkpL7XqPBXcfKIYey8tL19a9v6+VPpmv/kW26+/JXzA7P4wK9/RL7AEDg+TVFWvSTVHqau9ictX6fcdf/DSOkji3rvz14H+dB9kGgtx9A4Mktkl77zniUUX1lF0ovL5fO7CJd2l8KYpp/AAAAwKeRsltccZn08jfuGew/2ZZU48tdcZl7twvv6NSmv0YNuE7nDZikK0feq7l3/KTmMfH64pfXlJ1/1OzwPC7Q2y+xDwAElh92Ghc/umOw/7j8Yunlb6Vth9y3TXgP50H2QaC3H0BgyS6U5n7tnsH+k63cLv3rh7rNFgAAAADAexjwt7AKu/T6d9LeDM9sf+9RY/t8sbO+iLAodW13hhwOhw4d2212OF4X6O2X2AcA/Ne6FOn9Xzyz7bIK6Y3vjZwI1sZ5kH0Q6O0H4L8KS6QXvzEebeQJ6/d5LtcCAAAA4B5M2G5h32w1ppt1xfTRUnSEMdXb01/WXn7nYemb36Tze9YtRviOtP91bEZHxpociTkCvf0S+wCA/8kqkBb9LDlcWMfVXKisQnrnB2nmOB51ZHWcB9kHgd5+AP5pyVrpSK7z5V3NhSTpp91S11ZS33Z1ixEAAACAZ9FtaVGHsqSlW1xfLzpCahzp2jpLN0s920itm7heH8xRXFaonIIMORzGM0s/+fFl7Tq4Xl0TBiu+eWezw/O4QG+/xD4A4P8cDundn6SSctfWq0sudCxf+nS9dNkg19aDeTgPsg8Cvf0AAsOWVGltimvr1CUXkqT310gdW0qNwl1fFwAAAIBnBcSAf0ZGhmbPnq0lS5YoNTVVzZs314QJEzRr1ixNmzZNb7zxhubNm6epU6eaHarTPljrvan2K+xGfXec5536PKG4TNqaKuUWS0E2qVkj4+r0YD99qMXbXz2kt796qMprI3pO0B2XvmBSRN4V6O2X2AcA/N/GA9L2dO/Vt3KHdEaS1MaiF0A6HMajCQ5mGbMWRIZJ3dpIMRFmR+YZnAfZB4HefgD+r8IuLV7jvfoKSqTPN0pXDfFene6WVyT9dshoS2iw1Kqx1LGFZLOZHRkAAABQP34/4L9hwwaNGTNG6enpioqKUvfu3XXo0CHNnTtXu3fvVmZmpiSpb9++5gbqgkNZ0u4j3q1z9xGjXqvd5Z9VIH29Vfp176l3AMZESMM6ScndpDA/eyeMGzJFZ/W+QuX2Mu1N26xFK55QRk6qwkJPXIpfWl6i257tr+R+12riufdXvj773cnKzj+sWX/+wozQ3cKZ9v/znatld9j1wKT3Kl/LLczUTXN6aMqFc3Ru/4lmhO42zuyDzXtW6m+vjzll3fKKUtntFVo6u8KbIQOAS1Zu936dq3ZYr5Pb7pB+2mXsr7ScqsuCbFLvBOncHlKCn81wHui5kEQ+RC4EwN9tTpWyC71b59q90vi+UmQD79ZbX4eypGVbjQtG/3jzTIto6czO0vBOUpCf3hQCAAAA/+fXqWxGRobGjx+v9PR0zZgxQ2lpaVq3bp3S09P1xBNP6LPPPtOaNWtks9nUu3dvs8N12qodJtW705x66yo103ge3Q87q5/uN6dI+mKT9MIy4+puf9KmWSf17zxKg7uO0VXJM/XoDZ9oe+oaPffBLZVlwkIaaObVb+vdb2Zp96GNkqTVWz7ST9s+0fQrXjcrdLdwpv13THhRW1NWa/n6hZWvzfvwdvXoMMLSndvHObMPeiWeqU/+mV/l582ZOxQd1UzXX/CoidEDwOmlZXv/4kfJuICwsNT79dZVhV16Z7X03i+nDvZLxsUAG/ZLzy2VNh3wfnyeFOi5kEQ+RC4EwN+tNqFvqKxC+mWP9+utj98OSs8sldbtq36mzCO5xqyWC1ZJ5VznBQAAAIvy6wH/adOmKTU1VVOnTtWcOXPUqFGjymUzZ85Unz59VF5ervbt2ys6OtrESJ3n+F/HrBk27DPqt4KsAumVb6W84trL7jsmvf6d9x6RYIYe7YdpVP9JWrFxkbam/FD5euf4Abr87Hs0+90/6Wh2qp5dPEV3XPqCmsW0NjFa96uu/dGRsZpxxet6/qOpysg5pO83Ldam3St014SXTY7WM2o6Bk5WWl6iR96eoJ7tR+jac//m5QgBwHnr95lTb2mF0WlsFUvWGp3btSm3S2+tkvaYcBGFtwR6LiSRD5ELAfAnecXSzsPm1O1MbuEr9h+T3lxpXKhQm00HpEW/eD4mAAAAwBP8dsB/27ZtWrRokZo1a6bHHnus2jIDBgyQJPXp06fyteMXCAwePFgNGjSQzcce5HUs37w7ywpLjfqtYPk2KdeJwf7j9hw1psPzZxNHPaCgoGC9tfTBP7z+dwUHhejWZ/upT1KykvtebVKEnlVd+wd1Ha2ze1+pJxZep3lLbtP0K15TdFRTE6P0rJqOgeOe++AWlZYV696rFng3MABw0f5j5tV9INO8ul1xOFda7cLsTBV26bONnovHFwR6LiSRD5ELAfAXB0zMhQ5lWeeGic83OjfYf9yaPdLBLM/FAwAAAHiK3w74L1y4UHa7XRMnTlTDhg2rLRMRESGp6oD/rl279MEHHyguLk6DBg3ySqyuMLuT2ez6nVFSZnxJc5UZ0+F5U5tmSUruc7XW7/pGm/esrHw9JDhU3dsPU05Bhi4YeIOJEXpWTe2fMn6ODh7bpUFdx2hIt3EmRuh5Ne0DSfpw1Vz9vO1TPTL5I4WHRZoUIQDUzuEwHttjFjM72F3xQx0exbT7iPG4BH8V6LmQRD5ELgTAX5jZN1Nul9KreVSQrzmaJ/2e5vp6/t43BAAAAP/ktwP+y5cvlyQlJyfXWCY11bil++QB/7POOktpaWn6+OOPNWrUKM8GWQdHcgO7fmdsS5OKy1xfb+dhKbfI/fH4kmvOvV9BtiC99dWJu5o271mpr9Yu0MXDp+rFj+9USZn/7oTq2h8RFqVWsYnqENfLxMi8p7p9sGHXt3rts/v0wKT3FRfb3rzgAMAJRaVSfol59R/JM69uV6xL8e56VhHouZBEPkQuBMAfmN03Y3b9zqjrI6Cs9MgCAAAA4Dibw2GVp7K7JiEhQampqVq/fr369u17yvLy8nK1atVKGRkZ2r17txITE08p8/DDD+uRRx5RXXdRQUFB5ewCrVq1UlBQ/a+v6HnBfep6zh3VLps+WoqOOP360eFSUJBkt59+yvvcIunpL099/ffl87Rl6RMuROx9HYder36X/LNO6379zHnKSd/m5ohcFxYSoVem1uHWPBcVleTr5qf76LKzpmv80Fs14+Wz1Tl+oG696BmXtzXl+U4qLXdfB7m39oEkzXhppM7odqGuGHlPvbbjzn3grfanZ6Zo6txBuu68h3TJ8Kn12pa7jwFPGPu3NYqMaaXCnDR9Psv3ZnEBULvwRi114d9/rXF5bflQfXOhspJ8/ffBri5EbI7LHtsnW1Cwy+vt+eU/WvfBTA9E5DpvnAvdmQtJ1swFjnNHPmTFfDDQciGJfAjwB2dc94rie42tdpm7ciGp5nxozXt3a9+v77sQsff1Gf+IOo24sU7rLvlbouwVJj1PE4DHkQsBAHxVXFyc1q5dW6d1Q9wci88oKCiQJBUVVd/hsmjRImVkZKhRo0bq0KGDx+NJS6vDPGLVaJtb87xp0RFSYydnnwwKcr7syXJys3Xw4EHXV/SiJseO1nndtEMHlJVmfvvCQ70zjej8T2YoLraDLhp2m2w2m+69coFuebavhve8VL0Tz3JpW2mHDqm4rNBtsXlrH7iTO/eBN9pfXFqohxZcoqHdL6p3B7fk/mPAEyoqKip/+/pnGYDqRUaffhofZ/OhuuZC9vJyS3x+2O0VCq7DgH9eTpbPtM8b50J35kKS9XIBd7NaPhiIuZBEPgT4g6LCghqXeToXkqTMYxk+//mRmJtd53VTD+yTw2F3XzAAfAq5EADAH/ntgH9cXJyysrK0bt06DR06tMqytLQ03XvvvZKk3r17y2azeTwed93hHx5a8zJnpqN35a626kSEBalNmza1V2SikPJsSZLD4XDpb1tRVqzocIcifaB9YSG1TNXgBr/8/oVWbFykV6ZvqtxPrZt11I1jHtecRTdo/oxNigiLcnp7rVq3dvsdXVbjzn3gjfav3PyB9qRt1MGMHVqxcdEpy1+/5ze1aNLW6e25+xjwhODg4Mrfvv5ZBqB6waGn/3ysLR+qby5UUVZgic+P/GN7FdOyi8vrOYqP+Ez7PH0udHcuJFkvF3A3q+WDgZgLSeRDgD8IsZXXuMxdudDpttUwMtT3Pz+K63YzSP6xFLVu3crNwQDwJeRCAABfFRcXV+d1/XZK/2nTpmnevHlKSEjQsmXL1LlzZ0nSmjVrNGnSJO3Zs0dlZWW6/fbb9fzzz1e7DXdO6Z+fn6+oKNc6DKuz7ZA0/9u6r//wpcYV3NmF0sMfur7+zclSt9Z1r98b7A5p1idShovP2B2cKF07tPZy3lBRKn071+woXJM8TQoOc9/2An0fBHr7PeWhJVJOkRQTIT0ywexoANTVPz52/Tx/XH1zoR5tpJtG1q1ub/rud+nDmp98UK2QIOOzMaqBZ2JyVaCfCwO9/ZL19oEVciGJfAjwB3U5zx9X31xIkh64WGrasG7rektRqfTQh1JpzddGVOvCvtKoHh4JCYCPIBcCAPij+t9y7qNmzpyppk2b6sCBA+rRo4d69eqlTp06afDgwUpMTNQ555wjSerTp4/JkbomITaw63dGkE0a3sn19UZ0dn8sAADA/czMR+ItkAtJ0qBEKczFGf37tfOdwX4AAFCztk3NqzsyTIqt//0sHhcRJg1s79o6wUHSkI4eCQcAAADwKL8d8I+Pj9fKlSs1btw4hYeHKyUlRbGxsZo/f74+++wz7dixQ5L1BvwbhkvNGplTd/NGRv1WcGZnqVNL58uf18PcL8wAAMB57ZsFZt2uiAyTrj7D+fJNG0oX9fdcPAAAwH3aNJFCXbywz13aN5O88GRMtxjXV2oR7Xz5q4ZIjSzS7wUAAACczG8H/CWpW7du+vTTT5WXl6e8vDz9/PPPmjJligoKCpSSkqKgoCD17NnT7DBddoZJVxtb6SrnkGDpz2c79/iB83pIY6113QcAAAFtQHtj+nlvaxIpdan7o7S8rn976bphxt1qpxMXI00dRQc3AABWERZizMxjhjOSzKm3LqIaSLefa1wgcTpBNunqIcajHgEAAAArCjE7ADNs3bpVDodDnTt3VmRk5CnLFy9eLEn67bffqvy/ffv2GjhwoPcCrcEZHaUvNkkVdu/VGRxk3oUGddUg1HjG7u+HpNU7pd8OSo6Tlg9NMqb+t8rUvAAAwNAw3OjkXrPXu/UO6yQFWexy2YEdpMTm0o+7jJ/8khPL2jczHmnUp615dwkCAIC6GdFZ+mWPd+tsHCn1aOPdOusrJlK6+wJp0wFp1Q5pz9ETy2ySkrtLw5LMm00TAAAAcIeAHPDfvHmzpJqn87/iiiuq/f/111+vBQsWeDQ2ZzQMN646/nGX9+ockmid6fxPFmSTurcxfgpLpVkfGx3d0eHGVG0AAMCaRnaT1qZIDketRd0iPNRad7SdLLahMaXt6N7Sw0ukvBLjbv67LjA7MgAAUFdtm0pJLaVdh71X59lda585yBeFBBszH/VvL+UXS49/avQNNQqXLupndnQAAABA/THgXw2Ht3qO62F8P+OO9Zwiz9cVEyFd6AdfgCLDTnwxtcrz5gBnlJYV65//vlr7Dv+mBqERatywhaZNeEltmlUdmVqzfale++y+yv9nFxxRbKM4vXTXOu1N26zHF06qXFZQnK3C4lwt+b9Mr7UDAFzRpol0Tjfpm9+8U9+lA6w/5X1w0IkZCoL8PBdy9txYVJKvR96+TDtTf1WFvVwfPZptTsD15Gx7Jem9FU/q67Vvye6wK6F5F91z1ZtqGNFYGTmHNOe9G3Q4K0WhwQ3Uplkn3XnZy2rcsLkJLXJO6tGdenLR9copyFBUeIzuvWqB2sf1qFImPTNFTy6arF2H1iuuSQfNn76hctn6Xcv1+ud/UVFJvmw2m4Z0Hacbxz6uoD9M5TH73cn6+te39OH/ZalhRGMvtAwAnHPVEGn2Z1JZhefrattUOquL5+vxtIbh9A0BAADA/zDgb1GRYcYXu1dWuLZeblHV3864aohRH6zD2U7f05VzpePYF73w0TT9+NvHOpy1Ty/dtV5JbfpWW27N71/qzaV/V3l5qRqERequy+arY+sTnw0/b/tcC5b+XXa7XXZ7ua4Yea/OH3i9l1rhvLFDpmhw1zGy2Wz6aPXzevr9P+upW1dUKTOoywUa1OXE7Zx/f+NC9emYLEnq0KpXlQ7weR9OlY3eDwA+bkxvaetBKT3H+XXqkgt1b80zXa3ImXNjcHCorkq+T40iYnXPyyNNidNdnGnvrzu+1tI1b2reHT8rMryR/r3sH3rji/s1bcILCg4K1nWjHlDPDiMkSa98eq9e+fRezbx6gfcb46TnPrhZY4dM0QWDJuv7TYv15KLJeuHONVXKRIZH64bR/1BBcY7e+OL+KssaRTTR/RPfVaumiSotK9bMV0bp61/f1gWDJleWWbl5iUKCQ73RHABwWfNG0oV9pQ9/dX6duuRCwUHStUOteXc/AAAAEAgCMlVfvny5HA6Hxo0bZ3Yo9dK9jTS2t2vrPP2l9PCHxm9njO1t1APrGTtkit6cuV3zp2/U0B4X6+n3/+xyOWe34YvO7H25nrltlVo2aVdjmbzCLD22cKJmXvWWXpmxSVPGPanH/zOxcrnD4dATC6/TvVcu0PzpG/To//tUz35wswqL87zRBKeFhYZrSLexlQP03dqeocNZKaddJyPnkNbv/EajBkw6ZVlpWbGWr/+3Rg+60RPhAoDbhARL/+8s1x475GouFBdjdHBzDZS1OHtuDAtpoH5J51j+rm1n27vn0Eb17DBCkeHGg4oHdx2rb9b9S5LUpFHLysF+Seradkit+YSZsvKPaEfqWo3qf50k6cxel+lo9gEdzKj63LPoyFj17DBC4WFRp2wjqU0/tWpqXM0TFhqujq37VmlzVt5hLVw+S7eMf9pzDQGAejqzizSog/PlXc2FbJImDjVyIgAAAAC+KSAH/P3JeT2lC3p5ZtsX9DK2D+txupP7NOXqMojsS3onnqXmjeNPW+bQsd2KjmxaOfVrr8QzdSR7v3amrjtRyGZTfnG2JKmwOFfRkU0VGtLAU2G7xYerntPQHheftsxXaxdocNexatKwxSnLVm1ZolaxiTXOigAAvqRFtHTbOZ6Zbj8uRrr1HNcuKIBvcubc6E9qam+n+AFat3OZMnPT5XA49M36f6uwJE+5hVUf4VNhr9B/Vz+vYT68z45mH1BsdCsFBxuT1tlsNrVo0lZHsvfXaXuZuelauWmxhnS7sPK1pxffpJvGza68QAIAfFGQTbr6DGlAe/dv22aTrhkq9ffAtgEAAAC4T0BO6e9PbDZjOtsmUdKHa6WS8vpvs0GIdOlA6YyO9d8WfIOzndynK+ePHeXxzTopt/CYtqb8oB7th+mHrR+rsCRP6Vkp6hTfXzabTX+fuEiPvDVB4WFRyi/K0kN/WqLQEN99xsV/vpmlQxm7NPvmb2os43A4tHTNG7rt4rnVLv/il9c1ejB39wOwjtZNpDvPl975QUrJcM82eycYjzWK8u1rvOAEZ86N/uR07e2blKwrzr5Hf3/zQgXbgjW856WSpOCgE18LHQ6H5i25TY0imujSEXd6LW4zFRTn6oE3x+vKkTPVJWGgJOnzn19Ti8Zt1S/pHJOjA4DaBQdJE4cZU/x/tUWyO+q/zegI6ZozpG6t678tAAAAAJ7FgL+fOKOj1Lml9O7P0o70um+nc5x09RAptqH7YoP7TZs3VAczdla77KW716tF44TK/zvbyX26cr7WUe5K+08nKiJGD05arNe/+KuKS/LVrd1QtWvZvbLTu6KiXP/+5h966Pol6p14lrYfWKMH37xIr8zYrJioZm5rj7u8v2KOVm1ZotlTlik8LLLGcpv2fKfS8mIN7HLBKcvSMvfq930/6aE/feDJUAHA7Zo1kqadJ323Xfp8o1RWUbftRIZJlw2S+rdjGn9/4Oy50V84096Lht2mi4bdJkn6bd9Pah4Tr6jw6MrlL/x3mo7kHNAj13+koCDfnRCueeMEZeamqaKiXMHBIXI4HDqStV8tGrd1aTuFxXn622ujNazHxbr87OmVr2/c/a027/leP2/7tPK1KU/31v9N/q+S2vRzWzsAwF2CbNLo3lLPeOk/P0mHsuq+rUEdpEsHSJFc+AgAAABYAgP+fiS2oTHt7K7D0qqd0uYDzl3VHWSTeiVIwztJnVrSuW0Fc+/40alyznZyn66cL3aUO9t+Z/RNSlbfpGRJUml5ia76vzi1a9ldkrTr0AYdyz2k3olnSZK6JAxSs5h47Tq4XgM6n+e2GNxh8XdP69sNC/XElGW1Pof4i19e1/kDJys4KPiUZUt/eUPDe15q+WcZAwhMQUFScjdjsP7H3dKPO6WcIufWbRFt5EKDEo1Bf1ifK+dGf+Bse4/lpqlpdCsVlxbqraUP6sqRMyuXvfDRNB3K2KWHJ3/k0zMaSVKThi2U1Ka/lq17RxcMmqyVmz9Qs8bxatMsyeltFJXk66+vjdbALqM1cdTfqyz767X/rvL/8+616ZXpmwLiWAJgbfGx0ozR0taD0qodzt8UEhpsPBZgeGcpIdajIQIAAABwMwb8/YzNJnWKM35yCo3B//2ZUmqmlFsklVdIIcHG1GzxsVLbWCmppRTjG+O4cCNnO31PVy4QOsqPd3pL0r+XPaq+Hc+p7Chu0ThBmXlp2nd4m9q17KaDGbuUdmy3Epp3MTPkUxzNTtX8T2eoVWyi7nnZuHghLKSB5k37WQuWPqim0a01fugtkqSCohyt3rxEr8zYfMp27Ha7vlq7QDOvftur8QOAu8VESqN7Sef1kHYfkfYfkw5kSodzpNJyI19qECK1aiwlNJXaNZXaN+OiR3/iyrlxylO9lVNwVIUlubrmH/Hq0zFZf7nmX2aG7zJX2vuXV8+Xw2FXWUWpRvWfpIuHT5Ukbdm7Wh+tnqeEFl11x9whkqRWsR308OQPzWmUE+66bL6eXDRZC5fPUmR4tO698k1J0lPv/1lDu1+kYT0uUnFpoW6Y3Vll5SUqKM7RNf+I16j+k3Tj2Me0ZNVz2n7gFxWXFmjV5iWSpLP6XKGJ595vZrMAoN6Cg4zHE/VOkI7mGflQ6v/yoYJSqeJ/fUOxDY3B/YRY4wYQ7ugHAAAArMnmcDjc8GQvVKegoEANGxpz4+fn5ysqKsrkiPDQEuNOv5gI6ZEJZkdTs4pS6dvqH6/ulKPZqbr2nwlqFZuoiAaNJJ3o9JVOdIJ2atO/xnK1beOPkqdJwW68Eay+++DZxTfr598/U2ZeuqIjmyqyQSO99Zddkqp2Aj/9/k3asnelKuzl6tZuqKZeMq/KxQ3L1y/UwuWzFGQLkt1h1zXn/FXn9Lu22jrduQ/q234zuPsY8ASrfAYAgKdY6XMw0M+Fgd5+yXr7wAq5kGStzwEA8AQ+B4HAxmcAAMAfcYc/4IeaN47X10/WfC3PjCteq/x3TeVq24avu+vy+TUuO7n906949bTbOaffNTqn3zVuiwsAAAAAAAAAAABwlyCzAwAAAAAAAAAAAAAAAK5jwB8AAAAAAAAAAAAAAAtiwB8AAAAAAAAAAAAAAAtiwB8AAAAAAAAAAAAAAAsKMTsAAKcKCpWSp5kdhWuCQt2/vUDeB4HefgAAAv1cGOjtP749K+0DciEAAAAAAGAGBvwBH2SzScFhZkdhrkDfB4HefgAAAv1cGOjtl9gHAAAAAAAAzmBKfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALIgBfwAAAAAAAAAAAAAALCjE7AAAoDoOh2QvMzsK1wSFSjabe7YV6O0HxwAABBIrfuY7qz7nBqvtF86D7mW1v7/EMQAAAAAAMAcD/gB8kr1M+nau2VG4JnmaFBzmnm0FevvBMQAAgcSKn/nOqs+5wWr7hfOge1nt7y9xDAAAAAAAzMGU/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWFCI2QEAnuJwSKmZ0v5M4/fhHCm/2FiWXyJ9+KuUECu1byY1a2RurAAAAJ6QVyTtzZAOHJNSs6rmQv9abeRCCU2ldk2lkGBzYwUAAHC3Cru0L0M6kGn8HMs7kQ8VlEifbvhf31BzKSbC1FABAACAOmPAH36nsFRas0datUM6mld9mQq79N3vJ/7fsYU0orPUO0EKZt4LAABgYQ6HtPOwtHqHtDlVsjtOLVNhl35NMX4kqVG4NDTJ+GkS5c1oAQAA3C+nUPpxl/GTU1R9mXK7tGyr8W+bTerRxugb6hwnBdm8FysAAABQXwz4w284HNIPu6SP10kl5a6tu/uI8dO0oXTNGVJSS8/ECAAA4EmHc6WFP0opGa6tl1csfbXF6PQ+p7s0uhd3/AMAAOspr5C+3ip9vaX6ix5r4nBIW1KNn4RY6dqhUqvGHgsTAAAAcCsG/OEXsgqkhT9JO9Lrt51j+dLzy6QzO0vj+0lhvEMsZePuFbrn5eQqr4WHRSm+eWeN6j9Jlwy/Q8HB/v1HZR8ENv7+QOByOKQVv0ufb5TKKuq+HbvDGPTfkipNHGpM9w9rCfRzQaC3HxwDQCA7lCW986Pxuz4OZEpzvjAugDy3B3f7AwAAwPfxLReWdzhHemm5lF3ovm2u3CEdypZuGimFh7pvu/CO5L7XaHDXsXLIoay8dH3969t6+ZPp2n9km+6+/BWzw/MK9kFg4+8PBBa7XXrvF+mn3e7bZnqONO9r6f+dLXVt5b7twnsC/VwQ6O0HxwAQaHYell5b4fqMjzWpsEufbTT6nK4ZyuMfAQAA4NtIV2FpGXnSC9+4d7D/uN1HpFe+lUrd9GUR3tOpTX+NGnCdzhswSVeOvFdz7/hJzWPi9cUvryk7/6jZ4XkF+yCw8fcHAofD4f7B/uNKK4yO852H3b9teF6gnwsCvf3gGAACyZ7/9d+4a7D/ZGtTjMclufJ4AAAAAMDbGPCHZZVXSK9/J+UWea6OPUelD9Z6bvvwjoiwKHVtd4YcDocOHfPAiIgFsA8CG39/wH99v90zg/3HldulN773zMWV8K5APxcEevvBMQD4q7xi6fXv6/dIo9qsTZG+/c1z2wcAAADqiyn9YVlLN0tpOa6tM320FB1hXCTw9JfOrfPzbqlvW6lba9djhO9I+1+nXnRkrMmRmId9ENj4+wP+52ie9OkG19apSy5UVCq997PxqCMbz7C1tEA/FwR6+8ExAPijxWukghLX1qlLPvTFJqlHvBQX43qMAAAAgKcFxB3+GRkZmjlzppKSkhQeHq6EhATdeeedKigo0I033iibzabnn3/e7DDhgtRM6Zs6XF0dHSE1jjR+u2LRz1JJmev1+RKHw5jerqDEeBadPysuK1ROQYay849qb9pmzV1yu3YdXK+uCYMV37yz2eF5BfsgsPH3B/yfw2HkJ67ezVbXXOi3Q9KvKa6t44sq7EYuVFpu7EN/FujngkBvPzgGgECwcb/x46q65EPldmnhT9bPHxwOqbhMKizhMQUAAAD+xO/v8N+wYYPGjBmj9PR0RUVFqXv37jp06JDmzp2r3bt3KzMzU5LUt29fcwOFS77d5t0vJtmFRif3sE7eq9Nd8ouNWQp+2CUdyzdeC7JJPeOl4Z2kznH+d7fe2189pLe/eqjKayN6TtAdl75gUkTexz4IbPz9Af+375i067B36/xmqzSgvfXyBrtD+v2QtGqHtO2QdDyFbN7IyIUGJ0qRDUwN0SMC/VwQ6O0HxwAQCOpyI0h97MuQdh+Rklp6t153yMw3+oV+2iXl/29GhNBgqV87aURnqW1Tc+MDAABA/fj1gH9GRobGjx+v9PR0zZgxQw899JAaNWokSZo9e7buu+8+hYSEyGazqXfv3iZHC2flF0sb6nAFd32t2ikNTbJWJ/fG/dI7P5x695/dIW06YPx0aindcJYUGWZOjJ4wbsgUndX7CpXby7Q3bbMWrXhCGTmpCgsNryxTWl6i257tr+R+12riufdXvj773cnKzj+sWX/+wozQ3caZffDPd66W3WHXA5Peq3wttzBTN83poSkXztG5/SeaETrcwJm//+Y9K/W318ecsm55Rans9gotne3Bh2ACqLfVO7xfZ1qOtOeo1LGF9+uuq/xi6dXvjA76PzqaJ320zpiid/KZ/vf4pkDPh8iFQD4E+Lf9x4wfb1u1w1oD/g6H9N3v0n/Xnzo7QVmF9Mse42dQB+mqIVJIsDlxAgAAoH78ekr/adOmKTU1VVOnTtWcOXMqB/slaebMmerTp4/Ky8vVvn17RUdHmxgpXLFmrzlT0h/Kkg5ker/eutq4X1qwsvapfncell5ebkz37y/aNOuk/p1HaXDXMboqeaYeveETbU9do+c+uKWyTFhIA828+m29+80s7T60UZK0estH+mnbJ5p+xetmhe42zuyDOya8qK0pq7V8/cLK1+Z9eLt6dBhBB7fFOfP375V4pj75Z36Vnzdn7lB0VDNdf8GjJkYPoDbFZdL6febU/eMuc+qti8JS6YVl1Q/2n6ykXHp1hXH3vz8J9HyIXAjkQ4B/Mysn2XTAuKDQKpb/ZlzgWNujCNbslf61WrL7+SMgAQAA/JXfDvhv27ZNixYtUrNmzfTYY49VW2bAgAGSpD59+lS+tnjxYl122WVq166dIiMj1bVrV91///3Kz8/3Styo3d6j5tW9x8S6XVFQIv37hxNT1tZm/zHpy00eDclUPdoP06j+k7Ri4yJtTfmh8vXO8QN0+dn3aPa7f9LR7FQ9u3iK7rj0BTWL8bNb/FT9PoiOjNWMK17X8x9NVUbOIX2/abE27V6huya8bHK0cLea3gMnKy0v0SNvT1DP9iN07bl/83KEAFyx/5jxHFkzmJmHueqT9casBM6wO4xObn+6APKPAj0fIhcC+RDgX8zKSewOc2YWqIuDWdInG5wvv/GA9NNuj4UDAAAAD/LbAf+FCxfKbrdr4sSJatiwYbVlIiIiJFUd8J8zZ46Cg4M1a9YsffHFF7r11lv10ksvafTo0bJzmatPOGDiF6tUi3yp+2WPVOri7JM/75ZK/biTe+KoBxQUFKy3lj74h9f/ruCgEN36bD/1SUpWct+rTYrQ86rbB4O6jtbZva/UEwuv07wlt2n6Fa8pOoqH9/mjmt4Dxz33wS0qLSvWvVct8G5gAFyWauKMQ8fypcIS8+p3VmGptHav6+usS/FIOD4j0PMhciGQDwH+oaRcOpxrXv1Wmf2xLo+AWrWj9tkAAAAA4Hv8dsB/+fLlkqTk5OQay6SmpkqqOuD/ySef6L333tPEiRN19tln684779Tzzz+v1atXa9WqVZ4NGrUqKJGyCs2rPzXLvLpd8cNO19cpLJU27Hd/LL6iTbMkJfe5Wut3faPNe1ZWvh4SHKru7YcppyBDFwy8wcQIPa+mfTBl/BwdPLZLg7qO0ZBu40yMEJ5U099fkj5cNVc/b/tUj0z+SOFhkSZFCMBZB03OR8yu3xm/7q39sUbVqUsOZSWBng+RC4F8CPAPadnmDkpbIRcqKZPWpri+3qFsaZ9FbnYBAADACSFmB+Ap+/YZDzZt165dtcvLy8u1evVqSVUH/Js3b35K2YEDB0qSDh48WOd4OnXqpKAgv72+wmuiYttqzH3VT78oSdNHS9ERNa8fHX7i98OX1lwut0h6+stTX9+7P13x8QOdjNYctqAQXfZYSp3WfeixF7X5i1nuDaiOwkIi9MpU9/a6X3Pu/fp2w0K99dWDmnPLt5KkzXtW6qu1C3Tx8Kl68eM79XLHDWoQepqD6DQ6de6k0vIit8TqifZL1e+DiLAotYpNVIe4XvXatjvb7ylj/7ZGkTGtlJaepvj4QWaHc1reeg9s2PWtXvvsPs368xeKi21fr+1b4RgA/MHwyW+pVbdzq13mrlxIqjkfmnj9TTq09QsnozVHv0tmqePQP7m83t70IsXHd/JARKfnqfN+dTydD/1Rfc4N3joXBlIuJFknH/JmPuyufMgqxwBgdS07j9SZN75T7bLaciGp/n1Dy75drQevvcrJaM3RqEUnXTDj2zqte92fZyhl7SI3RwT4DqvkQgCAwBMXF6e1a9fWaV2/HfAvKCiQJBUVVf9le9GiRcrIyFCjRo3UoUOH027r22+NBLlbt251jictLa3O6+KExhWnv9MiOkJq7MTNGEFBzpX7I4ds9brwwxtCGtT9bpTC4lKfaV94qOvt6NNxpL5+subL/Nu17Kals0/c7ldUkq8nF03WjWMe1/iht2rGy2frjS/+plsveqZOMacdOqTiMvdMQVGX9kuu7wN3cmf7PaWioqLyt68c6zXxxnsgPTNF/3jnSt104ZPq03FkXcKswgrHAOAPSstqfgaPp3MhScrOyfH5z9BudXxOUVBImCltq+t5vzpm50N/VJ9zgzfOhe5klfOgVfIhb+XD7syHrHIMAFYX1jy7xmXO5kJS3fOhsnLf/vyUpOYhp97Q5Ky8giKfbx9QH1bJhQAAcIXfDvjHxcUpKytL69at09ChQ6ssS0tL07333itJ6t27t2w2W43bOXjwoB544AGNHj1affv2rXM8rVq14g5/N4iIiTnt8txabqaIDje+0NntUm6x69txVJSqTZs2tURpvoqyYgWHhru8XqjNd9oXFuKeu8pOZ/4nMxQX20EXDbtNNptN9165QLc821fDe16q3olnuby9Vq1bu/UOf6txZ/s9JTg4uPK3rxzrNfH0MVBcWqiHFlyiod0v0iXDp7plm1Y4BgB/EBJc80CWu3Kh020rplGkz3+GhjhK6rReaUGmKW0z87zv7nzoj+pzbrBaPmSV86BV8iFv/P3dnQ9Z5RgArK5xdFSNy2rLhaT69w0FB9l9+vNTkiKijC5fh8Nx2n7P6oSHVPh8+4D6sEouBAAIPHFxcXVe128H/EeNGqVt27bpiSee0HnnnafOnTtLktasWaNJkyYpIyNDkk47iJ+fn6+LL75YYWFheuONN+oVz86dOxUVVfMXEjinwi795b2an8la3VRrJ3v4UuPq7dxi6eEPXa+/X/cEvZqa6vqKXvbO6ro9q+2deTMVFzPT7fHURUWp9O1cz23/l9+/0IqNi/TK9E2VX35bN+uoG8c8rjmLbtD8GZsUEebae3bnjp0KDnNPfJ5uvye4s/2e8tASKadIahXXSqk+/l729DGwcvMH2pO2UQczdmjFxlOna3z9nt/Uoklbl7ZphWMA8Acf/Sqt+L36ZZ7OhSTp08UL1PL012Cabl+G9MxS19cb1b+5Xjbh/GDWed8T+dAf1efcYLV8yCrnQavkQ974+7s7H7LKMQBY3bF86dH/Vr+stlxIqn8+dPm4kXr3Ed/9/Dzu6S+l/cdcG+wPC5G++3i+wkM9FBTgA6ySCwEA4Aq/HfCfOXOm/vOf/+jAgQPq0aOHunbtquLiYu3atUtjxoxR+/bttXTpUvXp06fa9YuKijR+/Hjt3btXK1euVKtWrbzcAlQnOEhq00RKyTCn/oRYc+p11Ygurg/4d2opxfl45707De46Rh89mn3K6xcPv10XD7/d+wGZ7KlbV5gdArzsvAGTdN6ASWaHAaAOzMxHGoRIzaPNq99ZbZsa++lApmvrDe/smXh8FfnQCeRCgYl8CLCm2CgpMkwqLDWn/vim5tTrqhGdpf/86No6AzuIwX4AAAAL8ts55uPj47Vy5UqNGzdO4eHhSklJUWxsrObPn6/PPvtMO3bskKRqB/zLysp0+eWXa+3atfriiy/UvXt3b4eP00gw8YuVVQb82zWVesY7Xz44SBrT23PxAAAA9zEzF2rTRApy7UYxU9hs0tg+kiuhDmwvtWrsoYAAAIDb2Gz0DTmjXzsjd3NWRJh0TjfPxQMAAADP8dsBf0nq1q2bPv30U+Xl5SkvL08///yzpkyZooKCAqWkpCgoKEg9e/asso7dbtfEiRP1zTff6L///a8GDx5sUvSoSd8Ec+ptECJ1bW1O3a6y2aRJw6WklrWXDQ6SJg2TElt4Pi4AAFB/zRuZNzDdt5059dZFt9bS1Wc4d4FC9/+VBQAA1tDXtaePuY2ZeZirQoOlKSOllk7MzhQRapRt1sjTUQEAAMAT/HrAvyZbt26Vw+FQp06dFBkZWWXZ7bffrvfff1933323IiMj9dNPP1X+HD161KSIcbLEFuZMPW+1ac0ahEi3JEuje0nR4dWX6dJKuv1ca3XeAwAQ6Gw2aUQn79cbFiwN6uD9eutjSEfplnOMRxdVp3GkNK6PdOPZUkiwd2MDAAB117+9OX00wztbY7aj42IipTvPl87sUv3+CrIZF0/cdYHUobn34wMAAIB7hJgdgBk2b94sqfrp/L/44gtJ0uOPP67HH3+8yrI333xTkydP9nh8OD2bzXgO2eI13q13uAkd6/UVEiyN7i2N6iFtOSgt/FEqKZfCQ6QZY6zxDF4AAHCqAR2kTzZIxWXerTMizHv1uUvnOOMnPUfakip9vcXIhyLDpAcuNmY7AgAA1tIgRBqcKH2/3Xt1hgVLgy128aMkRTaQLhsoXdhH2rBfWrL2f31DodJfx0sxEWZHCAAAgPpiwP8PUlJSvBwN6mJokvTTLik1yzv1jegktXbhuWe+JiTYuGL7w/99qWsQ6l+D/aVlxfrnv6/WvsO/qUFohBo3bKFpE15Sm2ZJVcqlZe7Vo29frgp7hez2ciW07Ka7L3tFjSKbqKgkX4+8fZl2pv6qCnu5Pno025zG1NELH03Tj799rMNZ+/TSXeuV1KbvKWXSM1P05KLJ2nVoveKadND86Rsql/2W8qOeW3KrJKnCXqae7UfotkvmKiykgZdaAABwRXiodFE/6b1fvFNfVANpbG/v1OUpcTHGz8rtRj4UGsxgvz9xJhdyJt9xOByaOf9c7Ty4znL5IAAEmgt6Sev3SXnF3qlvXF9j8NyqGoQasx99vvF/fUMhDPYDAAD4Cwb8YUnBQdK1Q6WnvpQq7J6tKzZKGt/Ps3Wg/sYOmaLBXcfIZrPpo9XP6+n3/6ynbl1RpUzT6NZ65vZVahBqfKN94b936u2vH9btFz+n4OBQXZV8nxpFxOqel0d6vwH1dGbvy3XlyJm6+8URNZaJDI/WDaP/oYLiHL3xxf1VliW27qMX7lyjkOBQ2e12/d/bl+mTH17UZWfd7enQ4QHOXgQjSalHd+rJRdcrpyBDUeExuveqBWof16Ny+Zrfv9SbS/+u8vJSNQiL1F2XzVfH1pw/AV8wNMm4S2tHuufrumyg1IgOYZ9T22e4s+VqGiyv7Xzy87bPtWDp32W322W3l+uKkffq/IHXe6Xtf+RMLuRMvvPB98+oVdOO2nlwnTfChofd98r5yspLl80WpMjwRrr94rlKanPql7va3iOl5SWa/8kMrd2xVGEh4erYqo/+cu073mwKgGpENZCuHCy9/r3n60psbkyLDwAAAPiigLynZfny5XI4HBo3bpzZoaAeWjdxfSA+t0jKLjR+OyMkSLpumHEVNHxXWGi4hnQbK5vNeJBet7Zn6HBWyqnlQhpUDvZX2CtUXFogm2yVy/olnaOGEY29FbZb9U48S80bx5+2THRkrHp2GKHwsKhTloWHRSok2DjQyytKVVJWVLk/YU1jh0zRmzO3a/70jRra42I9/f6fqy333Ac3a+yQKVpw3w5dlXyfnlw0uXJZXmGWHls4UTOvekuvzNikKeOe1OP/meilFgCojc0mXXOGFB3u/Dqu5kKSNChR6tfO9fjgeaf7DHel3Jm9L9czt61Syyan/qFrOp84HA49sfA63XvlAs2fvkGP/r9P9ewHN6uwOM/dzXSKM7lQbflOSvpW/bD1I12d/BePxgrveWDSe3plxibNn75Bl505vc7vkdc//4tsNpsWzNyhV2ds1pQL53g+eABO6ZUgDXPxEYyu5kNRDYybToL4igwAAAAfFZAD/vAfI7tK5/d0vvzTX0oPf2j8rk1wkDT5TCmxRd3jgzk+XPWchva4uNplZeWluvnpvrr84WY6mLFT15//iJej813pmSm6+ek+uuzhZoqKiNH4obeZHRLqyNmLYLLyj2hH6lqN6n+dJOnMXpfpaPYBHczYJUk6dGy3oiObVt7h1ivxTB3J3q+dqdz1CPiKJlHSLecYHdHOcCUXkqRe8dLVQ4yLC+BbavsMd6VcTYPltZ5PbDblF2dLkgqLcxUd2VShPv44oJrynfKKMj2z+Cbdedl8BQUFmxwl3OXki3kLinMknfphVtt7pKi0QF/+8rpuGP3PyvdCbHScx2MH4LzLB7p2caIr+VBkmJFrNWtU9/gAAAAAT2PAH5Y3to/xDFt3dkRHhEk3jZR6nv4mIfig/3wzS4cydunGMY9Vuzw0JEzzp2/Qew8eVtvmXfXpT/O9HKHviottr/nTN+q9B9NVVl6iVVuWmB0S3KSmi2COZh9QbHQrBQcbT/ix2Wxq0aStjmTvlyTFN+uk3MJj2prygyTph60fq7AkT+nVXDwAwDytm0jTzpOaNnTvdod0NC5+5Dn3vqm2z3BXyznj5POJzWbT3ycu0iNvTdDEf7bT3S+O0Myr3lJoSFg9W+ZZNeU7//r6EY3oOUHtWnYzOUK42xML/6Rr/5Ggt5Y+oL9c869Tltf2HknL2K1GkbFauHyWbntuoO5+8Uyt2/mNV9sA4PSCgqRJw6QRLt7pX5smkdLUUVJCrHu3CwAAALhbiNkBAO5wTncpqaX0nx+l9Jz6batHG+MZcDGR7okN3vP+ijlatWWJZk9ZpvCw0/8BQ0PCdP6gG/TM4pt0VfJML0VoDRENGmpk36u1fN2/ldz3arPDQTWmzRuqgxk7q1320t3r1aJxQuX/j18EM/tm1zumoyJi9OCkxXr9i7+quCRf3doNVbuW3RUcRPoA+JqWMdLMsdKnG6SVO+q3rYbhRi7UO6H2svCc2j7rve2P55OKinL9+5t/6KHrl6h34lnafmCNHnzzIr0yY7Niopp5PT5X/THf2bTnOx3J2q///vC8KuzlKizJ1XWz2uv5aWvUuGFzs8NFNZzNh+675m1J0ldr39Krn9+nWTd+7lI9FfZyHc7ap3YtuuvPYx/XroPrdd8r5+m1e7aqSaOW9WsEALcJCpIuHyx1ayO997OU48Lji6ozNEm6uL8UziMeAQAAYAH02MNvtG0qzRgjffe7tGqH8Tw2VyTESsndjGngmLbWehZ/97S+3bBQT0xZVmXqzpMdztqnmKjmCg+LlN1u1/eb3ldiq97eDdRHHczYpZZN2ikkOFRl5aVaveVDdWDf+Ky5d/zoVLnaLoJp3jhBmblpqqgoV3BwiBwOh45k7VeLxm0ry/RNSlbfpGRJUml5ia76vzi1a9ndPQ0B4FYNQqXLBkl920pfb5V+T3Nt/YgwaXCi8bgkZx8RAM+p7bM+NKRBrZ/hknOf9bWp7nyy69AGHcs9pN6JZ0mSuiQMUrOYeO06uF4DOp/nYmu943T5zjO3rawsl56Zolue6at3/pZiUqRwhrP50HHnD7xez31wi3ILjik6qmnl67W9R1o0aasgW5DO6T9RkpTUpp/iYjtob9pmBvwBH9SjjXTfOGnZVumn3VJhqWvrd46TRvUwfgMAAABWwYA//EposPHFLLmb9NtBad0+6cAxKSP/1LJBNimusdS+qXRGknHBAKzpaHaq5n86Q61iE3XPy8bAZFhIA82b9rMWLH1QTaNba/zQW7QnbZPe/OJ+SZLDYVdSm/66/eK5lduZ8lRv5RQcVWFJrq75R7z6dEyudtpPX/Ts4pv18++fKTMvXX997QJFNmikt/6yS0+9/2cN7X6RhvW4SMWlhbphdmeVlZeooDhH1/wjXqP6T9KNYx/Thl3L9dGquQoKClaFvVz9ks7VdaMeMLtZqAdnLoJp0rCFktr017J17+iCQZO1cvMHatY4Xm2aJVWWOZabpqbRrSRJ/172qPp2PKfKcgC+p2NL4+donvTTLmn3EelgllRWcWrZmAgpPlbqlSD1byeF8e3AMpz5DHelXE1qOp+0aJygzLw07Tu8Te1adtPBjF1KO7ZbCc27uLOZTnMmFyLfCSz5RdkqLi1Us5jWkqTVWz5SdFRTNYqsOjd3be+RmKhm6pt0rtZuX6oh3cYqLXOv0jP3qi2PfwB8VmQD6aL+0uje0ob90qYDRt9QdXf9hwZLrRtLiS2MvqGW0V4PFwAAAKg3m8PhcJgdhL8qKChQw4bGw1Tz8/MVFRVlckSBq7BUOpIrlZUbA/0NQo2pb0ODzY7Mux5aYnzBjYmQHplgdjSnV1EqfTu39nK+JHmaFOymx9YGevs9JVDeA0ezU3XtPxPUKjZREQ0aSTpxEYykKoMfB45s15OLJiu38Jgiw6N175VvqkOrXpXbevr9m7Rl70pV2MvVrd1QTb1kXo0XEFjhGAACVYXduACgoMT4d2iw1LShFB1hdmTe54vngrp+5p/uM9yVz/qTB8ujI5tWDpbXdj5Zvn6hFi6fpSBbkOwOu6455686p9+1VWKsz7nBavmQVc6DvvgeqE59//6Hs/bp0X9doZKyIgXZghQT1VxTLpyjpDZ9Jbn2Hkk7tkdPvX+jcgoyFGQL0nWjHtSZvS87pU6rHANAoMorMm4IKauQgoOkyDCpRbTx70BilfMA4Cm8BwAA/oh7eBAQIsOk9r7/KFEAcIvmjeP19ZM1X88344rXKv+d0KLLaafEnX7Fq26NDYA5goOkuBizo4C7ne4z3JXP+rsun1/t67WdT87pd43O6XeNk9EC3tWySTs9P+2XGpe78h5p1TRRc2751q3xAfC+RhHGDwAAAOBvAuwaVgAAAAAAAAAAAAAA/AMD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWFCI2QEAQHWCQqXkaWZH4ZqgUPduK5DbD44BAAgkVvzMd1Z9zg1W2y+cB93Lan9/iWMAAAAAAGAOBvwB+CSbTQoOMzsK8wR6+8ExAACBhM/86rFfAht/fwAAAAAAnMOU/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWBAD/gAAAAAAAAAAAAAAWFCI2QEAAE7lcEj2MrOjcE1QqGSzmR2F/wj0YyDQ2w8AsN65gPOAe1nt7y+5/xhgHwAAAAAAnMGAPwD4IHuZ9O1cs6NwTfI0KTjM7Cj8R6AfA4HefgCA9c4FnAfcy2p/f8n9xwD7AAAAAADgDKb0BwAAAAAAAAAAAADAghjwBwAAAAAAAAAAAADAghjwBwAAAAAAAAAAAADAghjwBwAAAAAAAAAAAADAgkLMDgCAZ9kd0rE8KTVLKik3Xistl3YelhJipfBQc+MDAADwtMISIxc6ln8iHyqrkDLypKYNJZvN3PgAAAA8qcIuHc6R0rKr9g2lZEitG0th9BADAABYGukc4Icq7NLWg9KPu6S9R6XisqrLi8qkF5YZ/27eSOqdIA3rZHR4AwAA+IOjudLqndLmVGOg/48KS6V/fCxFhEkdW0jDkqSuraQg5kADAAB+oLRcWr9P+nm3dCDTuNjxZEVl0rNLpSCbFBcj9W8vndFRahhuSrgAAACoBwb8AT9id0irdkjLf5OyC51b52ie9M1vxjrd20gX9ZdaRns2TgAAAE85lCX9d720Pc258kWl0pZU46dpQ2lUD6Ozm7v+AQCAFZVVSF9vMfqHCktrL293SIeypUMbpC82GQP/4/tK0RGejRMAAADuw4A/4CeO5kkLf5T2HK3b+g4ZswLsSJfG9pHO7sIdbla0cfcK3fNycpXXwsOiFN+8s0b1n6RLht+h4GA++v1ZoB8Dgd5+IJBV2KVlW6Wvthj/rotj+dKin4274a4eIsUy+5HlcB5AoB8Dgd5+INClZEj/+VE6klu39Svs0po90tZU6bJBUv92XAQJAABgBXzLA/zA1oPSgpWnTs9WF2UV0n/XSb8fkv7fWVKD0PpvE96X3PcaDe46Vg45lJWXrq9/fVsvfzJd+49s092Xv2J2ePCCQD8GAr39QKApLJFe/c54lJE77EiXnvhMuvFsqXOce7YJ7+I8gEA/BgK9/UAgWrVD+mCt5HDUf1uFpdK/Vhs50ZWDpWBuCAEAAPBppGuAxW06IL3+nXsG+0+2PV16eblUUu7e7cI7OrXpr1EDrtN5AybpypH3au4dP6l5TLy++OU1Zee7aTQEPi3Qj4FAbz8QSApLpRe/cd9g/3El5dIr3zr/aAD4Fs4DCPRjINDbDwSaFdukxWvcM9h/sp93S+/8INnrOHsSAAAAvIMBf8DCUjKkt1YZz1vzhL0Z0lsr3f+FEd4XERalru3OkMPh0KFju80OByYI9GMg0NsP+Cu73bjwMTXLM9sv/9/2D3po+/AezgMI9GMg0NsP+LN1KdJH6zy3/fX7PLt9AAAA1B9T+gMWVVou/fsH155RO320FB0h5RZJT3/p3Dq/HZJ+2CUN71S3OOE70v7XsRcdGWtyJDBLoB8Dgd5+wB+t+F3afcS1dVzNh0orjGfhTh/NdLZWx3kAgX4MBHr7AX+UUyi9v8a1derSN/T9dqlnPI86AgAA8FUM+AMW9flG6Wiea+tER0iNI12v6+N1UtdWUtOGrq8LcxSXFSqnIEMOh/HMzk9+fFm7Dq5X14TBim/e2ezw4AWBfgwEevuBQJCeY+RDrqpLPnQwS/p6izS6t+v1wRycBxDox0Cgtx8IBA6HtOhnqajUtfXq2je08CfpvnFSeKjr6wIAAMCzAmLAPyMjQ7Nnz9aSJUuUmpqq5s2ba8KECZo1a5amTZumN954Q/PmzdPUqVPNDhVwSlaB9N1279VXUi4t3SxdO9R7dbpTeYW06YD020GpqEwKC5ESYqXBiVLDcLOj84y3v3pIb3/1UJXXRvScoDsufcGkiOBtgX4MBHr7gUDw+UZjyn1v+XqrNKKzdXOHtGzjObyZBcYAQUyENDBRatdUstnMjs79OA8g0I+BQG8/EAh2HTFmZfSWrAJp5XbpvJ7eq9OdSsqktSnS7sNGP1eDECmppTSgg/FvAAAAK/P7dGbDhg0aM2aM0tPTFRUVpe7du+vQoUOaO3eudu/erczMTElS3759zQ0UcMEPO42OWm9av0+6uL8U1cC79daHw2FMO7dsq5RXXHXZ+n3GQMHADtKlA6QGfnaF+rghU3RW7ytUbi/T3rTNWrTiCWXkpCos9MQoRWl5iW57tr+S+12riefeX/n67HcnKzv/sGb9+QszQoebOHMM/POdq2V32PXApPcqX8stzNRNc3poyoVzdG7/iWaE7hbOtH/znpX62+tjTlm3vKJUdnuFls6u8GbIAFyQXShtSfVunRV2Y8D83B7erbe+0rKlxWuqf/TBqp1SfBMjF+rY0uuheRS5EMiFyIUAf7d6h/fr/GGXdG53KchCjzmqsBv9P6t2GAP9J1u3T/p4vXRmZ2MmJx7fBAAArMqv05iMjAyNHz9e6enpmjFjhtLS0rRu3Tqlp6friSee0GeffaY1a9bIZrOpd2/m54Q1lFdIP+72fr1lFUYnt1U4HNKSX6UPfz11sP+4crv0027p+WWuT4Hn69o066T+nUdpcNcxuip5ph694RNtT12j5z64pbJMWEgDzbz6bb37zSztPmTMibx6y0f6adsnmn7F62aFDjdx5hi4Y8KL2pqyWsvXL6x8bd6Ht6tHhxGW7uCWnGt/r8Qz9ck/86v8vDlzh6Kjmun6Cx41MXoAtflxl2T38sWPkrR6p2T34qwC9bUvQ3ruq+oH+49LzZJeXO79Cyg8jVwI5ELkQoA/yykyZjL0tqwC784qUF8Vdun176Rvfjt1sP+44jJjJqc3VxrlAQAArMivB/ynTZum1NRUTZ06VXPmzFGjRo0ql82cOVN9+vRReXm52rdvr+joaBMjBZx3IFPKr2EA29Os9KVu1Q5jqjlnHMiU/rXas/GYrUf7YRrVf5JWbFykrSk/VL7eOX6ALj/7Hs1+9086mp2qZxdP0R2XvqBmMa1NjBaeUN0xEB0ZqxlXvK7nP5qqjJxD+n7TYm3avUJ3TXjZ5Gjdr6b3wMlKy0v0yNsT1LP9CF177t+8HCEAV2w9aE69mQXS4Vxz6nZVXrH06gqjE7s2FXZpwSpjNgB/RS4EciFyIcCf/H7InIsfJeNxiVbx0Trn+7K2pBp3+wMAAFiR3w74b9u2TYsWLVKzZs302GOPVVtmwIABkqQ+ffpUvrZy5UqNGjVKrVq1UoMGDRQfH6+rrrpK27Zt80rcQG0OZJpXd2qmeV8oXVFhN6bxd8Vvh4z2+bOJox5QUFCw3lr64B9e/7uCg0J067P91CcpWcl9rzYpQnhadcfAoK6jdXbvK/XEwus0b8ltmn7Fa4qOampilJ5T03vguOc+uEWlZcW696oF3g0MgEvKK8wdmDYzF3PFjzul/BLny5dXSCt+91w8voBcCORC5EKAvzCz/8IquVBesfFITFes3ikVuJA/AQAA+Aq/HfBfuHCh7Ha7Jk6cqIYNG1ZbJiIiQlLVAf+srCz16tVLc+fO1VdffaUnnnhCW7du1dChQ5Wa6mfzXMKSzPxiVVwmHcszr35nbUk1prdz1WoXvwhaTZtmSUruc7XW7/pGm/esrHw9JDhU3dsPU05Bhi4YeIOJEcLTajoGpoyfo4PHdmlQ1zEa0m2ciRF6Vk3tl6QPV83Vz9s+1SOTP1J4WKRJEQJwRlq2udOtWqGTu8JuPGPXVb+mSIV+3MlNLgRyIXIhwF+YmY8cyjYuFPR1P+1yPWcst9jjLAEAAI7z2wH/5cuXS5KSk5NrLHN8AP/kAf+LLrpIzzzzjK644gqdffbZmjhxopYsWaKcnBx98MEHng0acEJ2gbn1ZxWaW78zttXx0QN1Xc9Krjn3fgXZgvTWVyfu6tm8Z6W+WrtAFw+fqhc/vlMlZXW4WgKWUd0xEBEWpVaxieoQ18vEyLyjuvZv2PWtXvvsPj0w6X3FxbY3LzgATjE7FzE7F3PG4Rwpuw77qbxC2nXE/fH4EnIhkAuRCwH+IMvEfKTC7tosQmb5Pa1u6wVC3xAAAPA/NofDYYEJul2XkJCg1NRUrV+/Xn379j1leXl5uVq1aqWMjAzt3r1biYmJNW7r2LFjatasmZ5//nndfvvtTsdQUFBQObtAq1atFBTkt9dXwItG3vqhmrUfVO2y6aOl6Iia140Ol4KCJLtdyi0+fT25RdLTX576+qo3/qT07ctdiNj7hkx8SQm9x7u8XllJvv77YFcPROS6sJAIvTLV81MOFJXk6+an++iys6Zr/NBbNePls9U5fqBuvegZl7c15flOKi337Q7ysX9bo8iYVirMSdPns6p/H/kKbx0Dx814aaTO6Hahrhh5T5234c5jwFvtT89M0dS5g3TdeQ/pkuFT67UtK7wHAH8Q3+cinXHti9Uuqy0XkpzPh2rKhdK3f6tVb0xyIWLva9Z+sEbeuqRO665ZdJf2rVvs5ojqxhvngkDLhSTr5EOBngtJ3tkH7syFJOu8DwCrG//ARjVoWP3jR9zVN1RTLiRJX8weoYJjKc4HbIJRdy5V49Y9XF4vM3Wjls/z39leYJ1cCAAQeOLi4rR27do6rRvi5lh8RkGBcalrUVH1XzQXLVqkjIwMNWrUSB06dDhleUVFhex2u/bt26e//vWviouL05VXXlnneNLS6nhZKfAHRYX5NS6LjpAaOzH7YlCQc+Wqc+TwIR08eLBuK3tJXvaxOq1XWpTnM20LD/XONJrzP5mhuNgOumjYbbLZbLr3ygW65dm+Gt7zUvVOPMulbaUdOqTiMt+eAqKioqLyt6/8rWvirWPAndx5DHij/cWlhXpowSUa2v0it3RwW+E9APiDiNaHa1zmbC4k1T0fKsjP9flzSFnovjqveyQ91Wfa541zQaDlQpJ18qFAz4Ukz+8Dd+dCknXeB4DVlZWVqEENy7zRN5R2cL/yjvnuOUSSCvNz1Lgu6+Vl+/T5EfVnlVwIAABX+O2Af1xcnLKysrRu3ToNHTq0yrK0tDTde++9kqTevXvLZrOdsv7ZZ5+t1atXS5KSkpK0fPlyNW/evM7xcIc/3MVWXvO8bbm13Ejh6h3+1WkUEaQ2bdrUEqW5io/tqNN6OQc3+UzbwkJquT3RDX75/Qut2LhIr0zfVPk52LpZR9045nHNWXSD5s/YpIiwKKe316p1a5+/myc4OLjyt6/8rWvijWPA3dx5DHij/Ss3f6A9aRt1MGOHVmxcdMry1+/5TS2atHV6e1Z4DwD+IKqm3m3VngtJrt3VVp2gikKfP4cE2/JUVpyn0PBGLq3nsNtlKzroM+3z9LkgEHMhyTr5UKDnQpLn94G7cyHJOu8DwOrKi7KlJq2rXeauvqGatuOw29UkOlzR4b57DpGkgiO/SZ2H1WG9bT59fkT9WSUXAgAEnri4uDqv67dT+k+bNk3z5s1TQkKCli1bps6dO0uS1qxZo0mTJmnPnj0qKyvT7bffrueff/6U9bdv367s7Gzt3btXTz75pI4cOaLVq1erbVvnv+yePKV/fn6+oqKc7ywCarJsq/Tphrqt+/ClxtXb2YXSwx+6vn5IkPT4lVJIcN3q95biMumhJVJJuWvr3Zwsdav++7LXVZRK3841OwrXJE+TgsPMjuL0Hloi5RRJMRHSIxPMjub0Av0YCPT2A6hZcZn01/ekun6JqW8+dPkgaUTnOlbuRR+skVa6eA1k99bSlGTPxFMXVjsXWOU8YJV8yGp/f8n9xwD7AEBN/vOj9Mueuq1b31yoZbT0V9efouh1h3Olxz5xfb2/XyQ1c+2aSViMVXIhAABc4be3nM+cOVNNmzbVgQMH1KNHD/Xq1UudOnXS4MGDlZiYqHPOOUeS1KdPn2rX79Kli4YMGaKrr75a33zzjfLy8jR79mxvNgGoVkKseXW3buL7g/2SFB4qDeno2joto6UurTwTDwAAcJ/wUKl5tHn1x5uYi7lieGcp6NSJzE7rrK6eiQUAALiXmX1DZtbtipbRrt/U0TOewX4AAGBNfjvgHx8fr5UrV2rcuHEKDw9XSkqKYmNjNX/+fH322WfascO43aWmAf+TNW7cWElJSdq1a5enwwZq1a6ZFGrSoHtSS3PqrYvx/aSOLZwrG9VAuvFs1zvFAQCAOczKSSLCpDZNzKnbVXEx0tVnOF9+dG+pKxc/AgBgCZ3qPttrvSWZWLerJg6Vmjs5gN8iWrrGhdwJAADAl4SYHYAndevWTZ9++ukpr+fn5yslJUVBQUHq2bNnrds5cuSItm/friFDhngiTMAl4aHSgPbST7u9W69N0rAk79ZZH6HBxhT9C3+S1u+ruVxcjPT/zjK+2AEAAGsY3kn6Yaf36x2caN6Fl3UxOFEKC5YW/SIVlVZfJjRYGtdXGsnd/QAAWEZcjJTYXNpz1Lv1hodK/dp5t876aBguTTtPWrBK2n2k5nKdWkrXjzBuCAEAALAivx7wr8nWrVvlcDjUuXNnRUZGVll23XXXKSkpSX379lXjxo21c+dOPfPMMwoJCdHdd99tUsRAVcM7e3/Av2tr601rFhZifGEb3dsYFPjtoJSRZzzzNyTIeEZtp5aSjTv7AQCwlDZNpA7Npb1e7uQe0cm79blD33ZS9zbSun3Sz7ullKNGLhRkky7qLw3uIEXSuQ0AgOWM6Oz9Af/BiVIDi/UmN4qQ7jhP2n9MWrVDWrNXcjiMvqDBiUZ+l9DU7CgBAADqx2+n9D+dzZs3S6p+Ov8zzjhDn3/+uW644QaNGTNGTz75pM4880xt2LBBSUkWur0Zfi0h1niumLfYJJ1f+2QYPqtltHTpAOn+i6ToCOO1qAZS5zgG+wEAsKrRvbxb34D2UnOLzggUFiKd0VG68/wTuVCjcOOufgb7AQCwpt4JUqsY79XXIERK7ua9+tytbVPp2qFSdLjx/+hwYwp/BvsBAIA/sNg1me5xugH/qVOnaurUqd4OCXDZFYOM6chqmp7Vnc7uatxFB8C3pB7dqScXXa+cggxFhcfo3qsWqH1cjypl1u9artc//4uKSvJls9k0pOs43Tj2cQUFGdf8Hcnar3kf3q7UjB0KsgVr/NBbdcmIO1RUkq9H3r5MO1N/VYW9XB89mm1CCwGgZl1aGYPY3pj1qFG4NGGg5+sB4B6lZcX657+v1r7Dv6lBaIQaN2yhaRNeUptmp17E/96KJ/X12rdkd9iV0LyL7rnqTTWMaOz9oAHARSHBxgD2M0slu8Pz9V3UX2oS5fl6AAAA4DoG/AGLiomULhsovfOD8+vkFlX97YwW0dJY3iqW4koH532vnK+svHTZbEGKDG+k2y+eq6Q2/SRJuQXHdO/8cyvLlpQVKi1zj95/6IiiI2O91h7U7LkPbtbYIVN0waDJ+n7TYj25aLJeuHNNlTKNIpro/onvqlXTRJWWFWvmK6P09a9v64JBk+VwOPTwW5fqquS/6Ow+V0iSsvIOS5KCg0N1VfJ9ahQRq3teHuntprnFCx9N04+/fazDWfv00l3rldSmb7Xl1vz+pd5c+neVl5eqQVik7rpsvjq27uPydgB43yUDpO3pUlaB8+vUJR+6cjDPdLUaZ/Oh2vKd2s4R8F1jh0zR4K5jZLPZ9NHq5/X0+3/WU7euqFLm1x1fa+maNzXvjp8VGd5I/172D73xxf2aNuEFc4J2I1e+Exz35Zo39dR7/08PX/+hhve8RBLfCQBfl9BUGtVD+mqL8+vUJRfqEicNY+JTAAAAnxWQA/7Lly83OwTALQZ2kI7mSUs3O1f+6S9d235MhHRzsjENLKzFmQ5OSXpg0nuVdzCt2vyhnlw0WfOnb5QkRUc11fzpGyrLvr9ijjbt+Y6OPR+RlX9EO1LX6vGbvpIkndnrMj3/4VQdzNhVpSP3+AUckhQWGq6OrfvqcFaKJGn9zm8UGtKgcrBfkpo0ammUDWmgfknnKD0zxfON8ZAze1+uK0fO1N0vjqixTF5hlh5bOFFP3/q92sf10OY9K/X4fybq1Xu2uLQdAOYIDzVylXlfSwUlzq3jaj50cX+pV4LrscF8zuRDp8t3nDlHwDeFhYZrSLexlf/v1vYMLf5uzinl9hzaqJ4dRigyvJEkaXDXsbrn5ZF+MeAvOf+dQJLSM1P0xc+vqlvbM6q8zncCwPeN7i0dy5d+TXGuvKu5UJsm0vVn8khEAAAAXxZkdgAA6md0L2lMb/dvNzZKmnqe1LSh+7cNzzrewWn737fxbm3PqBzg/aOTpystKM6RVPM3+C/WvK7Rg290Y6Soj6PZBxQb3UrBwcYVOTabTS2atNWR7P01rpOZm66VmxZrSLcLJUn7jvymmKjm+uc7V+uWZ/rp4QWXKu3YHq/E7w29E89S88bxpy1z6NhuRUc2rXwUQq/EM3Uke792pq5zaTsAzBMXI00ddeLZ9O50SX9rP6s2kLmSD53s5HzHmXMErOHDVc9paI+LT3m9U/wArdu5TJm56XI4HPpm/b9VWJKn3MJME6J0L1feA3a7XU+//2fdfsk8hYacfjoTvhMAvifIZkztP6iD+7fdtql027lSZJj7tw0AAAD3YcAfsDibTbqgl/T/zpIahrtnm33aSnePlpo3cs/2YK6aOjiPe2Lhn3TtPxL01tIH9Jdr/lVtma0pPyi/MEtn/G+gGNZTUJyrB94crytHzlSXBONB1BUV5dqwe7kmjnpAL9+9XgO6XKBH37nS5Ei9K75ZJ+UWHtPWFOP5KD9s/ViFJXlKd2JQCIDvaNVYmjFa6t7aPduLiZCmjJRGMtjvN2rLh6RT8x3OEf7hP9/M0qGMXbpxzGOnLOublKwrzr5Hf3/zQk2bd4YaRzWXJAUH+d8UZ6d7D3zw/dPq0X64OscPOO02+E4A+K7gIOmaocajH8OC3bPNs7pIt4/isUYAAABW4H/fYoEA1TtBSmwuffirtC5FctRhG9ER0qUDpH7t3B0d3GnavKE6mLGz2mUv3b1eLRqfmHf4eAfn7Ju/qXF7913ztiTpq7Vv6dXP79OsGz8/pcyXv7yu8wb8qfJucpiveeMEZeamqaKiXMHBIXI4HDqStV8tGrc9pWxhcZ7+9tpoDetxsS4/e3rl6y2atFVS636Vdy6OGjBJ8z68TeUVZQoJDvVaW+rClffB6URFxOjBSYv1+hd/VXFJvrq1G6p2Lbv7ZUc/4O9iIqWbRkpr9kofr5PynZzi/2Q2mzQkUbqoP3ey+Tp350PSqfkO5wjre3/FHK3askSzpyxTeFhktWUuGnabLhp2myTpt30/qXlMvKLCo70ZZp246z2wN32LVm7+QE/f9n2tdfKdAPBtQTbpzC5St9bS+79I29Prtp0W0dJVg6WOLd0bHwAAADyHb2mAH2kYLk0aLo3tI/2wU/ppt3PPs01qKY3oZDyfNph5P3ze3Dt+dKqcMx2cJzt/4PV67oNblFtwTNFRTStfLyrJ13eb3tPz09bUOWa4X5OGLZTUpr+WrXtHFwyarJWbP1CzxvFq0yypSrmiknz99bXRGthltCaO+nuVZYO6jtGrn81URs5BNYtpo1+2fa62Lbr5/GC/5Pz7wBl9k5LVNylZklRaXqKr/i9O7Vp2d9v2AXiPzSYNTpT6t5M27pdW7ZT2Hq19vehw6YwkaVgnqXHtp0z4AHfnQzXlO5wjrGvxd0/r2w0L9cSUZVUeY/VHx3LT1DS6lYpLC/XW0gd15ciZ3guyHtz1HtiyZ6UOZ6Vo8hOdJEmZeel6dvEUZeamafywWyvL8Z0AsI5mjaRbz5UOZRm50Nq9Umn56dex2aSebaQRnaVOccbFAwAAALAOBvwBP9S0oTS+nzSuj3QkTzpwTErNkopKpQq7FBpsfAFMiJXiY5mezR8508GZX5St4tJCNYsx5j9eveUjRUc1VaPI2CrlVmxcpMRWfdS2RVdPhw0X3XXZfD25aLIWLp+lyPBo3Xvlm5Kkp97/s4Z2v0jDelykJaue0/YDv6i4tECrNi+RJJ3V5wpNPPd+RYRF6c4JL+v+18dJcigqPEb3T3y3cvtTnuqtnIKjKizJ1TX/iFefjsk1PvbByo539EvSv5c9qr4dzznlwgkA1hISLA3oYPzkF0sHMqXUTOlYvlRWYVzgGNVAim8ixTc1HmNEx7b/cXbAV6o53+EcYU1Hs1M1/9MZahWbqHteNi7YCAtpoHnTftaCpQ+qaXRrjR96iyTpL6+eL4fDrrKKUo3qP0kXD59qZuhu5cx7YPywW6sM7M94aaQmnHmXhve8pEo5vhMA1tO6iXTlYGnCACktx8iFDmVJxWWSwyGFhkgtY4y+oTZNpHDfv+4bAAAANWDAH/BjQUFSXIzxM8jsYOA1p+vglE4MBnds3UeP/usKlZQVKcgWpJio5nr0hk9ls1Ud8fjyl9c1ZshNXm8HapfQoku1d3fNuOK1yn9PPPd+TTz3/hq3MbDL+RrY5fxql70yY1P9gzTRs4tv1s+/f6bMvHT99bULFNmgkd76yy5JVS+KeGvpg9qyd6Uq7OXq1m6oZlz5utPbAeD7GoYbU9t2a212JPAmZ/OhYT0uklRzvlPbOQK+qXnjeH39ZPUPOZt8wf9V+f+rMzZ7IySvc/U9UBu+EwDWFRJsDOonxNZeFgAAANbEgD8A+JnTdXBKVQeDn5/2S63be27qD26JC/C2uy6fX+Oyk98H0694tc7bAQD4JlfyIanmfKe2cwTgq1x9Dxz31K0rqn2d7wQAAAAA4Lt4WjcAAAAAAAAAAAAAABbEgD8AAAAAAAAAAAAAABbEgD8AAAAAAAAAAAAAABbEgD8AAAAAAAAAAAAAABYUYnYAAIBTBYVKydPMjsI1QaFmR+BfAv0YCPT2AwCsdy7gPOBeVvv7S+4/BtgHAAAAAABnMOAPAD7IZpOCw8yOAmYK9GMg0NsPAOBcEOj4+7MPAAAAAADOYUp/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsiAF/AAAAAAAAAAAAAAAsKMTsAAAAwKkcDsleZnYUrgkKlWw292wr0NsvWW8fuLv9AIDAZrXzoEQuIJEPkg8BAAAAMAMD/gAA+CB7mfTtXLOjcE3yNCk4zD3bCvT2S9bbB+5uPwAgsFntPCiRC0jkg+RDAAAAAMzAlP4AAAAAAAAAAAAAAFgQA/4AAAAAAAAAAAAAAFgQA/4AAAAAAAAAAAAAAFgQA/4AAAAAAAAAAAAAAFgQA/4AAobdLjkcxr+P/wYAAAgUDge5EAAACGzkQgAAwB+FmB0AAHhKaqa09aB0IFM6cEzKKTqxLLdYen6ZlBArJbWQurWWgrgECgAA+JGiUmnjfiklw8iL0nKkCruxLLdYevxTIxdq21Tq21ZqFGFuvAAAAO7kcEi7j0jb0/7XN5QpFZQYy3KLpb8vluJjjXyoa2spsblks5kbMwAAQF0w4A/Ar1TYpfX7pFU7jM7t09l12Pj5dpvUJFIa1sn4iWrgnVgBAAA8IS1b+n679OteqbSi5nLpOcbPmr3SR+uk3gnSWV2kDs29FioAAIDblZRLv+w2+oYO59ZcLr9E+j3N+Pl6qxQXIw3vJA3pKIXRaw4AACyE1AWA30jPkf7zo7T/mOvrZhVKn22UvvtdumKw1Ket++MDPG3j7hW65+XkKq+Fh0Upvnlnjeo/SZcMv0PBwf596g/0fRDo7QcCXXmFtHSz9M1vkt3FaWqPXzS5fp90RkfpkgFSeKhn4gQ8KdDPhYHefol9AAS63Yel//wkHct3fd30HOmDtcaFk9ecISW2cH98AAAAnsA3HAB+4bvfpY/Xn5imtq7yS6Q3V0r920lXn8EV3bCm5L7XaHDXsXLIoay8dH3969t6+ZPp2n9km+6+/BWzw/OKQN8Hgd5+IBAdzpEWrDSm7a+vn3Ybd7r9aTgd3bCuQD8XBnr7JfYBEGjsdqNfaMXv9d/W0Txp3tdScjfpwr48AhIAAPg+hrIAWJrDIX26wbiTzZ3W7ZOyC6UpydzdBuvp1Ka/Rg24rvL/44fdphtnd9UXv7ymG0b/U40b+v9czYG+DwK9/UCgSc2UXlp+4pm07pBdaGzz/50ldWvtvu0C3hLo58JAb7/EPgACSYVd+tdqacN+923TIWn5NiMnmjhMCmbQHwAA+DBSFQCWtnSL+wf7j9tzVHrtO6nsNM++BawgIixKXdudIYfDoUPHdpsdjikCfR8EevsBf3Y41/2D/ceVVUhvfC/tPuL+bQPeFujnwkBvv8Q+APyVwyEt/NG9g/0nW7dPWvSzUQ8AAICvYsAfgGXtSJe+3OTZOnYd9nwdgDek/a9TMzoy1uRIzBPo+yDQ2w/4owq79PYqzwz2H1dWYdRRWOq5OgBvCfRzYaC3X2IfAP7ox13S2hTP1vHLHuMHAADAVzGlPwBLKimT3v3J9fWmj5aiI6TcIunpL51bZ/k2qVeC1L6Z6/UBZiguK1ROQYYcDuN5pZ/8+LJ2HVyvrgmDFd+8s9nheUWg74NAbz8QKL7eKh3Mcm2duuRCOUXSR79K1w51PUbALIF+Lgz09kvsAyAQZOZL/13n2jp1yYUk6cNfpS6tpMaRrtUHAADgDX4/4J+RkaHZs2dryZIlSk1NVfPmzTVhwgTNmjVL06ZN0xtvvKF58+Zp6tSpZocKwAVfbpYyC1xfLzrC9S9nDocxfdvMsZLN5nqdvsDhkNKypawC4zl0jSOlNk2s2x6c3ttfPaS3v3qoymsjek7QHZe+YFJE3hfo+yDQ2w8EgqN50lebXV+vLrmQZNzVNihR6tTS9XV9RUGJlJoplZRL4aFS26bGb/inQD8XBnr7JfYBEAiW/Gqc111R11youEz6cK10w1mur+sr7A5p/zEpv1gKsklNG0kto82OCgAAuINfD/hv2LBBY8aMUXp6uqKiotS9e3cdOnRIc+fO1e7du5WZmSlJ6tu3r7mBAnBJSbkxZZs3pWVLOw9LneO8W299lVdIa/ZKq3caHdwna9VYGtFJGtxRCg02JTx4yLghU3RW7ytUbi/T3rTNWrTiCWXkpCosNLyyTGl5iW57tr+S+12riefeX/n67HcnKzv/sGb9+QszQncbZ/bBP9+5WnaHXQ9Meq/ytdzCTN00p4emXDhH5/afaEbobuFM+zfvWam/vT7mlHXLK0plt1do6ewKb4YMwEWrdxidtt60crs1B/wPZErfb5fWp0jl9hOvNwiRBnWQzuxKZ7c/CvR8KNBzIYl8CPB3GXnS1lTv1rkp1ZhVILahd+utr+Iyox9t9Q4pI7/qssTm0ojOUt92xkUAAADAmvx2wD8jI0Pjx49Xenq6ZsyYoYceekiNGjWSJM2ePVv33XefQkJCZLPZ1Lt3b5OjBeCKdSnGlxVvW73DWgP+RaXSG98bFypUJy1ben+NtHav9OeRUlQDb0YHT2rTrJP6dx4lSRrcdYx6dhihu18coec+uEX3X/euJCkspIFmXv22Zrx4ls7odqE6tu6j1Vs+0k/bPtEr0+twy6iPcWYf3DHhRU15qpeWr1+oc/pdI0ma9+Ht6tFhhOU7uJ1pf6/EM/XJP6v29mTkHNLtcwfq4mHMfAT4stJy6WcTniO7OdWYLahJlPfrrqsfd0nv/1L9xREl5dKqnca+vH6E1DPe+/HBcwI9Hwr0XEgiHwL83eqdxgyG3uRwSD/ski7s6+WK6yGrQJr/rZSeU/3yPUeNn00HpOuGSSHcEAIAgCUFmR2Ap0ybNk2pqamaOnWq5syZUznYL0kzZ85Unz59VF5ervbt2ys6mts5ACtZY0IHt2R0cptxoUFdlFecfrD/ZHszpNdWSGXcvOK3erQfplH9J2nFxkXamvJD5eud4wfo8rPv0ex3/6Sj2al6dvEU3XHpC2oW09rEaD2jun0QHRmrGVe8ruc/mqqMnEP6ftNibdq9QndNeNnkaN2vpmPgZKXlJXrk7Qnq2X6Erj33b16OEIArfjtoXNjnbQ6H9GuK9+utq3UpxmOZapsJoaxCenOltDPdK2HBJIGeDwV6LiSRDwH+xOEwbl4wg1l9UnVRUCK9tLzmwf6Tbdgv/edHY98CAADr8csB/23btmnRokVq1qyZHnvssWrLDBgwQJLUp0+fGrczZswY2Ww2Pfzww54IE0Ad2O2nTk3vtbod5tXtqnX7nBvsP25vhvTzbs/FA/NNHPWAgoKC9dbSB//w+t8VHBSiW5/tpz5JyUrue7VJEXpedftgUNfROrv3lXpi4XWat+Q2Tb/iNUVHNTUxSs+p6Rg47rkPblFpWbHuvWqBdwMD4LJ9x8yr+4CJdbuirEL6YK3z5Svs0uI1dHL7u0DPhwI9F5LIhwB/kV0o5RWbU3dOkZRTaE7drlqxTTqS63x5V/uSAACA7/DLAf+FCxfKbrdr4sSJatiw+ocqRURESKp5wP+9997Thg0bPBUigDo6nCuVmngn+gGLDPiv2uH6Oqt30Mntz9o0S1Jyn6u1ftc32rxnZeXrIcGh6t5+mHIKMnTBwBtMjNDzatoHU8bP0cFjuzSo6xgN6TbOxAg9q6b2S9KHq+bq522f6pHJHyk8LNKkCAE4y8wLEPdbJBfauN+4q80Vh3OlXUc8Ew98Q6DnQ4GeC0nkQ4C/2G/yBYhW6Bsqr5B+rMONHXXpTwIAAObzywH/5cuXS5KSk5NrLJOamiqp+gH/3Nxc3XXXXZozZ45nAgRQZ85MQ+ZJadnm1u+Mwzl1+/KblmOdGQxQN9ece7+CbEF666sTdzRt3rNSX61doIuHT9WLH9+pkrIiEyP0vOr2QURYlFrFJqpDXC8TI/OO6tq/Yde3eu2z+/TApPcVF9vevOAAOC3NxHwoq0AqscAjjn6p43S7dV0P1hHo+VCg50IS+RDgD8zuGzK7fmdsOyTl12EWhC2pUqGLF00CAADz2RwO/7ufMyEhQampqVq/fr369u17yvLy8nK1atVKGRkZ2r17txITE6ssv+OOO7R582atWLFCNptNDz30UJ2m9S8oKKicYaBVq1YKCvLL6ysAr2o/8CoNvOKpapdNHy1FR5x+/ehwKSjIeDRA7mm++OQWSU9/eerrBzZ9op//fasLEXtfi05n6qw/L6zTuj+8daMO/bbUzRH5jrF/W6PImFYqzEnT57MGmR3OaYWFROiVqTs9WkdRSb5ufrqPLjtrusYPvVUzXj5bneMH6taLnqnT9qY830ml5e7pIPdG+08246WROqPbhbpi5D113oY72y95Zx+kZ6Zo6txBuu68h3TJ8Kn12pa72w+gZpc8ukMhNdx9Wls+VN9cSJI+ebSvSvIzXIjY+86fsULRLZJcXu/IrlX6/lX/nM79OKvkQ97KBdyZD1kxFzjOHbmQZM18kHwIsJ6eo/+qrsm3V7vMXbmQVHM+tO2b57T1qyddiNj7Og6drH6X/KNO6y59Kll5R7z3fdzbrJILAQACT1xcnNaudeH5hCcJcXMsPqGgoECSVFRU/ZesRYsWKSMjQ40aNVKHDh2qLFu7dq1effVV/frrr26NKS0tza3bAwJVTMeab0GPjpAaOznzYlCQ82VPVlRYqIMHD7q+ohcFNT5a53WPHcvw+fbVR0VFReVvX29neKjnpxGd/8kMxcV20EXDbpPNZtO9Vy7QLc/21fCel6p34lkuby/t0CEVl7nnYYbeaL+7ubP9kuf3QXFpoR5acImGdr+o3p3bkvvbD6Bmdru9xmXO5kN1zYUkKe3QQRXl+faAf3l5eZ3WKy4u9vkcob6skg95KxdwZz5ktVzAE6yWD5IPAdbULq/mB9N7IxfKzc3x6XOoJDXNzqrzuocPpyvrkG+3rz6skgsBAOAKvxzwj4uLU1ZWltatW6ehQ4dWWZaWlqZ7771XktS7d2/ZbLbKZRUVFbr55ps1depU9ejRw60xcYc/4B4NI8NqXJbrxI0UrtzVVp3QYIfatGlTe0UmigwplSQ5HI4qn3Gnc7xsRHCJz7evPoKDgyt/+3o7w0Jqma6inn75/Qut2LhIr0zfVHmctG7WUTeOeVxzFt2g+TM2KSIsyqVttmrd2q13dFmNO9sveX4frNz8gfakbdTBjB1asXHRKctfv+c3tWjS1untubv9AGpmLyuUwhtWu6y2fKi+uZAkNW/aWBXRDZyI1Dxl+YcldXV5PXvxMZ/PEerLKvmQN3IBd+dDVssFPMFq+SD5EGBNEQ2Ca1zmrlzodNuKDA/16XOoJIWpbhcf2SvKFRNhU6SPt68+rJILAQACT1xcXJ3X9csp/adNm6Z58+YpISFBy5YtU+fOnSVJa9as0aRJk7Rnzx6VlZXp9ttv1/PPP1+53nPPPacnn3xSv//+e+VU/O6a0j8/P19RUa4NnAA4VXqO9PindV//4UuNK7izC6WHP3R9/Yv7S8nd6l6/tzyzVNrn4o13rRtL946VnLxGwJIeWiLlFEkxEdIjE8yO5vQqSqVv55odhWuSp0nBNV+T45JAb79kvX3g7vYDqNkLy6Sdh+u2bn1zodgo6cFL6la3N/26V/rXD66vd8coqWNL98fjS6ySD1ntPCiRC0jkg+RDgHdsOiC98X3d1q1vLiRJN50t9Yiv27reUl4hPfKRlFfLRQ1/1KetdMOZHgnJZ1glFwIAwBV+ecv5zJkz1bRpUx04cEA9evRQr1691KlTJw0ePFiJiYk655xzJEl9+vSpXCcjI0MPPPCAHnzwQZWXlys7O1vZ2dmSjGkds7OzTzt1JgDvaNFICjNxbpKEWPPqdsWITq6vM7yzfw/2AwDgL8zMR6ySC/VpKzV0cRKCuBgpsYVn4gEAAO5jdj4S39Tc+p0REiwNTXJ9vbr0JwEAAPP55YB/fHy8Vq5cqXHjxik8PFwpKSmKjY3V/Pnz9dlnn2nHjh2Sqg74p6amKi8vTzfffLOaNGlS+SNJTzzxhJo0aaL9+/eb0h4AJwQFmffFLsgmxVukk7t/e6lLK+fLd2whDUn0WDgAAMCN2jYzsW4LdHBLRif3FYMlZ69lDA76X3kufgQAwOc1jjSm5jer7hiLPHFlZDfjgkZnDewgJfn5TEcAAPgrE++T9axu3brp009Pnfc7Pz9fKSkpCgoKUs+ePStfT0pK0rfffntK+eTkZF1//fWaPHlyvZ6dAMB9BnWQdh/xfr29E6TwUO/XWxfBQcYUbG9+L21PP33Zji2kG88yOsYBAIDv695aigyTCku9W2+QTRrQwbt11kefttI1Q6V3f5Lsp3mQXViINHmEkRMBAADfZ7NJAxOl5b95v+5BFsqFIsOkW86R5n8rpWWfvuyA9tLVQ7j4EQAAq/LbAf+abN26VQ6HQ507d1ZkZGTl6w0bNtTIkSOrXad9+/Y1LgPgff3bS/9dLxV5uZN7eGfv1ldf4aHSlGTp1xRp9Q5p37Gqy+NjpRGdpYHtGewHAMBKwkKkIR2lb7d5t96e8cZdbVYyOFGKbyJ9v93IicoqTiwLDzWWn9lFat7ItBABAEAdDO8kffubdJpr+tzOZpOGWWzK+8aR0l3nSz/tllbvlI7kVl3eOc7Yl70SjIs7AQCANQXcgP/mzZslVZ3OH4C1hIVIw5Kkb7x4JXfrJlKSBe/6Cg4yOrIHJ0rpOdLcr4y7ARs2kGaM5sptAACsangnYxC7wu69Os/u6r263Kl1E+nqM6SL+kn//FgqKJWiwqQHL5UaBNw3YgAA/EPThsYg9aYD3quzT4LUJMp79blLg1Ajjzuri5SaJb30zYm+odvONTs6AADgDgHXveHqgL/D4c3rRAE46/xe0ob90rF8z9cVZJOu8YNpzeJipND/3ckfHGT99uCE0rJi/fPfV2vf4d/UIDRCjRu20LQJL6lNs6Qq5dIy9+rRty9Xhb1Cdnu5Elp2092XvaJGkU2qlJv97mR9/etb+vD/stQworEXWwJXpB7dqScXXa+cggxFhcfo3qsWqH1cjypl7Ha7Xv1sptZu/1IV9nL1aD9c0ya8pNCQMEnST799qlc+vUcVjgp1iOule69aoKjwaK3ZvlSvfXZf5XayC44otlGcXrprnVfbCKBmzRpJF/SSPt/onfrO6Gj9Ke8jG5yY1SgkmMF+f+DMufDLNW/qw5XPVf4/IydVvRLP0sPXL5EkvfvtE/p67VsKCQ5TWGi4br94rrq2HVxlG28tfUjvLPs/vXTXeiW16evxdqFuXvhomn787WMdztpX49/qt5Qf9dySWyVJFfYy9Ww/QrddMldhIQ20cfcK/e21MYpv3qWy/Nw7flSDUIs8rBsIQJcOkHakS8Vlnq8rIsyoz8psNikhtmrfEAAA8A8B18XBHf6Af2gQIl1zhvT8MtfWyy2q+tsZ53aXEpq6Vg/gbWOHTNHgrmNks9n00ern9fT7f9ZTt66oUqZpdGs9c/uqyk7LF/57p97++mHdfvGJTvCVm5coJDjUm6F7jDOdvs6Uu25We4WGNFBYiLHfrjnnrxrZ9yoPR1+75z64WWOHTNEFgybr+02L9eSiyXrhzjVVyny55nXtOrhOL961TiHBoXpm8RR9uOo5XTnyXhWV5Oup92/UU7d+p7Ytumreh1P172WPasqFT2pQlws0qMsFldv5+xsXqk/HZG83EUAtzu0ubT4gHch0fp265EKNI6WL+7sWG+ANzpwLRw+6QaMH3VD5/5vm9NS5/SZKknYd3KBPfnhRr92zVRENGmrZr+/o+Y+m6vlpv1SW/33/L9qeukYtm7TzTqPcyB25UG7BMd07/8TtnyVlhUrL3KP3Hzqi6MhYD7fANWf2vlxXjpypu18cUWOZxNZ99MKdaxQSHCq73a7/e/syffLDi7rsrLslSfHNu2j+9A1eihhAfTWJki7pL737s/Pr1CUXkozB/hiLPdoIAAAEjoC7jm/58uVyOBwaN26c2aEAqKekltI4F6/defpL6eEPjd/O6Bxn3D0H+LKw0HAN6TZWtv9N29Ct7Rk6nJVyarmQBpWD/RX2ChWXFsimE1M9ZOUd1sLls3TL+Ke9Erenndn7cj1z26paO+idKXf/xEWaP32D5k/f4BOD/Vn5R7Qjda1G9b9OknRmr8t0NPuADmbsqlJu96GN6tdplEJDwmSz2TSo6xgt+/VfkqRffv9CSa37qW0LY47ui4bdpm83LDylroycQ1q/8xuNGjDJw60C4KrgIOlPI6SG4c6v42ouFBYsXT/CuKsN8CXOngtPtm3/z8rOP6KhPS6SJNlsNpXby1RcWiBJyi/OVrOY+MryxaWFev6jqbrrsvkebInnuCMXio5qWpkDzZ++QeOGTNHgLmN8brBfknonnqXmjeNPWyY8LLLy4tbyilKVlBVV5tAArGlIR+Mxhs5yNReSjJmOBnVwPTYAAABvCbg7/AH4l1E9pLIK6ast7t92xxbSjWedmPoVsIoPVz2noT0urnZZWXmpps4drCPZ+9ShVW89OvnjymVPL75JN42brcjwRt4K1aN6J57l1nK+5Gj2AcVGt1JwsJHK2Ww2tWjSVkey91d5lEOn+AH67Kf5unj4VDUIjdD3G9+rvBjkSPb+Kh37LZu0V2Zumioqyiu3K0lfrV2gwV3HqklDi8/lDfip5o2k286RXlwu5Re7d9thwdKNZ0sdmrt3u4A7OHsuPNmXv7yucwdMqhzw7di6jy47825NeqyDGkXGKjS4gZ6+7fvK8q9+NlMXDr1VLRoneL5BHuCJXOiLNa/rxjGP1TUkn5CemaKHFlysQ8d2a0i3cRo/9LbKZWmZu3Xrs/0VZAvWBYNu0EXDbjvNlgD4AptNumqIVF4hrdvn/u0P7CBdOZjHIgIAAN8WcHf4A/AvNps0to902cATzyBzh4EdpJuTpQb+MbM5Ash/vpmlQxm7auyIDQ0J0/zpG/Teg4fVtnlXffqTccfa5z+/phaN26pf0jneDNcyZr/7J930VC899d6Nys4/anY4Trvg/7d331FWVefDx7/3TmUGBhh6EZDepCkIigU71tiNJdGf0ViJimKMseQ1scUYu2JsMUYk1liiWAAFRQUBARtdQerQy8C0+/5x4+gERmbGW+bM/X7WYuHcU/azj7M4z93POXvvdTYDux3ByAcPYOSDB9CmWVfSwlV/3jMSiTBu6mMcMejcOEYp6adq3Rh+c2j071hpnAsXHQLdWsXunFIyFRZtYeLMZxj+g3va8rWLmDz7BZ64ej5jfr+UE/a/nD8+FZ3J55O5b7Fq3dcVlgNIdZ8t/oDNW9cxuMfRyQ7lJ2mZ34HRV3zKv65fQXHJdibPeQGAzm0GMObapTx42XRu/OWLvDrlId799F9JjlZSVaSF4cx9ossdxaouHwrBob3g9CEQdgRdkiTVcr7hL6lO2K8bdG0FY6bA4oKanycvG07ZG3r/+EyQUq307MQ7mDznBW4//22yM398ccGM9EwOG3gOf33uPE4dNopPF0xg9sL3+OiLV8v3Of/OPvy/s/9N5zb94x16tY24dwjfFszb6bYHL58R0zfx7rzwPZo3bkdJaTGPv/F7bh/7S24+9z8xO39NNGu0W4W38SORCKvWfUPzRu0q7BcKhfjFYTfyi8NuBGDCzGdo37IXAM0btWP63LfK9125bnGFNyUBZi18l6KSbezV7fD4d0rST9IsD644HN7+LDrzUVmk5ufatwsc0x+yffBRtVhV74Xfee/TZ2nfohftW/Qs/2zyrOfZvdUeNG3YGoDDB57D/S9dSnFJETPnj2fet9M58+YOAKzesJRrHzuSy04czZCex8S9f7uSyFzoO298/CiH7vmLCrlCkNXLqs+B/U5j/PR/MqzfaeRm55Vva9aoLcP6/5zZiyZxQN9TkhilpKoKh6P5S++28PQUWL2p5udqnhct9HdoGrv4JEmS4qlufEuTJKBFHow4FGZ+A+/PgwWrqn5sk/rRwe3BnSAnK34xSvHy3Lt3MmHmGG47/23q12u0031WrvuahrnNyM7MoaysjPdmPUvHVn0AuOb0f1bY99CrQjx8xaxKz5Vs91w6JWFtNW8cLRykp2Vwwn6Xcc7tXRPWdmUa129O5zYDeHv6Uxw+8GwmzX6epo3a7jCFcVHxNrYXF9IgpzEbthTwzPhbOfuImwAY2O0I7nvxYr5Z9SXtmnfn5Q8e4MC+p1U4/vWPH+Wwvc4mLezaJlIQpKfBEX2gfweY9BVMXQjbS6p2bFoY+reH/btBuyZxDVOKiareC7/zxtRHd5ixpmWTjoyb9jiF2zdTL6s+H33+Km2bdSUjPZNzj7yFc4/8fsakM2/uwI2/fInObfrFs1tVlshcCKBw+2benfUv7hsxNaHtxtq3BfNp0bg96WkZFJcU8f6cF9n9v/nwmo3LaVy/BeFwmK3bNvHh569WmBFCUjDs3gyuOhKmLoL358Ky9VU/tk1jGNo1OutjLGeRlCRJijcL/pLqlHAYBnSI/lm+Hj77FpashaVrYe0WiPz3bbfcLGibD20bQ+cW0elqw67HpoBavX4po18dSav8jlz50DAAMtOzuHfERzwx7nqa5LXmmCEXsHD5LB5//VoAIpEyOrcZwMXH3ZPM0Gu9wqItlJYWlz/4MGHGGDq3rh0zHlx24mj+PPZsxoy/mZzsPK465XEA/vLsrxjS81j26XUsW7ZtYORDBxIOhSmLlHH80N+Uv5WYk92Ay09+hBuf+BmlZSV0aNmbUaf+vfz8Wwo38P7sF3h45Oyk9E9SzbXIg5MGwtH9YNYS+KYgmg8tXw9FpdF90sPQoiHslg+7NYG+u0H97GRGLVVfVe6FAEtWfcWCZTP50/9VnKFnaO/jmbtkKhffvRcZ6VlkZ+ZyzelPJ7wfQTDx07F0bNWXds27JzuUSt313K/56MvXWLtpBdc8cjg5WQ34+2/nV/h9mDl/PC9NvodwOI3SshL6dz6YMw+5DoBJs5/n1SkPkhZOp7SshP37nMzhLukgBVJmevSljn06w6LV8NWK6LjQkrWwqRAiRKf+z6sXHRvaLT86LtShaXQqf0mSpKAJRSKRnzDZo37Mli1bqF+/PgCbN28mNzc3yRFJqS0SiU5vGw6l7he4G16ADYXQsB784YRkR5N4Qep/aRFMCFgtftgISMuMzbli0f8fDvrm5TQpH/SFioWAH9tv+ZqF/OHJEykrKyVChFb5HbnouLtpmd9hh/Zi2X8I3u9ArPsvKT7KyqKD3GkpuhZtkHKBeAnKNQjafRBqXy4Qi1zoO7+5bx+G730eR+yiAF7b8sFEMx+Saj/HhoKTC8RLqvdfklQ3+Ya/pJQRCkFain6Zk1LRZSeNrnTbyJMfqdJ+rZp05KHLZ8Q0LklKpnCKFvqlVBSLXOg7d1/yQUxikqRkc2xIkiTVRQ73SJIkSZIkSZIkSZIUQBb8JUmSJEmSJEmSJEkKIAv+kiRJkiRJkiRJkiQFkAV/SZIkSZIkSZIkSZICKD3ZAUiSpB2FM2DYiGRHUT3hjNieK5X7/935gnQNYt1/SVJqC9p9EMwFwHzQfEiSJElSMljwlySpFgqFIC0z2VEkT6r3H7wGkqTU5n3Qa5Dq/ZckSZKkqnJKf0mSJEmSJEmSJEmSAsiCvyRJkiRJkiRJkiRJAWTBX5IkSZIkSZIkSZKkALLgL0mSJEmSJEmSJElSAFnwlyRJkiRJkiRJkiQpgCz4S5IkSZIkSZIkSZIUQBb8JUmSJEmSJEmSJEkKIAv+kiRJkiRJkiRJkiQFkAV/SZIkSZIkSZIkSZICyIK/JEmSJEmSJEmSJEkBZMFfkiRJkiRJkiRJkqQAsuAvSZIkSZIkSZIkSVIAWfCXJEmSJEmSJEmSJCmALPhLkiRJkiRJkiRJkhRAFvwlSZIkSZIkSZIkSQogC/6SJEmSJEmSJEmSJAWQBX9JkiRJkiRJkiRJkgIoPdkBSJKkHUUiUFac7CiqJ5wBoVBszpXq/YfgXYNY91+SlNqCdh8EcwEwHzQfkiRJkpQMFvwlSaqFyophwj3JjqJ6ho2AtMzYnCvV+w/Buwax7r8kKbUF7T4I5gJgPmg+JEmSJCkZnNJfkiRJkiRJkiRJkqQAsuAvSZIkSZIkSZIkSVIAWfCXJEmSJEmSJEmSJCmALPhLkiRJkiRJkiRJkhRAFvwlSZIkSZIkSZIkSQqg9GQHIEmKn+3F8O06WLIWVmyArUXRz7cWwcQvYLd8aJMP2RnJjVOSJCkeyiJQsCmaCy1dWzEXenUmtM2P5kP5uRAKJTVUSZKkuNi6HZaugyVrYNWm7/OhwiKYPDeaC7VuDBlpyY1TkiTVnAV/SapjIhFYtDr6pe3TJVBatuM+xaXw0vTof4dDsMduMLQLdG7hYLckSQq+Tdvgw/nwwTxYt3XH7cWl8PZn3//cPA/27QIDO0JOZuLilCRJioeyMvhiGUyeB18ug8hO9ikqheemRv87Iw327ABDu0YfiJQkScFiwV+S6pCla2HsR9G32KqqLAKffhP907oRnDYY2jWJW4iSJElxU1QCr8+C977a+UOPlVm1EV78BF6bCYf2hoN6QpoL4EmSpAD6ajn862NYs7nqxxSXwocLon+6tIBT94amDeIXoyRJii0L/pJUB5SWwZtz4K050QJ+TS1bD38dBwf1gOF9IN3p3ALl0wUTufKhYRU+y87MpW2zrhwy4Cx+tu+lpKXV7Vt/ql+DVO+/pNS2aDU8PQVWb6r5OYpK4bVPYdYSOH0ItGoUs/CUIKl+L0z1/oPXQFLq2lYM/54OU+b/tPPMWwm3vwZH9YP9ukVnhpQkSbWb33AkKeCKSuCx9+DL5bE5XyQC73wOX6+BXx0A2RmxOa8SZ1i/nzOo+5FEiLBu0wre+uRJHnrlCr5Z9QWXn/RwssNLiFS/Bqnef0mp55PF8M8PftqDjz+0ZC389Q049wDo1io251Ripfq9MNX7D14DSallUyE8OD76IkcsFJVGZz9ashZ+PtiZjyRJqu28VUtSgJWUwiPvxq7Y/0PzV8LoCdEHChQsXdoM4JA9z+TQPc/ilAOv4p5LP6RZw7a8/vEjrN+8OtnhJUSqX4NU77+k1DJ9MTz1fuyK/d8pKoW/TYS5K2J7XiVGqt8LU73/4DWQlDo2b4P734ldsf+Hpi2KzqAU6zxLkiTFlgV/SQqw56bGdxB60WoY82H8zq/EqJeZS/f2g4lEIixbsyDZ4SRFql+DVO+/pLrrmzXw1AcQrzHokjJ49F0o+AnLBKh2SPV7Yar3H7wGkuqmsgg8PglWbIhfG58shnGz43d+SZL00zmlvyQF1OffwofVHKe64gjIqwcbC+HON6p2zIyvoV876Nuu+jGq9lj+30HNvJz8JEeSPKl+DVK9/5LqnpLS6r9xVpNcaHsJPPMRXHSwa9gGXarfC1O9/+A1kFT3TPoKFqyq3jE1yYfemgN7tIW2/vMpSVKtZMFfkgJoWzGM/aj6x+XVg0Y51T/u2Y+hcwvIzar+sUq8bcVb2bClgEgkul7pK1MeYv63M+i+2yDaNuua7PASItWvQar3X1JqGDe7+m+z1TQXmr8SPpgHQ/0nNDBS/V6Y6v0Hr4Gkuq9gE7w6s/rH1SQfKovAP6fAlcMhzTmDJUmqdVKi4F9QUMDtt9/OCy+8wNKlS2nWrBknnHACN998MyNGjOCxxx7j3nvv5ZJLLkl2qJJUJR8ugA2FiWtv83Z4fx4c1jtxbcZKJBJd9uD9edGn1wE2bos+MDG0K7RpnNz44uHJN2/gyTdvqPDZ0N4ncOnx9ycposRL9WuQ6v2XVPdtLYJ3v0xsm2/NgSGdgznIvX4rTJkfnbnpu3xo8/bourx920FGWnLji4dUvxemev/BayCp7hv/BRSXJq695eth9hLo1z5xbcZKaRnMWVpxbGjTNnh5BuzbBZrUT258kiT9VHW+4D9z5kyGDx/OihUryM3NpWfPnixbtox77rmHBQsWsHbtWgD69euX3EAlqYrKIvD+3MS3+8E8OLhnsAa5CzbBo+9Fv5T+UCQSHfSeMh96toaz9oV6mUkJMS6O2vt89u9zMiVlxSxaPpuxE2+jYMNSMjOyy/cpKtnORXcNYFj/0znj4GvLP7/9mbNZv3klN//q9WSEHjNVuQZ/euo0yiJlXHfWv8o/27h1Lefd0Yvzj76DgweckYzQY6Iq/Z+9cBK/e3T4DseWlBZRVlbKuNsTOHIkSdU0dSEUJfifqQ2F0YHiIC1zVBaJvvk38Ysdlz4oLYOnPoCXPonmQt1aJSXEuEn1fCjVcyEwH5JUt20rjj64l2iT5wWv4L+4AJ6YFH0A8ofKIjD+c5jwOQzuDCcNDNaYlyRJP1Snb2EFBQUcc8wxrFixgpEjR7J8+XKmT5/OihUruO2223jttdeYOnUqoVCIPn36JDtcSaqS+Sth9abEt7t+K3yxLPHt1lTBJrj7zR2L/f/r82XwwDuwvTghYSVEm6ZdGND1EAZ1H86pw0Zx0zmv8NXSqdz9/AXl+2SmZzHqtCd55p2bWbDsUwDen/MSH37xClec/GiyQo+ZqlyDS094gM8Wv8/4GWPKP7v3xYvptfvQwA9wV6X/e3Tcj1f+tLnCn8dHzSUvtym/PPymJEYvSbs2ZX5y2n1/XnLarYlIBP71UXQg+3+L/T+0eTuMngCff5u42BIh1fOhVM+FwHxIUt02bREUlSS+3fkrYeXGxLdbU4tWw/1v71js/6EI0dzyiUlQVpaw0CRJiqk6XfAfMWIES5cu5ZJLLuGOO+6gQYMG5dtGjRpF3759KSkpoUOHDuTl5SUxUkmquvkrk9f2vCS2XR2RCDw+KTo9W1UsWQsvfBLfmJKpV4d9OGTAWUz8dCyfLf6g/POubffkpAOu5PZnfsHq9Uu567nzufT4+2nasHUSo42PnV2DvJx8Rp78KPe9dAkFG5bx3qznmLVgIped8FCSo429yn4HfqioZDt/ePIEencYyukH/y7BEUpS1W3aBis2JKftRaujb8YHwUcLo8tAVUVZBJ6YXPXcKYhSPR9K9VwIzIck1S3JHBtKZtvVsb0EHnm36ssezF4aXSZBkqQgqrMF/y+++IKxY8fStGlTbrnllp3us+eeewLQt2/f8s8mTpxIKBTa4Y9T/kuqLZasTV7bS5PYdnUsWAXfrqveMdMWweY6PMh9xiHXEQ6n8fdx1//P578nLZzOhXf1p2/nYQzrd1qSIoy/nV2Dgd2P4IA+p3DbmDO594WLuOLkR8jLbZLEKOOnst+B79z9/AUUFW/jqlOfSGxgklRNS9Ykr+3iUliZpIcNqiMSgXe/rN4xRSXwYZJmTkiUVM+HUj0XAvMhSXWHY0O7Nn0xbNlevWMmzw3Ow52SJP1QnS34jxkzhrKyMs444wzq16+/033q1asHVCz4f+f+++9nypQp5X/+8Y9/xDVeSaqqZH6xWro2OoBc202eW/1jSsvgoyq+BRdEbZp2Zljf05gx/x1mL5xU/nl6WgY9O+zDhi0FHL7XOUmMMP4quwbnH3MH366Zz8Duw9m7x1FJjDC+Kus/wIuT7+GjL17lD2e/RHZmTpIilKSqWVrNh/piLZkD7FW1aPWulzXamQ/m1e2pbFM9H0r1XAjMhyTVDYVFsGZz8toPQi4ENRsbWr8VPqtjyxxJklJDerIDiJfx48cDMGzYsEr3Wbp0KbDzgn/Pnj0ZPHhwzOLp0qUL4XCdfb5CUgKdeMvXhMJpO912xRGQV6/yY/Oyv//7xuMr329jIdz5xo6fby+B9h06UlZaVI2IE++IUZOp36RDtY974MlX+OU/L4x9QDWQmV6Phy+J7ULBPz/4WibMHMPf37yeOy6YAMDshZN4c9oTHLfvJTzw8m94qNNMsjJ+5JfoR3Tp2oWiksKYxBqP/sPOr0G9zFxa5Xdk95Z7/KRzx7L/kLjfgZnzJ/DIa1dz869ep2V+hxqfO9b9l6TK9D3mRroM/dVOt8UqF4LK86FrrruJue+NrmK0ydF56Ln0O+YP1T5u3Vbo0rMf2zcXxCGq6klkLhCrfCiouUCsciEIbj5oPiQpSHLz2zP86vcr3R7vsaGvFiylbdvYjZvHQyicxom3fF2jY6+64W4+e/PPMY5IkqRda9myJdOmTavRsXW24P/119Ebevv27Xe6vaSkhPffjyZGOyv4x9ry5cvj3oakFBAKVVrsh+gXukZVeBklHK7afjuzYtVqircl8VHyKgilZdXouJKyMN9+Wzse5c7OqP7/oL6dDuStP1c+BUP7Fj0Yd/v3i9cVbt/Mn8eezbnDb+WYIRcy8qEDeOz133HhsX+tUczLly1jW/HWGh37v2rSf6j+NYilWPYfEvM7sGLtYv741Cmcd/Sf6dvpwJqEWS7W/ZekynQprHxu1kTkQpu3FNaafKEyrQuLa3xswdqNbFyd/P4lKheIZT4UxFwg1oKYD5oPSQqaxmU7n832O3HPh0JptT4Xysj+8Wv0Y7YVldX6/kmS9L/qbMF/y5YtABQW7vzJ6rFjx1JQUECDBg3Yfffdd9h+6qmnUlBQQJMmTTj22GO59dZbadq0abViyMnJYfPmzQwdOpRVq1YRCoWq3xFJ+h9lJUWE0zN3um3jLl4mycuOfqErK4ONP7Je/Y+dp0WzJkTKGlYh0uQpLdpSo+PSIkW0adMmxtHUTGZ6zd6yr47Rr4ykZf7uHLvPRYRCIa465QkuuKsf+/Y+nj4d96/2+Vq1bh3TN7qCJpb9h/hfg21FW7nhiZ8xpOex/GzfS37y+WLdf0mqTL3sjEq3xSoX+rFz5eZk1Zp8oTL1Mmv+3TO/YQ4NMpPfv0TlArHMh4KWC8RD0PJB8yFJQZTTuPGPbo/72FBZca3PhQiFKCstIZxW/fJHZnpZ7e+fJKlOatmyZY2PrbMF/5YtW7Ju3TqmT5/OkCFDKmxbvnw5V111FQB9+vSpUIhv2LAhV111Ffvvvz/169dnypQp3HLLLXz44YdMmzaN7OzsKscQCoXIzc1lxowZsemUJAF//DcUVPKC/c6mWvuhG4+PPr29cRvc+GL1287LhiXfLK7+gQn24ifw7pfVP+53Fx/PXnfsYn7fBCktggn3xO/8H3/5OhM/HcvDV8wqvw+2btqJc4ffyh1jz2H0yFnUy8yt1jnnzZ1H2s6fRam2ePc/HmLZf4j/NZg0+3kWLv+UbwvmMvHTsTtsf/TKz2neuF2Vzxfr/ktSZd79Mnqv35l450IAf73tBvq1u6FmBydIwSb448vVP659E1g0b07sA6qBROQCsc6HgpYLxEPQ8kHzIUlBVFwKV4+FskomM4l3PtSnezv+9t+lcmuzhyfA58uqf9yjfxlJuyYjYx+QJElxVGcL/occcghffPEFt912G4ceeihdu3YFYOrUqZx11lkUFETXJOzXr1+F4/r370///v3Lfz7wwAPp3bs3xx57LGPGjOGcc85JWB8kaWfa5lde8E9E20Gwb5fqF/xzs6Bf1cfyAm9Q9+G8dNP6HT4/bt+LOW7fixMfUJL95cKJyQ4h4Q7d8ywO3fOsZIchSdW2W5LzkWS3XxVNG0D3VvBlNVeW27drfOKprcyHvpeKuRCYD0kKpow0aNkIlq1LTvtByIUAhnatfsF/t3xo1yQ+8UiSFE/hZAcQL6NGjaJJkyYsWbKEXr16sccee9ClSxcGDRpEx44dOeiggwDo27fvLs919NFHk5uby7Rp0+IdtiTtUjK/WO0WkC89zfOgf/vqHXNwT0hPi088kiQpdto0hmQtlpaTCfnVmwAnaQ7tBeFqXKhmDaqfP0mSpORI6thQQAr+3VtVP9bDescnFkmS4q3OFvzbtm3LpEmTOOqoo8jOzmbx4sXk5+czevRoXnvtNebOnQtUreD/nR9O/S9JydKrbRLbDtASZj8fDJ2aV23ffTrDsB7xjUeSJMVGVgZ0qfmydj9JzzYQlK+FnVrAqXtX7eGIRjnw62HRNwYlSVLtl6zxmbQwdGuVnLarKxyG8w6MPtRYFccNgD12i2tIkiTFTZ2d0h+gR48evPrqqzt8vnnzZhYvXkw4HKZ3710/tvfyyy+zZcsWBg0aFI8wJalaWjaEzi1g/srEttuuSbCmNctMhwsOgldmwIcLoKhkx30aZMNBPeHA7sEZvJckSdHle+auSHy7QwM25f3enaB+djQfWrFhx+2hEPRuAycOjBb9JUlSMPRqE713r9+a2Hb7tYvmFkGRVw8uOxyenwozv4GyyI77NK0PR/aFAR0SHp4kSTFTpwv+lfnss8+IRCJ07dqVnJyKoxpnnnkmHTt2ZMCAAdSvX58pU6Zw++23069fP0477bQkRSxJFQ3tkviC/75dEtteLGSkwQl7Rb+4TV0IX6+JFv6zM6BHa9ijrdP4S5IURL3bQsN6sKEwcW22zYf2AXr48Tu92kDP1rBwFUz/GjZvh7QQNMuDwZ2gcUCWKJAkSd9LC0dnK/zPrMS2G8Sxodws+MVQ+FkhfLQg+hBkSSnkZEHfdtC1ZfWWQZIkqTZKyYL/7NmzgZ1P59+rVy+efvpp7rrrLgoLC2nbti3nnXceN9xwA5mZmYkOVZJ2qs9usHszWLQ6Me21zYe9dk9MW/GQnQH7dYP9kh2IJEmKibQwHNMfnvogcW0eNyC4MwKFQtEp/ju1SHYkkiQpVvbvDlPmw7oEveXf979jUUGVVw8O3fVkv5IkBZIF//9xzTXXcM011yQ6JEmqlnA4ukb9n/8DxaXxbSstDKcPjv6t2uv+l0Yw5fOXWbnuax68bAad2/TbYZ83pj7Oi5PuLv+5YMNS9ui4Pzf+8gVWrF3ML2/tRIeWe5Rvv+EXz9O6aadEhP+TVaX/M+aP59H//JbC7ZsJhULs3f0ozj3yVsLhMMvXLuKmJ0+itKyUsrISdmvRg8tPfJgGOY0T3xlJUpXs2SE6NeucpfFva2hX6GKxXLVYUfE2/vTP0/h65edkZdSjUf3mjDjhQdo07Vxhv6lfjeOR164u/3n9llXkN2jJg5dNp7BoC6MeOoiikm0A5DdoxW9OfIiW+R0S2ZUaqWr/AZ6ZcBtvTfs76WmZZGZkc/Fx99C93aBA919SasrOgNMGw4Pj499WbhacNCi4Dz9KklTXWfCXpIBqnhd90+y5qVU/ZmNhxb+r4sg+0NqaZ623X5+TOOXAUVz+wNBK9zli4DkcMfCc8p/Pu6M3B/c/o/znelkNGH3FzHiGGTdV6X+Deo259oxnaNWkI0XF2xj18CG89cmTHD7wbJrkteavF08mK6MeAPf/+zc8+daNXHzc3ZWery64+uHDWLdpBaFQmJzsBlx83D10btM/2WFJUpWEQnDKIPhmTdVzm5rkQs3z4Jh+1Q5PtUBVHggEKCrZzuhXRjJt7jgy07Pp1Kovvz39qfLtU798g8fH/Z6SkiKyMnO47MTRdGpd+8YTjtz7fAZ1H04oFOKl9+/jzmd/xV8unFhhn4HdDmdgt8PLf/79Y0fTt9MwALLS63Hb+W+Tk90AgOff+ysP/Ps3/L9z/p2wPvwUVen//G9n8soHD/DIlZ9RL6s+b3/yFPe9dAn3jfg48P2XlJq6tYL9u8F7X1X9mOrmQyHg1L2hQXa1w5MkSQmSkgX/8eMT8NijJCXA0K7RL2hvzqna/ne+Ub3zH9AdDupZ/biUeH067l+t/b/45iPWb17FkF7HximixKpK/39YyM7MyKZT636sXLc4+nN6Vvm20rJSthVtoV5m/ZjHWdtcd9a/qF+vEQCTZ7/In8eezegrPk1uUJJUDXn14MKD4L63Ycv2Xe9f3VwoPxcuOhiyMmoWn5KrKg8EAjz6n98SCoV4YtRcQqEQazeuKN+2aes6bhlzBnde+B4dWvZi9sJJ3Pr0Gfztyiom4AmSmZHN3j2OLP+5R7vBPPfuHT96TMGGZcyY9w4jT3kMgHA4XF7sjkQibN22kVBAXuWsav9DoRAlZcXRXC+rPpu3radpw7ZAsPsvKbX9bABs2gYzvq7a/tXNh04cGF1aUpIk1V4pWfCXpLpkeB/ISIPXYlyjO6x39NyOcdVNb3z8KAfveRbpad9XMLYVbeHiuwdSFilln14/4/SDryUtnJbEKONn7cYVTJr1HDf936vlnxWXFHHJPYNYtf5rdm/Vh5vOfjmJESbGd8V+gC3bNhB9d0OSgqVVI7j0UHhoPKyP4Rq2LRvCBQdBo5zYnVOJVZUHAguLtvDGx4/y9O+Xlhd38/Nalm9ftmYBeTlN6NCyFwB7dNyPVeu/Yd7S6XRpOyA+gcfAi5PvZkiv4350nzenPcGg7kfSuH7zCp+PGn0Ii1bMplFuM245b1w8w4ybyvrfqXVfTtzvcs66ZXca5OSTkZbFnRe9V2GfutB/SaklHIYz94GsdPhwQQzPG4JT9obBwVjpT5KklOaKzJIUcKEQHNobLjkEmsTgheRGOdE35Y7sa7G/rios2sLEmc8wfNC55Z/l57VizHXfcv9vpnLb+W8zZ9Eknnv3L0mMMn62bNvIdY8fwykHjqLbbnuVf56RnsnoK2byr+tX0q5Zd179cHQSo0yc28b8gtP/uBt/H3cdv/35P5IdjiTVSMuGMOpIGNTxp58rBBzYHa44wmJ/KlhesIAGOfmMGX8zF929F5c/sB/T571Tvr1t0y5s3LqGzxZ/AMAHn73M1u2bWPHfWYJqo6ffuZllBfM5d/gtle4TiUQYN/UxjvhBPvid23/9NmOvW84BfU/l6Xf+FM9Q4+LH+r987SImz36BJ66ez5jfL+WE/S/nj0+dWmGfoPdfUmpKC0en3f/FvpCbtev9d6V142guZLFfkqRg8A1/SaojOreAUUfBuNnwwTzYVly947PSYXDn6Fv92U5bW6e99+mztG/Ri/Ytvl+vITM9i8z/vt2Vl5PP4QP/jwkznubUYaOSFWZcbN22id89cgT79DqOkw64Yqf7ZKRnctjAc/jrc+cFuv8j7h3CtwXzdrrtwctn0LxRdE7Gq3/+JABvTvs7f/vP1dx87n8SFqMkxVJOFpw+BPq1g//MgqVrq3+OTs3hqL7Qsfmu91VyVfU+tyulZSWsXPc17Zv35FdH3sr8b2dw9cOH8siVn9G4QQty6zXk+rOe49HXr2Hb9s30aD+E9i16khauncMpz068g8lzXuD2898mO7PyJ1ZmLXyXopJt7NXt8J1uD4fDHLn3eZx9exdGnPBAvMKNuV31f/Ks59m91R40bdgagMMHnsP9L11KcUkRGemZ5fsFtf+SUlsoBAM6QJeW8OpMmL4Yikurd4762XBANxjWA9Lr5oR/kiTVSbXzG6okqUay0uHY/nD4HjBjMXy8EJasrfwLXkYatGkMA3eHPXe30J8q3pj66A5vc63bvIoG9RqTnpZBUcl2Js95gU4/WPO+LijcvplrHjmCvbodwRmH/L7CtpXrvqZhbjOyM3MoKyvjvVnP0rFVnyRFGhv3XDqlWvsfttcvufv5C9i4ZQ15uU3iFJUkxV/PNtCjNXyzBibPg3krfnyq/yb1oXsrGNo1ujyAgqG697nKNG/cjnAozEEDzgCgc5v+tMzfnUXLZ9O4QQsA+nUeRr/OwwAoKtnOqf+vZYUHJ2uL5969kwkzx3Db+W9XWLZnZ17/+FEO2+vsCss3rd24goz0LBrkNAZg4qdj2b1lcPKhqvS/ZZOOjJv2OIXbN1Mvqz4fff4qbZt1JSM9M/D9l6TvNMiGnw+Ojg99vBA+WQzL10Np2c73z0qH9k2jb/P32c1CvyRJQWTBX5LqoO/e1h/cOfqFbtXG6Je7ohKIAJlp0QHtFg2j074p+O567td89OVrrN20gmseOZycrAb8/bfz+cuzv2JIz2PZp9exACxZ9RULls3kT/9X8S3uOYsm8+S46wmH0ygtK6Ff54M4/eBrk9GVGqlK/1+YfDdfLfmYbUVbmDz7BQD273syZxx8LQuXz+Lx16P9jUTK6NxmABcfd08yuxR3mwvXs61oa/kbbu/PeYm83CY0yMlPcmSS9NOFQtGB6/ZNoz9vKow+BLlpWzQ3Sk+DhvWgbX5spr1VcDXMbUq/zgcz7atx7N3jSJavXcSKtYto16JH+T5rNi6nSV4rAP759k3063QQbZp2TlbIO7V6/VJGvzqSVvkdufKh6MMJmelZ3DviI54Ydz1N8lpzzJALANhSuIH3Z7/AwyNnVzjHqvXfcNfzv6asrJQIEVo36cRvT38q4X2piar2f2jv45m7ZCoX370XGelZZGfmcs3pTwPB7r8k7UxuVvRN/WE9oKQUlq2Pjg8Vl0Zzpex0aNUYmjWAsEs6SpIUaKFIJBJJdhCSJKmi0iKYELB687ARkJa56/2qItX7D/G/BivXfc1N/ziZ7cWFhENhGuY24/yj76Bzm341Ol+s+y9JSm2xuA/+8IHAvJwm5Q8EAhUeCly+ZiF/efZcNmwpIBwKc+Yh17NfnxPLz3Pns+cxZ9EkSstK6NF+CJf87N6dvkEetFwgHswHzYckSZIkJZ5v+EuSJKWgFo3bc9+Ij5MdhiRJcXPZSaMr3Tby5EfK/7tVk47cccGESve94uS/xTQuSZIkSZJiyYmcJUmSJEmSJEmSJEkKIAv+kiRJkiRJkiRJkiQFkAV/SZIkSZIkSZIkSZICKBSJRCLJDkKSJFUUiUBZcbKjqJ5wBoRCsTlXqvcfgncNYt1/SVJqC9p9EMwFwHzQfEiSJElSMljwlyRJkiRJkiRJkiQpgJzSX5IkSZIkSZIkSZKkALLgL0mSJEmSJEmSJElSAFnwlyRJkiRJkiRJkiQpgCz4S5IkSZIkSZIkSZIUQBb8JUmSJEmSJEmSJEkKIAv+kiRJkiRJkiRJkiQFkAV/SZIkSZIkSZIkSZICyIK/JEmSJEmSJEmSJEkBZMFfkiRJkiRJkiRJkqQAsuAvSZIkSZIkSZIkSVIAWfCXJEmSJEmSJEmSJCmALPhLkiRJkiRJkiRJkhRAFvwlSZIkSZIkSZIkSQogC/6SJEmSJEmSJEmSJAWQBX9JkiRJkiRJkiRJkgLIgr8kSZIkSZIkSZIkSQFkwV+SJEmSJEmSJEmSpACy4C9JkiRJkiRJkiRJUgBZ8JckSZIkSZIkSZIkKYAs+EuSJEmSJEmSJEmSFEAW/CVJkiRJkiRJkiRJCiAL/pIkSZIkSZIkSZIkBZAFf0mSJEmSJEmSJEmSAsiCvyRJkiRJkiRJkiRJAWTBX5IkSZIkSZIkSZKkAPr/SgHu8HuByPkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ansatz\n", + "layers = tq.RXYZCXLayer0(\n", + " {\n", + " \"n_blocks\": 6,\n", + " \"n_wires\": n_wires,\n", + " \"n_layers_per_block\": 1,\n", + " }\n", + ")\n", + "\n", + "# We use `tq2qiskit` to visualize the ansatz.\n", + "qdev = tq.QuantumDevice(n_wires=n_wires, bsz=1, device=\"cpu\")\n", + "tq.plugin.qiskit.tq2qiskit(qdev, layers).draw(output=\"mpl\", fold=30)" + ] + }, + { + "cell_type": "markdown", + "id": "a14839c3-d9ff-44dc-adf6-efeebae18bfe", + "metadata": {}, + "source": [ + "We can now simulate the circuit to model the gaussian mixture distribution. The algorithm minimizes the kerneled maximum mean discrepancy (MMD) loss to train the QCBM." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6490b2e9-d18d-42e0-9310-a3f644197c8f", + "metadata": {}, + "outputs": [], + "source": [ + "qcbm = QCBM(n_wires, layers)\n", + "\n", + "# To train QCBMs, we use MMDLoss with radial basis function kernel.\n", + "bandwidth = torch.tensor([0.25, 60])\n", + "space = torch.arange(2**n_wires)\n", + "mmd = MMDLoss(bandwidth, space)\n", + "\n", + "# Optimization\n", + "optimizer = torch.optim.RMSprop(qcbm.parameters(), lr=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4335ef3e-8dea-47be-a310-8146abc214fc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration: 0, Loss: 0.007511706091463566\n", + "Iteration: 10, Loss: 0.0008048344170674682\n", + "Iteration: 20, Loss: 0.0004957925993949175\n", + "Iteration: 30, Loss: 0.0003518108860589564\n", + "Iteration: 40, Loss: 0.0002739735064096749\n", + "Iteration: 50, Loss: 0.0002034252102021128\n", + "Iteration: 60, Loss: 0.00014893575280439109\n", + "Iteration: 70, Loss: 0.0001268944761250168\n", + "Iteration: 80, Loss: 0.00010558744543232024\n", + "Iteration: 90, Loss: 8.735547453397885e-05\n" + ] + } + ], + "source": [ + "for i in range(100):\n", + " optimizer.zero_grad(set_to_none=True)\n", + " pred_probs = qcbm()\n", + " loss = mmd(pred_probs, target_probs)\n", + " loss.backward()\n", + " optimizer.step()\n", + " if i % 10 == 0:\n", + " print(f\"Iteration: {i}, Loss: {loss.item()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "246f5d90-47af-4385-8d41-5ff6fadcb9ec", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the results\n", + "with torch.no_grad():\n", + " pred_probs = qcbm()\n", + "\n", + "plt.plot(x_input, target_probs, linestyle=\"-.\", color=\"black\", label=r\"$\\pi(x)$\")\n", + "plt.bar(x_input, pred_probs, color=\"green\", alpha=0.5, label=\"samples\")\n", + "plt.xlabel(\"Samples\")\n", + "plt.ylabel(\"Prob. Distribution\")\n", + "\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b7f5ff14-793c-4dc3-aad2-eed38aa3e5a5", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1. Liu, Jin-Guo, and Lei Wang. \"Differentiable learning of quantum circuit born machines.\" Physical Review A 98.6 (2018): 062324." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (tq-env)", + "language": "python", + "name": "tq-env" + }, + "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.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/QCBM/qcbm_gaussian_mixture.py b/examples/QCBM/qcbm_gaussian_mixture.py new file mode 100644 index 00000000..fdc2acbd --- /dev/null +++ b/examples/QCBM/qcbm_gaussian_mixture.py @@ -0,0 +1,129 @@ +import matplotlib.pyplot as plt +import numpy as np +import torch +from torchquantum.algorithm import QCBM, MMDLoss +import torchquantum as tq +import argparse +import os +from pprint import pprint + + +# Reproducibility +def set_seed(seed: int = 42) -> None: + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + # When running on the CuDNN backend, two further options must be set + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + # Set a fixed value for the hash seed + os.environ["PYTHONHASHSEED"] = str(seed) + print(f"Random seed set as {seed}") + + +def _setup_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--n_wires", type=int, default=6, help="Number of wires used in the circuit" + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--n_blocks", type=int, default=6, help="Number of blocks in ansatz" + ) + parser.add_argument( + "--n_layers_per_block", + type=int, + default=1, + help="Number of layers per block in ansatz", + ) + parser.add_argument( + "--plot", + action="store_true", + help="Visualize the predicted probability distribution", + ) + parser.add_argument( + "--optimizer", type=str, default="Adam", help="optimizer class from torch.optim" + ) + parser.add_argument("--lr", type=float, default=1e-2) + return parser + + +# Function to create a gaussian mixture +def gaussian_mixture_pdf(x, mus, sigmas): + mus, sigmas = np.array(mus), np.array(sigmas) + vars = sigmas**2 + values = [ + (1 / np.sqrt(2 * np.pi * v)) * np.exp(-((x - m) ** 2) / (2 * v)) + for m, v in zip(mus, vars) + ] + values = np.sum([val / sum(val) for val in values], axis=0) + return values / np.sum(values) + + +def main(): + set_seed() + parser = _setup_parser() + args = parser.parse_args() + + print("Configuration:") + pprint(vars(args)) + + # Create a gaussian mixture + n_wires = args.n_wires + assert n_wires >= 1, "Number of wires must be at least 1" + + x_max = 2**n_wires + x_input = np.arange(x_max) + mus = [(2 / 8) * x_max, (5 / 8) * x_max] + sigmas = [x_max / 10] * 2 + data = gaussian_mixture_pdf(x_input, mus, sigmas) + + # This is the target distribution that the QCBM will learn + target_probs = torch.tensor(data, dtype=torch.float32) + + # Ansatz + layers = tq.RXYZCXLayer0( + { + "n_blocks": args.n_blocks, + "n_wires": n_wires, + "n_layers_per_block": args.n_layers_per_block, + } + ) + + qcbm = QCBM(n_wires, layers) + + # To train QCBMs, we use MMDLoss with radial basis function kernel. + bandwidth = torch.tensor([0.25, 60]) + space = torch.arange(2**n_wires) + mmd = MMDLoss(bandwidth, space) + + # Optimization + optimizer_class = getattr(torch.optim, args.optimizer) + optimizer = optimizer_class(qcbm.parameters(), lr=args.lr) + + for i in range(args.epochs): + optimizer.zero_grad(set_to_none=True) + pred_probs = qcbm() + loss = mmd(pred_probs, target_probs) + loss.backward() + optimizer.step() + print(i, loss.item()) + + # Visualize the results + if args.plot: + with torch.no_grad(): + pred_probs = qcbm() + + plt.plot(x_input, target_probs, linestyle="-.", label=r"$\pi(x)$") + plt.bar(x_input, pred_probs, color="green", alpha=0.5, label="samples") + plt.xlabel("Samples") + plt.ylabel("Prob. Distribution") + + plt.legend() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/examples/amplitude_encoding_mnist/mnist_example.py b/examples/amplitude_encoding_mnist/mnist_example.py index ad92bb1f..b56efb83 100644 --- a/examples/amplitude_encoding_mnist/mnist_example.py +++ b/examples/amplitude_encoding_mnist/mnist_example.py @@ -100,10 +100,23 @@ def forward(self, x, use_qiskit=False): bsz = x.shape[0] x = F.avg_pool2d(x, 6).view(bsz, 16) + + print("Shape 1:") + print(self.q_device.states.shape) self.encoder(self.q_device, x) self.q_layer(self.q_device) + + + + print("X shape before measurement") + print(x.shape) + x = self.measure(self.q_device) + + print("X shape after measurement") + print(x.shape) + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() x = F.log_softmax(x, dim=1) diff --git a/examples/amplitude_encoding_mnist/mnist_example_noise.py b/examples/amplitude_encoding_mnist/mnist_example_noise.py new file mode 100644 index 00000000..0b07e237 --- /dev/null +++ b/examples/amplitude_encoding_mnist/mnist_example_noise.py @@ -0,0 +1,223 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse + +import torchquantum as tq +import torchquantum.functional as tqf + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + + +class QFCModel(tq.QuantumModule): + class QLayer(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.random_layer = tq.RandomLayer( + n_ops=50, wires=list(range(self.n_wires)) + ) + + # gates with trainable parameters + self.rx0 = tq.RX(has_params=True, trainable=True) + self.ry0 = tq.RY(has_params=True, trainable=True) + self.rz0 = tq.RZ(has_params=True, trainable=True) + self.crx0 = tq.CRX(has_params=True, trainable=True) + + @tq.static_support + def forward(self, q_device: tq.NoiseDevice): + """ + 1. To convert tq QuantumModule to qiskit or run in the static + model, need to: + (1) add @tq.static_support before the forward + (2) make sure to add + static=self.static_mode and + parent_graph=self.graph + to all the tqf functions, such as tqf.hadamard below + """ + self.q_device = q_device + + self.random_layer(self.q_device) + + # some trainable gates (instantiated ahead of time) + self.rx0(self.q_device, wires=0) + self.ry0(self.q_device, wires=1) + self.rz0(self.q_device, wires=3) + self.crx0(self.q_device, wires=[0, 2]) + + # add some more non-parameterized gates (add on-the-fly) + tqf.hadamard( + self.q_device, wires=3, static=self.static_mode, parent_graph=self.graph + ) + tqf.sx( + self.q_device, wires=2, static=self.static_mode, parent_graph=self.graph + ) + tqf.cnot( + self.q_device, + wires=[3, 0], + static=self.static_mode, + parent_graph=self.graph, + ) + + def __init__(self): + super().__init__() + self.n_wires = 4 + self.q_device = tq.NoiseDevice(n_wires=self.n_wires, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08}) + ) + self.encoder = tq.AmplitudeEncoder() + + self.q_layer = self.QLayer() + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + bsz = x.shape[0] + x = F.avg_pool2d(x, 6).view(bsz, 16) + self.encoder(self.q_device, x) + self.q_layer(self.q_device) + x = self.measure(self.q_device) + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() + x = F.log_softmax(x, dim=1) + return x + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--static", action="store_true", help="compute with " "static mode" + ) + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--wires-per-block", type=int, default=2, help="wires per block int static mode" + ) + parser.add_argument( + "--epochs", type=int, default=5, help="number of training epochs" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[3, 6], + n_test_samples=75, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=256, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QFCModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + if args.static: + # optionally to switch to the static mode, which can bring speedup + # on training + model.q_layer.static_on(wires_per_block=args.wires_per_block) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + valid_test(dataflow, "valid", model, device) + scheduler.step() + + # test + valid_test(dataflow, "test", model, device, qiskit=False) + + +if __name__ == "__main__": + main() diff --git a/examples/amplitude_encoding_mnist/mnist_new.py b/examples/amplitude_encoding_mnist/mnist_new.py index 491a1e20..9ce0bd42 100644 --- a/examples/amplitude_encoding_mnist/mnist_new.py +++ b/examples/amplitude_encoding_mnist/mnist_new.py @@ -171,3 +171,4 @@ def train_tq(model, device, train_dl, epochs, loss_fn, optimizer): print("--Training--") train_losses = train_tq(model, device, train_dl, 1, loss_fn, optimizer) + diff --git a/examples/amplitude_encoding_mnist/mnist_new_noise.py b/examples/amplitude_encoding_mnist/mnist_new_noise.py new file mode 100644 index 00000000..b15ae417 --- /dev/null +++ b/examples/amplitude_encoding_mnist/mnist_new_noise.py @@ -0,0 +1,175 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +""" +author: Vivek Yanamadula @Vivekyy +""" + +import torch +import torch.nn.functional as F + +import torchquantum as tq + +from torchquantum.dataset import MNIST +from torchquantum.operator import op_name_dict +from typing import List + + +class TQNet(tq.QuantumModule): + def __init__(self, layers: List[tq.QuantumModule], encoder=None, use_softmax=False): + super().__init__() + + self.encoder = encoder + self.use_softmax = use_softmax + + self.layers = tq.QuantumModuleList() + + for layer in layers: + self.layers.append(layer) + + self.service = "TorchQuantum" + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, device, x): + bsz = x.shape[0] + device.reset_states(bsz) + + x = F.avg_pool2d(x, 6) + x = x.view(bsz, 16) + + if self.encoder: + self.encoder(device, x) + + for layer in self.layers: + layer(device) + + meas = self.measure(device) + + if self.use_softmax: + meas = F.log_softmax(meas, dim=1) + + return meas + + +class TQLayer(tq.QuantumModule): + def __init__(self, gates: List[tq.QuantumModule]): + super().__init__() + + self.service = "TorchQuantum" + + self.layer = tq.QuantumModuleList() + for gate in gates: + self.layer.append(gate) + + @tq.static_support + def forward(self, q_device): + for gate in self.layer: + gate(q_device) + + +def train_tq(model, device, train_dl, epochs, loss_fn, optimizer): + losses = [] + for epoch in range(epochs): + running_loss = 0.0 + batches = 0 + for batch_dict in train_dl: + x = batch_dict["image"] + y = batch_dict["digit"] + + y = y.to(torch.long) + + x = x.to(torch_device) + y = y.to(torch_device) + + optimizer.zero_grad() + + preds = model(device, x) + + loss = loss_fn(preds, y) + loss.backward() + + optimizer.step() + + running_loss += loss.item() + batches += 1 + + print(f"Epoch {epoch + 1} | Loss: {running_loss/batches}", end="\r") + + print(f"Epoch {epoch + 1} | Loss: {running_loss/batches}") + losses.append(running_loss / batches) + + return losses + + +torch_device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# encoder = None +# encoder = tq.AmplitudeEncoder() +encoder = tq.MultiPhaseEncoder(["u3", "u3", "u3", "u3"]) + + +random_layer = tq.RandomLayer(n_ops=50, wires=list(range(4))) +trainable_layer = [ + op_name_dict["rx"](trainable=True, has_params=True, wires=[0]), + op_name_dict["ry"](trainable=True, has_params=True, wires=[1]), + op_name_dict["rz"](trainable=True, has_params=True, wires=[3]), + op_name_dict["crx"](trainable=True, has_params=True, wires=[0, 2]), +] +trainable_layer = TQLayer(trainable_layer) +layers = [random_layer, trainable_layer] + +device = tq.NoiseDevice(n_wires=4, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08})).to(torch_device) + +model = TQNet(layers=layers, encoder=encoder, use_softmax=True).to(torch_device) + +loss_fn = F.nll_loss +optimizer = torch.optim.SGD(model.parameters(), lr=0.05) + +dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[0, 1, 3, 6], + n_test_samples=200, +) + +train_dl = torch.utils.data.DataLoader( + dataset["train"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["train"]), +) +val_dl = torch.utils.data.DataLoader( + dataset["valid"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["valid"]), +) +test_dl = torch.utils.data.DataLoader( + dataset["test"], + batch_size=32, + sampler=torch.utils.data.RandomSampler(dataset["test"]), +) + +print("--Training--") +train_losses = train_tq(model, device, train_dl, 1, loss_fn, optimizer) + diff --git a/examples/clifford_qnn/mnist_clifford_qnn_noise.py b/examples/clifford_qnn/mnist_clifford_qnn_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/mnist/mnist_noise.py b/examples/mnist/mnist_noise.py new file mode 100644 index 00000000..252f25d0 --- /dev/null +++ b/examples/mnist/mnist_noise.py @@ -0,0 +1,263 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse +import random +import numpy as np + +import torchquantum as tq +from torchquantum.plugin import ( + tq2qiskit_measurement, + qiskit_assemble_circs, + op_history2qiskit, + op_history2qiskit_expand_params, +) + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + +import pickle + + +class QFCModel(tq.QuantumModule): + class QLayer(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.random_layer = tq.RandomLayer( + n_ops=50, wires=list(range(self.n_wires)) + ) + + # gates with trainable parameters + self.rx0 = tq.RX(has_params=True, trainable=True) + self.ry0 = tq.RY(has_params=True, trainable=True) + self.rz0 = tq.RZ(has_params=True, trainable=True) + self.crx0 = tq.CRX(has_params=True, trainable=True) + + def forward(self, qdev: tq.NoiseDevice): + self.random_layer(qdev) + + # some trainable gates (instantiated ahead of time) + self.rx0(qdev, wires=0) + self.ry0(qdev, wires=1) + self.rz0(qdev, wires=3) + self.crx0(qdev, wires=[0, 2]) + + # add some more non-parameterized gates (add on-the-fly) + qdev.h(wires=3) # type: ignore + qdev.sx(wires=2) # type: ignore + qdev.cnot(wires=[3, 0]) # type: ignore + qdev.rx( + wires=1, + params=torch.tensor([0.1]), + static=self.static_mode, + parent_graph=self.graph, + ) # type: ignore + + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder(tq.encoder_op_list_name_dict["4x4_u3_h_rx"]) + + self.q_layer = self.QLayer() + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=x.shape[0], device=x.device, record_op=True, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.08, "Phaseflip": 0.08}), + ) + + bsz = x.shape[0] + x = F.avg_pool2d(x, 6).view(bsz, 16) + devi = x.device + + if use_qiskit: + # use qiskit to process the circuit + # create the qiskit circuit for encoder + self.encoder(qdev, x) + op_history_parameterized = qdev.op_history + qdev.reset_op_history() + encoder_circs = op_history2qiskit_expand_params(self.n_wires, op_history_parameterized, bsz=bsz) + + # create the qiskit circuit for trainable quantum layers + self.q_layer(qdev) + op_history_fixed = qdev.op_history + qdev.reset_op_history() + q_layer_circ = op_history2qiskit(self.n_wires, op_history_fixed) + + # create the qiskit circuit for measurement + measurement_circ = tq2qiskit_measurement(qdev, self.measure) + + # assemble the encoder, trainable quantum layers, and measurement circuits + assembled_circs = qiskit_assemble_circs( + encoder_circs, q_layer_circ, measurement_circ + ) + + # call the qiskit processor to process the circuit + x0 = self.qiskit_processor.process_ready_circs(qdev, assembled_circs).to( # type: ignore + devi + ) + x = x0 + + else: + # use torchquantum to process the circuit + self.encoder(qdev, x) + qdev.reset_op_history() + self.q_layer(qdev) + x = self.measure(qdev) + + x = x.reshape(bsz, 2, 2).sum(-1).squeeze() + x = F.log_softmax(x, dim=1) + + return x + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + return accuracy, loss + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--static", action="store_true", help="compute with " "static mode" + ) + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--wires-per-block", type=int, default=20, help="wires per block int static mode" + ) + parser.add_argument( + "--epochs", type=int, default=20, help="number of training epochs" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + digits_of_interest=[3, 6], + n_test_samples=75, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=256, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QFCModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + accuracy_list = [] + loss_list = [] + + if args.static: + # optionally to switch to the static mode, which can bring speedup + # on training + model.q_layer.static_on(wires_per_block=args.wires_per_block) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + + # valid + accuracy, loss = valid_test(dataflow, "valid", model, device) + + accuracy_list.append(accuracy) + loss_list.append(loss) + + scheduler.step() + + with open('C:/Users/yezhu/OneDrive/Desktop/torchquantum/noisy_training_3.pickle', 'wb') as handle: + pickle.dump([accuracy_list, loss_list], handle, protocol=pickle.HIGHEST_PROTOCOL) + # test + + valid_test(dataflow, "test", model, device, qiskit=False) + + +if __name__ == "__main__": + main() diff --git a/examples/param_shift_onchip_training/param_shift_noise.py b/examples/param_shift_onchip_training/param_shift_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/qaoa/max_cut_backprop_noise.py b/examples/qaoa/max_cut_backprop_noise.py new file mode 100644 index 00000000..5ab4a4dd --- /dev/null +++ b/examples/qaoa/max_cut_backprop_noise.py @@ -0,0 +1,203 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torchquantum as tq + +import random +import numpy as np + +from torchquantum.functional import mat_dict + +from torchquantum.measurement import expval_joint_analytical_density + +seed = 0 +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) + + +class MAXCUT(tq.QuantumModule): + """computes the optimal cut for a given graph. + outputs: the most probable bitstring decides the set {0 or 1} each + node belongs to. + """ + + def __init__(self, n_wires, input_graph, n_layers): + super().__init__() + + self.n_wires = n_wires + + self.input_graph = input_graph # list of edges + self.n_layers = n_layers + + self.betas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + self.gammas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + + def mixer(self, qdev, beta): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + mixer = exp(-i * beta * sigma_x) + """ + for wire in range(self.n_wires): + qdev.rx( + wires=wire, + params=beta.unsqueeze(0), + ) # type: ignore + + def entangler(self, qdev, gamma): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + entangler = exp(-i * gamma * (1 - sigma_z * sigma_z)/2) + """ + for edge in self.input_graph: + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + qdev.rz( + wires=edge[1], + params=gamma.unsqueeze(0), + ) # type: ignore + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + + def edge_to_PauliString(self, edge): + # construct pauli string + pauli_string = "" + for wire in range(self.n_wires): + if wire in edge: + pauli_string += "Z" + else: + pauli_string += "I" + return pauli_string + + def circuit(self, qdev): + """ + execute the quantum circuit + """ + # print(self.betas, self.gammas) + for wire in range(self.n_wires): + qdev.h( + wires=wire, + ) # type: ignore + + for i in range(self.n_layers): + self.mixer(qdev, self.betas[i]) + self.entangler(qdev, self.gammas[i]) + + def forward(self, measure_all=False): + """ + Apply the QAOA ansatz and only measure the edge qubit on z-basis. + Args: + if edge is None + """ + qdev = tq.NoiseDevice( + n_wires=self.n_wires, device=self.betas.device, record_op=False, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.12, "Phaseflip": 0.12}) + ) + + self.circuit(qdev) + + # turn on the record_op above to print the circuit + # print(op_history2qiskit(self.n_wires, qdev.op_history)) + + # print(tq.measure(qdev, n_shots=1024)) + # compute the expectation value + # print(qdev.get_states_1d()) + if measure_all is False: + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + expv = expval_joint_analytical_density(qdev, observable=pauli_string) + expVal += 0.5 * expv + # print(pauli_string, expv) + # print(expVal) + return expVal + else: + return tq.measure_density(qdev, n_shots=1024, draw_id=0) + + +def backprop_optimize(model, n_steps=100, lr=0.1): + """ + Optimize the QAOA ansatz over the parameters gamma and beta + Args: + betas (np.array): A list of beta parameters. + gammas (np.array): A list of gamma parameters. + n_steps (int): The number of steps to optimize, defaults to 10. + lr (float): The learning rate, defaults to 0.1. + """ + # measure all edges in the input_graph + optimizer = torch.optim.Adam(model.parameters(), lr=lr) + print( + "The initial parameters are betas = {} and gammas = {}".format( + *model.parameters() + ) + ) + # optimize the parameters and return the optimal values + for step in range(n_steps): + optimizer.zero_grad() + loss = model() + loss.backward() + optimizer.step() + if step % 2 == 0: + print("Step: {}, Cost Objective: {}".format(step, loss.item())) + + print( + "The optimal parameters are betas = {} and gammas = {}".format( + *model.parameters() + ) + ) + return model(measure_all=True) + + +def main(): + # create a input_graph + input_graph = [(0, 1), (0, 3), (1, 2), (2, 3)] + n_wires = 4 + n_layers = 3 + model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) + # model.to("cuda") + # model.to(torch.device("cuda")) + # circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model) + # print(circ) + # print("The circuit is", circ.draw(output="mpl")) + # circ.draw(output="mpl") + # use backprop + backprop_optimize(model, n_steps=300, lr=0.01) + # use parameter shift rule + # param_shift_optimize(model, n_steps=500, step_size=100000) + + +""" +Notes: +1. input_graph = [(0, 1), (3, 0), (1, 2), (2, 3)], mixer 1st & entangler 2nd, n_layers >= 2, answer is correct. + +""" + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + + main() diff --git a/examples/qaoa/max_cut_parametershift_noise.py b/examples/qaoa/max_cut_parametershift_noise.py new file mode 100644 index 00000000..11fd79e2 --- /dev/null +++ b/examples/qaoa/max_cut_parametershift_noise.py @@ -0,0 +1,305 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torchquantum as tq + +import random +import numpy as np + +from torchquantum.measurement import expval_joint_analytical_density + +seed = 0 +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) + +from torchquantum.plugin import QiskitProcessor, op_history2qiskit + + +class MAXCUT(tq.QuantumModule): + """computes the optimal cut for a given graph. + outputs: the most probable bitstring decides the set {0 or 1} each + node belongs to. + """ + + def __init__(self, n_wires, input_graph, n_layers): + super().__init__() + + self.n_wires = n_wires + + self.input_graph = input_graph # list of edges + self.n_layers = n_layers + self.n_edges = len(input_graph) + + self.betas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + self.gammas = torch.nn.Parameter(0.01 * torch.rand(self.n_layers)) + + self.reset_shift_param() + + def mixer(self, qdev, beta, layer_id): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + mixer = exp(-i * beta * sigma_x) + """ + + for wire in range(self.n_wires): + if ( + self.shift_param_name == "beta" + and self.shift_wire == wire + and layer_id == self.shift_layer + ): + degree = self.shift_degree + else: + degree = 0 + qdev.rx( + wires=wire, + params=(beta.unsqueeze(0) + degree), + ) # type: ignore + + def entangler(self, qdev, gamma, layer_id): + """ + Apply the single rotation and entangling layer of the QAOA ansatz. + entangler = exp(-i * gamma * (1 - sigma_z * sigma_z)/2) + """ + for edge_id, edge in enumerate(self.input_graph): + if ( + self.shift_param_name == "gamma" + and edge_id == self.shift_edge_id + and layer_id == self.shift_layer + ): + degree = self.shift_degree + else: + degree = 0 + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + qdev.rz( + wires=edge[1], + params=(gamma.unsqueeze(0) + degree), + ) # type: ignore + qdev.cx( + [edge[0], edge[1]], + ) # type: ignore + + def set_shift_param(self, layer, wire, param_name, degree, edge_id): + """ + set the shift parameter for the parameter shift rule + """ + self.shift_layer = layer + self.shift_wire = wire + self.shift_param_name = param_name + self.shift_degree = degree + self.shift_edge_id = edge_id + + def reset_shift_param(self): + """ + reset the shift parameter + """ + self.shift_layer = None + self.shift_wire = None + self.shift_param_name = None + self.shift_degree = None + self.shift_edge_id = None + + def edge_to_PauliString(self, edge): + # construct pauli string + pauli_string = "" + for wire in range(self.n_wires): + if wire in edge: + pauli_string += "Z" + else: + pauli_string += "I" + return pauli_string + + def circuit(self, qdev): + """ + execute the quantum circuit + """ + # print(self.betas, self.gammas) + for wire in range(self.n_wires): + qdev.h( + wires=wire, + ) # type: ignore + + for i in range(self.n_layers): + self.mixer(qdev, self.betas[i], i) + self.entangler(qdev, self.gammas[i], i) + + def forward(self, use_qiskit, measure_all=False): + """ + Apply the QAOA ansatz and only measure the edge qubit on z-basis. + Args: + if edge is None + """ + qdev = tq.NoiseDevice(n_wires=self.n_wires, device=self.betas.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.12, "Phaseflip": 0.12})) + + + # print(tq.measure(qdev, n_shots=1024)) + # compute the expectation value + # print(qdev.get_states_1d()) + + if not use_qiskit: + self.circuit(qdev) + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + expv = expval_joint_analytical_density(qdev, observable=pauli_string) + expVal += 0.5 * expv + else: + # use qiskit to compute the expectation value + expVal = 0 + for edge in self.input_graph: + pauli_string = self.edge_to_PauliString(edge) + + with torch.no_grad(): + self.circuit(qdev) + circ = op_history2qiskit(qdev.n_wires, qdev.op_history) + + expv = self.qiskit_processor.process_circs_get_joint_expval([circ], pauli_string)[0] + expVal += 0.5 * expv + expVal = torch.Tensor([expVal]) + return expVal + + +def shift_and_run(model, use_qiskit): + # flatten the parameters into 1D array + + grad_betas = [] + grad_gammas = [] + n_layers = model.n_layers + n_wires = model.n_wires + n_edges = model.n_edges + + for i in range(n_layers): + grad_gamma = 0 + for k in range(n_edges): + model.set_shift_param(i, None, "gamma", np.pi * 0.5, k) + out1 = model(use_qiskit) + model.reset_shift_param() + + model.set_shift_param(i, None, "gamma", -np.pi * 0.5, k) + out2 = model(use_qiskit) + model.reset_shift_param() + + grad_gamma += 0.5 * (out1 - out2).squeeze().item() + grad_gammas.append(grad_gamma) + + grad_beta = 0 + for j in range(n_wires): + model.set_shift_param(i, j, "beta", np.pi * 0.5, None) + out1 = model(use_qiskit) + model.reset_shift_param() + + model.set_shift_param(i, j, "beta", -np.pi * 0.5, None) + out2 = model(use_qiskit) + model.reset_shift_param() + + grad_beta += 0.5 * (out1 - out2).squeeze().item() + grad_betas.append(grad_beta) + + return model(use_qiskit), [grad_betas, grad_gammas] + + +def param_shift_optimize(model, n_steps=10, step_size=0.1, use_qiskit=False): + """finds the optimal cut where parameter shift rule is used to compute the gradient""" + # optimize the parameters and return the optimal values + # print( + # "The initial parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + n_layers = model.n_layers + for step in range(n_steps): + with torch.no_grad(): + loss, grad_list = shift_and_run(model, use_qiskit=use_qiskit) + # param_list = list(model.parameters()) + # print( + # "The initial parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + # param_list = torch.cat([param.flatten() for param in param_list]) + + # print("The shape of the params", len(param_list), param_list[0].shape, param_list) + # print("") + # print("The shape of the grad_list = {}, 0th elem shape = {}, grad_list = {}".format(len(grad_list), grad_list[0].shape, grad_list)) + # print(grad_list, loss, model.betas, model.gammas) + print(loss) + with torch.no_grad(): + for i in range(n_layers): + model.betas[i].copy_(model.betas[i] - step_size * grad_list[0][i]) + model.gammas[i].copy_(model.gammas[i] - step_size * grad_list[1][i]) + + # for param, grad in zip(param_list, grad_list): + # modify the parameters and ensure that there are no multiple views + # param.copy_(param - step_size * grad) + # if step % 5 == 0: + # print("Step: {}, Cost Objective: {}".format(step, loss.item())) + + # print( + # "The updated parameters are betas = {} and gammas = {}".format( + # *model.parameters() + # ) + # ) + return model(use_qiskit=False, measure_all=True) + + +""" +Notes: +1. input_graph = [(0, 1), (3, 0), (1, 2), (2, 3)], mixer 1st & entangler 2nd, n_layers >= 2, answer is correct. + +""" + + +def main(use_qiskit): + # create a input_graph + input_graph = [(0, 1), (0, 3), (1, 2), (2, 3)] + n_wires = 4 + n_layers = 1 + model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) + + # set the qiskit processor + # processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) + # model.set_qiskit_processor(processor_simulation) + + # firstly perform simulate + # model.to("cuda") + # model.to(torch.device("cuda")) + # circ = tq2qiskit(tq.QuantumDevice(n_wires=4), model) + # print(circ) + # print("The circuit is", circ.draw(output="mpl")) + # circ.draw(output="mpl") + # use backprop + # backprop_optimize(model, n_steps=300, lr=0.01) + # use parameter shift rule + param_shift_optimize(model, n_steps=500, step_size=0.01, use_qiskit=use_qiskit) + + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + use_qiskit = False + main(use_qiskit) diff --git a/examples/qaoa/max_cut_paramshift.py b/examples/qaoa/max_cut_paramshift.py index a8467c1d..48b79a44 100644 --- a/examples/qaoa/max_cut_paramshift.py +++ b/examples/qaoa/max_cut_paramshift.py @@ -148,7 +148,7 @@ def circuit(self, qdev): self.mixer(qdev, self.betas[i], i) self.entangler(qdev, self.gammas[i], i) - def forward(self, use_qiskit): + def forward(self, use_qiskit, measure_all=False): """ Apply the QAOA ansatz and only measure the edge qubit on z-basis. Args: @@ -266,7 +266,7 @@ def param_shift_optimize(model, n_steps=10, step_size=0.1, use_qiskit=False): # *model.parameters() # ) # ) - return model(measure_all=True) + return model(use_qiskit=False,measure_all=True) """ @@ -284,8 +284,8 @@ def main(use_qiskit): model = MAXCUT(n_wires=n_wires, input_graph=input_graph, n_layers=n_layers) # set the qiskit processor - processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) - model.set_qiskit_processor(processor_simulation) + #processor_simulation = QiskitProcessor(use_real_qc=False, n_shots=10000) + #model.set_qiskit_processor(processor_simulation) # firstly perform simulate # model.to("cuda") diff --git a/examples/quantum_lstm/qlstm_noise.py b/examples/quantum_lstm/qlstm_noise.py new file mode 100644 index 00000000..1587b545 --- /dev/null +++ b/examples/quantum_lstm/qlstm_noise.py @@ -0,0 +1,423 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import torch +import torch.nn as nn +import torchquantum as tq +import torchquantum.functional as tqf + + +class QLSTM(nn.Module): + # use 'qiskit.ibmq' instead to run on hardware + class QLayer_forget(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_input(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model = tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_update(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + class QLayer_output(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [{'input_idx': [0], 'func': 'rx', 'wires': [0]}, + {'input_idx': [1], 'func': 'rx', 'wires': [1]}, + {'input_idx': [2], 'func': 'rx', 'wires': [2]}, + {'input_idx': [3], 'func': 'rx', 'wires': [3]}, + ]) + self.rx0 = tq.RX(has_params=True, trainable=True) + self.rx1 = tq.RX(has_params=True, trainable=True) + self.rx2 = tq.RX(has_params=True, trainable=True) + self.rx3 = tq.RX(has_params=True, trainable=True) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x): + qdev = tq.NoiseDevice(n_wires=self.n_wires, bsz=x.shape[0], device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + self.encoder(qdev, x) + self.rx0(qdev, wires=0) + self.rx1(qdev, wires=1) + self.rx2(qdev, wires=2) + self.rx3(qdev, wires=3) + for k in range(self.n_wires): + if k == self.n_wires - 1: + tqf.cnot(qdev, wires=[k, 0]) + else: + tqf.cnot(qdev, wires=[k, k + 1]) + return (self.measure(qdev)) + + def __init__(self, + input_size, + hidden_size, + n_qubits=4, + n_qlayers=1, + batch_first=True, + return_sequences=False, + return_state=False, + backend="default.qubit"): + super(QLSTM, self).__init__() + self.n_inputs = input_size + self.hidden_size = hidden_size + self.concat_size = self.n_inputs + self.hidden_size + self.n_qubits = n_qubits + self.n_qlayers = n_qlayers + self.backend = backend # "default.qubit", "qiskit.basicaer", "qiskit.ibm" + + self.batch_first = batch_first + self.return_sequences = return_sequences + self.return_state = return_state + + self.clayer_in = torch.nn.Linear(self.concat_size, n_qubits) + self.VQC = { + 'forget': self.QLayer_forget(), + 'input': self.QLayer_input(), + 'update': self.QLayer_update(), + 'output': self.QLayer_output() + } + self.clayer_out = torch.nn.Linear(self.n_qubits, self.hidden_size) + # self.clayer_out = [torch.nn.Linear(n_qubits, self.hidden_size) for _ in range(4)] + + def forward(self, x, init_states=None): + ''' + x.shape is (batch_size, seq_length, feature_size) + recurrent_activation -> sigmoid + activation -> tanh + ''' + if self.batch_first is True: + batch_size, seq_length, features_size = x.size() + else: + seq_length, batch_size, features_size = x.size() + + hidden_seq = [] + if init_states is None: + h_t = torch.zeros(batch_size, self.hidden_size) # hidden state (output) + c_t = torch.zeros(batch_size, self.hidden_size) # cell state + else: + # for now we ignore the fact that in PyTorch you can stack multiple RNNs + # so we take only the first elements of the init_states tuple init_states[0][0], init_states[1][0] + h_t, c_t = init_states + h_t = h_t[0] + c_t = c_t[0] + + for t in range(seq_length): + # get features from the t-th element in seq, for all entries in the batch + x_t = x[:, t, :] + + # Concatenate input and hidden state + v_t = torch.cat((h_t, x_t), dim=1) + + # match qubit dimension + y_t = self.clayer_in(v_t) + + f_t = torch.sigmoid(self.clayer_out(self.VQC['forget'](y_t))) # forget block + i_t = torch.sigmoid(self.clayer_out(self.VQC['input'](y_t))) # input block + g_t = torch.tanh(self.clayer_out(self.VQC['update'](y_t))) # update block + o_t = torch.sigmoid(self.clayer_out(self.VQC['output'](y_t))) # output block + + c_t = (f_t * c_t) + (i_t * g_t) + h_t = o_t * torch.tanh(c_t) + + hidden_seq.append(h_t.unsqueeze(0)) + hidden_seq = torch.cat(hidden_seq, dim=0) + hidden_seq = hidden_seq.transpose(0, 1).contiguous() + return hidden_seq, (h_t, c_t) + + +def prepare_sequence(seq, to_ix): + idxs = [to_ix[w] for w in seq] + return torch.tensor(idxs, dtype=torch.long) + + +class LSTMTagger(nn.Module): + def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size, n_qubits=0): + super(LSTMTagger, self).__init__() + self.hidden_dim = hidden_dim + + self.word_embeddings = nn.Embedding(vocab_size, embedding_dim) + + # The LSTM takes word embeddings as inputs, and outputs hidden states + # with dimensionality hidden_dim. + if n_qubits > 0: + print("Tagger will use Quantum LSTM") + self.lstm = QLSTM(embedding_dim, hidden_dim, n_qubits=n_qubits) + else: + print("Tagger will use Classical LSTM") + self.lstm = nn.LSTM(embedding_dim, hidden_dim) + + # The linear layer that maps from hidden state space to tag space + self.hidden2tag = nn.Linear(hidden_dim, tagset_size) + + def forward(self, sentence): + embeds = self.word_embeddings(sentence) + lstm_out, _ = self.lstm(embeds.view(len(sentence), 1, -1)) + tag_logits = self.hidden2tag(lstm_out.view(len(sentence), -1)) + tag_scores = F.log_softmax(tag_logits, dim=1) + return tag_scores + + +def train(model, n_epochs, training_data, word_to_ix, tag_to_ix): + loss_function = nn.NLLLoss() + optimizer = optim.SGD(model.parameters(), lr=0.1) + + history = { + 'loss': [], + 'acc': [] + } + for epoch in range(n_epochs): + losses = [] + preds = [] + targets = [] + for sentence, tags in training_data: + # Step 1. Remember that Pytorch accumulates gradients. + # We need to clear them out before each instance + model.zero_grad() + + # Step 2. Get our inputs ready for the network, that is, turn them into + # Tensors of word indices. + sentence_in = prepare_sequence(sentence, word_to_ix) + labels = prepare_sequence(tags, tag_to_ix) + + # Step 3. Run our forward pass. + tag_scores = model(sentence_in) + + # Step 4. Compute the loss, gradients, and update the parameters by + # calling optimizer.step() + loss = loss_function(tag_scores, labels) + loss.backward() + optimizer.step() + losses.append(float(loss)) + + probs = torch.softmax(tag_scores, dim=-1) + preds.append(probs.argmax(dim=-1)) + targets.append(labels) + + avg_loss = np.mean(losses) + history['loss'].append(avg_loss) + + preds = torch.cat(preds) + targets = torch.cat(targets) + corrects = (preds == targets) + accuracy = corrects.sum().float() / float(targets.size(0)) + history['acc'].append(accuracy) + + print(f"Epoch {epoch + 1} / {n_epochs}: Loss = {avg_loss:.3f} Acc = {accuracy:.2f}") + + return history + + +def print_result(model, training_data, word_to_ix, ix_to_tag): + with torch.no_grad(): + input_sentence = training_data[0][0] + labels = training_data[0][1] + inputs = prepare_sequence(input_sentence, word_to_ix) + tag_scores = model(inputs) + + tag_ids = torch.argmax(tag_scores, dim=1).numpy() + tag_labels = [ix_to_tag[k] for k in tag_ids] + print(f"Sentence: {input_sentence}") + print(f"Labels: {labels}") + print(f"Predicted: {tag_labels}") + + +from matplotlib import pyplot as plt + + +def plot_history(history_classical, history_quantum): + loss_c = history_classical['loss'] + acc_c = history_classical['acc'] + loss_q = history_quantum['loss'] + acc_q = history_quantum['acc'] + n_epochs = max([len(loss_c), len(loss_q)]) + x_epochs = [i for i in range(n_epochs)] + + fig, ax1 = plt.subplots() + + ax1.set_xlabel("Epoch") + ax1.set_ylabel("Loss") + ax1.plot(loss_c, label="Classical LSTM loss", color='orange', linestyle='dashed') + ax1.plot(loss_q, label="Quantum LSTM loss", color='red', linestyle='solid') + + ax2 = ax1.twinx() + ax2.set_ylabel("Accuracy") + ax2.plot(acc_c, label="Classical LSTM accuracy", color='steelblue', linestyle='dashed') + ax2.plot(acc_q, label="Quantum LSTM accuracy", color='blue', linestyle='solid') + + plt.title("Part-of-Speech Tagger Training__torch") + plt.ylim(0., 1.1) + # plt.legend(loc="upper right") + fig.legend(loc="upper right", bbox_to_anchor=(1, 0.8), bbox_transform=ax1.transAxes) + + plt.savefig("pos_training_torch.pdf") + plt.savefig("pos_training_torch.png") + + plt.show() + + +def main(): + tag_to_ix = {"DET": 0, "NN": 1, "V": 2} # Assign each tag with a unique index + ix_to_tag = {i: k for k, i in tag_to_ix.items()} + + training_data = [ + # Tags are: DET - determiner; NN - noun; V - verb + # For example, the word "The" is a determiner + ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]), + ("Everybody read that book".split(), ["NN", "V", "DET", "NN"]) + ] + word_to_ix = {} + + # For each words-list (sentence) and tags-list in each tuple of training_data + for sent, tags in training_data: + for word in sent: + if word not in word_to_ix: # word has not been assigned an index yet + word_to_ix[word] = len(word_to_ix) # Assign each word with a unique index + + print(f"Vocabulary: {word_to_ix}") + print(f"Entities: {ix_to_tag}") + + embedding_dim = 8 + hidden_dim = 6 + n_epochs = 300 + + model_classical = LSTMTagger(embedding_dim, + hidden_dim, + vocab_size=len(word_to_ix), + tagset_size=len(tag_to_ix), + n_qubits=0) + + history_classical = train(model_classical, n_epochs, training_data, word_to_ix, tag_to_ix) + + print_result(model_classical, training_data, word_to_ix, ix_to_tag) + + n_qubits = 4 + + model_quantum = LSTMTagger(embedding_dim, + hidden_dim, + vocab_size=len(word_to_ix), + tagset_size=len(tag_to_ix), + n_qubits=n_qubits) + + history_quantum = train(model_quantum, n_epochs, training_data, word_to_ix, tag_to_ix) + + print_result(model_quantum, training_data, word_to_ix, ix_to_tag) + + plot_history(history_classical, history_quantum) + + +if __name__ == "__main__": + # import pdb + # pdb.set_trace() + + main() diff --git a/examples/quantumnat/quantumnat_noise.py b/examples/quantumnat/quantumnat_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/quanvolution/quanvolution_noise.py b/examples/quanvolution/quanvolution_noise.py new file mode 100644 index 00000000..33a329a1 --- /dev/null +++ b/examples/quanvolution/quanvolution_noise.py @@ -0,0 +1,250 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torchquantum as tq +import torchquantum.functional as tqf + +import torch +import torch.nn.functional as F +import torch.optim as optim +import numpy as np +import random + +from torchquantum.dataset import MNIST +from torch.optim.lr_scheduler import CosineAnnealingLR + + +class QuanvolutionFilter(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 4 + self.encoder = tq.GeneralEncoder( + [ + {"input_idx": [0], "func": "ry", "wires": [0]}, + {"input_idx": [1], "func": "ry", "wires": [1]}, + {"input_idx": [2], "func": "ry", "wires": [2]}, + {"input_idx": [3], "func": "ry", "wires": [3]}, + ] + ) + + self.q_layer = tq.RandomLayer(n_ops=8, wires=list(range(self.n_wires))) + self.measure = tq.MeasureAll_density(tq.PauliZ) + + def forward(self, x, use_qiskit=False): + bsz = x.shape[0] + qdev = tq.NoiseDevice(self.n_wires, bsz=bsz, device=x.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + size = 28 + x = x.view(bsz, size, size) + + data_list = [] + + for c in range(0, size, 2): + for r in range(0, size, 2): + data = torch.transpose( + torch.cat( + (x[:, c, r], x[:, c, r + 1], x[:, c + 1, r], x[:, c + 1, r + 1]) + ).view(4, bsz), + 0, + 1, + ) + if use_qiskit: + data = self.qiskit_processor.process_parameterized( + qdev, self.encoder, self.q_layer, self.measure, data + ) + else: + self.encoder(qdev, data) + self.q_layer(qdev) + data = self.measure(qdev) + + data_list.append(data.view(bsz, 4)) + + result = torch.cat(data_list, dim=1).float() + + return result + + +class HybridModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.qf = QuanvolutionFilter() + self.linear = torch.nn.Linear(4 * 14 * 14, 10) + + def forward(self, x, use_qiskit=False): + with torch.no_grad(): + x = self.qf(x, use_qiskit) + x = self.linear(x) + return F.log_softmax(x, -1) + + +class HybridModel_without_qf(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(28 * 28, 10) + + def forward(self, x, use_qiskit=False): + x = x.view(-1, 28 * 28) + x = self.linear(x) + return F.log_softmax(x, -1) + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs) + loss = F.nll_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}", end="\r") + + +def valid_test(dataflow, split, model, device, qiskit=False): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["image"].to(device) + targets = feed_dict["digit"].to(device) + + outputs = model(inputs, use_qiskit=qiskit) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + _, indices = output_all.topk(1, dim=1) + masks = indices.eq(target_all.view(-1, 1).expand_as(indices)) + size = target_all.shape[0] + corrects = masks.sum().item() + accuracy = corrects / size + loss = F.nll_loss(output_all, target_all).item() + + print(f"{split} set accuracy: {accuracy}") + print(f"{split} set loss: {loss}") + + return accuracy, loss + + +def main(): + train_model_without_qf = True + n_epochs = 15 + + random.seed(42) + np.random.seed(42) + torch.manual_seed(42) + dataset = MNIST( + root="./mnist_data", + train_valid_split_ratio=[0.9, 0.1], + n_test_samples=300, + n_train_samples=500, + ) + dataflow = dict() + + for split in dataset: + sampler = torch.utils.data.RandomSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=10, + sampler=sampler, + num_workers=8, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + model = HybridModel().to(device) + model_without_qf = HybridModel_without_qf().to(device) + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + accu_list1 = [] + loss_list1 = [] + accu_list2 = [] + loss_list2 = [] + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + accu, loss = valid_test( + dataflow, + "test", + model, + device, + ) + accu_list1.append(accu) + loss_list1.append(loss) + scheduler.step() + + if train_model_without_qf: + optimizer = optim.Adam( + model_without_qf.parameters(), lr=5e-3, weight_decay=1e-4 + ) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}:") + train(dataflow, model_without_qf, device, optimizer) + print(optimizer.param_groups[0]["lr"]) + + # valid + accu, loss = valid_test(dataflow, "test", model_without_qf, device) + accu_list2.append(accu) + loss_list2.append(loss) + + scheduler.step() + + # run on real QC + try: + from qiskit import IBMQ + from torchquantum.plugin import QiskitProcessor + + # firstly perform simulate + print(f"\nTest with Qiskit Simulator") + processor_simulation = QiskitProcessor(use_real_qc=False) + model.qf.set_qiskit_processor(processor_simulation) + valid_test(dataflow, "test", model, device, qiskit=True) + # then try to run on REAL QC + backend_name = "ibmq_quito" + print(f"\nTest on Real Quantum Computer {backend_name}") + processor_real_qc = QiskitProcessor(use_real_qc=True, backend_name=backend_name) + model.qf.set_qiskit_processor(processor_real_qc) + valid_test(dataflow, "test", model, device, qiskit=True) + except ImportError: + print( + "Please install qiskit, create an IBM Q Experience Account and " + "save the account token according to the instruction at " + "'https://github.com/Qiskit/qiskit-ibmq-provider', " + "then try again." + ) + + +if __name__ == "__main__": + main() diff --git a/examples/quanvolution/quanvolution_trainable_quantum_layer_noise.py b/examples/quanvolution/quanvolution_trainable_quantum_layer_noise.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/qubit_rotation/qubit_rotation_noise.py b/examples/qubit_rotation/qubit_rotation_noise.py new file mode 100644 index 00000000..13a20293 --- /dev/null +++ b/examples/qubit_rotation/qubit_rotation_noise.py @@ -0,0 +1,69 @@ +""" +Qubit Rotation Optimization, adapted from https://pennylane.ai/qml/demos/tutorial_qubit_rotation +""" + +# import dependencies +import torchquantum as tq +import torch +from torchquantum.measurement import expval_joint_analytical_density + + +class OptimizationModel(torch.nn.Module): + """ + Circuit with rx and ry gate + """ + + def __init__(self): + super().__init__() + self.rx0 = tq.RX(has_params=True, trainable=True, init_params=0.011) + self.ry0 = tq.RY(has_params=True, trainable=True, init_params=0.012) + + def forward(self): + # create a quantum device to run the gates + qdev = tq.NoiseDevice(n_wires=1, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.01, "Phaseflip": 0.01})) + + # add some trainable gates (need to instantiate ahead of time) + self.rx0(qdev, wires=0) + self.ry0(qdev, wires=0) + + # return the analytic expval from Z + return expval_joint_analytical_density(qdev, "Z") + + +# train function to get expval as low as possible (ideally -1) +def train(model, device, optimizer): + outputs = model() + loss = outputs + optimizer.zero_grad() + loss.backward() + optimizer.step() + + return loss.item() + + +# main function to run the optimization +def main(): + seed = 0 + torch.manual_seed(seed) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = OptimizationModel() + n_epochs = 200 + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + for epoch in range(1, n_epochs + 1): + # train + loss = train(model, device, optimizer) + output = (model.rx0.params[0].item(), model.ry0.params[0].item()) + + print(f"Epoch {epoch}: {output}") + + if epoch % 10 == 0: + print(f"Loss after step {epoch}: {loss}") + + +if __name__ == "__main__": + main() diff --git a/examples/regression/run_regression_noise.py b/examples/regression/run_regression_noise.py new file mode 100644 index 00000000..3a146721 --- /dev/null +++ b/examples/regression/run_regression_noise.py @@ -0,0 +1,267 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.nn.functional as F +import torch.optim as optim +import argparse + +import torchquantum as tq + +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + +# data is cos(theta)|000> + e^(j * phi)sin(theta) |111> + +from torchpack.datasets.dataset import Dataset + + +def gen_data(L, N): + omega_0 = np.zeros([2 ** L], dtype="complex_") + omega_0[0] = 1 + 0j + + omega_1 = np.zeros([2 ** L], dtype="complex_") + omega_1[-1] = 1 + 0j + + states = np.zeros([N, 2 ** L], dtype="complex_") + + thetas = 2 * np.pi * np.random.rand(N) + phis = 2 * np.pi * np.random.rand(N) + + for i in range(N): + states[i] = ( + np.cos(thetas[i]) * omega_0 + + np.exp(1j * phis[i]) * np.sin(thetas[i]) * omega_1 + ) + + X = np.sin(2 * thetas) * np.cos(phis) + + return states, X + + +class RegressionDataset: + def __init__(self, split, n_samples, n_wires): + self.split = split + self.n_samples = n_samples + self.n_wires = n_wires + + self.states, self.Xlabel = gen_data(self.n_wires, self.n_samples) + + def __getitem__(self, index: int): + instance = {"states": self.states[index], "Xlabel": self.Xlabel[index]} + return instance + + def __len__(self) -> int: + return self.n_samples + + +class Regression(Dataset): + def __init__(self, n_train, n_valid, n_wires): + n_samples_dict = {"train": n_train, "valid": n_valid} + super().__init__( + { + split: RegressionDataset( + split=split, n_samples=n_samples_dict[split], n_wires=n_wires + ) + for split in ["train", "valid"] + } + ) + + +class QModel(tq.QuantumModule): + def __init__(self, n_wires, n_blocks, add_fc=False): + super().__init__() + # inside one block, we have one u3 layer one each qubit and one layer + # cu3 layer with ring connection + self.n_wires = n_wires + self.n_blocks = n_blocks + self.u3_layers = tq.QuantumModuleList() + self.cu3_layers = tq.QuantumModuleList() + for _ in range(n_blocks): + self.u3_layers.append( + tq.Op1QAllLayer( + op=tq.U3, + n_wires=n_wires, + has_params=True, + trainable=True, + ) + ) + self.cu3_layers.append( + tq.Op2QAllLayer( + op=tq.CU3, + n_wires=n_wires, + has_params=True, + trainable=True, + circular=True, + ) + ) + self.measure = tq.MeasureAll_density(tq.PauliZ) + self.add_fc = add_fc + if add_fc: + self.fc_layer = torch.nn.Linear(n_wires, 1) + + def forward(self, input_states): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=input_states.shape[0], device=input_states.device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22})) + # firstly set the qdev + bsz = input_states.shape[0] + input_states = torch.reshape(input_states, [bsz] + [2] * self.n_wires) + + qdev.clone_from_states(input_states) + for k in range(self.n_blocks): + self.u3_layers[k](qdev) + self.cu3_layers[k](qdev) + + res = self.measure(qdev) + if self.add_fc: + res = self.fc_layer(res) + else: + res = res[:, 1] + return res + + +def train(dataflow, model, device, optimizer): + for feed_dict in dataflow["train"]: + inputs = feed_dict["states"].to(device).to(torch.complex64) + targets = feed_dict["Xlabel"].to(device).to(torch.float) + + outputs = model(inputs) + + loss = F.mse_loss(outputs, targets) + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"loss: {loss.item()}") + + +def valid_test(dataflow, split, model, device): + target_all = [] + output_all = [] + with torch.no_grad(): + for feed_dict in dataflow[split]: + inputs = feed_dict["states"].to(device).to(torch.complex64) + targets = feed_dict["Xlabel"].to(device).to(torch.float) + + outputs = model(inputs) + + target_all.append(targets) + output_all.append(outputs) + target_all = torch.cat(target_all, dim=0) + output_all = torch.cat(output_all, dim=0) + + loss = F.mse_loss(output_all, target_all) + + print(f"{split} set loss: {loss}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--bsz", type=int, default=32, help="batch size for training and validation" + ) + parser.add_argument("--n_wires", type=int, default=3, help="number of qubits") + parser.add_argument( + "--n_blocks", + type=int, + default=2, + help="number of blocks, each contain one layer of " + "U3 gates and one layer of CU3 with " + "ring connections", + ) + parser.add_argument( + "--n_train", type=int, default=300, help="number of training samples" + ) + parser.add_argument( + "--n_valid", type=int, default=1000, help="number of validation samples" + ) + parser.add_argument( + "--epochs", type=int, default=100, help="number of training epochs" + ) + parser.add_argument( + "--addfc", action="store_true", help="add a final classical FC layer" + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + dataset = Regression( + n_train=args.n_train, + n_valid=args.n_valid, + n_wires=args.n_wires, + ) + + dataflow = dict() + + for split in dataset: + if split == "train": + sampler = torch.utils.data.RandomSampler(dataset[split]) + else: + sampler = torch.utils.data.SequentialSampler(dataset[split]) + dataflow[split] = torch.utils.data.DataLoader( + dataset[split], + batch_size=args.bsz, + sampler=sampler, + num_workers=1, + pin_memory=True, + ) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QModel(n_wires=args.n_wires, n_blocks=args.n_blocks, add_fc=args.addfc).to( + device + ) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(dataflow, model, device, optimizer) + + # valid + valid_test(dataflow, "valid", model, device) + scheduler.step() + + # final valid + valid_test(dataflow, "valid", model, device) + + +if __name__ == "__main__": + main() diff --git a/examples/train_unitary_prep/train_unitary_prep_noise.py b/examples/train_unitary_prep/train_unitary_prep_noise.py new file mode 100644 index 00000000..6f38ca42 --- /dev/null +++ b/examples/train_unitary_prep/train_unitary_prep_noise.py @@ -0,0 +1,118 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torch +import torch.optim as optim +import argparse + +import torchquantum as tq +from torch.optim.lr_scheduler import CosineAnnealingLR + +import random +import numpy as np + + +class QModel(tq.QuantumModule): + def __init__(self): + super().__init__() + self.n_wires = 2 + self.u3_0 = tq.U3(has_params=True, trainable=True) + self.u3_1 = tq.U3(has_params=True, trainable=True) + self.cu3_0 = tq.CU3(has_params=True, trainable=True) + self.cu3_1 = tq.CU3(has_params=True, trainable=True) + self.u3_2 = tq.U3(has_params=True, trainable=True) + self.u3_3 = tq.U3(has_params=True, trainable=True) + + def forward(self, q_device: tq.NoiseDevice): + self.u3_0(q_device, wires=0) + self.u3_1(q_device, wires=1) + self.cu3_0(q_device, wires=[0, 1]) + self.u3_2(q_device, wires=0) + self.u3_3(q_device, wires=1) + self.cu3_1(q_device, wires=[1, 0]) + + +def train(target_unitary, model, optimizer): + result_unitary = model.get_unitary() + + # https://link.aps.org/accepted/10.1103/PhysRevA.95.042318 unitary fidelity according to table 1 + + # compute the unitary infidelity + loss = 1 - (torch.trace(target_unitary.T.conj() @ result_unitary) / target_unitary.shape[0]).abs() ** 2 + + optimizer.zero_grad() + loss.backward() + optimizer.step() + print( + f"infidelity (loss): {loss.item()}, \n target unitary : " + f"{target_unitary.detach().cpu().numpy()}, \n " + f"result unitary : {result_unitary.detach().cpu().numpy()}\n" + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--epochs", type=int, default=1000, help="number of training epochs" + ) + + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + + args = parser.parse_args() + + if args.pdb: + import pdb + pdb.set_trace() + + seed = 42 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + + model = QModel().to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=1e-2, weight_decay=0) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + target_unitary = torch.tensor( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1j] + ] + , dtype=torch.complex64) + + for epoch in range(1, n_epochs + 1): + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(target_unitary, model, optimizer) + scheduler.step() + + +if __name__ == "__main__": + main() diff --git a/examples/vqe/vqe_noise.py b/examples/vqe/vqe_noise.py new file mode 100644 index 00000000..f7d89109 --- /dev/null +++ b/examples/vqe/vqe_noise.py @@ -0,0 +1,179 @@ +""" +MIT License + +Copyright (c) 2020-present TorchQuantum Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import torchquantum as tq +import torch +from torchquantum.util.vqe_utils import parse_hamiltonian_file +import random +import numpy as np +import argparse +import torch.optim as optim + +from torch.optim.lr_scheduler import CosineAnnealingLR +from torchquantum.measurement import expval_joint_analytical_density + + +class QVQEModel(tq.QuantumModule): + def __init__(self, arch, hamil_info): + super().__init__() + self.arch = arch + self.hamil_info = hamil_info + self.n_wires = hamil_info["n_wires"] + self.n_blocks = arch["n_blocks"] + self.u3_layers = tq.QuantumModuleList() + self.cu3_layers = tq.QuantumModuleList() + for _ in range(self.n_blocks): + self.u3_layers.append( + tq.Op1QAllLayer( + op=tq.U3, + n_wires=self.n_wires, + has_params=True, + trainable=True, + ) + ) + self.cu3_layers.append( + tq.Op2QAllLayer( + op=tq.CU3, + n_wires=self.n_wires, + has_params=True, + trainable=True, + circular=True, + ) + ) + + def forward(self): + qdev = tq.NoiseDevice( + n_wires=self.n_wires, bsz=1, device=next(self.parameters()).device, + noise_model=tq.NoiseModel(kraus_dict={"Bitflip": 0.22, "Phaseflip": 0.22}) + ) + + for k in range(self.n_blocks): + self.u3_layers[k](qdev) + self.cu3_layers[k](qdev) + + expval = 0 + for hamil in self.hamil_info["hamil_list"]: + expval += ( + expval_joint_analytical_density(qdev, observable=hamil["pauli_string"]) + * hamil["coeff"] + ) + + return expval + + +def train(model, optimizer, n_steps=1): + for _ in range(n_steps): + loss = model() + optimizer.zero_grad() + loss.backward() + optimizer.step() + print(f"Expectation of energy: {loss.item()}") + + +def valid_test(model): + with torch.no_grad(): + loss = model() + + print(f"validation: expectation of energy: {loss.item()}") + + +def process_hamil_info(hamil_info): + hamil_list = hamil_info["hamil_list"] + n_wires = hamil_info["n_wires"] + all_info = [] + + for hamil in hamil_list: + pauli_string = "" + for i in range(n_wires): + if i in hamil["wires"]: + wire = hamil["wires"].index(i) + pauli_string += hamil["observables"][wire].upper() + else: + pauli_string += "I" + all_info.append({"pauli_string": pauli_string, "coeff": hamil["coefficient"]}) + hamil_info["hamil_list"] = all_info + return hamil_info + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--pdb", action="store_true", help="debug with pdb") + parser.add_argument( + "--n_blocks", + type=int, + default=2, + help="number of blocks, each contain one layer of " + "U3 gates and one layer of CU3 with " + "ring connections", + ) + parser.add_argument( + "--steps_per_epoch", type=int, default=10, help="number of training epochs" + ) + parser.add_argument( + "--epochs", type=int, default=100, help="number of training epochs" + ) + parser.add_argument( + "--hamil_filename", + type=str, + default="h2.txt", + help="number of training epochs", + ) + + args = parser.parse_args() + + if args.pdb: + import pdb + + pdb.set_trace() + + seed = 0 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + hamil_info = process_hamil_info(parse_hamiltonian_file(args.hamil_filename)) + + use_cuda = torch.cuda.is_available() + device = torch.device("cuda" if use_cuda else "cpu") + model = QVQEModel(arch={"n_blocks": args.n_blocks}, hamil_info=hamil_info) + + model.to(device) + + n_epochs = args.epochs + optimizer = optim.Adam(model.parameters(), lr=5e-3, weight_decay=1e-4) + scheduler = CosineAnnealingLR(optimizer, T_max=n_epochs) + + for epoch in range(1, n_epochs + 1): + # train + print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']}") + train(model, optimizer, n_steps=args.steps_per_epoch) + + scheduler.step() + + # final valid + valid_test(model) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 8bf4d45c..fc2e954c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,10 @@ opt_einsum pathos>=0.2.7 pylatexenc>=2.10 pyscf>=2.0.1 -qiskit>=0.39.0,<1.0.0 +qiskit>=1.0.0 recommonmark -qiskit_ibm_runtime==0.20.0 -qiskit-aer==0.13.3 +qiskit-ibm-runtime>=0.20.0 +qiskit-aer>=0.13.3 scipy>=1.5.2 setuptools>=52.0.0 diff --git a/test/algorithm/test_hamiltonian.py b/test/algorithm/test_hamiltonian.py index e5e8a60f..4b93fe45 100644 --- a/test/algorithm/test_hamiltonian.py +++ b/test/algorithm/test_hamiltonian.py @@ -132,8 +132,13 @@ def test_hamiltonian(): ] ), ) + import os - hamil = Hamiltonian.from_file("test/algorithm/h2.txt") + current_dir = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(current_dir, '..', 'algorithm', 'h2.txt') + hamil = Hamiltonian.from_file(file_path) + + #hamil = Hamiltonian.from_file("./h2.txt") assert np.allclose( hamil.matrix.cpu().detach().numpy(), diff --git a/test/algorithm/test_qcbm.py b/test/algorithm/test_qcbm.py new file mode 100644 index 00000000..333a25bb --- /dev/null +++ b/test/algorithm/test_qcbm.py @@ -0,0 +1,31 @@ +from torchquantum.algorithm.qcbm import QCBM, MMDLoss +import torchquantum as tq +import torch + + +def test_qcbm_forward(): + n_wires = 3 + n_layers = 3 + ops = [] + for l in range(n_layers): + for q in range(n_wires): + ops.append({"name": "rx", "wires": q, "params": 0.0, "trainable": True}) + for q in range(n_wires - 1): + ops.append({"name": "cnot", "wires": [q, q + 1]}) + + data = torch.ones(2**n_wires) + qmodule = tq.QuantumModule.from_op_history(ops) + qcbm = QCBM(n_wires, qmodule) + probs = qcbm() + expected = torch.tensor([1.0, 0, 0, 0, 0, 0, 0, 0]) + assert torch.allclose(probs, expected) + + +def test_mmd_loss(): + n_wires = 2 + bandwidth = torch.tensor([0.1, 1.0]) + space = torch.arange(2**n_wires) + + mmd = MMDLoss(bandwidth, space) + loss = mmd(torch.zeros(4), torch.zeros(4)) + assert torch.isclose(loss, torch.tensor(0.0), rtol=1e-5) diff --git a/test/functional/test_controlled_unitary.py b/test/functional/test_controlled_unitary.py index 652ece59..d7e78660 100644 --- a/test/functional/test_controlled_unitary.py +++ b/test/functional/test_controlled_unitary.py @@ -22,10 +22,12 @@ SOFTWARE. """ -import torchquantum as tq from test.utils import check_all_close + import numpy as np +import torchquantum as tq + def test_controlled_unitary(): state = tq.QuantumDevice(n_wires=2) diff --git a/test/functional/test_func_mat_exp.py b/test/functional/test_func_mat_exp.py index e2a2c293..ad8e17c1 100644 --- a/test/functional/test_func_mat_exp.py +++ b/test/functional/test_func_mat_exp.py @@ -22,9 +22,10 @@ SOFTWARE. """ +import numpy as np import torch + import torchquantum as tq -import numpy as np def test_func_mat_exp(): diff --git a/test/hadamard_grad/test_hadamard_grad.py b/test/hadamard_grad/test_hadamard_grad.py index 62fdb21e..2eb387b8 100644 --- a/test/hadamard_grad/test_hadamard_grad.py +++ b/test/hadamard_grad/test_hadamard_grad.py @@ -1,7 +1,9 @@ import numpy as np +import pytest + from examples.hadamard_grad.circ import Circ1, Circ2, Circ3 from examples.hadamard_grad.hadamard_grad import hadamard_grad -import pytest + @pytest.mark.skip def test_hadamard_grad(): @@ -38,4 +40,4 @@ def test_hadamard_grad(): if __name__ == "__main__": - test_hadamard_grad() \ No newline at end of file + test_hadamard_grad() diff --git a/test/layers/test_nlocal.py b/test/layers/test_nlocal.py index 62387190..83bd1f6e 100644 --- a/test/layers/test_nlocal.py +++ b/test/layers/test_nlocal.py @@ -1,12 +1,13 @@ -import torchquantum as tq from qiskit.circuit.library import ( - TwoLocal, EfficientSU2, ExcitationPreserving, PauliTwoDesign, RealAmplitudes, + TwoLocal, ) +import torchquantum as tq + def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): """ @@ -16,8 +17,8 @@ def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): qiskit_ops = [] for bit in qiskit_circuit.decompose(): wires = [] - for qu in bit.qubits: - wires.append(qu.index) + for qb in bit.qubits: + wires.append(qiskit_circuit.find_bit(qb).index) qiskit_ops.append( { "name": bit.operation.name, @@ -29,9 +30,9 @@ def compare_tq_to_qiskit(tq_circuit, qiskit_circuit, instance_info=""): tq_ops = [ { "name": op["name"], - "wires": (op["wires"],) - if isinstance(op["wires"], int) - else tuple(op["wires"]), + "wires": ( + (op["wires"],) if isinstance(op["wires"], int) else tuple(op["wires"]) + ), } for op in tq_circuit.op_history ] diff --git a/test/layers/test_rotgate.py b/test/layers/test_rotgate.py index 30f24b8a..593563c7 100644 --- a/test/layers/test_rotgate.py +++ b/test/layers/test_rotgate.py @@ -1,14 +1,10 @@ -import torchquantum as tq -import qiskit -from qiskit import Aer, execute - -from torchquantum.util import ( - switch_little_big_endian_matrix, - find_global_phase, -) - -from qiskit.circuit.library import GR, GRX, GRY, GRZ import numpy as np +from qiskit import transpile +from qiskit.circuit.library import GR, GRX, GRY, GRZ +from qiskit_aer import AerSimulator + +import torchquantum as tq +from torchquantum.util import find_global_phase, switch_little_big_endian_matrix all_pairs = [ {"qiskit": GR, "tq": tq.layer.GlobalR, "params": 2}, @@ -19,6 +15,7 @@ ITERATIONS = 2 + def test_rotgates(): # test each pair for pair in all_pairs: @@ -28,15 +25,18 @@ def test_rotgates(): for _ in range(ITERATIONS): # generate random parameters params = [ - np.random.uniform(-2 * np.pi, 2 * np.pi) for _ in range(pair["params"]) + np.random.uniform(-2 * np.pi, 2 * np.pi) + for _ in range(pair["params"]) ] # create the qiskit circuit qiskit_circuit = pair["qiskit"](num_wires, *params) # get the unitary from qiskit - backend = Aer.get_backend("unitary_simulator") - result = execute(qiskit_circuit, backend).result() + backend = AerSimulator(method="unitary") + qiskit_circuit = transpile(qiskit_circuit, backend) + qiskit_circuit.save_unitary() + result = backend.run(qiskit_circuit).result() unitary_qiskit = result.get_unitary(qiskit_circuit) # create tq circuit diff --git a/test/measurement/test_eval_observable.py b/test/measurement/test_eval_observable.py index 58245ee0..499c2ad1 100644 --- a/test/measurement/test_eval_observable.py +++ b/test/measurement/test_eval_observable.py @@ -22,19 +22,21 @@ SOFTWARE. """ -from qiskit import QuantumCircuit -import numpy as np import random -from qiskit.opflow import StateFn, X, Y, Z, I -import torchquantum as tq +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Pauli, Statevector +import torchquantum as tq from torchquantum.measurement import expval_joint_analytical, expval_joint_sampling from torchquantum.plugin import op_history2qiskit from torchquantum.util import switch_little_big_endian_state -import torch - +X = Pauli("X") +Y = Pauli("Y") +Z = Pauli("Z") +I = Pauli("I") pauli_str_op_dict = { "X": X, "Y": Y, @@ -67,20 +69,19 @@ def test_expval_observable(): for ob in obs[1:]: # note here the order is reversed because qiskit is in little endian operator = pauli_str_op_dict[ob] ^ operator - psi = StateFn(qiskit_circ) - psi_evaled = psi.eval()._primitive._data + psi = Statevector(qiskit_circ) state_tq = switch_little_big_endian_state( qdev.get_states_1d().detach().numpy() )[0] - assert np.allclose(psi_evaled, state_tq, atol=1e-5) + assert np.allclose(psi.data, state_tq, atol=1e-5) - expval_qiskit = (~psi @ operator @ psi).eval().real + expval_qiskit = psi.expectation_value(operator).real # print(expval_tq, expval_qiskit) assert np.isclose(expval_tq, expval_qiskit, atol=1e-5) if ( n_wires <= 3 ): # if too many wires, the stochastic method is not accurate due to limited shots - assert np.isclose(expval_tq_sampling, expval_qiskit, atol=1e-2) + assert np.isclose(expval_tq_sampling, expval_qiskit, atol=0.015) print("expval observable test passed") @@ -92,25 +93,25 @@ def util0(): qc.x(0) operator = Z ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) # result: 1.0, means measurement result is 0, so Z is on qubit 1 operator = I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) # result: -1.0 means measurement result is 1, so Z is on qubit 0 operator = I ^ I - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) operator = Z ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) qc = QuantumCircuit(3) @@ -118,8 +119,8 @@ def util0(): qc.x(0) operator = I ^ I ^ Z - psi = StateFn(qc) - expectation_value = (~psi @ operator @ psi).eval() + psi = Statevector(qc) + expectation_value = psi.expectation_value(operator) print(expectation_value.real) diff --git a/test/measurement/test_expval_joint_sampling_grouping.py b/test/measurement/test_expval_joint_sampling_grouping.py index 09492458..8a759518 100644 --- a/test/measurement/test_expval_joint_sampling_grouping.py +++ b/test/measurement/test_expval_joint_sampling_grouping.py @@ -22,15 +22,16 @@ SOFTWARE. """ +import random + +import numpy as np + import torchquantum as tq from torchquantum.measurement import ( expval_joint_analytical, expval_joint_sampling_grouping, ) -import numpy as np -import random - def test_expval_joint_sampling_grouping(): n_obs = 20 @@ -54,7 +55,7 @@ def test_expval_joint_sampling_grouping(): ) for obs in obs_all: # assert - assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=1e-2) + assert np.isclose(expval_ana[obs], expval_sam[obs][0].item(), atol=0.015) print(obs, expval_ana[obs], expval_sam[obs][0].item()) diff --git a/test/measurement/test_measure.py b/test/measurement/test_measure.py index 38c45df6..5fafa180 100644 --- a/test/measurement/test_measure.py +++ b/test/measurement/test_measure.py @@ -22,11 +22,12 @@ SOFTWARE. """ -import torchquantum as tq +import numpy as np +from qiskit import transpile +from qiskit_aer import AerSimulator +import torchquantum as tq from torchquantum.plugin import op_history2qiskit -from qiskit import Aer, transpile -import numpy as np def test_measure(): @@ -42,7 +43,7 @@ def test_measure(): circ = op_history2qiskit(qdev.n_wires, qdev.op_history) circ.measure_all() - simulator = Aer.get_backend("aer_simulator") + simulator = AerSimulator() circ = transpile(circ, simulator) qiskit_res = simulator.run(circ, shots=n_shots).result() qiskit_counts = qiskit_res.get_counts() diff --git a/test/operator/test_ControlledU.py b/test/operator/test_ControlledU.py index 5bc01096..e80dee1d 100644 --- a/test/operator/test_ControlledU.py +++ b/test/operator/test_ControlledU.py @@ -25,14 +25,13 @@ # test the controlled unitary function -import torchquantum as tq -import torchquantum.functional as tqf from test.utils import check_all_close # import pdb # pdb.set_trace() import numpy as np +import torchquantum as tq flag = 4 diff --git a/test/plugin/test_qiskit2tq_op_history.py b/test/plugin/test_qiskit2tq_op_history.py index 67a67e80..b94fb7fd 100644 --- a/test/plugin/test_qiskit2tq_op_history.py +++ b/test/plugin/test_qiskit2tq_op_history.py @@ -22,11 +22,11 @@ SOFTWARE. """ -from torchquantum.plugin import qiskit2tq_op_history -import torchquantum as tq -from qiskit.circuit.random import random_circuit from qiskit import QuantumCircuit +import torchquantum as tq +from torchquantum.plugin import qiskit2tq_op_history + def test_qiskit2tp_op_history(): circ = QuantumCircuit(3, 3) diff --git a/test/plugin/test_qiskit_plugins.py b/test/plugin/test_qiskit_plugins.py index 684dbfc6..76d0a4db 100644 --- a/test/plugin/test_qiskit_plugins.py +++ b/test/plugin/test_qiskit_plugins.py @@ -22,26 +22,24 @@ SOFTWARE. """ -from qiskit import QuantumCircuit -import numpy as np import random -from qiskit.opflow import StateFn, X, Y, Z, I -import torchquantum as tq +import numpy as np +import pytest +from qiskit.quantum_info import Pauli, Statevector -from torchquantum.plugin import op_history2qiskit, QiskitProcessor +import torchquantum as tq +from torchquantum.plugin import QiskitProcessor, op_history2qiskit from torchquantum.util import switch_little_big_endian_state -import torch -import pytest - pauli_str_op_dict = { - "X": X, - "Y": Y, - "Z": Z, - "I": I, + "X": Pauli("X"), + "Y": Pauli("Y"), + "Z": Pauli("Z"), + "I": Pauli("I"), } + @pytest.mark.skip def test_expval_observable(): # seed = 0 @@ -67,19 +65,18 @@ def test_expval_observable(): for ob in obs[1:]: # note here the order is reversed because qiskit is in little endian operator = pauli_str_op_dict[ob] ^ operator - psi = StateFn(qiskit_circ) - psi_evaled = psi.eval()._primitive._data + psi = Statevector(qiskit_circ) state_tq = switch_little_big_endian_state( qdev.get_states_1d().detach().numpy() )[0] - assert np.allclose(psi_evaled, state_tq, atol=1e-5) + assert np.allclose(psi.data, state_tq, atol=1e-5) - expval_qiskit = (~psi @ operator @ psi).eval().real + expval_qiskit = psi.expectation_value(operator).real # print(expval_qiskit_processor, expval_qiskit) if ( n_wires <= 3 ): # if too many wires, the stochastic method is not accurate due to limited shots - assert np.isclose(expval_qiskit_processor, expval_qiskit, atol=1e-2) + assert np.isclose(expval_qiskit_processor, expval_qiskit, atol=0.015) print("expval observable test passed") diff --git a/test/qiskit_plugin_test.py b/test/qiskit_plugin_test.py index d8b7e94b..a5aed71a 100644 --- a/test/qiskit_plugin_test.py +++ b/test/qiskit_plugin_test.py @@ -24,21 +24,22 @@ import argparse import pdb -import torch -import torchquantum as tq -import numpy as np +from test.static_mode_test import QLayer as AllRandomLayer -from qiskit import Aer, execute +import numpy as np +import torch +from qiskit_aer import AerSimulator from torchpack.utils.logging import logger + +import torchquantum as tq +from torchquantum.macro import F_DTYPE +from torchquantum.plugin import tq2qiskit from torchquantum.util import ( + find_global_phase, + get_expectations_from_counts, switch_little_big_endian_matrix, switch_little_big_endian_state, - get_expectations_from_counts, - find_global_phase, ) -from test.static_mode_test import QLayer as AllRandomLayer -from torchquantum.plugin import tq2qiskit -from torchquantum.macro import F_DTYPE def unitary_tq_vs_qiskit_test(): @@ -59,8 +60,9 @@ def unitary_tq_vs_qiskit_test(): # qiskit circ = tq2qiskit(q_layer, x) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circ, simulator).result() + simulator = AerSimulator(method="unitary") + circ.save_unitary() + result = simulator.run(circ).result() unitary_qiskit = result.get_unitary(circ) stable_threshold = 1e-5 @@ -115,10 +117,11 @@ def state_tq_vs_qiskit_test(): # qiskit circ = tq2qiskit(q_layer, x) # Select the StatevectorSimulator from the Aer provider - simulator = Aer.get_backend("statevector_simulator") + simulator = AerSimulator(method="statevector") + circ.save_statevector() # Execute and get counts - result = execute(circ, simulator).result() + result = simulator.run(circ).result() state_qiskit = result.get_statevector(circ) stable_threshold = 1e-5 @@ -175,11 +178,10 @@ def measurement_tq_vs_qiskit_test(): circ = tq2qiskit(q_layer, x) circ.measure(list(range(n_wires)), list(range(n_wires))) - # Select the QasmSimulator from the Aer provider - simulator = Aer.get_backend("qasm_simulator") + simulator = AerSimulator() # Execute and get counts - result = execute(circ, simulator, shots=1000000).result() + result = simulator.run(circ, shots=1000000).result() counts = result.get_counts(circ) measured_qiskit = get_expectations_from_counts(counts, n_wires=n_wires) diff --git a/torchquantum/algorithm/__init__.py b/torchquantum/algorithm/__init__.py index 7dfb672a..c7413a2e 100644 --- a/torchquantum/algorithm/__init__.py +++ b/torchquantum/algorithm/__init__.py @@ -22,7 +22,8 @@ SOFTWARE. """ -from .vqe import * -from .hamiltonian import * -from .qft import * -from .grover import * +from .vqe import VQE +from .hamiltonian import Hamiltonian +from .qft import QFT +from .grover import Grover +from .qcbm import QCBM, MMDLoss diff --git a/torchquantum/algorithm/qcbm.py b/torchquantum/algorithm/qcbm.py new file mode 100644 index 00000000..35a6fb75 --- /dev/null +++ b/torchquantum/algorithm/qcbm.py @@ -0,0 +1,96 @@ +import torch +import torch.nn as nn + +import torchquantum as tq + +__all__ = ["QCBM", "MMDLoss"] + + +class MMDLoss(nn.Module): + """Squared maximum mean discrepancy with radial basis function kerne""" + + def __init__(self, scales, space): + """ + Initialize MMDLoss object. Calculates and stores the kernel matrix. + + Args: + scales: Bandwidth parameters. + space: Basis input space. + """ + super().__init__() + + gammas = 1 / (2 * (scales**2)) + + # squared Euclidean distance + sq_dists = torch.abs(space[:, None] - space[None, :]) ** 2 + + # Kernel matrix + self.K = sum(torch.exp(-gamma * sq_dists) for gamma in gammas) / len(scales) + self.scales = scales + + def k_expval(self, px, py): + """ + Kernel expectation value + + Args: + px: First probability distribution + py: Second probability distribution + + Returns: + Expectation value of the RBF Kernel. + """ + + return px @ self.K @ py + + def forward(self, px, py): + """ + Squared MMD loss. + + Args: + px: First probability distribution + py: Second probability distribution + + Returns: + Squared MMD loss. + """ + pxy = px - py + return self.k_expval(pxy, pxy) + + +class QCBM(nn.Module): + """ + Quantum Circuit Born Machine (QCBM) + + Attributes: + ansatz: An Ansatz object + n_wires: Number of wires in the ansatz used. + + Methods: + __init__: Initialize the QCBM object. + forward: Returns the probability distribution (output from measurement). + """ + + def __init__(self, n_wires, ansatz): + """ + Initialize QCBM object + + Args: + ansatz (Ansatz): An Ansatz object + n_wires (int): Number of wires in the ansatz used. + """ + super().__init__() + + self.ansatz = ansatz + self.n_wires = n_wires + + def forward(self): + """ + Execute and obtain the probability distribution + + Returns: + Probabilities (torch.Tensor) + """ + qdev = tq.QuantumDevice(n_wires=self.n_wires, bsz=1, device="cpu") + self.ansatz(qdev) + probs = torch.abs(qdev.states.flatten()) ** 2 + return probs diff --git a/torchquantum/density/density_func.py b/torchquantum/density/density_func.py index fb15bf3d..5bde99a8 100644 --- a/torchquantum/density/density_func.py +++ b/torchquantum/density/density_func.py @@ -217,7 +217,7 @@ def apply_unitary_density_bmm(density, mat, wires): permute_to_dag = permute_to_dag + devices_dims_dag permute_back_dag = list(np.argsort(permute_to_dag)) original_shape = new_density.shape - permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[0]]) + permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[-1]]) if len(matdag.shape) > 2: # both matrix and state are in batch mode diff --git a/torchquantum/density/density_mat.py b/torchquantum/density/density_mat.py index 8260a01b..1bf406ea 100644 --- a/torchquantum/density/density_mat.py +++ b/torchquantum/density/density_mat.py @@ -126,6 +126,12 @@ def print_2d(self, index): _matrix = torch.reshape(self._matrix[index], [2 ** self.n_wires] * 2) print(_matrix) + + def get_2d_matrix(self, index): + _matrix = torch.reshape(self._matrix[index], [2 ** self.n_wires] * 2) + return _matrix + + def trace(self, index): """Calculate and return the trace of the density matrix at the given index. diff --git a/torchquantum/device/noisedevices.py b/torchquantum/device/noisedevices.py index 3da88eff..dded7a4d 100644 --- a/torchquantum/device/noisedevices.py +++ b/torchquantum/device/noisedevices.py @@ -30,17 +30,37 @@ from torchquantum.functional import func_name_dict, func_name_dict_collect from typing import Union -__all__ = ["NoiseDevice"] +__all__ = ["NoiseDevice", "NoiseModel"] + + +class NoiseModel: + '' + + def __init__(self, + kraus_dict + ): + """A quantum noise model + Args: + kraus_dict: the karus_dict for this noise_model. + For example: + kraus_dict={"Bitflip":0.5, "Phaseflip":0.5} + """ + self._kraus_dict = kraus_dict + # TODO: Check that the trace is preserved + + def kraus_dict(self): + return self._kraus_dict class NoiseDevice(nn.Module): def __init__( - self, - n_wires: int, - device_name: str = "noisedevice", - bsz: int = 1, - device: Union[torch.device, str] = "cpu", - record_op: bool = False, + self, + n_wires: int, + device_name: str = "noisedevice", + bsz: int = 1, + device: Union[torch.device, str] = "cpu", + record_op: bool = False, + noise_model: NoiseModel = NoiseModel(kraus_dict={"Bitflip": 0, "Phaseflip": 0}) ): """A quantum device that support the density matrix simulation Args: @@ -73,6 +93,50 @@ def __init__( self.record_op = record_op self.op_history = [] + self._noise_model = noise_model + + def reset_op_history(self): + """Resets the all Operation of the quantum device""" + self.op_history = [] + + def print_2d(self, index): + """Print the matrix value at the given index. + + This method prints the matrix value of `matrix[index]`. It reshapes the value into a 2D matrix + using the `torch.reshape` function and then prints it. + + Args: + index (int): The index of the matrix value to print. + + Examples: + >>> device = QuantumDevice(n_wires=2) + >>> device.matrix = torch.tensor([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + >>> device.print_2d(1) + tensor([[0, 0], + [0, 1]]) + + """ + + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + print(_matrix) + + def get_2d_matrix(self, index): + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + return _matrix + + def get_densities_2d(self): + """Return the states in a 1d tensor.""" + bsz = self.densities.shape[0] + return torch.reshape(self.densities, [bsz, 2 ** self.n_wires, 2 ** self.n_wires]) + + def get_density_2d(self): + """Return the state in a 1d tensor.""" + return torch.reshape(self.density, [2 ** self.n_wires, 2 ** self.n_wires]) + + def calc_trace(self, index): + _matrix = torch.reshape(self.densities[index], [2 ** self.n_wires] * 2) + return torch.trace(_matrix) + @property def name(self): """Return the name of the device.""" @@ -81,21 +145,41 @@ def name(self): def __repr__(self): return f" class: {self.name} \n device name: {self.device_name} \n number of qubits: {self.n_wires} \n batch size: {self.bsz} \n current computing device: {self.density.device} \n recording op history: {self.record_op} \n current states: {repr(self.get_probs_1d().cpu().detach().numpy())}" - ''' Get the probability of measuring each state to a one dimension tensor ''' + def get_probs_1d(self): """Return the states in a 1d tensor.""" bsz = self.densities.shape[0] - densities2d=torch.reshape(self.densities, [bsz, 2**self.n_wires,2**self.n_wires]) - return torch.diagonal(densities2d, offset=0, dim1=1, dim2=2) + densities2d = torch.reshape(self.densities, [bsz, 2 ** self.n_wires, 2 ** self.n_wires]) + return torch.abs(torch.diagonal(densities2d, offset=0, dim1=1, dim2=2)) def get_prob_1d(self): """Return the state in a 1d tensor.""" - density2d=torch.reshape(self.density, [2**self.n_wires,2**self.n_wires]) - return torch.diagonal(density2d, offset=0, dim1=0, dim2=1) + density2d = torch.reshape(self.density, [2 ** self.n_wires, 2 ** self.n_wires]) + return torch.abs(torch.diagonal(density2d, offset=0, dim1=0, dim2=1)) + + def clone_densities(self, existing_densities: torch.Tensor): + """Clone the densities of the other quantum device.""" + self.densities = existing_densities.clone() + + def clone_from_states(self, existing_states: torch.Tensor): + """Clone the densities of the other quantum device using the conjugate transpose.""" + # Ensure the dimensions match the expected shape for the outer product operation + assert 2 * (existing_states.dim() - 1) == (self.densities.dim() - 1) + #assert existing_states.shape[0] == self.densities.shape[0] + bsz = existing_states.shape[0] + state_dim = 2 ** self.n_wires + states_reshaped = existing_states.view(-1, state_dim, 1) # [batch_size, state_dim, 1] + states_conj_transpose = torch.conj(states_reshaped).transpose(1, 2) # [batch_size, 1, state_dim] + # Use torch.bmm for batched outer product + self.densities = torch.bmm(states_reshaped, states_conj_transpose) + self.densities = torch.reshape(self.densities, [bsz] + [2] * (2 * self.n_wires)) + + def noise_model(self): + return self._noise_model for func_name, func in func_name_dict.items(): diff --git a/torchquantum/encoding/encodings.py b/torchquantum/encoding/encodings.py index f8d2056d..d6463fc8 100644 --- a/torchquantum/encoding/encodings.py +++ b/torchquantum/encoding/encodings.py @@ -39,6 +39,7 @@ class Encoder(tq.QuantumModule): - forward(qdev: tq.QuantumDevice, x): Performs the encoding using a quantum device. """ + def __init__(self): super().__init__() pass @@ -133,6 +134,7 @@ def to_qiskit(self, n_wires, x): class PhaseEncoder(Encoder, metaclass=ABCMeta): """PhaseEncoder is a subclass of Encoder and represents a phase encoder. It applies a specified quantum function to encode input data using a quantum device.""" + def __init__(self, func): super().__init__() self.func = func @@ -163,6 +165,7 @@ def forward(self, qdev: tq.QuantumDevice, x): class MultiPhaseEncoder(Encoder, metaclass=ABCMeta): """PhaseEncoder is a subclass of Encoder and represents a phase encoder. It applies a specified quantum function to encode input data using a quantum device.""" + def __init__(self, funcs, wires=None): super().__init__() self.funcs = funcs if isinstance(funcs, Iterable) else [funcs] @@ -198,7 +201,7 @@ def forward(self, qdev: tq.QuantumDevice, x): func_name_dict[func]( qdev, wires=self.wires[k], - params=x[:, x_id : (x_id + stride)], + params=x[:, x_id: (x_id + stride)], static=self.static_mode, parent_graph=self.graph, ) @@ -208,6 +211,7 @@ def forward(self, qdev: tq.QuantumDevice, x): class StateEncoder(Encoder, metaclass=ABCMeta): """StateEncoder is a subclass of Encoder and represents a state encoder. It encodes the input data into the state vector of a quantum device.""" + def __init__(self): super().__init__() @@ -230,19 +234,24 @@ def forward(self, qdev: tq.QuantumDevice, x): ( x, torch.zeros( - x.shape[0], 2**qdev.n_wires - x.shape[1], device=x.device + x.shape[0], 2 ** qdev.n_wires - x.shape[1], device=x.device ), ), dim=-1, ) state = state.view([x.shape[0]] + [2] * qdev.n_wires) - qdev.states = state.type(C_DTYPE) + #TODO: Change to united format + if qdev.device_name == "noisedevice": + qdev.clone_from_states(state.type(C_DTYPE)) + else: + qdev.states = state.type(C_DTYPE) class MagnitudeEncoder(Encoder, metaclass=ABCMeta): """MagnitudeEncoder is a subclass of Encoder and represents a magnitude encoder. It encodes the input data by considering the magnitudes of the elements.""" + def __init__(self): super().__init__() diff --git a/torchquantum/functional/func_controlled_unitary.py b/torchquantum/functional/func_controlled_unitary.py index dc909815..f5d745c0 100644 --- a/torchquantum/functional/func_controlled_unitary.py +++ b/torchquantum/functional/func_controlled_unitary.py @@ -24,8 +24,9 @@ import numpy as np import torch + from torchquantum.functional.gate_wrapper import gate_wrapper -from torchquantum.macro import * +from torchquantum.macro import C_DTYPE def controlled_unitary( @@ -97,7 +98,7 @@ def controlled_unitary( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.tensor(torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE)) + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE) for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j diff --git a/torchquantum/functional/gate_wrapper.py b/torchquantum/functional/gate_wrapper.py index f1383f2f..cab7379f 100644 --- a/torchquantum/functional/gate_wrapper.py +++ b/torchquantum/functional/gate_wrapper.py @@ -1,16 +1,13 @@ import functools -import torch -import numpy as np +from typing import TYPE_CHECKING, Callable -from typing import Callable, Union, Optional, List, Dict, TYPE_CHECKING -from ..macro import C_DTYPE, F_DTYPE, ABC, ABC_ARRAY, INV_SQRT2 -from ..util.utils import pauli_eigs, diag -from torchpack.utils.logging import logger -from torchquantum.util import normalize_statevector +import numpy as np +import torch +from ..macro import ABC, ABC_ARRAY, C_DTYPE, F_DTYPE if TYPE_CHECKING: - from torchquantum.device import QuantumDevice, NoiseDevice + from torchquantum.device import QuantumDevice else: QuantumDevice = None @@ -59,7 +56,7 @@ def apply_unitary_einsum(state, mat, wires): # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] + new_indices = ABC[total_wires : total_wires + len(device_wires)] # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -181,16 +178,13 @@ def apply_unitary_density_einsum(density, mat, wires): # Tensor indices of the quantum state density_indices = ABC[:total_wires] - print("density_indices", density_indices) # Indices of the quantum state affected by this operation affected_indices = "".join(ABC_ARRAY[list(device_wires)].tolist()) - print("affected_indices", affected_indices) # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] - print("new_indices", new_indices) + new_indices = ABC[total_wires : total_wires + len(device_wires)] # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -199,7 +193,6 @@ def apply_unitary_density_einsum(density, mat, wires): zip(affected_indices, new_indices), density_indices, ) - print("new_density_indices", new_density_indices) # Use the last literal as the indice of batch density_indices = ABC[-1] + density_indices @@ -212,29 +205,24 @@ def apply_unitary_density_einsum(density, mat, wires): einsum_indices = ( f"{new_indices}{affected_indices}," f"{density_indices}->{new_density_indices}" ) - print("einsum_indices", einsum_indices) new_density = torch.einsum(einsum_indices, mat, density) - """ + r""" Compute U \rho U^\dagger """ - print("dagger") # Tensor indices of the quantum state density_indices = ABC[:total_wires] - print("density_indices", density_indices) # Indices of the quantum state affected by this operation affected_indices = "".join( ABC_ARRAY[[x + n_qubit for x in list(device_wires)]].tolist() ) - print("affected_indices", affected_indices) # All affected indices will be summed over, so we need the same number # of new indices - new_indices = ABC[total_wires: total_wires + len(device_wires)] - print("new_indices", new_indices) + new_indices = ABC[total_wires : total_wires + len(device_wires)] # The new indices of the state are given by the old ones with the # affected indices replaced by the new_indices @@ -243,7 +231,6 @@ def apply_unitary_density_einsum(density, mat, wires): zip(affected_indices, new_indices), density_indices, ) - print("new_density_indices", new_density_indices) density_indices = ABC[-1] + density_indices new_density_indices = ABC[-1] + new_density_indices @@ -255,7 +242,6 @@ def apply_unitary_density_einsum(density, mat, wires): einsum_indices = ( f"{density_indices}," f"{affected_indices}{new_indices}->{new_density_indices}" ) - print("einsum_indices", einsum_indices) new_density = torch.einsum(einsum_indices, density, matdag) @@ -274,6 +260,7 @@ def apply_unitary_density_bmm(density, mat, wires): device_wires = wires n_qubit = density.dim() // 2 mat = mat.type(C_DTYPE).to(density.device) + """ Compute U \rho """ @@ -284,7 +271,9 @@ def apply_unitary_density_bmm(density, mat, wires): permute_to = permute_to[:1] + devices_dims + permute_to[1:] permute_back = list(np.argsort(permute_to)) original_shape = density.shape - permuted = density.permute(permute_to).reshape([original_shape[0], mat.shape[-1], -1]) + permuted = density.permute(permute_to).reshape( + [original_shape[0], mat.shape[-1], -1] + ) if len(mat.shape) > 2: # both matrix and state are in batch mode @@ -295,10 +284,16 @@ def apply_unitary_density_bmm(density, mat, wires): expand_shape = [bsz] + list(mat.shape) new_density = mat.expand(expand_shape).bmm(permuted) new_density = new_density.view(original_shape).permute(permute_back) + r""" + Compute \rho U^\dagger """ - Compute \rho U^\dagger - """ - matdag = torch.conj(mat) + + matdag = mat.conj() + if matdag.dim() == 3: + matdag = matdag.permute(0, 2, 1) + else: + matdag = matdag.permute(1, 0) + matdag = matdag.type(C_DTYPE).to(density.device) devices_dims_dag = [n_qubit + w + 1 for w in device_wires] @@ -307,7 +302,9 @@ def apply_unitary_density_bmm(density, mat, wires): del permute_to_dag[d] permute_to_dag = permute_to_dag + devices_dims_dag permute_back_dag = list(np.argsort(permute_to_dag)) - permuted_dag = new_density.permute(permute_to_dag).reshape([original_shape[0], -1, matdag.shape[0]]) + permuted_dag = new_density.permute(permute_to_dag).reshape( + [original_shape[0], -1, matdag.shape[0]] + ) if len(matdag.shape) > 2: # both matrix and state are in batch mode @@ -321,17 +318,24 @@ def apply_unitary_density_bmm(density, mat, wires): return new_density +_noise_mat_dict = { + "Bitflip": torch.tensor([[0, 1], [1, 0]], dtype=C_DTYPE), + "Phaseflip": torch.tensor([[1, 0], [0, -1]], dtype=C_DTYPE) +} + + def gate_wrapper( - name, - mat, - method, - q_device: QuantumDevice, - wires, - params=None, - n_wires=None, - static=False, - parent_graph=None, - inverse=False, + name, + mat, + method, + q_device: QuantumDevice, + wires, + paramnum=0, + params=None, + n_wires=None, + static=False, + parent_graph=None, + inverse=False, ): """Perform the phaseshift gate. @@ -366,7 +370,12 @@ def gate_wrapper( else: # this is for directly inputting parameters as a number params = torch.tensor(params, dtype=F_DTYPE) - + ''' + Check whether user don't set parameters of multi parameters gate + in batch mode. + ''' + if params.dim() == 1 and params.shape[0] == paramnum: + params = params.unsqueeze(0) if name in ["qubitunitary", "qubitunitaryfast", "qubitunitarystrict"]: params = params.unsqueeze(0) if params.dim() == 2 else params else: @@ -382,9 +391,11 @@ def gate_wrapper( { "name": name, # type: ignore "wires": np.array(wires).squeeze().tolist(), - "params": params.squeeze().detach().cpu().numpy().tolist() - if params is not None - else None, + "params": ( + params.squeeze().detach().cpu().numpy().tolist() + if params is not None + else None + ), "inverse": inverse, "trainable": params.requires_grad if params is not None else False, } @@ -431,12 +442,26 @@ def gate_wrapper( else: matrix = matrix.permute(1, 0) assert np.log2(matrix.shape[-1]) == len(wires) - if q_device.device_name=="noisedevice": + + # TODO: There might be a better way to discriminate noisedevice and normal statevector device + if q_device.device_name == "noisedevice": density = q_device.densities - print(density.shape) if method == "einsum": return elif method == "bmm": + ''' + Apply kraus operator if there is noise + ''' + kraus_dict = q_device.noise_model().kraus_dict() + if (kraus_dict["Bitflip"] != 0 or kraus_dict["Phaseflip"] != 0): + p_identity = 1 - kraus_dict["Bitflip"] ** 2 - kraus_dict["Phaseflip"] ** 2 + if kraus_dict["Bitflip"] != 0: + noise_mat = kraus_dict["Bitflip"] * _noise_mat_dict["Bitflip"] + density_noise = apply_unitary_density_bmm(density, noise_mat, wires) + if kraus_dict["Phaseflip"] != 0: + noise_mat = kraus_dict["Phaseflip"] * _noise_mat_dict["Bitflip"] + density_noise = density_noise + apply_unitary_density_bmm(density, noise_mat, wires) + density = p_identity * density + density_noise q_device.densities = apply_unitary_density_bmm(density, matrix, wires) else: state = q_device.states @@ -444,4 +469,3 @@ def gate_wrapper( q_device.states = apply_unitary_einsum(state, matrix, wires) elif method == "bmm": q_device.states = apply_unitary_bmm(state, matrix, wires) - diff --git a/torchquantum/functional/hadamard.py b/torchquantum/functional/hadamard.py index a2a45c40..a2deb86b 100644 --- a/torchquantum/functional/hadamard.py +++ b/torchquantum/functional/hadamard.py @@ -160,7 +160,7 @@ def chadamard( name = "chadamard" - mat = mat_dict[name] + mat = _hadamard_mat_dict[name] gate_wrapper( name=name, mat=mat, diff --git a/torchquantum/functional/paulix.py b/torchquantum/functional/paulix.py index d07f066f..e2904d13 100644 --- a/torchquantum/functional/paulix.py +++ b/torchquantum/functional/paulix.py @@ -508,7 +508,7 @@ def toffoli( """ name = "toffoli" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -552,7 +552,7 @@ def rc3x( None. """ name = "rc3x" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, @@ -596,7 +596,7 @@ def rccx( None. """ name = "rccx" - mat = mat_dict[name] + mat = _x_mat_dict[name] gate_wrapper( name=name, mat=mat, diff --git a/torchquantum/functional/phase_shift.py b/torchquantum/functional/phase_shift.py index e873b834..e06bd901 100644 --- a/torchquantum/functional/phase_shift.py +++ b/torchquantum/functional/phase_shift.py @@ -88,6 +88,7 @@ def phaseshift( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/qubit_unitary.py b/torchquantum/functional/qubit_unitary.py index 151680a0..aea3510c 100644 --- a/torchquantum/functional/qubit_unitary.py +++ b/torchquantum/functional/qubit_unitary.py @@ -132,6 +132,7 @@ def qubitunitary( method=comp_method, q_device=q_device, wires=wires, + paramnum=4, params=params, n_wires=n_wires, static=static, @@ -227,6 +228,7 @@ def qubitunitarystrict( q_device=q_device, wires=wires, params=params, + paramnum=4, n_wires=n_wires, static=static, parent_graph=parent_graph, diff --git a/torchquantum/functional/r.py b/torchquantum/functional/r.py index d788e418..bd2aa0f6 100644 --- a/torchquantum/functional/r.py +++ b/torchquantum/functional/r.py @@ -97,6 +97,7 @@ def r( method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rot.py b/torchquantum/functional/rot.py index 1de26ef3..af45cc7c 100644 --- a/torchquantum/functional/rot.py +++ b/torchquantum/functional/rot.py @@ -134,6 +134,7 @@ def rot( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, @@ -181,6 +182,7 @@ def crot( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rx.py b/torchquantum/functional/rx.py index a1c5d732..47c3cfce 100644 --- a/torchquantum/functional/rx.py +++ b/torchquantum/functional/rx.py @@ -161,6 +161,7 @@ def rx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -208,6 +209,7 @@ def rxx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/ry.py b/torchquantum/functional/ry.py index d098c7df..29ec3330 100644 --- a/torchquantum/functional/ry.py +++ b/torchquantum/functional/ry.py @@ -143,6 +143,7 @@ def ryy( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -197,6 +198,7 @@ def cry( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -244,6 +246,7 @@ def ry( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/rz.py b/torchquantum/functional/rz.py index 0cc0c651..f2685d31 100644 --- a/torchquantum/functional/rz.py +++ b/torchquantum/functional/rz.py @@ -219,6 +219,7 @@ def multirz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -266,6 +267,7 @@ def crz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -313,6 +315,7 @@ def rz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -360,6 +363,7 @@ def rzz( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -407,6 +411,7 @@ def rzx( method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/test.py b/torchquantum/functional/test.py new file mode 100644 index 00000000..12cd9c59 --- /dev/null +++ b/torchquantum/functional/test.py @@ -0,0 +1,32 @@ +import torch + +from torchquantum.macro import C_DTYPE +from torchquantum.density import density_func +from torchquantum.density import density_mat + +if __name__ == "__main__": + mat = density_func.mat_dict["hadamard"] + + Xgatemat = density_func.mat_dict["paulix"] + print(mat) + D = density_mat.DensityMatrix(2, 1) + + rho = torch.zeros(2 ** 4, dtype=C_DTYPE) + rho = torch.reshape(rho, [4, 4]) + rho[0][0] = 1 / 2 + rho[0][3] = 1 / 2 + rho[3][0] = 1 / 2 + rho[3][3] = 1 / 2 + rho = torch.reshape(rho, [2, 2, 2, 2]) + D.update_matrix(rho) + D.print_2d(0) + newD = density_func.apply_unitary_density_bmm(D._matrix, Xgatemat, [1]) + + print("D matrix shape") + print(D._matrix.shape) + + print("newD shape") + print(newD.shape) + D.update_matrix(newD) + + D.print_2d(0) diff --git a/torchquantum/functional/u1.py b/torchquantum/functional/u1.py index 05a94910..be0efff1 100644 --- a/torchquantum/functional/u1.py +++ b/torchquantum/functional/u1.py @@ -110,13 +110,14 @@ def u1( """ name = "u1" - mat = mat_dict[name] + mat = _u1_mat_dict[name] gate_wrapper( name=name, mat=mat, method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, @@ -157,13 +158,14 @@ def cu1( """ name = "cu1" - mat = mat_dict[name] + mat = _u1_mat_dict[name] gate_wrapper( name=name, mat=mat, method=comp_method, q_device=q_device, wires=wires, + paramnum=1, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/u2.py b/torchquantum/functional/u2.py index 5a1d9b21..98d201bb 100644 --- a/torchquantum/functional/u2.py +++ b/torchquantum/functional/u2.py @@ -109,13 +109,14 @@ def u2( """ name = "u2" - mat = mat_dict[name] + mat = _u2_mat_dict[name] gate_wrapper( name=name, mat=mat, method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, @@ -156,13 +157,14 @@ def cu2( """ name = "cu2" - mat = mat_dict[name] + mat = _u2_mat_dict[name] gate_wrapper( name=name, mat=mat, method=comp_method, q_device=q_device, wires=wires, + paramnum=2, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/functional/u3.py b/torchquantum/functional/u3.py index 9dd0927f..076718e5 100644 --- a/torchquantum/functional/u3.py +++ b/torchquantum/functional/u3.py @@ -158,6 +158,7 @@ def u3( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, @@ -249,6 +250,7 @@ def cu3( method=comp_method, q_device=q_device, wires=wires, + paramnum=3, params=params, n_wires=n_wires, static=static, diff --git a/torchquantum/measurement/__init__.py b/torchquantum/measurement/__init__.py index bec5efe0..8d2ba360 100644 --- a/torchquantum/measurement/__init__.py +++ b/torchquantum/measurement/__init__.py @@ -23,3 +23,4 @@ """ from .measurements import * +from .density_measurements import * diff --git a/torchquantum/measurement/density_measurements.py b/torchquantum/measurement/density_measurements.py new file mode 100644 index 00000000..e1663eb2 --- /dev/null +++ b/torchquantum/measurement/density_measurements.py @@ -0,0 +1,330 @@ +import random + +import torch +import torchquantum as tq +import torchquantum.functional as tqf +import numpy as np +from torchquantum.macro import F_DTYPE + +from typing import Union, List +from collections import Counter, OrderedDict + +from torchquantum.functional import mat_dict +# from .operator import op_name_dict, Observable +import torchquantum.operator as op +from copy import deepcopy +import matplotlib.pyplot as plt +from torchquantum.measurement import gen_bitstrings +from torchquantum.measurement import find_observable_groups + +__all__ = [ + "expval_joint_sampling_grouping_density", + "expval_joint_sampling_density", + "expval_joint_analytical_density", + "expval_density", + "measure_density", + "MeasureAll_density" +] + + +def measure_density(noisedev: tq.NoiseDevice, n_shots=1024, draw_id=None): + """Measure the target density matrix and obtain classical bitstream distribution + Args: + noisedev: input tq.NoiseDevice + n_shots: number of simulated shots + Returns: + distribution of bitstrings + """ + bitstring_candidates = gen_bitstrings(noisedev.n_wires) + + state_mag = noisedev.get_probs_1d().abs().detach().cpu().numpy() + distri_all = [] + + for state_mag_one in state_mag: + state_prob_one = state_mag_one + measured = random.choices( + population=bitstring_candidates, + weights=state_prob_one, + k=n_shots, + ) + counter = Counter(measured) + counter.update({key: 0 for key in bitstring_candidates}) + distri = dict(counter) + distri = OrderedDict(sorted(distri.items())) + distri_all.append(distri) + + if draw_id is not None: + plt.bar(distri_all[draw_id].keys(), distri_all[draw_id].values()) + plt.xticks(rotation="vertical") + plt.xlabel("bitstring [qubit0, qubit1, ..., qubitN]") + plt.title("distribution of measured bitstrings") + plt.show() + return distri_all + + +def expval_joint_sampling_grouping_density( + noisedev: tq.NoiseDevice, + observables: List[str], + n_shots_per_group=1024, +): + assert len(observables) == len(set(observables)), "each observable should be unique" + # key is the group, values is the list of sub-observables + obs = [] + for observable in observables: + obs.append(observable.upper()) + # firstly find the groups + groups = find_observable_groups(obs) + + # rotation to the desired basis + n_wires = noisedev.n_wires + paulix = op.op_name_dict["paulix"] + pauliy = op.op_name_dict["pauliy"] + pauliz = op.op_name_dict["pauliz"] + iden = op.op_name_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + expval_all_obs = {} + for obs_group, obs_elements in groups.items(): + # for each group need to clone a new qdev and its densities + noisedev_clone = tq.NoiseDevice(n_wires=noisedev.n_wires, bsz=noisedev.bsz, device=noisedev.device) + noisedev_clone.clone_densities(noisedev.densities) + + for wire in range(n_wires): + for rotation in pauli_dict[obs_group[wire]]().diagonalizing_gates(): + rotation(noisedev_clone, wires=wire) + + # measure + distributions = measure_density(noisedev_clone, n_shots=n_shots_per_group) + # interpret the distribution for different observable elements + for obs_element in obs_elements: + expval_all = [] + mask = np.ones(len(obs_element), dtype=bool) + mask[np.array([*obs_element]) == "I"] = False + + for distri in distributions: + n_eigen_one = 0 + n_eigen_minus_one = 0 + for bitstring, n_count in distri.items(): + if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + n_eigen_one += n_count + else: + n_eigen_minus_one += n_count + + expval = n_eigen_one / n_shots_per_group + (-1) * n_eigen_minus_one / n_shots_per_group + + expval_all.append(expval) + expval_all_obs[obs_element] = torch.tensor(expval_all, dtype=F_DTYPE) + + return expval_all_obs + + +def expval_joint_sampling_density( + qdev: tq.NoiseDevice, + observable: str, + n_shots=1024, +): + """ + Compute the expectation value of a joint observable from sampling + the measurement bistring + Args: + qdev: the noise device + observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order + Returns: + the expectation value + Examples: + >>> import torchquantum as tq + >>> import torchquantum.functional as tqf + >>> x = tq.QuantumDevice(n_wires=2) + >>> tqf.hadamard(x, wires=0) + >>> tqf.x(x, wires=1) + >>> tqf.cnot(x, wires=[0, 1]) + >>> print(expval_joint_sampling(x, 'II', n_shots=8192)) + tensor([[0.9997]]) + >>> print(expval_joint_sampling(x, 'XX', n_shots=8192)) + tensor([[0.9991]]) + >>> print(expval_joint_sampling(x, 'ZZ', n_shots=8192)) + tensor([[-0.9980]]) + """ + # rotation to the desired basis + n_wires = qdev.n_wires + paulix = op.op_name_dict["paulix"] + pauliy = op.op_name_dict["pauliy"] + pauliz = op.op_name_dict["pauliz"] + iden = op.op_name_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + qdev_clone = tq.NoiseDevice(n_wires=qdev.n_wires, bsz=qdev.bsz, device=qdev.device) + qdev_clone.clone_densities(qdev.densities) + + observable = observable.upper() + for wire in range(n_wires): + for rotation in pauli_dict[observable[wire]]().diagonalizing_gates(): + rotation(qdev_clone, wires=wire) + + mask = np.ones(len(observable), dtype=bool) + mask[np.array([*observable]) == "I"] = False + + expval_all = [] + # measure + distributions = measure_density(qdev_clone, n_shots=n_shots) + for distri in distributions: + n_eigen_one = 0 + n_eigen_minus_one = 0 + for bitstring, n_count in distri.items(): + if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + n_eigen_one += n_count + else: + n_eigen_minus_one += n_count + + expval = n_eigen_one / n_shots + (-1) * n_eigen_minus_one / n_shots + expval_all.append(expval) + + return torch.tensor(expval_all, dtype=F_DTYPE) + + +def expval_joint_analytical_density( + noisedev: tq.NoiseDevice, + observable: str, + n_shots=1024 +): + """ + Compute the expectation value of a joint observable in analytical way, assuming the + density matrix is available. + Args: + qdev: the quantum device + observable: the joint observable, on the qubit 0, 1, 2, 3, etc in this order + Returns: + the expectation value + Examples: + >>> import torchquantum as tq + >>> import torchquantum.functional as tqf + >>> x = tq.QuantumDevice(n_wires=2) + >>> tqf.hadamard(x, wires=0) + >>> tqf.x(x, wires=1) + >>> tqf.cnot(x, wires=[0, 1]) + >>> print(expval_joint_analytical(x, 'II')) + tensor([[1.0000]]) + >>> print(expval_joint_analytical(x, 'XX')) + tensor([[1.0000]]) + >>> print(expval_joint_analytical(x, 'ZZ')) + tensor([[-1.0000]]) + """ + # compute the hamiltonian matrix + paulix = mat_dict["paulix"] + pauliy = mat_dict["pauliy"] + pauliz = mat_dict["pauliz"] + iden = mat_dict["i"] + pauli_dict = {"X": paulix, "Y": pauliy, "Z": pauliz, "I": iden} + + observable = observable.upper() + assert len(observable) == noisedev.n_wires + densities = noisedev.get_densities_2d() + + hamiltonian = pauli_dict[observable[0]].to(densities.device) + for op in observable[1:]: + hamiltonian = torch.kron(hamiltonian, pauli_dict[op].to(densities.device)) + + batch_size = densities.shape[0] + expanded_hamiltonian = hamiltonian.unsqueeze(0).expand(batch_size, *hamiltonian.shape) + + product = torch.bmm(expanded_hamiltonian, densities) + + # Extract the diagonal elements from each matrix in the batch + diagonals = torch.diagonal(product, dim1=-2, dim2=-1) + + # Sum the diagonal elements to get the trace for each batch + trace = torch.sum(diagonals, dim=-1).real + + # Should use expectation= Tr(observable \times density matrix) + return trace + + +def expval_density( + noisedev: tq.NoiseDevice, + wires: Union[int, List[int]], + observables: Union[op.Observable, List[op.Observable]], +): + all_dims = np.arange(noisedev.n_wires + 1) + if isinstance(wires, int): + wires = [wires] + observables = [observables] + + # rotation to the desired basis + for wire, observable in zip(wires, observables): + for rotation in observable.diagonalizing_gates(): + rotation(noisedev, wires=wire) + + # compute magnitude + state_mag = noisedev.get_probs_1d() + bsz = state_mag.shape[0] + state_mag = torch.reshape(state_mag, [bsz] + [2] * noisedev.n_wires) + expectations = [] + for wire, observable in zip(wires, observables): + # compute marginal magnitude + reduction_dims = np.delete(all_dims, [0, wire + 1]) + if reduction_dims.size == 0: + probs = state_mag + else: + probs = state_mag.sum(list(reduction_dims)) + res = probs.mv(observable.eigvals.real.to(probs.device)) + expectations.append(res) + + return torch.stack(expectations, dim=-1) + + +class MeasureAll_density(tq.QuantumModule): + """Obtain the expectation value of all the qubits.""" + + def __init__(self, obs, v_c_reg_mapping=None): + super().__init__() + self.obs = obs + self.v_c_reg_mapping = v_c_reg_mapping + + def forward(self, qdev: tq.NoiseDevice): + x = expval_density(qdev, list(range(qdev.n_wires)), [self.obs()] * qdev.n_wires) + + if self.v_c_reg_mapping is not None: + c2v_mapping = self.v_c_reg_mapping["c2v"] + """ + the measurement is not normal order, need permutation + """ + perm = [] + for k in range(x.shape[-1]): + if k in c2v_mapping.keys(): + perm.append(c2v_mapping[k]) + x = x[:, perm] + + if self.noise_model_tq is not None and self.noise_model_tq.is_add_noise: + return self.noise_model_tq.apply_readout_error(x) + else: + return x + + def set_v_c_reg_mapping(self, mapping): + self.v_c_reg_mapping = mapping + + +if __name__ == '__main__': + qdev = tq.NoiseDevice(n_wires=2, bsz=5, device="cpu", record_op=True) # use device='cuda' for GPU + qdev.h(wires=0) + qdev.cnot(wires=[0, 1]) + # tqf.h(qdev, wires=1) + # tqf.x(qdev, wires=1) + # tqf.y(qdev, wires=1) + # tqf.cnot(qdev,wires=[0, 1]) + # op = tq.RX(has_params=True, trainable=True, init_params=0.5) + # op(qdev, wires=0) + result = tq.expval_density(qdev, [0, 1], [tq.PauliZ(), tq.PauliZ()]) + print(result.shape) + + # measure the state on z basis + # print(tq.measure_density(qdev, n_shots=1024)) + + # obtain the expval on a observable + # expval = expval_joint_sampling_density(qdev, 'XZ', 100000) + + # print("expval") + # print(expval) + + # expval_ana = expval_joint_analytical_density(qdev, 'XZ') + # print("expval_ana") + # print(expval_ana) diff --git a/torchquantum/measurement/measurements.py b/torchquantum/measurement/measurements.py index c3c2daad..41331a55 100644 --- a/torchquantum/measurement/measurements.py +++ b/torchquantum/measurement/measurements.py @@ -43,13 +43,9 @@ def measure(qdev, n_shots=1024, draw_id=None): distribution of bitstrings """ bitstring_candidates = gen_bitstrings(qdev.n_wires) - if isinstance(qdev, tq.QuantumDevice): - state_mag = qdev.get_states_1d().abs().detach().cpu().numpy() - elif isinstance(qdev, tq.NoiseDevice): - ''' - Measure the density matrix in the computational basis - ''' - state_mag = qdev.get_probs_1d().abs().detach().cpu().numpy() + + #state_prob = + state_mag = qdev.get_states_1d().abs().detach().cpu().numpy() distri_all = [] for state_mag_one in state_mag: @@ -285,6 +281,7 @@ def expval( observables: Union[op.Observable, List[op.Observable]], ): all_dims = np.arange(qdev.states.dim()) + if isinstance(wires, int): wires = [wires] observables = [observables] @@ -295,9 +292,9 @@ def expval( rotation(qdev, wires=wire) states = qdev.states + # compute magnitude state_mag = torch.abs(states) ** 2 - expectations = [] for wire, observable in zip(wires, observables): # compute marginal magnitude diff --git a/torchquantum/noise_model/noise_models.py b/torchquantum/noise_model/noise_models.py index 571314e9..2309c7e8 100644 --- a/torchquantum/noise_model/noise_models.py +++ b/torchquantum/noise_model/noise_models.py @@ -24,12 +24,11 @@ import numpy as np import torch -import torchquantum as tq - +from qiskit_aer.noise import NoiseModel from torchpack.utils.logging import logger -from qiskit.providers.aer.noise import NoiseModel -from torchquantum.util import get_provider +import torchquantum as tq +from torchquantum.util import get_provider __all__ = [ "NoiseModelTQ", @@ -50,31 +49,31 @@ def cos_adjust_noise( orig_noise_total_prob, ): """ - Adjust the noise probability based on the current epoch and a cosine schedule. + Adjust the noise probability based on the current epoch and a cosine schedule. + + Args: + current_epoch (int): The current epoch. + n_epochs (int): The total number of epochs. + prob_schedule (str): The probability schedule type. Possible values are: + - None: No schedule, use the original noise probability. + - "increase": Increase the noise probability using a cosine schedule. + - "decrease": Decrease the noise probability using a cosine schedule. + - "increase_decrease": Increase the noise probability until a separator epoch, + then decrease it using cosine schedules. + prob_schedule_separator (int): The epoch at which the schedule changes for + "increase_decrease" mode. + orig_noise_total_prob (float): The original noise probability. + + Returns: + float: The adjusted noise probability based on the schedule. + + Note: + The adjusted noise probability is returned as a float between 0 and 1. + + Raises: + None. - Args: - current_epoch (int): The current epoch. - n_epochs (int): The total number of epochs. - prob_schedule (str): The probability schedule type. Possible values are: - - None: No schedule, use the original noise probability. - - "increase": Increase the noise probability using a cosine schedule. - - "decrease": Decrease the noise probability using a cosine schedule. - - "increase_decrease": Increase the noise probability until a separator epoch, - then decrease it using cosine schedules. - prob_schedule_separator (int): The epoch at which the schedule changes for - "increase_decrease" mode. - orig_noise_total_prob (float): The original noise probability. - - Returns: - float: The adjusted noise probability based on the schedule. - - Note: - The adjusted noise probability is returned as a float between 0 and 1. - - Raises: - None. - - """ + """ if prob_schedule is None: noise_total_prob = orig_noise_total_prob @@ -134,31 +133,31 @@ def cos_adjust_noise( def apply_readout_error_func(x, c2p_mapping, measure_info): """ - Apply readout error to the measurement outcomes. - - Args: - x (torch.Tensor): The measurement outcomes, represented as a tensor of shape (batch_size, num_qubits). - c2p_mapping (dict): Mapping from qubit indices to physical wire indices. - measure_info (dict): Measurement information dictionary containing the probabilities for different outcomes. - - Returns: - torch.Tensor: The measurement outcomes after applying the readout error, represented as a tensor of the same shape as x. - - Note: - The readout error is applied based on the given mapping and measurement information. - The measurement information dictionary should have the following structure: - { - (wire_1,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, - (wire_2,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, - ... - } - where wire_1, wire_2, ... are the physical wire indices, and p_0 and p_1 are the probabilities of measuring 0 and 1, respectively, - for each wire. - - Raises: - None. + Apply readout error to the measurement outcomes. + + Args: + x (torch.Tensor): The measurement outcomes, represented as a tensor of shape (batch_size, num_qubits). + c2p_mapping (dict): Mapping from qubit indices to physical wire indices. + measure_info (dict): Measurement information dictionary containing the probabilities for different outcomes. + + Returns: + torch.Tensor: The measurement outcomes after applying the readout error, represented as a tensor of the same shape as x. + + Note: + The readout error is applied based on the given mapping and measurement information. + The measurement information dictionary should have the following structure: + { + (wire_1,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, + (wire_2,): {"probabilities": [[p_0, p_1], [p_0, p_1]]}, + ... + } + where wire_1, wire_2, ... are the physical wire indices, and p_0 and p_1 are the probabilities of measuring 0 and 1, respectively, + for each wire. + + Raises: + None. - """ + """ # add readout error noise_free_0_probs = (x + 1) / 2 noise_free_1_probs = 1 - (x + 1) / 2 @@ -196,21 +195,22 @@ def apply_readout_error_func(x, c2p_mapping, measure_info): class NoiseCounter: """ - A class for counting the occurrences of Pauli error gates. + A class for counting the occurrences of Pauli error gates. - Attributes: - counter_x (int): Counter for Pauli X errors. - counter_y (int): Counter for Pauli Y errors. - counter_z (int): Counter for Pauli Z errors. - counter_X (int): Counter for Pauli X errors (for two-qubit gates). - counter_Y (int): Counter for Pauli Y errors (for two-qubit gates). - counter_Z (int): Counter for Pauli Z errors (for two-qubit gates). + Attributes: + counter_x (int): Counter for Pauli X errors. + counter_y (int): Counter for Pauli Y errors. + counter_z (int): Counter for Pauli Z errors. + counter_X (int): Counter for Pauli X errors (for two-qubit gates). + counter_Y (int): Counter for Pauli Y errors (for two-qubit gates). + counter_Z (int): Counter for Pauli Z errors (for two-qubit gates). - Methods: - add(error): Adds a Pauli error to the counters based on the error type. - __str__(): Returns a string representation of the counters. + Methods: + add(error): Adds a Pauli error to the counters based on the error type. + __str__(): Returns a string representation of the counters. + + """ - """ def __init__(self): self.counter_x = 0 self.counter_y = 0 @@ -220,51 +220,51 @@ def __init__(self): self.counter_Z = 0 def add(self, error): - if error == 'x': + if error == "x": self.counter_x += 1 - elif error == 'y': + elif error == "y": self.counter_y += 1 - elif error == 'z': + elif error == "z": self.counter_z += 1 - if error == 'X': + if error == "X": self.counter_X += 1 - elif error == 'Y': + elif error == "Y": self.counter_Y += 1 - elif error == 'Z': + elif error == "Z": self.counter_Z += 1 else: pass - - def __str__(self) -> str: - return f'single qubit error: pauli x = {self.counter_x}, pauli y = {self.counter_y}, pauli z = {self.counter_z}\n' + \ - f'double qubit error: pauli x = {self.counter_X}, pauli y = {self.counter_Y}, pauli z = {self.counter_Z}' + def __str__(self) -> str: + return ( + f"single qubit error: pauli x = {self.counter_x}, pauli y = {self.counter_y}, pauli z = {self.counter_z}\n" + + f"double qubit error: pauli x = {self.counter_X}, pauli y = {self.counter_Y}, pauli z = {self.counter_Z}" + ) class NoiseModelTQ(object): """ - A class for applying gate insertion and readout errors. - - Attributes: - noise_model_name (str): Name of the noise model. - n_epochs (int): Number of epochs. - noise_total_prob (float): Total probability of noise. - ignored_ops (tuple): Operations to be ignored. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - factor (float): Factor for adjusting probabilities. - add_thermal (bool): Flag indicating whether to add thermal relaxation. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - clean_parsed_noise_model_dict(nm_dict, ignored_ops): Cleans the parsed noise model dictionary. - parse_noise_model_dict(nm_dict): Parses the noise model dictionary. - magnify_probs(probs): Magnifies the probabilities based on a factor. - sample_noise_op(op_in): Samples a noise operation based on the given operation. - apply_readout_error(x): Applies readout error to the input. - - """ + A class for applying gate insertion and readout errors. + + Attributes: + noise_model_name (str): Name of the noise model. + n_epochs (int): Number of epochs. + noise_total_prob (float): Total probability of noise. + ignored_ops (tuple): Operations to be ignored. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + factor (float): Factor for adjusting probabilities. + add_thermal (bool): Flag indicating whether to add thermal relaxation. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + clean_parsed_noise_model_dict(nm_dict, ignored_ops): Cleans the parsed noise model dictionary. + parse_noise_model_dict(nm_dict): Parses the noise model dictionary. + magnify_probs(probs): Magnifies the probabilities based on a factor. + sample_noise_op(op_in): Samples a noise operation based on the given operation. + apply_readout_error(x): Applies readout error to the input. + """ def __init__( self, @@ -295,7 +295,9 @@ def __init__( self.ignored_ops = ignored_ops self.parsed_dict = self.parse_noise_model_dict(self.noise_model_dict) - self.parsed_dict = self.clean_parsed_noise_model_dict(self.parsed_dict, ignored_ops) + self.parsed_dict = self.clean_parsed_noise_model_dict( + self.parsed_dict, ignored_ops + ) self.n_epochs = n_epochs self.prob_schedule = prob_schedule self.prob_schedule_separator = prob_schedule_separator @@ -313,39 +315,66 @@ def adjust_noise(self, current_epoch): @staticmethod def clean_parsed_noise_model_dict(nm_dict, ignored_ops): - # remove the ignored operation in the instructions and probs + # remove the ignored operation in the instructions and probs # --> only get the pauli-x,y,z errors. ignore the thermal relaxation errors (kraus operator) def filter_inst(inst_list: list) -> list: new_inst_list = [] for inst in inst_list: - if inst['name'] in ignored_ops: + if inst["name"] in ignored_ops: continue new_inst_list.append(inst) return new_inst_list - ignored_ops = set(ignored_ops) - single_depolarization = set(['x', 'y', 'z']) - double_depolarization = set(['IX', 'IY', 'IZ', 'XI', 'XX', 'XY', 'XZ', 'YI', 'YX', 'YY', 'YZ', 'ZI', 'ZX', 'ZY', 'ZZ']) # 16 - 1 = 15 combinations + ignored_ops = set(ignored_ops) + single_depolarization = set(["x", "y", "z"]) + double_depolarization = set( + [ + "IX", + "IY", + "IZ", + "XI", + "XX", + "XY", + "XZ", + "YI", + "YX", + "YY", + "YZ", + "ZI", + "ZX", + "ZY", + "ZZ", + ] + ) # 16 - 1 = 15 combinations for operation, operation_info in nm_dict.items(): for qubit, qubit_info in operation_info.items(): inst_all = [] prob_all = [] if qubit_info["type"] == "qerror": - for inst, prob in zip(qubit_info["instructions"], qubit_info["probabilities"]): - if operation in ['x', 'sx', 'id', 'reset']: # single qubit gate - if any([inst_one["name"] in single_depolarization for inst_one in inst]): + for inst, prob in zip( + qubit_info["instructions"], qubit_info["probabilities"] + ): + if operation in ["x", "sx", "id", "reset"]: # single qubit gate + if any( + [ + inst_one["name"] in single_depolarization + for inst_one in inst + ] + ): inst_all.append(filter_inst(inst)) prob_all.append(prob) - elif operation in ['cx']: # double qubit gate + elif operation in ["cx"]: # double qubit gate try: - if inst[0]['params'][0] in double_depolarization and (inst[1]['name'] == 'id' or inst[2]['name'] == 'id'): + if inst[0]["params"][0] in double_depolarization and ( + inst[1]["name"] == "id" or inst[2]["name"] == "id" + ): inst_all.append(filter_inst(inst)) prob_all.append(prob) except: pass # don't know how to deal with this case else: - raise Exception(f'{operation} not considered...') + raise Exception(f"{operation} not considered...") nm_dict[operation][qubit]["instructions"] = inst_all nm_dict[operation][qubit]["probabilities"] = prob_all return nm_dict @@ -364,8 +393,13 @@ def parse_noise_model_dict(nm_dict): } if info["operations"][0] not in parsed.keys(): - parsed[info["operations"][0]] = {tuple(info["gate_qubits"][0]): val_dict} - elif tuple(info["gate_qubits"][0]) not in parsed[info["operations"][0]].keys(): + parsed[info["operations"][0]] = { + tuple(info["gate_qubits"][0]): val_dict + } + elif ( + tuple(info["gate_qubits"][0]) + not in parsed[info["operations"][0]].keys() + ): parsed[info["operations"][0]][tuple(info["gate_qubits"][0])] = val_dict else: raise ValueError @@ -432,30 +466,36 @@ def sample_noise_op(self, op_in): ops = [] for instruction in instructions: - v_wires = [self.p_v_reg_mapping["p2v"][qubit] for qubit in instruction["qubits"]] + v_wires = [ + self.p_v_reg_mapping["p2v"][qubit] for qubit in instruction["qubits"] + ] if instruction["name"] == "x": ops.append(tq.PauliX(wires=v_wires)) - self.noise_counter.add('x') + self.noise_counter.add("x") elif instruction["name"] == "y": ops.append(tq.PauliY(wires=v_wires)) - self.noise_counter.add('y') + self.noise_counter.add("y") elif instruction["name"] == "z": ops.append(tq.PauliZ(wires=v_wires)) - self.noise_counter.add('z') + self.noise_counter.add("z") elif instruction["name"] == "reset": ops.append(tq.Reset(wires=v_wires)) elif instruction["name"] == "pauli": - twoqubit_depolarization = list(instruction['params'][0]) # ['XY'] --> ['X', 'Y'] - for singlequbit_deloparization, v_wire in zip(twoqubit_depolarization, v_wires): - if singlequbit_deloparization == 'X': + twoqubit_depolarization = list( + instruction["params"][0] + ) # ['XY'] --> ['X', 'Y'] + for singlequbit_deloparization, v_wire in zip( + twoqubit_depolarization, v_wires + ): + if singlequbit_deloparization == "X": ops.append(tq.PauliX(wires=[v_wire])) - self.noise_counter.add('X') - elif singlequbit_deloparization == 'Y': + self.noise_counter.add("X") + elif singlequbit_deloparization == "Y": ops.append(tq.PauliY(wires=[v_wire])) - self.noise_counter.add('Y') - elif singlequbit_deloparization == 'Z': + self.noise_counter.add("Y") + elif singlequbit_deloparization == "Z": ops.append(tq.PauliZ(wires=[v_wire])) - self.noise_counter.add('Z') + self.noise_counter.add("Z") else: pass # 'I' case else: @@ -474,25 +514,24 @@ def apply_readout_error(self, x): class NoiseModelTQActivation(object): """ - A class for adding noise to the activations. - - Attributes: - mean (tuple): Mean values of the noise. - std (tuple): Standard deviation values of the noise. - n_epochs (int): Number of epochs. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - after_norm (bool): Flag indicating whether noise should be added after normalization. - factor (float): Factor for adjusting the noise. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - sample_noise_op(op_in): Samples a noise operation. - apply_readout_error(x): Applies readout error to the input. - add_noise(x, node_id, is_after_norm): Adds noise to the activations. - - """ + A class for adding noise to the activations. + + Attributes: + mean (tuple): Mean values of the noise. + std (tuple): Standard deviation values of the noise. + n_epochs (int): Number of epochs. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + after_norm (bool): Flag indicating whether noise should be added after normalization. + factor (float): Factor for adjusting the noise. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + sample_noise_op(op_in): Samples a noise operation. + apply_readout_error(x): Applies readout error to the input. + add_noise(x, node_id, is_after_norm): Adds noise to the activations. + """ def __init__( self, @@ -560,23 +599,23 @@ def add_noise(self, x, node_id, is_after_norm=False): class NoiseModelTQPhase(object): """ - A class for adding noise to rotation parameters. - - Attributes: - mean (float): Mean value of the noise. - std (float): Standard deviation value of the noise. - n_epochs (int): Number of epochs. - prob_schedule (list): Probability schedule. - prob_schedule_separator (str): Separator for probability schedule. - factor (float): Factor for adjusting the noise. - - Methods: - adjust_noise(current_epoch): Adjusts the noise based on the current epoch. - sample_noise_op(op_in): Samples a noise operation. - apply_readout_error(x): Applies readout error to the input. - add_noise(phase): Adds noise to the rotation parameters. + A class for adding noise to rotation parameters. + + Attributes: + mean (float): Mean value of the noise. + std (float): Standard deviation value of the noise. + n_epochs (int): Number of epochs. + prob_schedule (list): Probability schedule. + prob_schedule_separator (str): Separator for probability schedule. + factor (float): Factor for adjusting the noise. + + Methods: + adjust_noise(current_epoch): Adjusts the noise based on the current epoch. + sample_noise_op(op_in): Samples a noise operation. + apply_readout_error(x): Applies readout error to the input. + add_noise(phase): Adds noise to the rotation parameters. - """ + """ def __init__( self, @@ -638,40 +677,43 @@ def add_noise(self, phase): class NoiseModelTQReadoutOnly(NoiseModelTQ): """ - A subclass of NoiseModelTQ that applies readout errors only. + A subclass of NoiseModelTQ that applies readout errors only. + + This class inherits from NoiseModelTQ and overrides the sample_noise_op method to exclude the insertion of any noise operations other than readout errors. It is designed for scenarios where only readout errors are considered, and all other noise sources are ignored. - This class inherits from NoiseModelTQ and overrides the sample_noise_op method to exclude the insertion of any noise operations other than readout errors. It is designed for scenarios where only readout errors are considered, and all other noise sources are ignored. + Methods: + sample_noise_op(op_in): Returns an empty list, indicating no noise operations are applied. + """ - Methods: - sample_noise_op(op_in): Returns an empty list, indicating no noise operations are applied. - """ def sample_noise_op(self, op_in): return [] class NoiseModelTQQErrorOnly(NoiseModelTQ): """ - A subclass of NoiseModelTQ that applies only readout errors. + A subclass of NoiseModelTQ that applies only readout errors. - This class inherits from NoiseModelTQ and overrides the apply_readout_error method to apply readout errors. It removes activation noise and only focuses on readout errors in the noise model. + This class inherits from NoiseModelTQ and overrides the apply_readout_error method to apply readout errors. It removes activation noise and only focuses on readout errors in the noise model. - Methods: - apply_readout_error(x): Applies readout error to the given activation values. + Methods: + apply_readout_error(x): Applies readout error to the given activation values. + + """ - """ def apply_readout_error(self, x): return x class NoiseModelTQActivationReadout(NoiseModelTQActivation): """ - A subclass of NoiseModelTQActivation that applies readout errors. + A subclass of NoiseModelTQActivation that applies readout errors. - This class inherits from NoiseModelTQActivation and overrides the apply_readout_error method to incorporate readout errors. It combines activation noise and readout errors into the noise model. + This class inherits from NoiseModelTQActivation and overrides the apply_readout_error method to incorporate readout errors. It combines activation noise and readout errors into the noise model. + + Methods: + apply_readout_error(x): Applies readout error to the given activation values + """ - Methods: - apply_readout_error(x): Applies readout error to the given activation values - """ def __init__( self, noise_model_name, @@ -713,13 +755,14 @@ def apply_readout_error(self, x): class NoiseModelTQPhaseReadout(NoiseModelTQPhase): """ - A subclass of NoiseModelTQPhase that applies readout errors to phase values. + A subclass of NoiseModelTQPhase that applies readout errors to phase values. - This class inherits from NoiseModelTQPhase and overrides the apply_readout_error method to apply readout errors specifically to phase values. It uses the noise model provided to introduce readout errors. + This class inherits from NoiseModelTQPhase and overrides the apply_readout_error method to apply readout errors specifically to phase values. It uses the noise model provided to introduce readout errors. + + Methods: + apply_readout_error(x): Applies readout error to the given phase values. + """ - Methods: - apply_readout_error(x): Applies readout error to the given phase values. - """ def __init__( self, noise_model_name, diff --git a/torchquantum/operator/standard_gates/qubit_unitary.py b/torchquantum/operator/standard_gates/qubit_unitary.py index 5f7fd9b1..4b087cd1 100644 --- a/torchquantum/operator/standard_gates/qubit_unitary.py +++ b/torchquantum/operator/standard_gates/qubit_unitary.py @@ -1,11 +1,12 @@ -from ..op_types import * from abc import ABCMeta -from torchquantum.macro import C_DTYPE -import torchquantum as tq + +import numpy as np import torch -from torchquantum.functional import mat_dict + import torchquantum.functional as tqf -import numpy as np +from torchquantum.macro import C_DTYPE + +from ..op_types import * class QubitUnitary(Operation, metaclass=ABCMeta): @@ -118,7 +119,7 @@ def from_controlled_operation( n_wires = n_c_wires + n_t_wires # compute the new unitary, then permute - unitary = torch.tensor(torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE)) + unitary = torch.zeros(2**n_wires, 2**n_wires, dtype=C_DTYPE) for k in range(2**n_wires - 2**n_t_wires): unitary[k, k] = 1.0 + 0.0j diff --git a/torchquantum/plugin/qiskit/qiskit_plugin.py b/torchquantum/plugin/qiskit/qiskit_plugin.py index 1011fa8b..9177d5bd 100644 --- a/torchquantum/plugin/qiskit/qiskit_plugin.py +++ b/torchquantum/plugin/qiskit/qiskit_plugin.py @@ -22,18 +22,16 @@ SOFTWARE. """ -from __future__ import annotations -from typing import Iterable, List +from typing import Iterable import numpy as np +import qiskit import qiskit.circuit.library.standard_gates as qiskit_gate -import symengine -import sympy import torch -from qiskit import Aer, ClassicalRegister, QuantumCircuit, execute -from qiskit.circuit import CircuitInstruction, Parameter, ParameterExpression -from qiskit.circuit.parametervector import ParameterVectorElement +from qiskit import ClassicalRegister, QuantumCircuit, transpile +from qiskit.circuit import Parameter +from qiskit_aer import AerSimulator from torchpack.utils.logging import logger import torchquantum as tq @@ -67,7 +65,7 @@ def qiskit2tq_op_history(circ): if getattr(circ, "_layout", None) is not None: try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -83,7 +81,7 @@ def qiskit2tq_op_history(circ): ops = [] for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] wires = [p2v[wire] for wire in wires] # sometimes the gate.params is ParameterExpression class init_params = ( @@ -234,7 +232,7 @@ def append_parameterized_gate(func, circ, input_idx, params, wires): ) else: raise NotImplementedError( - f"{func} cannot be converted to " f"parameterized Qiskit QuantumCircuit" + f"{func} cannot be converted to parameterized Qiskit QuantumCircuit" ) @@ -261,7 +259,7 @@ def append_fixed_gate(circ, func, params, wires, inverse): elif func == "sx": circ.sx(*wires) elif func in ["cnot", "cx"]: - circ.cnot(*wires) + circ.cx(*wires) elif func == "cz": circ.cz(*wires) elif func == "cy": @@ -347,7 +345,7 @@ def append_fixed_gate(circ, func, params, wires, inverse): def tq2qiskit_initialize(q_device: tq.QuantumDevice, all_states): - """Call the qiskit initialize funtion and encoder the current quantum state + """Call the qiskit initialize function and encoder the current quantum state using initialize and return circuits Args: @@ -447,7 +445,7 @@ def tq2qiskit( # generate only one qiskit QuantumCircuit assert module.params is None or module.params.shape[0] == 1 except AssertionError: - logger.exception(f"Cannot convert batch model tq module") + logger.exception("Cannot convert batch model tq module") n_removed_ops = 0 @@ -500,7 +498,7 @@ def tq2qiskit( elif module.name == "SX": circ.sx(*module.wires) elif module.name == "CNOT": - circ.cnot(*module.wires) + circ.cx(*module.wires) elif module.name == "CZ": circ.cz(*module.wires) elif module.name == "CY": @@ -598,7 +596,7 @@ def tq2qiskit( if n_removed_ops > 0: logger.warning( - f"Remove {n_removed_ops} operations with small " f"parameter magnitude." + f"Remove {n_removed_ops} operations with small parameter magnitude." ) return circ @@ -698,10 +696,10 @@ def qiskit2tq_Operator(circ: QuantumCircuit, initial_parameters=None): if getattr(circ, "_layout", None) is not None: try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: try: p2v_orig = circ._layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.initial_layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -726,7 +724,7 @@ def qiskit2tq_Operator(circ: QuantumCircuit, initial_parameters=None): ops = [] for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] wires = [p2v[wire] for wire in wires] init_params = qiskit2tq_translate_qiskit_params( @@ -860,11 +858,11 @@ def test_qiskit2tq(): circ.sx(3) circ.crx(theta=0.4, control_qubit=0, target_qubit=1) - circ.cnot(control_qubit=2, target_qubit=1) + circ.cx(control_qubit=2, target_qubit=1) circ.u3(theta=-0.1, phi=-0.2, lam=-0.4, qubit=3) - circ.cnot(control_qubit=3, target_qubit=0) - circ.cnot(control_qubit=0, target_qubit=2) + circ.cx(control_qubit=3, target_qubit=0) + circ.cx(control_qubit=0, target_qubit=2) circ.x(2) circ.x(3) circ.u2(phi=-0.2, lam=-0.9, qubit=3) @@ -872,8 +870,10 @@ def test_qiskit2tq(): m = qiskit2tq(circ) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circ, simulator).result() + backend = AerSimulator(method="unitary") + circ = transpile(circ, backend) + circ.save_unitary() + result = backend.run(circ).result() unitary_qiskit = result.get_unitary(circ) unitary_tq = m.get_unitary(q_dev) @@ -1037,8 +1037,10 @@ def test_tq2qiskit(): circuit = tq2qiskit(test_module, inputs) - simulator = Aer.get_backend("unitary_simulator") - result = execute(circuit, simulator).result() + backend = AerSimulator(method="unitary") + circuit = transpile(circuit, backend) + circuit.save_unitary() + result = backend.run(circuit).result() unitary_qiskit = result.get_unitary(circuit) unitary_tq = test_module.get_unitary(q_dev, inputs) @@ -1065,8 +1067,10 @@ def test_tq2qiskit_parameterized(): for k, x in enumerate(inputs[0]): binds[params[k]] = x.item() - simulator = Aer.get_backend("unitary_simulator") - result = execute(circuit, simulator, parameter_binds=[binds]).result() + backend = AerSimulator(method="unitary") + circuit = transpile(circuit, backend) + circuit.save_unitary() + result = backend.run(circuit, parameter_binds=[binds]).result() unitary_qiskit = result.get_unitary(circuit) # print(unitary_qiskit) diff --git a/torchquantum/plugin/qiskit/qiskit_processor.py b/torchquantum/plugin/qiskit/qiskit_processor.py index 2d91e7c3..1a484d7b 100644 --- a/torchquantum/plugin/qiskit/qiskit_processor.py +++ b/torchquantum/plugin/qiskit/qiskit_processor.py @@ -22,34 +22,29 @@ SOFTWARE. """ -import torch -import torchquantum as tq -import pathos.multiprocessing as multiprocessing +import datetime import itertools -from qiskit import Aer, execute, IBMQ, transpile, QuantumCircuit -from qiskit.providers.aer.noise import NoiseModel -from qiskit.tools.monitor import job_monitor +import numpy as np +import pathos.multiprocessing as multiprocessing +import torch +from qiskit import QuantumCircuit, transpile from qiskit.exceptions import QiskitError -from .qiskit_plugin import ( - tq2qiskit, - tq2qiskit_parameterized, - tq2qiskit_measurement, -) +from qiskit.transpiler import PassManager +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel +from torchpack.utils.logging import logger +from tqdm import tqdm + +import torchquantum as tq from torchquantum.util import ( + get_circ_stats, get_expectations_from_counts, - get_provider, get_provider_hub_group_project, - get_circ_stats, ) -from .qiskit_macros import IBMQ_NAMES -from tqdm import tqdm -from torchpack.utils.logging import logger -from qiskit.transpiler import PassManager -import numpy as np -import datetime -from .my_job_monitor import my_job_monitor +from .qiskit_macros import IBMQ_NAMES +from .qiskit_plugin import tq2qiskit, tq2qiskit_measurement, tq2qiskit_parameterized class EmptyPassManager(PassManager): @@ -60,13 +55,10 @@ def run(self, circuits, output_name: str = None, callback=None): def run_job_worker(data): while True: try: - job = execute(**(data[0])) - qiskit_verbose = data[1] - if qiskit_verbose: - job_monitor(job, interval=1) + job = AerSimulator(**(data)) result = job.result() counts = result.get_counts() - # circ_num = len(data[0]['parameter_binds']) + # circ_num = len(data['parameter_binds']) # logger.info( # f'run job worker successful, circuit number = {circ_num}') break @@ -191,7 +183,6 @@ def qiskit_init(self): if self.backend is None: # initialize now - IBMQ.load_account() self.provider = get_provider_hub_group_project( hub=self.hub, group=self.group, @@ -203,9 +194,7 @@ def qiskit_init(self): self.coupling_map = self.get_coupling_map(self.backend_name) else: # use simulator - self.backend = Aer.get_backend( - "qasm_simulator", max_parallel_experiments=0 - ) + self.backend = AerSimulator(max_parallel_experiments=0) self.noise_model = self.get_noise_model(self.noise_model_name) self.coupling_map = self.get_coupling_map(self.coupling_map_name) self.basis_gates = self.get_basis_gates(self.basis_gates_name) @@ -320,7 +309,6 @@ def process_parameterized( for i in range(0, len(binds_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_bind in split_binds: feed_dict = { @@ -332,7 +320,7 @@ def process_parameterized( "noise_model": self.noise_model, "parameter_binds": split_bind, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -345,16 +333,14 @@ def process_parameterized( results[-1] = [results[-1]] counts = list(itertools.chain(*results)) else: - job = execute( - experiments=transpiled_circ, - backend=self.backend, + job = self.backend.run( + transpiled_circ, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, parameter_binds=binds_all, ) - job_monitor(job, interval=1) result = job.result() counts = result.get_counts() @@ -497,7 +483,6 @@ def process_parameterized_and_shift( for i in range(0, len(binds_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_bind in split_binds: feed_dict = { @@ -509,7 +494,7 @@ def process_parameterized_and_shift( "noise_model": self.noise_model, "parameter_binds": split_bind, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -533,16 +518,15 @@ def process_parameterized_and_shift( for circ in split_circs: while True: try: - job = execute( - experiments=circ, - backend=self.backend, + job = self.backend.run( + circ, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, parameter_binds=binds_all, ) - job_monitor(job, interval=1) + result = ( job.result() ) # qiskit.providers.ibmq.job.exceptions.IBMQJobFailureError:Job has failed. Use the error_message() method to get more details @@ -555,7 +539,7 @@ def process_parameterized_and_shift( # total_cont += 1 # print(total_time_spent / total_cont) break - except (QiskitError) as e: + except QiskitError as e: logger.warning("Job failed, rerun now.") print(e.message) @@ -613,7 +597,6 @@ def process_multi_measure( circ_all[i : i + chunk_size] for i in range(0, len(circ_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 2 feed_dicts = [] for split_circ in split_circs: feed_dict = { @@ -624,7 +607,7 @@ def process_multi_measure( "seed_simulator": self.seed_simulator, "noise_model": self.noise_model, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -661,9 +644,8 @@ def process( transpiled_circs = self.transpile(circs) self.transpiled_circs = transpiled_circs - job = execute( - experiments=transpiled_circs, - backend=self.backend, + job = self.backend.run( + transpiled_circs, shots=self.n_shots, # initial_layout=self.initial_layout, seed_transpiler=self.seed_transpiler, @@ -673,7 +655,6 @@ def process( noise_model=self.noise_model, optimization_level=self.optimization_level, ) - job_monitor(job, interval=1) result = job.result() counts = result.get_counts() @@ -704,7 +685,6 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): for i in range(0, len(circs_all), chunk_size) ] - qiskit_verbose = self.max_jobs <= 6 feed_dicts = [] for split_circ in split_circs: feed_dict = { @@ -715,7 +695,7 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): "seed_simulator": self.seed_simulator, "noise_model": self.noise_model, } - feed_dicts.append([feed_dict, qiskit_verbose]) + feed_dicts.append(feed_dict) p = multiprocessing.Pool(self.max_jobs) results = p.map(run_job_worker, feed_dicts) @@ -728,15 +708,13 @@ def process_ready_circs_get_counts(self, circs_all, parallel=True): results[-1] = [results[-1]] counts = list(itertools.chain(*results)) else: - job = execute( - experiments=circs_all, - backend=self.backend, + job = self.backend.run( + circs_all, pass_manager=self.empty_pass_manager, shots=self.n_shots, seed_simulator=self.seed_simulator, noise_model=self.noise_model, ) - job_monitor(job, interval=1) result = job.result() counts = [result.get_counts()] @@ -758,9 +736,9 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): for circ_ in circs_all: circ = circ_.copy() for k, obs in enumerate(observable): - if obs == 'X': + if obs == "X": circ.h(k) - elif obs == 'Y': + elif obs == "Y": circ.z(k) circ.s(k) circ.h(k) @@ -771,8 +749,10 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): mask = np.ones(len(observable), dtype=bool) mask[np.array([*observable]) == "I"] = False - - counts = self.process_ready_circs_get_counts(circs_all_diagonalized, parallel=parallel) + + counts = self.process_ready_circs_get_counts( + circs_all_diagonalized, parallel=parallel + ) # here we need to switch the little and big endian of distribution bitstrings distributions = [] @@ -786,19 +766,25 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): n_eigen_one = 0 n_eigen_minus_one = 0 for bitstring, n_count in distri.items(): - if np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 == 0: + if ( + np.dot(list(map(lambda x: eval(x), [*bitstring])), mask).sum() % 2 + == 0 + ): n_eigen_one += n_count else: n_eigen_minus_one += n_count - - expval = n_eigen_one / self.n_shots + (-1) * n_eigen_minus_one / self.n_shots + + expval = ( + n_eigen_one / self.n_shots + (-1) * n_eigen_minus_one / self.n_shots + ) expval_all.append(expval) return expval_all -if __name__ == '__main__': +if __name__ == "__main__": import pdb + pdb.set_trace() circ = QuantumCircuit(3) circ.h(0) @@ -806,11 +792,9 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): circ.cx(1, 2) circ.rx(0.1, 0) - qiskit_processor = QiskitProcessor( - use_real_qc=False - ) + qiskit_processor = QiskitProcessor(use_real_qc=False) - qiskit_processor.process_circs_get_joint_expval([circ], 'XII') + qiskit_processor.process_circs_get_joint_expval([circ], "XII") qdev = tq.QuantumDevice(n_wires=3, bsz=1) qdev.h(0) @@ -819,5 +803,5 @@ def process_circs_get_joint_expval(self, circs_all, observable, parallel=True): qdev.rx(0, 0.1) from torchquantum.measurement import expval_joint_sampling - print(expval_joint_sampling(qdev, 'XII', n_shots=8192)) + print(expval_joint_sampling(qdev, "XII", n_shots=8192)) diff --git a/torchquantum/plugin/qiskit/qiskit_pulse.py b/torchquantum/plugin/qiskit/qiskit_pulse.py index b9c78760..ab28774f 100644 --- a/torchquantum/plugin/qiskit/qiskit_pulse.py +++ b/torchquantum/plugin/qiskit/qiskit_pulse.py @@ -22,12 +22,8 @@ SOFTWARE. """ -import torch -import torchquantum as tq -from qiskit import pulse, QuantumCircuit -from qiskit.pulse import library -from qiskit.test.mock import FakeQuito, FakeArmonk, FakeBogota -from qiskit.compiler import assemble, schedule +from qiskit import pulse + from .qiskit_macros import IBMQ_PNAMES diff --git a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py index ce46ff04..b60427dd 100644 --- a/torchquantum/plugin/qiskit/qiskit_unitary_gate.py +++ b/torchquantum/plugin/qiskit/qiskit_unitary_gate.py @@ -15,19 +15,16 @@ """ from collections import OrderedDict -import numpy -from qiskit.circuit import Gate, ControlledGate -from qiskit.circuit import QuantumCircuit -from qiskit.circuit import QuantumRegister, Qubit -from qiskit.circuit.exceptions import CircuitError +import numpy +import qiskit +from qiskit.circuit import ControlledGate, Gate, QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit._utils import _compute_control_matrix +from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.library.standard_gates import U3Gate -from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.quantum_info.operators.predicates import is_unitary_matrix -from qiskit.quantum_info import OneQubitEulerDecomposer -from qiskit.quantum_info.synthesis.two_qubit_decompose import two_qubit_cnot_decompose -from qiskit.extensions.exceptions import ExtensionError +from qiskit.quantum_info.operators.predicates import is_unitary_matrix, matrix_equal +from qiskit.synthesis import OneQubitEulerDecomposer +from qiskit.synthesis.two_qubit.two_qubit_decompose import two_qubit_cnot_decompose _DECOMPOSER1Q = OneQubitEulerDecomposer("U3") @@ -58,12 +55,12 @@ def __init__(self, data, label=None): data = numpy.array(data, dtype=complex) # Check input is unitary if not is_unitary_matrix(data, atol=1e-5): - raise ExtensionError("Input matrix is not unitary.") + raise ValueError("Input matrix is not unitary.") # Check input is N-qubit matrix input_dim, output_dim = data.shape num_qubits = int(numpy.log2(input_dim)) if input_dim != output_dim or 2**num_qubits != input_dim: - raise ExtensionError("Input matrix is not an N-qubit operator.") + raise ValueError("Input matrix is not an N-qubit operator.") self._qasm_name = None self._qasm_definition = None @@ -116,7 +113,9 @@ def _define(self): else: q = QuantumRegister(self.num_qubits, "q") qc = QuantumCircuit(q, name=self.name) - qc.append(qiskit.circuit.library.Isometry(self.to_matrix(), 0, 0), qargs=q[:]) + qc.append( + qiskit.circuit.library.Isometry(self.to_matrix(), 0, 0), qargs=q[:] + ) self.definition = qc def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): @@ -155,7 +154,7 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): pmat = Operator(iso.inverse()).data @ cmat diag = numpy.diag(pmat) if not numpy.allclose(diag, diag[0]): - raise ExtensionError("controlled unitary generation failed") + raise ValueError("controlled unitary generation failed") phase = numpy.angle(diag[0]) if phase: # need to apply to _definition since open controls creates temporary definition diff --git a/torchquantum/plugin/qiskit_pulse.py b/torchquantum/plugin/qiskit_pulse.py index 81775b0d..30a4b162 100644 --- a/torchquantum/plugin/qiskit_pulse.py +++ b/torchquantum/plugin/qiskit_pulse.py @@ -1,10 +1,6 @@ -import torch -import torchquantum as tq -from qiskit import pulse, QuantumCircuit -from qiskit.pulse import library -from qiskit.test.mock import FakeQuito, FakeArmonk, FakeBogota -from qiskit.compiler import assemble, schedule -from .qiskit_macros import IBMQ_PNAMES +from qiskit import pulse + +from .qiskit.qiskit_macros import IBMQ_PNAMES def circ2pulse(circuits, name): @@ -24,7 +20,7 @@ def circ2pulse(circuits, name): >>> qc.cx(0, 1) >>> circ2pulse(qc, 'ibmq_oslo') """ - + if name in IBMQ_PNAMES: backend = name() with pulse.build(backend) as pulse_tq: diff --git a/torchquantum/pulse/pulse_utils.py b/torchquantum/pulse/pulse_utils.py index 68c66568..51803ab0 100644 --- a/torchquantum/pulse/pulse_utils.py +++ b/torchquantum/pulse/pulse_utils.py @@ -23,55 +23,30 @@ """ import copy -import sched -import qiskit -import itertools -import numpy as np +from typing import Union -from itertools import repeat -from qiskit.providers import aer -from qiskit.providers.fake_provider import * -from qiskit.circuit import Gate +import numpy as np +import qiskit +from qiskit import QuantumCircuit, pulse from qiskit.compiler import assemble -from qiskit import pulse, QuantumCircuit, IBMQ +from qiskit.providers.fake_provider import * +from qiskit.pulse import Schedule from qiskit.pulse.instructions import Instruction from qiskit.pulse.transforms import block_to_schedule -from qiskit_nature.drivers import UnitsType, Molecule -from scipy.optimize import minimize, LinearConstraint -from qiskit_nature.converters.second_quantization import QubitConverter -from qiskit_nature.properties.second_quantization.electronic import ParticleNumber -from qiskit_nature.problems.second_quantization import ElectronicStructureProblem -from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Any -from qiskit.pulse import ( - Schedule, - GaussianSquare, - Drag, - Delay, - Play, - ControlChannel, - DriveChannel, -) -from qiskit_nature.mappers.second_quantization import ParityMapper, JordanWignerMapper -from qiskit_nature.transformers.second_quantization.electronic import ( - ActiveSpaceTransformer, -) -from qiskit_nature.drivers.second_quantization import ( - ElectronicStructureDriverType, - ElectronicStructureMoleculeDriver, -) +from scipy.optimize import LinearConstraint def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): """ - Check if the instruction is a parametric pulse. + Check if the instruction is a parametric pulse. - Args: - t0 (tuple): Tuple containing the time and instruction. - inst (tuple): Tuple containing the instruction. + Args: + t0 (tuple): Tuple containing the time and instruction. + inst (tuple): Tuple containing the instruction. - Returns: - bool: True if the instruction is a parametric pulse, False otherwise. - """ + Returns: + bool: True if the instruction is a parametric pulse, False otherwise. + """ inst = t0[1] t0 = t0[0] if isinstance(inst, pulse.Play): @@ -82,14 +57,14 @@ def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): def extract_ampreal(pulse_prog): """ - Extract the real part of pulse amplitudes from the pulse program. + Extract the real part of pulse amplitudes from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - np.array: Array of real parts of pulse amplitudes. - """ + Returns: + np.array: Array of real parts of pulse amplitudes. + """ # extract the real part of pulse amplitude, igonred the imaginary part. amp_list = list( map( @@ -104,14 +79,14 @@ def extract_ampreal(pulse_prog): def extract_amp(pulse_prog): """ - Extract the pulse amplitudes from the pulse program. + Extract the pulse amplitudes from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - np.array: Array of pulse amplitudes. - """ + Returns: + np.array: Array of pulse amplitudes. + """ # extract the pulse amplitdue. amp_list = list( map( @@ -132,15 +107,15 @@ def extract_amp(pulse_prog): def is_phase_pulse(t0, *inst: Union["Schedule", Instruction]): """ - Check if the instruction is a phase pulse. + Check if the instruction is a phase pulse. - Args: - t0 (tuple): Tuple containing the time and instruction. - inst (tuple): Tuple containing the instruction. + Args: + t0 (tuple): Tuple containing the time and instruction. + inst (tuple): Tuple containing the instruction. - Returns: - bool: True if the instruction is a phase pulse, False otherwise. - """ + Returns: + bool: True if the instruction is a phase pulse, False otherwise. + """ inst = t0[1] t0 = t0[0] if isinstance(inst, pulse.ShiftPhase): @@ -150,14 +125,14 @@ def is_phase_pulse(t0, *inst: Union["Schedule", Instruction]): def extract_phase(pulse_prog): """ - Extract the phase values from the pulse program. + Extract the phase values from the pulse program. - Args: - pulse_prog (Schedule): The pulse program. + Args: + pulse_prog (Schedule): The pulse program. - Returns: - list: List of phase values. - """ + Returns: + list: List of phase values. + """ for _, ShiftPhase in pulse_prog.filter(is_phase_pulse).instructions: # print(play.pulse.amp) @@ -175,15 +150,15 @@ def extract_phase(pulse_prog): def cir2pul(circuit, backend): """ - Transform a quantum circuit to a pulse schedule. + Transform a quantum circuit to a pulse schedule. - Args: - circuit (QuantumCircuit): The quantum circuit. - backend: The backend for the pulse schedule. + Args: + circuit (QuantumCircuit): The quantum circuit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule. - """ + Returns: + Schedule: The pulse schedule. + """ # transform quantum circuit to pulse schedule with pulse.build(backend) as pulse_prog: pulse.call(circuit) @@ -192,15 +167,15 @@ def cir2pul(circuit, backend): def snp(qubit, backend): """ - Create a Schedule for the simultaneous z measurement of a qubit and a control qubit. + Create a Schedule for the simultaneous z measurement of a qubit and a control qubit. - Args: - qubit (int): The target qubit. - backend: The backend for the pulse schedule. + Args: + qubit (int): The target qubit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule for simultaneous z measurement. - """ + Returns: + Schedule: The pulse schedule for simultaneous z measurement. + """ circuit = QuantumCircuit(qubit + 1) circuit.h(qubit) sched = cir2pul(circuit, backend) @@ -210,16 +185,16 @@ def snp(qubit, backend): def tnp(qubit, cqubit, backend): """ - Create a Schedule for the simultaneous controlled-x measurement of a qubit and a control qubit. + Create a Schedule for the simultaneous controlled-x measurement of a qubit and a control qubit. - Args: - qubit (int): The target qubit. - cqubit (int): The control qubit. - backend: The backend for the pulse schedule. + Args: + qubit (int): The target qubit. + cqubit (int): The control qubit. + backend: The backend for the pulse schedule. - Returns: - Schedule: The pulse schedule for simultaneous controlled-x measurement. - """ + Returns: + Schedule: The pulse schedule for simultaneous controlled-x measurement. + """ circuit = QuantumCircuit(cqubit + 1) circuit.cx(qubit, cqubit) sched = cir2pul(circuit, backend) @@ -229,30 +204,30 @@ def tnp(qubit, cqubit, backend): def pul_append(sched1, sched2): """ - Append two pulse schedules. + Append two pulse schedules. - Args: - sched1 (Schedule): The first pulse schedule. - sched2 (Schedule): The second pulse schedule. + Args: + sched1 (Schedule): The first pulse schedule. + sched2 (Schedule): The second pulse schedule. - Returns: - Schedule: The combined pulse schedule. - """ + Returns: + Schedule: The combined pulse schedule. + """ sched = sched1.append(sched2) return sched def map_amp(pulse_ansatz, modified_list): """ - Map modified pulse amplitudes to the pulse ansatz. + Map modified pulse amplitudes to the pulse ansatz. - Args: - pulse_ansatz (Schedule): The pulse ansatz. - modified_list (list): List of modified pulse amplitudes. + Args: + pulse_ansatz (Schedule): The pulse ansatz. + modified_list (list): List of modified pulse amplitudes. - Returns: - Schedule: The pulse schedule with modified amplitudes. - """ + Returns: + Schedule: The pulse schedule with modified amplitudes. + """ sched = Schedule() for inst, amp in zip( pulse_ansatz.filter(is_parametric_pulse).instructions, modified_list @@ -274,18 +249,18 @@ def get_from(d: dict, key: str): def run_pulse_sim(measurement_pulse): """ - Run pulse simulations for the given measurement pulses. + Run pulse simulations for the given measurement pulses. - Args: - measurement_pulse (list): List of measurement pulses. + Args: + measurement_pulse (list): List of measurement pulses. - Returns: - list: List of measurement results. - """ + Returns: + list: List of measurement results. + """ measure_result = [] for measure_pulse in measurement_pulse: shots = 1024 - pulse_sim = qiskit.providers.aer.PulseSimulator.from_backend(FakeJakarta()) + pulse_sim = qiskit_aer.PulseSimulator.from_backend(FakeJakarta()) pul_sim = assemble( measure_pulse, backend=pulse_sim, @@ -306,14 +281,14 @@ def run_pulse_sim(measurement_pulse): def gen_LC(parameters_array): """ - Generate linear constraints for the optimization. + Generate linear constraints for the optimization. - Args: - parameters_array (np.array): Array of parameters. + Args: + parameters_array (np.array): Array of parameters. - Returns: - LinearConstraint: Linear constraint for the optimization. - """ + Returns: + LinearConstraint: Linear constraint for the optimization. + """ dim_design = int(len(parameters_array)) Mid = int(len(parameters_array) / 2) bound = np.ones((dim_design, 2)) * np.array([0, 0.9]) @@ -327,15 +302,15 @@ def gen_LC(parameters_array): def observe_genearte(pulse_ansatz, backend): """ - Generate measurement pulses for observing the pulse ansatz. + Generate measurement pulses for observing the pulse ansatz. - Args: - pulse_ansatz (Schedule): The pulse ansatz. - backend: The backend for the pulse schedule. + Args: + pulse_ansatz (Schedule): The pulse ansatz. + backend: The backend for the pulse schedule. - Returns: - list: List of measurement pulses. - """ + Returns: + list: List of measurement pulses. + """ qubits = 0, 1 with pulse.build(backend) as pulse_measurez0: # z measurement of qubit 0 and 1 diff --git a/torchquantum/pulse/templates/pulse_utils.py b/torchquantum/pulse/templates/pulse_utils.py index bad2a9b5..30d4c2f7 100644 --- a/torchquantum/pulse/templates/pulse_utils.py +++ b/torchquantum/pulse/templates/pulse_utils.py @@ -1,40 +1,15 @@ import copy -import sched -import qiskit -import itertools -import numpy as np +from typing import Union -from itertools import repeat -from qiskit.providers import aer -from qiskit.providers.fake_provider import * -from qiskit.circuit import Gate +import numpy as np +import qiskit +from qiskit import QuantumCircuit, pulse from qiskit.compiler import assemble -from qiskit import pulse, QuantumCircuit, IBMQ +from qiskit.providers.fake_provider import * +from qiskit.pulse import Schedule from qiskit.pulse.instructions import Instruction from qiskit.pulse.transforms import block_to_schedule -from qiskit_nature.drivers import UnitsType, Molecule -from scipy.optimize import minimize, LinearConstraint -from qiskit_nature.converters.second_quantization import QubitConverter -from qiskit_nature.properties.second_quantization.electronic import ParticleNumber -from qiskit_nature.problems.second_quantization import ElectronicStructureProblem -from typing import List, Tuple, Iterable, Union, Dict, Callable, Set, Optional, Any -from qiskit.pulse import ( - Schedule, - GaussianSquare, - Drag, - Delay, - Play, - ControlChannel, - DriveChannel, -) -from qiskit_nature.mappers.second_quantization import ParityMapper, JordanWignerMapper -from qiskit_nature.transformers.second_quantization.electronic import ( - ActiveSpaceTransformer, -) -from qiskit_nature.drivers.second_quantization import ( - ElectronicStructureDriverType, - ElectronicStructureMoleculeDriver, -) +from scipy.optimize import LinearConstraint def is_parametric_pulse(t0, *inst: Union["Schedule", Instruction]): @@ -154,7 +129,7 @@ def run_pulse_sim(measurement_pulse): measure_result = [] for measure_pulse in measurement_pulse: shots = 1024 - pulse_sim = qiskit.providers.aer.PulseSimulator.from_backend(FakeJakarta()) + pulse_sim = qiskit_aer.PulseSimulator.from_backend(FakeJakarta()) pul_sim = assemble( measure_pulse, backend=pulse_sim, diff --git a/torchquantum/util/utils.py b/torchquantum/util/utils.py index caeee471..23f67e5b 100644 --- a/torchquantum/util/utils.py +++ b/torchquantum/util/utils.py @@ -22,27 +22,26 @@ SOFTWARE. """ +from __future__ import annotations + import copy -from typing import Dict, Iterable, List, TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from opt_einsum import contract -from qiskit_ibm_runtime import QiskitRuntimeService from qiskit.exceptions import QiskitError -from qiskit.providers.aer.noise.device.parameters import gate_error_values from torchpack.utils.config import Config from torchpack.utils.logging import logger import torchquantum as tq from torchquantum.macro import C_DTYPE - if TYPE_CHECKING: - from torchquantum.module import QuantumModule from torchquantum.device import QuantumDevice + from torchquantum.module import QuantumModule else: QuantumModule = None QuantumDevice = None @@ -98,14 +97,14 @@ def pauli_eigs(n) -> np.ndarray: def diag(x): """ - Compute the diagonal matrix from a given input tensor. + Compute the diagonal matrix from a given input tensor. - Args: - x (torch.Tensor): Input tensor. + Args: + x (torch.Tensor): Input tensor. - Returns: - torch.Tensor: Diagonal matrix with the diagonal elements from the input tensor. - """ + Returns: + torch.Tensor: Diagonal matrix with the diagonal elements from the input tensor. + """ # input tensor, output tensor with diagonal as the input # manual implementation because torch.diag does not support autograd of # complex number @@ -120,20 +119,21 @@ def diag(x): class Timer(object): """ - Timer class to measure the execution time of a code block. + Timer class to measure the execution time of a code block. - Args: - device (str): Device to use for timing. Can be "gpu" or "cpu". - name (str): Name of the task being measured. - times (int): Number of times the task will be executed. + Args: + device (str): Device to use for timing. Can be "gpu" or "cpu". + name (str): Name of the task being measured. + times (int): Number of times the task will be executed. - Example: - # Measure the execution time of a code block on the GPU - with Timer(device="gpu", name="MyTask", times=100): - # Code block to be measured - ... + Example: + # Measure the execution time of a code block on the GPU + with Timer(device="gpu", name="MyTask", times=100): + # Code block to be measured + ... + + """ - """ def __init__(self, device="gpu", name="", times=100): self.device = device self.name = name @@ -158,20 +158,20 @@ def __exit__(self, exc_type, exc_value, tb): def get_unitary_loss(model: nn.Module): """ - Calculate the unitary loss of a model. + Calculate the unitary loss of a model. - The unitary loss measures the deviation of the trainable unitary matrices - in the model from the identity matrix. + The unitary loss measures the deviation of the trainable unitary matrices + in the model from the identity matrix. - Args: - model (nn.Module): The model containing trainable unitary matrices. + Args: + model (nn.Module): The model containing trainable unitary matrices. - Returns: - torch.Tensor: The unitary loss. + Returns: + torch.Tensor: The unitary loss. - Example: - loss = get_unitary_loss(model) - """ + Example: + loss = get_unitary_loss(model) + """ loss = 0 for name, params in model.named_parameters(): if "TrainableUnitary" in name: @@ -187,21 +187,21 @@ def get_unitary_loss(model: nn.Module): def legalize_unitary(model: nn.Module): """ - Legalize the unitary matrices in the model. + Legalize the unitary matrices in the model. - The function modifies the trainable unitary matrices in the model by applying - singular value decomposition (SVD) and reassembling the matrices using the - reconstructed singular values. + The function modifies the trainable unitary matrices in the model by applying + singular value decomposition (SVD) and reassembling the matrices using the + reconstructed singular values. - Args: - model (nn.Module): The model containing trainable unitary matrices. + Args: + model (nn.Module): The model containing trainable unitary matrices. - Returns: - None + Returns: + None - Example: - legalize_unitary(model) - """ + Example: + legalize_unitary(model) + """ with torch.no_grad(): for name, params in model.named_parameters(): if "TrainableUnitary" in name: @@ -212,22 +212,22 @@ def legalize_unitary(model: nn.Module): def switch_little_big_endian_matrix(mat): """ - Switches the little-endian and big-endian order of a multi-dimensional matrix. + Switches the little-endian and big-endian order of a multi-dimensional matrix. - The function reshapes the input matrix to a 2D or multi-dimensional matrix with dimensions - that are powers of 2. It then switches the order of the dimensions, effectively changing - the little-endian order to big-endian, or vice versa. The function can handle both - batched and non-batched matrices. + The function reshapes the input matrix to a 2D or multi-dimensional matrix with dimensions + that are powers of 2. It then switches the order of the dimensions, effectively changing + the little-endian order to big-endian, or vice versa. The function can handle both + batched and non-batched matrices. - Args: - mat (numpy.ndarray): The input matrix. + Args: + mat (numpy.ndarray): The input matrix. - Returns: - numpy.ndarray: The matrix with the switched endian order. + Returns: + numpy.ndarray: The matrix with the switched endian order. - Example: - switched_mat = switch_little_big_endian_matrix(mat) - """ + Example: + switched_mat = switch_little_big_endian_matrix(mat) + """ if len(mat.shape) % 2 == 1: is_batch_matrix = True bsz = mat.shape[0] @@ -251,25 +251,25 @@ def switch_little_big_endian_matrix(mat): def switch_little_big_endian_state(state): """ - Switches the little-endian and big-endian order of a quantum state vector. + Switches the little-endian and big-endian order of a quantum state vector. - The function reshapes the input state vector to a 1D or multi-dimensional state vector with - dimensions that are powers of 2. It then switches the order of the dimensions, effectively - changing the little-endian order to big-endian, or vice versa. The function can handle both - batched and non-batched state vectors. + The function reshapes the input state vector to a 1D or multi-dimensional state vector with + dimensions that are powers of 2. It then switches the order of the dimensions, effectively + changing the little-endian order to big-endian, or vice versa. The function can handle both + batched and non-batched state vectors. - Args: - state (numpy.ndarray): The input state vector. + Args: + state (numpy.ndarray): The input state vector. - Returns: - numpy.ndarray: The state vector with the switched endian order. + Returns: + numpy.ndarray: The state vector with the switched endian order. - Raises: - ValueError: If the dimension of the state vector is not 1 or 2. + Raises: + ValueError: If the dimension of the state vector is not 1 or 2. - Example: - switched_state = switch_little_big_endian_state(state) - """ + Example: + switched_state = switch_little_big_endian_state(state) + """ if len(state.shape) > 1: is_batch_state = True @@ -279,7 +279,7 @@ def switch_little_big_endian_state(state): is_batch_state = False reshape = [2] * int(np.log2(state.size)) else: - logger.exception(f"Dimension of statevector should be 1 or 2") + logger.exception("Dimension of statevector should be 1 or 2") raise ValueError original_shape = state.shape @@ -310,25 +310,25 @@ def switch_little_big_endian_state_test(): def get_expectations_from_counts(counts, n_wires): """ - Calculate expectation values from counts. + Calculate expectation values from counts. - This function takes a counts dictionary or a list of counts dictionaries - and calculates the expectation values based on the probability of measuring - the state '1' on each wire. The expectation values are computed as the - flipped difference between the probability of measuring '1' and the probability - of measuring '0' on each wire. + This function takes a counts dictionary or a list of counts dictionaries + and calculates the expectation values based on the probability of measuring + the state '1' on each wire. The expectation values are computed as the + flipped difference between the probability of measuring '1' and the probability + of measuring '0' on each wire. - Args: - counts (dict or list[dict]): The counts dictionary or a list of counts dictionaries. - n_wires (int): The number of wires. + Args: + counts (dict or list[dict]): The counts dictionary or a list of counts dictionaries. + n_wires (int): The number of wires. - Returns: - numpy.ndarray: The expectation values. + Returns: + numpy.ndarray: The expectation values. - Example: - counts = {'000': 10, '100': 5, '010': 15} - expectations = get_expectations_from_counts(counts, 3) - """ + Example: + counts = {'000': 10, '100': 5, '010': 15} + expectations = get_expectations_from_counts(counts, 3) + """ exps = [] if isinstance(counts, dict): counts = [counts] @@ -349,29 +349,33 @@ def get_expectations_from_counts(counts, n_wires): def find_global_phase(mat1, mat2, threshold): """ - Find a numerical stable global phase between two matrices. - - This function compares the elements of two matrices `mat1` and `mat2` - and identifies a numerical stable global phase by finding the first - non-zero element pair with absolute values greater than the specified - threshold. The global phase is calculated as the ratio of the corresponding - elements in `mat2` and `mat1`. - - Args: - mat1 (numpy.ndarray): The first matrix. - mat2 (numpy.ndarray): The second matrix. - threshold (float): The threshold for identifying non-zero elements. - - Returns: - float or None: The global phase ratio if a numerical stable phase is found, - None otherwise. - - Example: - mat1 = np.array([[1+2j, 0+1j], [0-1j, 2+3j]]) - mat2 = np.array([[2+4j, 0+2j], [0-2j, 4+6j]]) - threshold = 0.5 - global_phase = find_global_phase(mat1, mat2, threshold) - """ + Find a numerical stable global phase between two matrices. + + This function compares the elements of two matrices `mat1` and `mat2` + and identifies a numerical stable global phase by finding the first + non-zero element pair with absolute values greater than the specified + threshold. The global phase is calculated as the ratio of the corresponding + elements in `mat2` and `mat1`. + + Args: + mat1 (numpy.ndarray): The first matrix. + mat2 (numpy.ndarray): The second matrix. + threshold (float): The threshold for identifying non-zero elements. + + Returns: + float or None: The global phase ratio if a numerical stable phase is found, + None otherwise. + + Example: + mat1 = np.array([[1+2j, 0+1j], [0-1j, 2+3j]]) + mat2 = np.array([[2+4j, 0+2j], [0-2j, 4+6j]]) + threshold = 0.5 + global_phase = find_global_phase(mat1, mat2, threshold) + """ + if not isinstance(mat1, np.ndarray): + mat1 = np.asarray(mat1) + if not isinstance(mat2, np.ndarray): + mat2 = np.asarray(mat2) for i in range(mat1.shape[0]): for j in range(mat1.shape[1]): # find a numerical stable global phase @@ -380,7 +384,7 @@ def find_global_phase(mat1, mat2, threshold): return None -def build_module_op_list(m: QuantumModule, x=None) -> List: +def build_module_op_list(m: QuantumModule, x=None) -> list: """ serialize all operations in the module and generate a list with [{'name': RX, 'has_params': True, 'trainable': True, 'wires': [0], @@ -435,39 +439,39 @@ def build_module_op_list(m: QuantumModule, x=None) -> List: def build_module_from_op_list( - op_list: List[Dict], remove_ops=False, thres=None + op_list: list[dict], remove_ops=False, thres=None ) -> QuantumModule: """ - Build a quantum module from an operation list. - - This function takes an operation list, which contains dictionaries representing - quantum operations, and constructs a quantum module from those operations. - The module can optionally remove operations based on certain criteria, such as - low parameter values. The removed operations can be counted and logged. - - Args: - op_list (List[Dict]): The operation list, where each dictionary represents - an operation with keys: "name", "has_params", "trainable", "wires", - "n_wires", and "params". - remove_ops (bool): Whether to remove operations based on certain criteria. - Defaults to False. - thres (float): The threshold for removing operations. If a parameter value - is smaller in absolute value than this threshold, the corresponding - operation is removed. Defaults to None, in which case a threshold of - 1e-5 is used. - - Returns: - QuantumModule: The constructed quantum module. - - Example: - op_list = [ - {"name": "RX", "has_params": True, "trainable": True, "wires": [0], "n_wires": 2, "params": [0.5]}, - {"name": "CNOT", "has_params": False, "trainable": False, "wires": [0, 1], "n_wires": 2, "params": None}, - {"name": "RY", "has_params": True, "trainable": True, "wires": [1], "n_wires": 2, "params": [1.2]}, - ] - module = build_module_from_op_list(op_list, remove_ops=True, thres=0.1) - """ - logger.info(f"Building module from op_list...") + Build a quantum module from an operation list. + + This function takes an operation list, which contains dictionaries representing + quantum operations, and constructs a quantum module from those operations. + The module can optionally remove operations based on certain criteria, such as + low parameter values. The removed operations can be counted and logged. + + Args: + op_list (List[Dict]): The operation list, where each dictionary represents + an operation with keys: "name", "has_params", "trainable", "wires", + "n_wires", and "params". + remove_ops (bool): Whether to remove operations based on certain criteria. + Defaults to False. + thres (float): The threshold for removing operations. If a parameter value + is smaller in absolute value than this threshold, the corresponding + operation is removed. Defaults to None, in which case a threshold of + 1e-5 is used. + + Returns: + QuantumModule: The constructed quantum module. + + Example: + op_list = [ + {"name": "RX", "has_params": True, "trainable": True, "wires": [0], "n_wires": 2, "params": [0.5]}, + {"name": "CNOT", "has_params": False, "trainable": False, "wires": [0, 1], "n_wires": 2, "params": None}, + {"name": "RY", "has_params": True, "trainable": True, "wires": [1], "n_wires": 2, "params": [1.2]}, + ] + module = build_module_from_op_list(op_list, remove_ops=True, thres=0.1) + """ + logger.info("Building module from op_list...") thres = 1e-5 if thres is None else thres n_removed_ops = 0 ops = [] @@ -499,38 +503,38 @@ def build_module_from_op_list( if n_removed_ops > 0: logger.warning(f"Remove in total {n_removed_ops} pruned operations.") else: - logger.info(f"Do not remove any operations.") + logger.info("Do not remove any operations.") return tq.QuantumModuleFromOps(ops) def build_module_description_test(): """ - Test function for building module descriptions. - - This function demonstrates the usage of `build_module_op_list` and `build_module_from_op_list` - functions to build module descriptions and create quantum modules from those descriptions. - - Example: - import pdb - from torchquantum.plugins import tq2qiskit - from examples.core.models.q_models import QFCModel12 - - pdb.set_trace() - q_model = QFCModel12({"n_blocks": 4}) - desc = build_module_op_list(q_model.q_layer) - print(desc) - q_dev = tq.QuantumDevice(n_wires=4) - m = build_module_from_op_list(desc) - tq2qiskit(q_dev, m, draw=True) - - desc = build_module_op_list( - tq.RandomLayerAllTypes(n_ops=200, wires=[0, 1, 2, 3], qiskit_compatible=True) - ) - print(desc) - m1 = build_module_from_op_list(desc) - tq2qiskit(q_dev, m1, draw=True) - """ + Test function for building module descriptions. + + This function demonstrates the usage of `build_module_op_list` and `build_module_from_op_list` + functions to build module descriptions and create quantum modules from those descriptions. + + Example: + import pdb + from torchquantum.plugins import tq2qiskit + from examples.core.models.q_models import QFCModel12 + + pdb.set_trace() + q_model = QFCModel12({"n_blocks": 4}) + desc = build_module_op_list(q_model.q_layer) + print(desc) + q_dev = tq.QuantumDevice(n_wires=4) + m = build_module_from_op_list(desc) + tq2qiskit(q_dev, m, draw=True) + + desc = build_module_op_list( + tq.RandomLayerAllTypes(n_ops=200, wires=[0, 1, 2, 3], qiskit_compatible=True) + ) + print(desc) + m1 = build_module_from_op_list(desc) + tq2qiskit(q_dev, m1, draw=True) + """ import pdb from torchquantum.plugin import tq2qiskit @@ -560,7 +564,7 @@ def get_p_v_reg_mapping(circ): """ try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() mapping = { "p2v": {}, @@ -601,7 +605,7 @@ def get_v_c_reg_mapping(circ): """ try: p2v_orig = circ._layout.final_layout.get_physical_bits().copy() - except: + except AttributeError: p2v_orig = circ._layout.get_physical_bits().copy() p2v = {} for p, v in p2v_orig.items(): @@ -630,15 +634,15 @@ def get_v_c_reg_mapping(circ): def get_cared_configs(conf, mode) -> Config: """ - Get the relevant configurations based on the mode. + Get the relevant configurations based on the mode. - Args: - conf (Config): The configuration object. - mode (str): The mode indicating the desired configuration. + Args: + conf (Config): The configuration object. + mode (str): The mode indicating the desired configuration. - Returns: - Config: The modified configuration object with only the relevant configurations preserved. - """ + Returns: + Config: The modified configuration object with only the relevant configurations preserved. + """ conf = copy.deepcopy(conf) ignores = [ @@ -706,55 +710,39 @@ def get_cared_configs(conf, mode) -> Config: def get_success_rate(properties, transpiled_circ): """ - Estimate the success rate of a transpiled quantum circuit. - - Args: - properties (list): List of gate error properties. - transpiled_circ (QuantumCircuit): The transpiled quantum circuit. - - Returns: - float: The estimated success rate. - """ - # estimate the success rate according to the error rates of single and - # two-qubit gates in transpiled circuits - - gate_errors = gate_error_values(properties) - # construct the error dict - gate_error_dict = {} - for gate_error in gate_errors: - if gate_error[0] not in gate_error_dict.keys(): - gate_error_dict[gate_error[0]] = {tuple(gate_error[1]): gate_error[2]} - else: - gate_error_dict[gate_error[0]][tuple(gate_error[1])] = gate_error[2] + Estimate the success rate of a transpiled quantum circuit. - success_rate = 1 - for gate in transpiled_circ.data: - gate_success_rate = ( - 1 - gate_error_dict[gate[0].name][tuple(map(lambda x: x.index, gate[1]))] - ) - if gate_success_rate == 0: - gate_success_rate = 1e-5 - success_rate *= gate_success_rate + Args: + properties (list): List of gate error properties. + transpiled_circ (QuantumCircuit): The transpiled quantum circuit. + + Returns: + float: The estimated success rate. + """ + raise NotImplementedError - return success_rate def get_provider(backend_name, hub=None): """ - Get the provider object for a specific backend from IBM Quantum. + Get the provider object for a specific backend from IBM Quantum. - Args: - backend_name (str): Name of the backend. - hub (str): Optional hub name. + Args: + backend_name (str): Name of the backend. + hub (str): Optional hub name. - Returns: - IBMQProvider: The provider object. - """ + Returns: + IBMQProvider: The provider object. + """ # mass-inst-tech-1 or MIT-1 if backend_name in ["ibmq_casablanca", "ibmq_rome", "ibmq_bogota", "ibmq_jakarta"]: if hub == "mass" or hub is None: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/mass-inst-tech-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/mass-inst-tech-1/main" + ) elif hub == "mit": - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/MIT-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/MIT-1/main" + ) else: raise ValueError(f"not supported backend {backend_name} in hub " f"{hub}") elif backend_name in [ @@ -764,38 +752,51 @@ def get_provider(backend_name, hub=None): "ibmq_guadalupe", "ibmq_montreal", ]: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-ornl/anl/csc428") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-ornl/anl/csc428" + ) else: if hub == "mass" or hub is None: try: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/mass-inst-tech-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", + instance="ibm-q-research/mass-inst-tech-1/main", + ) except QiskitError: # logger.warning(f"Cannot use MIT backend, roll back to open") - logger.warning(f"Use the open backend") - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q/open/main") + logger.warning("Use the open backend") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q/open/main" + ) elif hub == "mit": - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q-research/MIT-1/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q-research/MIT-1/main" + ) else: - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = "ibm-q/open/main") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance="ibm-q/open/main" + ) return provider def get_provider_hub_group_project(hub="ibm-q", group="open", project="main"): - provider = QiskitRuntimeService(channel = "ibm_quantum", instance = f"{hub}/{group}/{project}") + provider = QiskitRuntimeService( + channel="ibm_quantum", instance=f"{hub}/{group}/{project}" + ) return provider def normalize_statevector(states): """ - Normalize a statevector to ensure the square magnitude of the statevector sums to 1. + Normalize a statevector to ensure the square magnitude of the statevector sums to 1. - Args: - states (torch.Tensor): The statevector tensor. + Args: + states (torch.Tensor): The statevector tensor. - Returns: - torch.Tensor: The normalized statevector tensor. - """ + Returns: + torch.Tensor: The normalized statevector tensor. + """ # make sure the square magnitude of statevector sum to 1 # states = states.contiguous() original_shape = states.shape @@ -826,7 +827,7 @@ def get_circ_stats(circ): for gate in circ.data: op_name = gate[0].name - wires = list(map(lambda x: x.index, gate[1])) + wires = [circ.find_bit(qb).index for qb in gate.qubits] if op_name in n_gates_dict.keys(): n_gates_dict[op_name] += 1 else: @@ -854,7 +855,7 @@ def get_circ_stats(circ): def partial_trace( q_device: QuantumDevice, - keep_indices: List[int], + keep_indices: list[int], ) -> torch.Tensor: """Returns a density matrix with only some qubits kept. Args: @@ -957,22 +958,22 @@ def dm_to_mixture_of_state(dm: torch.Tensor, atol=1e-10): def partial_trace_test(): """ - Test function for performing partial trace on a quantum device. + Test function for performing partial trace on a quantum device. - This function demonstrates how to use the `partial_trace` function from `torchquantum.functional` - to perform partial trace on a quantum device. + This function demonstrates how to use the `partial_trace` function from `torchquantum.functional` + to perform partial trace on a quantum device. - The function applies Hadamard gate on the first qubit and a CNOT gate between the first and second qubits. - Then, it performs partial trace on the first qubit and converts the resulting density matrices into - mixtures of states. + The function applies Hadamard gate on the first qubit and a CNOT gate between the first and second qubits. + Then, it performs partial trace on the first qubit and converts the resulting density matrices into + mixtures of states. - Prints the resulting mixture of states. + Prints the resulting mixture of states. - Note: This function assumes that you have already imported the necessary modules and functions. + Note: This function assumes that you have already imported the necessary modules and functions. - Returns: - None - """ + Returns: + None + """ import torchquantum.functional as tqf n_wires = 4 @@ -987,7 +988,8 @@ def partial_trace_test(): print(mixture) -def pauli_string_to_matrix(pauli: str, device=torch.device('cpu')) -> torch.Tensor: + +def pauli_string_to_matrix(pauli: str, device=torch.device("cpu")) -> torch.Tensor: mat_dict = { "paulix": torch.tensor([[0, 1], [1, 0]], dtype=C_DTYPE), "pauliy": torch.tensor([[0, -1j], [1j, 0]], dtype=C_DTYPE), @@ -1008,68 +1010,82 @@ def pauli_string_to_matrix(pauli: str, device=torch.device('cpu')) -> torch.Tens matrix = torch.kron(matrix, pauli_dict[op].to(device)) return matrix + if __name__ == "__main__": build_module_description_test() switch_little_big_endian_matrix_test() switch_little_big_endian_state_test() -def parameter_shift_gradient(model, input_data, expectation_operator, shift_rate=np.pi*0.5, shots=1024): - ''' - This function calculates the gradient of a parametrized circuit using the parameter shift rule to be fed into - a classical optimizer, its formula is given by - gradient for the ith parameter =( expectation_value(the_ith_parameter + shift_rate)-expectation_value(the_ith_parameter - shift_rate) ) *0.5 - Args: +def parameter_shift_gradient( + model, input_data, expectation_operator, shift_rate=np.pi * 0.5, shots=1024 +): + """ + This function calculates the gradient of a parametrized circuit using the parameter shift rule to be fed into + a classical optimizer, its formula is given by + gradient for the ith parameter =( expectation_value(the_ith_parameter + shift_rate)-expectation_value(the_ith_parameter - shift_rate) ) *0.5 + Args: model(tq.QuantumModule): the model that you want to use, which includes the quantum device and the parameters input(torch.tensor): the input data that you are using - expectation_operator(str): the observable that you want to calculate the expectation value of, usually the Z operator + expectation_operator(str): the observable that you want to calculate the expectation value of, usually the Z operator (i.e 'ZZZ' for 3 qubits or 3 wires) shift_rate(float , optional): the rate that you would like to shift the parameter with at every iteration, by default pi*0.5 shots(int , optional): the number of shots to use per parameter ,(for 10 parameters and 1024 shots = 10240 shots in total) by default = 1024. Returns: - torch.tensor : An array of the gradients of all the parameters in the circuit. - ''' + torch.tensor : An array of the gradients of all the parameters in the circuit. + """ par_num = [] - for p in model.parameters():#since the model.parameters() Returns an iterator over module parameters,to get the number of parameter i have to iterate over all of them + for ( + p + ) in ( + model.parameters() + ): # since the model.parameters() Returns an iterator over module parameters,to get the number of parameter i have to iterate over all of them par_num.append(p) gradient_of_par = torch.zeros(len(par_num)) - - def clone_model(model_to_clone):#i have to note:this clone_model function was made with GPT + + def clone_model( + model_to_clone, + ): # i have to note:this clone_model function was made with GPT cloned_model = type(model_to_clone)() # Create a new instance of the same class - cloned_model.load_state_dict(model_to_clone.state_dict()) # Copy the state dictionary + cloned_model.load_state_dict( + model_to_clone.state_dict() + ) # Copy the state dictionary return cloned_model # Clone the models - model_plus_shift = clone_model(model) + model_plus_shift = clone_model(model) model_minus_shift = clone_model(model) - state_dict_plus_shift = model_plus_shift.state_dict() + state_dict_plus_shift = model_plus_shift.state_dict() state_dict_minus_shift = model_minus_shift.state_dict() ##################### for idx, key in enumerate(state_dict_plus_shift): - if idx < 2: # Skip the first two keys because they are not paramters + if idx < 2: # Skip the first two keys because they are not parameters continue - state_dict_plus_shift[key] += shift_rate - state_dict_minus_shift[key] -= shift_rate - - model_plus_shift.load_state_dict(state_dict_plus_shift ) + state_dict_plus_shift[key] += shift_rate + state_dict_minus_shift[key] -= shift_rate + + model_plus_shift.load_state_dict(state_dict_plus_shift) model_minus_shift.load_state_dict(state_dict_minus_shift) - + model_plus_shift.forward(input_data) model_minus_shift.forward(input_data) - - state_dict_plus_shift = model_plus_shift.state_dict() + + state_dict_plus_shift = model_plus_shift.state_dict() state_dict_minus_shift = model_minus_shift.state_dict() - - - - expectation_plus_shift = tq.expval_joint_sampling(model_plus_shift.q_device, observable=expectation_operator, n_shots=shots) - expectation_minus_shift = tq.expval_joint_sampling(model_minus_shift.q_device, observable=expectation_operator, n_shots=shots) + expectation_plus_shift = tq.expval_joint_sampling( + model_plus_shift.q_device, observable=expectation_operator, n_shots=shots + ) + expectation_minus_shift = tq.expval_joint_sampling( + model_minus_shift.q_device, observable=expectation_operator, n_shots=shots + ) + + state_dict_plus_shift[key] -= shift_rate + state_dict_minus_shift[key] += shift_rate - state_dict_plus_shift[key] -= shift_rate - state_dict_minus_shift[key] += shift_rate - - gradient_of_par[idx-2] = (expectation_plus_shift - expectation_minus_shift) * 0.5 + gradient_of_par[idx - 2] = ( + expectation_plus_shift - expectation_minus_shift + ) * 0.5 return gradient_of_par