├── LICENSE ├── README.md ├── notebooks ├── README.md ├── cubature_example.ipynb ├── ekf_example.ipynb ├── linear_test.ipynb └── runtime.ipynb ├── parsmooth ├── __init__.py ├── cubature_common │ └── __init__.py ├── models │ ├── __init__.py │ ├── bearings.py │ └── linear.py ├── parallel │ ├── __init__.py │ ├── ckf.py │ ├── cks.py │ ├── ekf.py │ ├── eks.py │ └── operators.py ├── sequential │ ├── __init__.py │ ├── cubature.py │ └── extended.py └── utils.py ├── requirements.txt └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 EEA-sensors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parallel Iterated Extended and Sigma-Point Kalman Smoothers 2 | 3 | Companion code in JAX for the paper Parallel Iterated Extended and Sigma-Point Kalman Smoothers [2]. 4 | 5 | What is it? 6 | ----------- 7 | 8 | This is an implementation of parallelized Extended and Sigma-Points Bayesian Filters and Smoothers with CPU/GPU/TPU support coded using [JAX](https://github.com/google/jax) primitives, in particular [associative scan](https://en.wikipedia.org/wiki/Prefix_sum?wprov=sfla1). 9 | 10 | Supported features 11 | ------------------ 12 | 13 | * Extended Kalman Filtering and Smoothing 14 | * Cubature Kalman Filtering and Smoothing 15 | * Iterated versions of the above 16 | 17 | Installation 18 | ------------ 19 | - With GPU CUDA 11.0 support 20 | - Using pip 21 | Run `pip install https://github.com/EEA-sensors/parallel-non-linear-gaussian-smoothers.git -f https://storage.googleapis.com/jax-releases/jax_releases.html` 22 | - By cloning 23 | Clone https://github.com/EEA-sensors/parallel-non-linear-gaussian-smoothers.git 24 | Run `pip install -r requirements.txt -f https://storage.googleapis.com/jax-releases/jax_releases.html` 25 | Run `python setup.py [install|develop]` depending on the level of installation you want 26 | - Without GPU support 27 | - By cloning 28 | Clone https://github.com/EEA-sensors/parallel-non-linear-gaussian-smoothers.git 29 | Run `python setup.py [install|develop] --no-deps` depending on the level of installation you want 30 | 31 | Manually install the dependencies `jax` and `jaxlib`, and for examples only `matplotlib`, `numba`, `tqdm` 32 | 33 | Example 34 | ------- 35 | 36 | ```python 37 | from parsmooth.parallel import ieks 38 | from parsmooth.utils import MVNormalParameters 39 | 40 | initial_guess = MVNormalParameters(...) 41 | data = ... 42 | Q = ... # transition noise covariance matrix 43 | R = ... # observation error covariance matrix 44 | 45 | def transition_function(x): 46 | ... 47 | return next_x 48 | 49 | def observation_function(x): 50 | ... 51 | return obs 52 | 53 | iterated_smoothed_trajectories = ieks(initial_guess, 54 | data, 55 | transition_function, 56 | Q, 57 | observation_function, 58 | R, 59 | n_iter=100) # runs the parallel IEKS 100 times. 60 | 61 | ``` 62 | 63 | For more examples, see the [notebooks](https://github.com/EEA-sensors/parallel-non-linear-gaussian-smoothers/tree/master/notebooks). 64 | 65 | Acknowlegments 66 | -------------- 67 | This JAX-based code was created by [Adrien Corenflos](https://adriencorenflos.github.io/) to implement the original idea by [Fatemeh Yaghoobi](https://fatameh-yaghoobi.github.io/) [2] who provided the initial code for the parallelized extended Kalman filter in pure Python. The sequential cubature filtering code was adapted from some original code by [Zheng Zhao](https://users.aalto.fi/~zhaoz1/). 68 | 69 | References 70 | ---------- 71 | 72 | [1] S. Särkkä and A. F. García-Fernández. *Temporal Parallelization of Bayesian Smoothers.* In: IEEE Transactions on Automatic Control 2020. 73 | 74 | [2] F. Yaghoobi and A. Corenflos and S. Hassan and S. Särkkä. *Parallel Iterated Extended and Sigma-Points Kalman Smoothers.* To appear in Proceedings of IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP). ([arXiv](http://arxiv.org/abs/2102.00514)) 75 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | ### Author: [Adrien Corenflos](https://github.com/AdrienCorenflos/) 2 | 3 | These notebooks illustrate the parallel implementation of iterated Kalman filters and smoothers on GPU. They can be downloaded to be run locally or on Google Colab. 4 | -------------------------------------------------------------------------------- /notebooks/ekf_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Imports" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 16, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import jax.numpy as jnp\n", 17 | "from jax.config import config\n", 18 | "\n", 19 | "from parsmooth.sequential import ieks as seq_ieks\n", 20 | "from parsmooth.models.bearings import get_data, make_parameters, plot_bearings\n", 21 | "from parsmooth.parallel import ekf, eks, ieks\n", 22 | "from parsmooth.utils import MVNormalParameters\n", 23 | "\n", 24 | "# from jax import jit\n", 25 | "\n", 26 | "jax.config.update(\"jax_debug_nans\", False)\n", 27 | "jax.config.update(\"jax_enable_x64\", False)\n", 28 | "jax.config.update(\"jax_platform_name\", \"gpu\")" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "### Input parameters" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "s1 = jnp.array([-1.5, 0.5]) # First sensor location\n", 45 | "s2 = jnp.array([1., 1.]) # Second sensor location\n", 46 | "r = 0.5 # Observation noise (stddev)\n", 47 | "dt = 0.01 # discretization time step\n", 48 | "x0 = jnp.array([0.1, 0.2, 1, 0]) # initial true location\n", 49 | "qc = 0.01 # discretization noise\n", 50 | "qw = 0.1 # discretization noise\n", 51 | "\n", 52 | "T = 1000 # number of observations" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "### Get parameters" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 3, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "Q, R, observation_function, transition_function = make_parameters(qc, qw, r, dt, s1, s2)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "### Get data" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 4, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "ts, true_states, observations = get_data(x0, dt, r, T, s1, s2, random_state=42)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "### We can now run the filter and smoother" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "Initial state guess" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 5, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "m = jnp.array([-1., -1., 0., 0., 0.])\n", 108 | "P = jnp.eye(5)\n", 109 | "\n", 110 | "initial_guess = MVNormalParameters(m, P)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "Run the filter" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 6, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "initial_states = MVNormalParameters(jnp.zeros((T + 1, 5)), jnp.repeat(jnp.eye(5).reshape(1, 5, 5), T + 1, axis=0))" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 7, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "filtered = ekf(initial_guess,\n", 136 | " observations,\n", 137 | " transition_function,\n", 138 | " Q,\n", 139 | " observation_function,\n", 140 | " R,\n", 141 | " initial_states.mean)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 8, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "smoothed = eks(transition_function, Q, filtered)\n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "source": [ 156 | "### And the iterated one" 157 | ], 158 | "metadata": { 159 | "collapsed": false 160 | } 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 9, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "iterated_smoothed_trajectories = ieks(initial_guess,\n", 169 | " observations,\n", 170 | " transition_function,\n", 171 | " Q,\n", 172 | " observation_function,\n", 173 | " R,\n", 174 | " n_iter=15)" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 17, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "seq_iterated_smoothed_trajectories = seq_ieks(initial_guess,\n", 184 | " observations,\n", 185 | " transition_function,\n", 186 | " Q,\n", 187 | " observation_function,\n", 188 | " R,\n", 189 | " None,\n", 190 | " n_iter=15,\n", 191 | " propagate_first=False)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "### For comparison we can run the sequential iterated smoother too" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "### Plot the result" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 11, 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "data": { 215 | "text/plain": "
", 216 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA3IAAAI/CAYAAAAyZuvnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3gc1dXA4d9sVVn13iWry92We+82vZleA8kXQhJCSQgJpBBCAqEkkARIAiGQ0DsY44Yb7r3IsizJ6pLV26ptm++PMRjjIsmWtCrnfR492p2dcsaW5T177z1HUVUVIYQQQgghhBADh87dAQghhBBCCCGE6B5J5IQQQgghhBBigJFETgghhBBCCCEGGEnkhBBCCCGEEGKAkUROCCGEEEIIIQYYSeSEEEIIIYQQYoAxuDuAswkODlbj4+PdHYYQQgghhBBCuMWuXbtqVFUN+fb2fp3IxcfHs3PnTneHIYQQQgghhBBuoShK0em2y9RKIYQQQgghhBhgJJETQgghhBBCiAFGEjkhhBBCCCGEGGAkkRNCCCGEEEKIAUYSOSGEEEIIIYQYYHokkVMU5WVFUaoURTl4htdnK4rSqCjK3uNfv+qJ6wohhBBCCCHEUNRT7QdeAf4KvHqWfTaqqnpRD11PCCGEEEIIIYasHhmRU1V1A1DXE+cSQgghhBBCCHF2fblGboqiKPsURVmuKMrwPryuEEIIIYQQQgwqPTW1sjO7gThVVa2KolwAfAgkn25HRVG+B3wPIDY2to/CE0IIIYQQQoiBo09G5FRVbVJV1Xr88WeAUVGU4DPs+w9VVTNVVc0MCQnpi/CEEEIIIYQQYkDpk0ROUZRwRVGU448nHr9ubV9cWwghhBBCCCEGmx6ZWqkoyhvAbCBYUZRS4NeAEUBV1ReAq4A7FUVxAG3Ataqqqj1xbSGEEEIIIYQYanokkVNV9bpOXv8rWnsCIYQQQgghhBDnqS+rVgohhBBCCCGE6AGSyAkhhBBCCCHEACOJnBBCCCGEEEIMMJLICSGEEEIIIcQAI4mcEEIIIYQQQgwwksgJIYQQQgghxAAjiZwQQgghhBBCDDCSyAkhhBBCCCHEANMjDcGFEEIIIUT/5nC6qGhsp6q5nermDmqsNoItZhaPCAfgNx9n0dLhQK9T0OkUjDqFcXEBXDomCoBdRXWE+ngQ6e+JXqe481aEEEgiJ4QQQggx6LhcKkV1rTS22RkT4w/AT9/dT0Vj29f7GPU6piUFf/083M+D1g4HTlXF6QK704XTpQLQ4XDy5IojqKgY9TpiA70YEeXHtMRgYoO8+vTehBAaSeSEEEIIIQaBkrpWdhXVk13RxJHKZtrsToK8zfz1+rEoisJlY6PQKwrhfh6EWMz4ehpQlBMja9+flXjGcxt1Op64ahSFtS0U1raSX2Xlk33l+HsaiQ3yot3uJK/KyvBI35POKYToPZLICSGEEEIMQB0OJ4fKmxgT44+iKKzIOsbq7EpiAryYlhRMeoQvqeE+XydWs1JCzvlaOp1CTKAXMYFezEjWtrXaHCho595ytJYX1+cT6uPBnLQQ5qeH4eNhPO97FEKcmaKqqrtjOKPMzEx1586d7g5DCCGEEKJfcLlUssqb2JhXzY6COtrsTh69bCRJoRZqrB0YdAr+XqY+j6vD4WRnYT1fHK4iq7wRs0HPgowwrs6MwWSQ2npCnA9FUXapqpr57e0yIieEEEIIMQCU1LXy2GfZ1Lfa8DTqmTQsiKmJQcQfX6MWbDG7LTazQc+0pGCmJQVTUtfKh3vKyKuyYtTLNEsheoskckIIIYQQ/ZDLpbKnpB67U2XysCDC/TxIj/BlYkIgY2P9MRv07g7xtGICvfjRvGScLhVFUWhotfGnFTncMCmOjEhfd4cnxKAhiZwQQgghRD/SbneyJruKz7OOUd3cTkqYD5OHBWHU6/jxvGR3h9dlX7UoqG+109zu4JFPs7hgZATXTIjpt0moEAOJJHJCCCGEEP3El7k1vLK5EGuHnbRwX26YFEtmXIC7wzovCcHePHHVKF7fVsxnByo4VN7EvQtSCPX1cHdoQgxoksgJIYQQQriRtcOBAnibDVg8DCSHWbh8bBQpYT7uDq3HeBj1fGd6AmNj/fnrF3m8t7uMO2efud2BEKJzUrVSCCGEEMINOhxOPj94jI/3ljM7LZSbJse5O6Q+UdXUjo+HEU+THrvThVEvVS2FOBupWimEEEII0Q84nC7W5lTz/u5S6lttjIsNYFbyufd4G2i+mlLZZnPyyKdZzEoJYfGICDdHJcTAI4mcEEIIIUQfem1rESuyjpEa5sPd85NJCx+alRyNeoUQi5lXNheiUxTmp4fiaGnH0dqOo7UD1dMbs8UTo9pOR2kZ9TV2UFVUlzabzBQZiU+kH2adA3tjEza7DoOXB2Z/b3RSTEUMAZLICSGEEEL0sqrmdnSKQrDFzJIREYyI8iMzLgBFGfx91lSHA1vFMSpzqmipaqS1toW2xnaU6HguHhWDV2kBzT/7Ka+HL8KgO/HnYYyJJX1eIvHmcsqffYGd5jknndcUZ2X4gkSi7PmU/eM19phnfP2aQadiGZHC8PlJBNtKqNu4jTJbGF4BXngFWfAK9cc3NQ5Pf88h8XcgBidJ5IQQQggheonD6eLjfeV8sKeM8XEB/GR+CuF+HoT7Da6KjarDQV12MfX5x2iuaMBaZcUeGEXQ8FjSk1SOPfQw283zcKKNlJkMKhavNlSHyi2LRvF+1l6q2zsYmxRCSKAXerMBz8RYAhICMJssRP7sHqbXOABQdAooCoawcCxhvpg6kgn9zo2MLrPhaHdga7Vhb7Wjj/bFw9uIs9ZKc14ZBa3eqKoVqALAnNxM5iVJBFQdpHr7QaqVSCxhvvgnhBGUEYs5YPAUmxGDkxQ7EUIIIYToBXlVzby4/igl9a1MSgjipilxBFvM7g7rvHXUN1O97yjNrToMIaEMS/em7Mc/Zo9uMm2KNwBmo4pfUiQxM4aTOMqftj17sOKDZ3gg3hHB6M3Gk85p7XDw/Lo8bpkS32ttCVwuF+3VDbSU19Fa1YAzNJawYX44t62ncOUespui+fptsaLgnzmSKZcnYSrLoa2pA2NcHN4xoeh0UpxF9K0zFTuRRE4IIYQQoodtya/l2TW5BHgbuX16AuPjAt0d0jlRVRVFUbBu/JLcLaWUljpoadOmIup8fQkek8Tcm9JpeP8DrB6heESF45sYicnieV7XVFXQ6fp2yqPL4cRaUkn9kXIaKpqwhyYwak4M9c8+Td7hNkoMSZgMKv6BBoISg4lcMImgKEufxymGHknkhBBCCCF6mcPpwqDX0dLh4MO9ZVwxNhpP08ApvOFyuWjILqZ8Ry611Q7afKNZcPtwqn77GwqqvWgNiCMw2peQtEiCRsbjFdqzzcqdLpXnvsglyt+TpZkxPXruc6Xa7dRlFXJsXxG1hXXU13TQYfDFMyGOC+8aRf2/X6ZBH4pHUgJhE1IweAz8UVfRv0j7ASGEEEKIXuJwunh/dxl7Shp45NLheJsN3DBp4PSFaztwkMLPd5J91IDNoY0weXgoxF6UjsPmJPRnPyXcYun1wiB6nYJBp/Dh3jImJgQSF+Tdq9frCsVoJGhMMkFjkr/e1tHcTkuzE8XlxFZayqFSOy2bm9G9tp+AAIXoSUnEzh2Nt78kdaL3SCInhBBCCHEeyhva+OvaPI5WW5mdGorTpWLsx4NwLpeLuoOFlG4+TKNPPKlTo/DIz4PCbILCxhOWGkrExGT8EqO+cZTxjOfrabdMjedAWSMvrM/n95eN7JdTF80+HpiP10KJ+M1vWFjXRPnWHCqzSqkqbiFrr5UmQxnjZwTQtHw5HTHDCZ08HL1R3nqLniNTK4UQQgghztGXuTX8c+NRjHod35s5jIkJ/XMtnKqqtOYe5dCyg5TnN9PWoSVHwaOHMXxJBmExnihGY78pxb85r4Znv8jlezOHMTctzN3hdJu1oR2XU8VYkkPps/9gl34aRoNKeLQHsZMTiZwxEr1JkjrRNTK1UgghhBCiB2mtBcpICPbmx/OSCfQ2uTukk7hcLhrLm2lrVwk2NlL96COUmmdhCfQgZUo4MbNGYokOcXeYpzUlMYgVWcf4dF8Fc1JD+02C2VUW/+OVN4PGEP/cU+jX7qN4RxHlxe2UFB7CctDJ5CvTCPBX0Hl7Dbj7E/2DjMgJIYQQQnRDZVM7vh5GPE16Glpt+HgY0fej6X/W0mryP9tF8cEa2s0B+KbGseA7GbRt24YpYyRGP4u7Q+ySY43teJn1+Hr03bTO3mZvaadsSw41Nj9Gzo6i8flnqSjpwDFsFIlLxuKfHNX5ScSQI1UrhRBCCCHO0/aCOp5fl8fUxGC+O3OYu8M5Sevu3Rz6cDd5FVrpfz8fiJsQQ9LlUzF5DNxJWKqq4nSpGPSDr39by9at7P80m6JKbTTX3xeGzUgk8cKJ6I2D737FuZGplUIIIYQQ50hVVd7dVcp7u0tJDLFw2dj+MXLSWFBBaamLmPRAHAez8G4qJnnkRIYtGk1Aav8o338+2u1OfvvJIaYmBnHx6Eh3h9PjvCdPZsrkyYwoqeLoZ7spOljN/u3NVNnzmXZZPI7aWozh4e4OU/RTksgJIYQQQpxFu93J39fmsb2wjlkpodwxIwGjG0eHXA4nRSt3k78xj5oaF6b4OLz9zMRevZSAm8yDar2Vh1GPh1HHykOVXDgyol9WsOwJPjGhjP6/xYx0uagtbgKdntadOzn2/L8oCJ9D4uwU4hZnotPJKJ04QRI5IYQQQoizaGq3k1PZzE2T47lgZLjbEiWXzUbzmi9Y/2klrTYDJoNK8kh/Ui5PwScm2C0x9YXFw8N5ZvURdhfXkxnfP6uC9hSdTkdIvD8ATt90jIsvo3lTA9s+yGX/8hySJ0aRsnQGhkG0blCcO0nkhBBCCCFOo7S+lSh/T0J9PPjzNWPxNLmnOVxrQxsVR5tJGO5H07JPiQoYTsDkscQtGI/ePPjf0GfGBxLgZWL9kepBn8h9k97fn9jrLyZqqYOCT7dzZMNRDmyrp9CazbxbMzAZQTHIW/mhTP72hRBCCCG+ZevRWv62No/rJ8ayZGSEW5K4mv1Hyf5wFxVVKqZhSYTG+xL52GPE+Pr2eSzupNcpTE0MYkVWJdYOBxbz0Hr7qjcaSLp8KsMunUxNbjW1tU6Mqo2y+36BNW06MZfOxjty8I7IijMbWv8ShBBCCCE68dmBCl7bUkRymIXpyX3/BrkpO58dr22nutqFToHYRC+GX5+IT6AH4NHn8fQHc9PDiAn0wqgfnGvkukKn0xGaGkYo4GxoQJ86nP2729m/ZwWxSV6MuH56v+0LKHqHtB8QQgghhECrTPnfrUUsO1DBxPhA7pqbhNnQdyNxdpsTZ2E+5Y/+gQNeM4gZGUb6NTPwDB5aI3Ci6xpySzn45hZKi20oCqRePI70+cmYPWWsZjCR9gNCCCGEEGdRVNvK51nHWDw8nJunxPdZhcTqPbkc3nqMdnMgc29JJ+TmG7hoyhT03t59cv2Bor7FxraCOualh7q1amh/4p8czfSHl9KYX8ahzw5RUtBBss2J60gW5pQUdJ6e7g5R9CJJ5IQQQggxpKmqiqIoxAd788crRhEd4NknlSnrc0rY+9/NVB5zYDDqGH5jIrjAZ/78Xr/2QJRfbeWVzQXEBXmRHiGjlN/klxjFlB9FYWtzoLe1UPaXv5DnMYbQCemkXz97SBTFGYrk4wwhhBBCDFntdie/X5bN5vwaAGICvXo9iXM2NVH44husfGoD1VV2kkf5cfGjF5I+NRK9Ud6anUlquA8AOcea3RxJ/2XyNKD38yP4gV/g8A7i4OYqPr3/LQqW78Dlcrk7PNHDZEROCCGEEENSq83BH5cfJq/Kypy00F6/ntPuorG6De/WKti6kqThF5B+w1y8IoJ6/dqDgY+HkSh/Lw5LItcpr9Qk5j8+jNLVu9n3STbbPsilrM6D8Zek4uljcnd4oodIIieEEEKIIafV5uD3y7Ipqm3lJ/NTmJjQe/3JXC4Xue98yZGsVnRhESz67giin36a+ICAXrvmYDUsxJtDFU3uDmNA0Ol0xC7MJGrWaI6syqK0WofeqMO6fj1emZnoZA3mgCeJnBBCCCGGFJvDxePLD1NU28o9C5IZH9d7SVzVriPs+t92GpsVfH1gwu2TMZj0YJIk7lxE+XuyMbeaNpvTbQ3aBxq92Uj6RWNIc6k4Ko9R/fIrHHkrm8QFoxh26WR0OpnOO1BJ+wEhhBBCDCmqqvLOzlJig7yYPKx3pjU6rS2U/+99Nu3UYTSojJgTR/JV0+RN83mydjhQAO8h1hS8JzVl5/Pli1toalEIDlKY8N1Z+CVGuTsscRZnaj8giZwQQgghhgSH00Vdi41Q395rqq26VOqPteJjaKHioYdoGrmQ5BsXYQ7w6bVrCtFdLoeTQ699QfbWStAZGH3LTJImhKOTtg79kiRyQgghhBiyXC6Vv67N42BZI09dPRofj54vx16z/yi7PsymzSeC+bdm4Gl0orfIOqSepKoqb+0oITnM0qtTYoeK5uJK9q8soN3kx6zrU1BrazCGhbk7LPEt0hBcCCGEEEOSqqr8e3Mhm/NruH5SXI8ncY7Wdvb+cyX5Wc0Y9Crjl4zA29/cJ73ohhpFUVidXUmb3SmJXA/wiQ1j2h1h2DuctG1YT/V/36Bl8hVk3LYAvVHShP5O/oaEEEIIMah9tLecVYeOcfHoSC4ZHdmj527NPswXf9+OtV1PZLSJCXctxDPYr0evIU7m42Gksc3u7jAGFaNZjy4zk+atxWTvqKMk+00m3zGdwOHx7g5NnIVMhBVCCCHEoLWrqI43dxQzPSmY6yfG9th5nQ4Xqs1G3d//TphSydRrMpj566WSxPUBP08jze0Od4cx6Oh9fRn14C1kXpBAWxus+ssmsl7fQH9ehjXUyYicEEIIIQatEVF+LB0fw6VjIntsqmPZpkMcyHIyZkEsoffdS2R4ODqP3iugIk7mYdTT1C4jcr1BURSSLp9K5KQUNj+3ipx8HbotFaRP7dmRbNEzJJETQgghxKBTUtdKkMWEl8nAleOje+ScjtZ2dv19OQU5rfjGh2H2HIYpPL5Hzi26Tgor9j6vyGDmPXYtBXtrCE/0o2n5coyxsXgOH+7u0MQ3SCInhBBCiEGlqqmdR5cdIinUwk8XpfXIOat357L139toaVeITfQg84fTMFk8e+Tcont66u9UnJ2iKAwbG4Jqs1G+fiMHqsMJGpnPuLsukEIo/YR8piGEEEKIQaOlw8Hjn+fgdKlc10Nr4prXrqXgb//B7oCp16Qz9edXShInhgzFZCL0Vw/jkxBJflYzq3/xNq0Vte4OSyCJnBBCCCEGCYfTxV/W5FLZ1M69C1KJDvA6r/O1NtmoLGjCFBtLwvgoLnj0EmLnj+uhaMW5+nhfOR/tLXN3GEOK0cuTaQ9dxdj50TQ2qqx49HNqCuvdHdaQJ4mcEEIIIQaFd3aVsr+0gdtnJJAR6Xte5yr4bAcrn/iCPSuLMMQlEPKDO/EIOr9zip6xs7COA6WN7g5jSEq9ZhZz75qMR2wUu1aX4XK63B3SkCYTXIUQYoBxOp3odLpuVeBTVRWXy4Ver+/FyIRwrwUZYQR4mZiTGnrO53B22Nnx7KcUHmnFx1tl6mXT0Bvkc+/+xNrhIDbw/EZbxbkLHp3I/LR4WptstO/eRcuuPQTcfDMGL6nc2tfkN5MQQgwgTqeT2267jXvuuafLvX1UVeWee+7htttuw+l09nKEQvS90vpWVFUl2GJm8Yjwcz5Pe9kxVvz8LQqPtBKf5MWiPy7FN8ynByMVPcHa7sBilrEIdzKa9fiFeGKvqiJ7ezUrH3oPa2m1u8MaciSRE0KIAUSn0xEYGMhf/vKXLiVzXyVxf/nLXwgMDESnk1/7YnAprm3loQ8P8uaOkvM6j8tmo+aPj+HbVs7ESxOZ/MDlGDzMPRSl6CkOpwtrhwMfD0nk+gO/Cy8k5op5WFtUVj62gsodOe4OaUiRfwVCCDGAKIrCM888A8Bf/vIXAJ555pnTTrP8ZhJ39913n3E/IQaqhlYbj684jKdRz6Lh5zYS53K6OLz1GJFJ/gTdcTvhUVEYgoN7OFLRUxrb7Ph4GAj3k6qh/UXCRZPxiQ3lyxc3sf6fO5nkNBI3eZi7wxoSeiSRUxTlZeAioEpV1RGneV0B/gJcALQCt6qqursnri2EEENNV5I5SeLEYOdwunh61RGa2x389pLhBHqbun2O1qp6Nj21nCZzBIqSSvrU0b0QqehJQRYzL96U2eWp5aJvBI8axsJf+bPl1d3s295E2Ag7Hhaju8Ma9HpqRO4V4K/Aq2d4fQmQfPxrEvD88e9CCCHOwdmSOUnixFDwny1FHKls5u55KSQEe3f7+OrduWx6aSsddh2j5+pImxrZC1GK3iK/0/ofr7BAZt87l4bKVnR1FdR/uhm/pUvRSZGtXtMjiZyqqhsURYk/yy6XAq+q2scnWxVF8VcUJUJV1YqeuL4QQgxFZ0rmJIkTQ8GM5GCCLSamJAZ1+9iyZRvZ9FExRqPC7O+OJ2xCWi9EKHrDe7tKqW3p4HszE90dijgNvV5HUKSFxk/Wkv/5PpoOqMz4xeUYvaWiZW/oqzVyUcA3VyGXHt8miZwQQpyHbydzXyV0ksSJwcraoVUsTAnzIeUcKkraCguxv/USMdEzGH335XiFBfZClKK3bDlaS7BFitD0d34XX4xnrQd5m6pY/fB7zHrwQrxC/N0d1qDT78qXKYryPUVRdiqKsrO6WsqYCiFEZ76ZzH1FkjgxGNVYO7jv7b0sP9D9z4FtHQ72rCrGFRJF2L0/YfLvbpUkboBp6XBQWt9KSpjF3aGILhh+6wImXZpIkxXW/O5TrBW17g5p0OmrRK4MiPnG8+jj206hquo/VFXNVFU1MyQkpE+CE0KIgeyrNXHf1J0+c0IMBB0OJ0+tPILN4WJktF+3jm0qrGDlz9+mcEcJtWVWPMeMQZF1OwNOXpUV4JxGYoV7JFw0mek3jMBm8mHzp+XYbdLLtCf1VSL3MXCzopkMNMr6OCGEOH/fLmzicrm4++67u9xnToiBQFVV/rWxgIIaKz+cm0R0gFeXjz225RCrHl9De5uLzCkWolICejFS0ZuyK5rQKQqJITIiN5BEzRrN3PvnkzAmFF1HK/ay047liHPQU+0H3gBmA8GKopQCvwaMAKqqvgB8htZ6IA+t/cBtPXFdIYQYys5UnbKrfeaEGChWHapkY241S8fHMD6u69Mhiz7ZxNZPCvEwwcwfziQgLbYXoxS9zcfDyPTkYDxNMpo60ASEexMQ7k3VM3+m5sgxgm6/jbDMVHeHNeD1VNXK6zp5XQXu6olrCSGEOHufOEnmxGBjNOiYGB/IFeOiunxM+6FD2N/5N+GR05h4/xV4BndvOqbofy4cFeHuEMR58r/uOnY8+hmH/rWDGSDJ3Hnqq6qVQgghekhX+sRJMicGkzmpocxOCenSz6/qUinYV0PsyFRCb7yaxDlzUIzSmHigq2+x4etpRK+T32EDmSk8jBn3LeKLJ9ewUZK58yaJnBBCDCDdafYtyZwYyFRV5dk1eWTGBzAtKbhLP7f2lnY2Pb2celMkRg89MQsX9kGkoi88veoIep3Cby4Z7u5QxHnyTYhk7v3z+OLJNXz56n7mREUTGOHt7rAGJEnkhBBiAHG5XNTV1XW5T9w3k7m6ujpcLhd6qdYnBoCP95Wz5WgNqeFdq1DYVtPE+j98QkMTjJjdQUy6tBYYLErrW8mtaubGyXHuDkX0EN+ESOb+dD6bP6vg6J5qSeTOkSRyQggxgOj1ev7973+j0+m6PLL2VTInSZwYKLLKG3lzewlThgWzaHhYp/s3l1Sx7omVtHYoZF6YQNJlU/sgStFX1uVUo9cpzEiStlSDiW98BLNvDsZgVqh7/XUsM2dhiu76OljRDxuCCyGEODu9Xt/t6ZGKokgSJwaE+hYbz67JJcLPg+/NHNbpz7q9qoqKp57FYXMw/cZRksQNMu12J2sPV5EZF4ifl6x1HGw8LEawNtO4ZRfrHvuEhjxpTdAdksgJIYQQot/YXlhHm93FPQtSOi0z3261o/P2xi/Sj8U/m0nUzJF9FKXoK1uO1tJic3CRVKwctAwBAfj+349ocllY98xamkur3R3SgKH052axmZmZ6s6dO90dhhBCCCH6UI21g2CL+az7VO7KY8cWK+lTI0kcF9pHkYm+pqoqOZXNpIX7ujsU0csqdxxmw7924mGCeQ9fiFdogLtD6jcURdmlqmrmt7fLiJwQQggh3O5IZTNHq60AnSZxJV/sZf0/tkJFCeGJ0h9usHK5VBRFkSRuiAibkMbUG0bRZtez5Z0cHHanu0Pq9ySRE0IIIYRbNbfb+fPqIzy/Lp/OZgoVrdjJ5jcP4uUBs38wGW+/syd9YmBqtTm49+29bMyVaXZDSdTMUUy/ex54+2Jrk0SuM1K1UgghhBBuo6oqL64/SlObg58uSjtrcZP6zbvY9t5hLF4w96FL8AyWkZrB6u0dJVQ2dRAd4OXuUEQfi0wPJTwlmJb166gtLiHg5pvQ6WTs6XTkT0UIIYQQbrPqUCU7i+q4bmIsCcFn7iXlstloefNVMoKrmPewJHGD2dFqKyuyKlmQEXbWnwkxeOn0OmzVtezZUMO+Fz93dzj9lozICSGEEMItyhraeG1rEWNiArhgZPgZ96vIa0DRK4Q98DMi/f3Recub+8Gqw+Hkb2vz8fcycs2EGHeHI9woYOmVmA6/S87uerze3kjq1TPcHVK/IyNyQgghhHCLcF8PrhwXzZ2zE884pfLop1vZ/PJ2crdXYoiMlCRukDtQ2kh5Qxt3zk7E2yzjDUOZTqdj2s8vJ9BfYe/qYiq2ZLk7pH5HEjkhhBBC9Dmbw4Vep3DZ2Cj8PE/f6Dn/4y3s+Cgfr/ZqJl0Y02lzcDHwZcYH8sw1YxgV7e/uUEQ/oDcbmfHAhXiYVXasrKClscPdIfUrksgJIYQQok8dLGvkJ2/tobi29Yz7HP10Kzs+OYqvD8z99aWYLJ59GKHoa3lVzRwsawQg3M/DzdGI/sQz2I/Zv7wEj6hw2prt7g6nX5FETgghhBB9pqXDwfPr8zEb9ISdoXVA2/79FH+4Hj8flbm/vhSzn6WPoxR9qaSulT8uP8xLXxbgcLrcHY7oh/zCfZl/WwaWpmJq//WvTtuUDBWSyAkhhBCiz7yyuZD6Fht3zUnCbNCf8rqqqrisVlIi25j3K0niBrvS+lZ+vywbo17Hg0vSMOjlrak4PZ1eR0dJKTmbStj912XuDqdfkFWkQgghhOgT247WsjG3mivGRZMUemqCVlvSwN61FUy6eDwRkyejSO+oQS2vysoflx9Gr4OHLswg1FemVIqz81kwH9f2RnL3NxK0fAfxSya4OyS3kt+QQgghhOgTO4vqGRZi4YqxUae8VrP/KOseW4atqha9UZEkbgjYUViHl0nPI5eOkMbfokt0Oh2T7r0YH2+VnR/l0JBb5u6Q3Erpz3NMMzMz1Z07d7o7DCGEEEL0AFVVabE5sXyrrHx9Tglf/Hk9OgXm3j8Xv2GRbopQ9DaH00Vti40wXw+cLhVrh+OMVUuFOJPG/DJW/Wkt3hGBLPzFYvTGwf3Bj6Iou1RVzfz29sF910IIIYRwu93F9VQ1t6MoyilJXHPhMdY9uw6A2T+aIUncIFZQ08IvPjjAbz/Jot3uRK9TJIkT58QvMYop/zcdm18E1aXN7g7HbWSNnBBCCCF6TVVTO8+uyWVEpB/3L0o96TVXayt1f38Ob2ckY7+/iID0ODdFKXqTtcPBuztLWHmoEl8PI9+dOQwP46mFboTojqix8QQkRmJsb6B19268xo1zd0h9ThI5IYQQQvQKVVX5+7p8dIrCrdPiT3rNaXehGs34jhvFzLFj8EhNck+QoldVNbfzi/cP0NLhZF56KNdOjD1lVFaIc+Xla6Lq329QcaCCmHsCCRwe7+6Q+pT8SxJCCCFEr1iRdYzDx5r4/qxEgi0nesY57Q62vZeDS29i2jVXoyiKG6MUPa2isY3i2lYmDQsixGJmVkoIs1JCiQ2Sgiai5/ledyMbD39G8YtfsvgP4Ri9h071U1kjJ4QQQogeV9XUzuvbSxgTE8CslJCvt7tcLrY+/gEl6w8SHuMhSdwgUWvt4PODFfzm4yzueWsv/9hwFLvThaIo3DQlXpI40Ws8QoOYcEUGLW0K258dWv3lZEROCCGEED3Ox8PI3NQQLh4deVKytuevn1FSZCN5pC9Jk2PcGKH4is3hYvnBCqIDvIgO8CTI23TGxtxfVR491thGlL8XniY9y/ZX8NrWQgBiAry4OjOGOamhGKW5t+gjsQvGU3mglPxsK/mfbifxoonuDqlPSCInhBBd0dYARk8wmDvdVYihTlVVPE16bp2WcNL27P9+Qe6BRqJjTYz94YVuik58W3FdK29sLz5pm8Vs5M7ZwxgfF8iRymZeXH8Um9NFY5sdm8MJwC8uSGdUtD8jony5cXIcY2MDiPL3dMctCMG4uy6g4akvyC4wEtVix8N78FdElUROCCE6s+pXULYbRl0D425ydzRC9GvVzR0890Uu350xjJjAE9Pp2g4chNXvERU9hSk/vxydNPzuN5JCLbx0ywTKGtooa2ijzmqjvtWGn6cJAA+DnqgAT8wGHb6eRoK8TYT4mEkI9gYgLsibuCBvd96CEOjNRqbcOZuakmbMXkMjxRkadymEEOdCVWH/W1oSB5A4x73xCNHPqarKPzcepai2FfM3GvR2tNoxJQ4jfNF0Uq+4Ap1R3n70N95mAylhPqSE+ZzyWmyQF/cuSHFDVEJ0jyXAjCVg6Myckd+kQghxOk0V0FAMe/4LMRNh1gMyrVKITqzNqWJ/aQPfmZZAqI9WOa6lvJYNH5cSNzKEjOuuc3OEQggxeEgiJ4QQ31a4CTb9BcIy4IInISQVpLKeEGdVa+3gtS1FZET4sSAjDACbtY11TyynHS8ir0h2c4RCCDG4SCInhBBfcTlh7e+hZLv2fNKd4BPm3piEGCA+3V+BU4X/mzUMRVFwOZ18+YcPaW5RmHJVLP6hUn5eCCF6kiRyQggBWlXKt2488XzxHyWJE6IbbpgUy7SkYMJ8tSmVO//yCVVVLkZMDSVuUaaboxNCiMFHSkYJIQRA7soTj6/6N4SPcF8sQgwgjW12rB0ODHodSaEWAKwbNmDc/yXDkj3IuGWemyMUQojBSUbkhBBDl6pC3how+8DuVyE6UytqYpQ+SEJ01Usbj1JY28pTV4/GqNfhsDsxp6YSN2ckATdehiJtBoQQolfIb1chxND15dOw6c/QWgMzfwpzfyVJnBDdsPVoLdsL65iXHopRr8N6rIFVLx+iotZA4C23oOj17g5RCCEGLRmRE0IMTRX7IH8tRI2H1AukKqUQ3dTcbuflLwtICPbmolGR2FvaWf/HT+nwCCBgqVSoFEKI3iYjckKIoacyC1b8UnscN1WSOCHOwatbimixOfj+rEQUVLb86SOaWxTGzw7FJ9DD3eEJIcSgJyNyQoihJW8NbH72xPPkhe6LRYgByuF00dzu4NLRUcQFebP/n59TXuYgdVwAcYsnuDs8IYQYEiSRE0IMHYc+hu3/gPBR4B0EljAZjRPiHBj0Oh5YnIpLhbYDB7Bu2UZ4VCqj/2+xu0MTQoghQxI5IcTQET9dS96ixoNefv0JcS5WZB1jbIw/ob4e6BVA0TEsyUjIPZeikwqVQgjRZ+Q3rhBi6PAKhNhJYLNqrQeEEN2SVd7IvzcVsCq7EofNyaZ3c2n2iSHswQfRe0rFVyGE6EuSyAkhhob6Ijj4HrQ3wcpfwro/ujsiIQaUNpuTF9cfJczXg6vGR7P1Tx9SsbsQl0tFkSnKQgjR5ySRE0IMDWU7Yee/oaVaS+qCpTy6EN3x361FVDd3cOfsRAre3UhpYTsJ4e2EJ/i5OzQhhBiSJJETQgwNNXngHQx1R7XnUePdG48QA8j+0gbWHK7kotERBFVUsf+LMgIDFMb+4AJ3hyaEEEOWJHJCiKGhNheCkqF0p7ZWLiDe3REJMWAkh/pwxbhorkwPJOeV5RgNKtPvW4zOoHd3aEIIMWRJIieEGPw6mqH5GAQOg/I92micrOkRokscTheeJj1XZ8bgysslxrqPud/PxCss0N2hCSHEkCb1t4UQg19jKSg6bV3cjPu0KZZCiE5tya/l3V2l/OKCNFqKrQQmDCf66afQ+8m6OCGEcDdJ5IQQg19oOtzwrpbMSf84IbqkrsXGS19qVSo7DuSzc1UNcRNjGb843t2hCSGEQKZWCiEGG3s7bH0edr8GVdng6NC2G0yQtxoait0bnxADgMul8twXudicKt8bG8b2/+5CV1nMyOnh7g5NCCHEcfLRtBBicLFZ4fAy7fH+t7S1cKoKE26HHS/BuJvBP9a9MQrRz723u5TsiibunDmMgpfX0m7TMfv2cZgsHu4OTQghxHGSyAkhBhfvYLjlE2itg2P7oHQXONqk7YAQXeRwuthX2sCM5BAi9maxu8JB2vhAwienuzs0IYQQ3yBTK4UQg4+igHcQFG/TkrlxN4PLBR5+WuVKIcQZGfQ6fnPxcG7L8MW4+i2SI1oZ9b1F7g5LCCHEt0giJ4QYvMbeACiw/AEoWA9R46TtgBBnoKoqH+4pw9rhQHGB0T+QkJtvYMy9S9Hp5O2CEEL0NzK1UggxePnHwgV/gvfu0J4bPN0bjxD92Kf7K3hzRzF+Xkb8chuoq3Yw58bp6I2SxAkhRH8kv52FEIObTzgs/Q94h0DJNnA5z+08DcXwykVw+LOejU+IfiCvqpk3dxQzMSGI+PxCjnyyi2BfuyRxQgjRj8mIXDc5HA5aWlqwWCzo9Xp3hyOEOB2XC+yt4GgHaxW0N8KFT2nb6gqgNhfsbdrrX32f/IMTFS6/Pf0ybw18+Yz22Gnr+/sRohe1dDj4y5o8ArxM3Jzqx/o/bMfHE0ZelOHu0IQQQpyFJHJd0NHRwTvvvMPjjz9OVlYWRqMRu93O8OHDeeCBB1i6dClms9ndYQohQOsb98a14LSfvP2a18AvGna/CvvfPrFdbwKjJ0y4Awxm2PQX0Olh5FJtNA+07QCe/pBxaZ/chhB95fVtxdS1dPCrC9PZ9+xnOJwKU747Db3Z6O7QhBBCnIWiqqq7YzijzMxMdefOnW6NYfv27SxZsgSbzYbVaj3ldYvFgslk4vPPP2fChAluiFAIcRJVhTW/hdJv/O4YuRRGX6slZK118PmD0FQGk74P6RedfOz2f0DOZyemYF7+IvhFQcV+7fiQ1L69HyF6WUOrjeyKZlKP7mPzm4eJm57MyNsWuDssIYQQxymKsktV1cxvb5fJ72exY8cO5s6dS11d3WmTOACr1UpdXR1z5sxhx44dfRyhEOIUigIJs7RRNd8omHIXpCw6MarmFQiXPAfRE2DbC3Dg3ZOPzbwdhs0+se2D/4O81RAxSpI4MajUt9hwuVT8vUxMSQyC6mNkprQw/JZ57g5NCCFEF/RIIqcoymJFUXIURclTFOXnp3n9VkVRqhVF2Xv8646euG5v6ujoYPHixbS0tHRp/5aWFhYvXkxHR0cvRyaEOCunAw6+C6EZsPgPsP8t+PRerVjJVwwmmPNLiJ8Bu16BvW9o25sqYPlPtTVxyQvgyn/BiCsgYow77kSIXtNud/K7Tw/xt7V5uFwqWRvL8L5sKWH33SOtBoQQYoA479/WiqLogb8BS4AM4DpFUU63QvotVVXHHP/61/let7e988472GzdK2pgs9l49913O99RCNHznHZtfZzeAAt+p315BcLC34PqhN2vnby/3gAzfwopiyEgHjqa4dOfQFM5zHoApt2trZHL/A54B7vjjoToNS9vKqCisZ05aaHsf2UtORuKqCm1ohhk6bwQQgwUPfGx20QgT1XVo6qq2oA3gQFfDeDxxx8/43TKM7Farfzxj3/spYiEEGfU3gQrH9IqS6qqlsDpj78h9YuCtIugeMvJo3IAOh1M/SHETQGzD0z8LlzyV0iY0ff3IEQf2XCkmg1Hqrl8XBShFZUc3lJBsKOMqJQAd4cmhBCiG3oikYsCSr7xvPT4tm+7UlGU/YqivKsoSkwPXLfXOJ1OsrKyzunYrKwsnM5z7FMlhOi+pnL47H6ozoHYKae2DgDIuESrTnmgkxHzpPlgCemdOIXoB8ob2njpywLSwn25NCOEba9ux2RQmfSjRe4OTQghRDf11UT4T4B4VVVHAauA/5xpR0VRvqcoyk5FUXZWV1f3UXgns1qtGI3nVnbZYDB0eyRPCHGOKrNg2X3atMhFj8GwWaffz8MPxlyvrZsTYgjrcLiIDvDkx/OSyXppFS1tCuMvS8Mc6Ovu0IQQQnRTTyRyZcA3R9iij2/7mqqqtaqqflUF5F/A+DOdTFXVf6iqmqmqamZIiHs+GbdYLNjt9s53PA2Hw4HFYunhiIQQp3DYYN0ftSmRFz4FYZ0kaSOvgtTFfRObEP1UQrA3j142AktdJb67PiE9GeIWnVLRWgghxADQE4ncDiBZUZQERVFMwLXAx9/cQVGUiG88vQTI7oHr9hq9Xs/w4cPP6djhw4ej1+t7OCIhxCkMJpj3ay2J843s2jEOGxz+DNrqezc2IfqZ7QV1/GdzIQ6nC9WlYggLJ/Sayxnxg8vcHZoQQohzdN6JnKqqDuCHwAq0BO1tVVWzFEV5RFGUS47v9mNFUbIURdkH/Bi49Xyv29seeOCBbo+sWSwWfv7zU7ovCCF6mrUaqrIhOEkbkeuqlmrY+nc49HHn+woxSFQ1t/Pi+nxyKpsBOLy5nC/fO4r3/EXoLd5ujk4IIcS56pE1cqqqfqaqaoqqqomqqv7++LZfqar68fHHD6qqOlxV1dGqqs5RVfVwT1y3Ny1duhSTydStY0wmE1dddVUvRSSE+Fr2R7Dil90/zi8K4qfD4U/B1rUekUIMZA6ni+fW5OFSVe6el0z9vnwOvLEJs9qC3ij94oQQYiCT3+JnYDab+fzzz/H27tqnld7e3nz++eeYzeZejkwIgdkXnDatb1x3jVwK9jY4vKzn4xKin3l7Zym5Vc18d+Ywgs16tv1nG0bVzqiFSe4OTQghxHmSRO4sJkyYwNq1awkMDDzjNEuLxUJgYCBr165lwoQJfRyhEP2Ho66u7y721XTK7o6q2Vq15t+RY+HQR+eWCAoxQDS22lmRdYx5aWFMTQxm7z9WYG1VGHtxKp7BUqVSCCEGOknkOjFhwgTKy8t54YUXGDFiBIqiYDQaURSFESNG8MILL1BeXi5JnBjS2nOOUHrvfez407s0ldb0/gVNxz9Y6Wg6sc3VSf/Gmlx451Ztjdyoa8ASCmW7oXwvFGyEnOXa2jlba29FLUSf8vMy8vvLR3Dz1Djai0so3V9BZJSRhAvk/yshhBgMDO4OYCAwm83ccMMN3HDDDTidTqxWKxaLRapTCnGcMSoS3dQFFOxoo+B3K0kcE8TIm2dh8vbonQt+NSJXfQRKdkDRJghOhil3nbxfdQ6sfwKMHlBfpG07sgLG3gQXPg3v3gYt30o8LWEQO6l34haiD7hcKvtKGxgbG0B0gBcA9Zs3M9qwn/Af/s7N0QkhhOgpksh1k16vx8/Pz91hCNGv6C0WYr93Pf6zi9jzn43k7q6h+PBHjLl9LvEjglF0Ss9esDJL+775We17cDIEJmqPHTatNQGAtQqslacen7McxlwH034COr02wqfooC5fO5cQA9iHe8t4e2cJD1+UwfBIP2rLrfhdcRU+8+dhCPZ3d3hCCCF6iCRyQoge45sSx6zfx1GxYS9Zuxo4sr2SmPRAXOUVmKKje+5C7Q0Qmg5x07QvS4i2vWAjbHsBLnkOvAKh+diJY8bdDEFJkPUBHFmuTa+MHHPyeQPiei5GIdwgu6KJd3aWMi0xmIwIX1rKatj8VgGRGSGMXxzv7vCEEEL0IEnkhBA9LmLmGMJnqLRb7XTs3E7VC/+kLHkxI26chX982PlfYNKdULEHvENOJHEuF7TWQHsjfPgDuP7NE69NvwdM3rDqVxAzERY9BrpvLREu262NzIWknH98QrhBc7udv36RR5ivmTtmDENRFLY/v5q2GpXkG1LdHZ4QQogeJsVOhBC9QlEUPH1MeI4eDTMvoKK4gxV/WMP2Zz+jo7nt3E/8+S9g/5uw5hHIWwOtdbD3DXjvO7DjJW0fmxVq86F4q7aeLn4GRI6DlEVQsh22Pg9tDSefd/s/tfP2IlVVeePwG2TXZvfqdcTQo6oqL6zPp7HNzt3zU/A06Sn4bAeVlU5SxvjhGyZVKoUQYrCRRE4I0at0Xl4k3H4Vix+cR2QYHD1Qz2e/WsbRvdWoLrX7J2yt1QqXmH2goxlWPgR7/wd+0TDnF3Dt/7Q+c+ufgKLNkDRPWzNnMMHUH8G0u6FiH7x1o1YsBcDpgOZy8Ivp2Zv/lnZnO4dqD/HsnmcpaCzo1WuJoUVRFGalhHLbtHgSgr3paLSy99McvD1VRt62wN3hCSGE6AWSyAkh+oRPQiQzHrmeubePJiA1mtLsOlx2Gx25ud07kXewNoXS7KslcpN/AFf8A2b+FOKmgocfjL0BmspAdUHKkpOPT14Ai/8A/rGgPz67vLlCa1/gH9szN3sGngZP7h1/Lz5GH57Z9QzHWo51fpAQnXA4XQBMTAhkXro2dbn23Y8x2RqZcP049GajO8MTQgjRSySRE0L0qdDJI5j1f5OYfHki1tWrKf7dE6z91VvU55V37QTewdBSfWJELnwEdFjh3e/A4WXaPimLtWImaReBX9Sp5wgfCZf9HQKHac+byrTv325F0AsCPAK4N/NeFBSe3PkktW21vX5NMXi12Zz8/P0DrD1c9fU2VVXxMNiZPMOX8MkZboxOCCFEb5JETgjR5xRFweRhwGfePHSzL6LmmI2VT6xj69Of0F7ffPaDvUO0dXFGL7Ad39cvCsJGaGvfNv8VVBXG3QSTvw9H12kFUM4mKBlC0sDe0iP315lw73DuzbwXh8sho3LivLy8qYCy+jbC/bSejapLJXtzBV5XXk/gzTe6OTohhBC9SVHVc1ij0kcyMzPVnTt3ujsMIUQvaymtYu/LX1BS4sTs582oG6YybEwIinKa/nNFm7UiJyOuAIMHBB3vH+dywZ5X4cC72vNrXtMSuvfuAM8AmPcwBMSfPgBbC+x8GYJTtIIofcTmtGHSaz3vVFU9/f0KcQYbjlTz93V5LB0fw5XjtfYeh9/bwqHDLiZcmU5MWqCbIxRCCNETFEXZpapq5re3y4icEMLtvKNDmfara5l/ZybBI2Kpr2jB2dBAW1bWqTtHjYeEmScncaC1Exh/K8y8X3t+ZKXWS27xH8Fpg2X3axUrv61kB3x4JxxZoY309aGvkriNpRt5Zvcz2F32Pr2+GLjKG9p46csC0iN8uXysNn24taqeg6vy8W4oIjo1wM0RCiGE6G3SR04I0W8Ej0tl+lgVl1Ol8e03ObZiE6URMxh77QRCQhq0aZJFW8DRDukXn5zIfWXYbIiZDAaz9jwkBS76M3zxiNayIPM72mheexNsfxGOrteKnMx5yG095HSKjoM1B/nX/n/xf6P/D50in7GJs8uuaMJs0PGjucnodNpI7p5/rsHhUsi8ZaqM7gohxBAgiZwQol9RFAW9QSHgqquocwbQ/GUNa/68lkjPo4wbVYQlfQIUbgRL6JlPYvQ4+bl3ECx5Ar58BnTHf+1VH4aCjTD6Wq0wit59lf2mRU3DarfyVs5beB3y4uaMm+WNuDireelhTEkMwsuk/TxXbj9MSWEHw9IsBI2Id29wQggh+oQkckKI/qWxDArWoxw7SNKNvyf6gkb2/+V1CspSWFEwleFjxpOm39b9CpMGM8x64MRzoydc8KTbRuG+bVH8Iqw2K8sKluFr8uXy5MvdHZLoh/YU12PU6xgR5fd1Eqe6XLR//A6RJh9G33GpmyMUQgjRVySRE0K4X1sDFGyAo2uh5nhfufBR0N6IR5A/Ex/5AWl5pRze34zLpWJrt9CxbR9e45zoDfquX+erUS5HB6z7o1bN0hIKlz1/YiqmG12RfAVWuxW9rhv3JIaMWmsHf1ubR5ivB49eNuLEqK3Lhf/kcQSHhmD2s7g3SCGEEH1GqlYKIdzD1gqqU+sHV7oTVv9G6+s2bDYkzNKmQ56GqqrUP3QdxYdUimMuYdRFGcQtGNP9qYi1+fDJ3dpjoydc/za01WuPjZ7ndWvn45vVKzucHZj17k8whfupqsrvl2VzpMrK41eOJMJP+xntaHOw+/Mihs+MxDfIfT+3Qggheo9UrRRCuJ/TAcXbYN3j8NaNcPB9bXvEGK1B9yXPaoVIzpDEgbaGLmDheALH+YPLydZ3DrHq529Ste9o92IJSoSrX9Ue29vg9avhkx9rvejc6KskrqS5hJ+t/xl7q/a6NR7RP3x24BgHyxu5ZUrc10kcwO6/f07F/lLov5/JCiGE6CWSyAkh+sb2f2rJ2xe/g4q9kDQf4qZqr+kNWuXILlKixhK7eAIXPnktI6cG09zoYMObeRxYV9q9mLwC4cb3tMf2NjB4Qv4XsP/t7p2nFwR7BhPkGcTz+54npy7H3eEINzrW2M4b24sZHxfA3LQTRX6qd+dSdLiRKO86fINlNE4IIYYamVophOgdDpuWsMVM1J5/+Wetn9uw2RA5Tkveekh7bRN5B+rxDbEQbCumPb8Q74ULMft6d+0EHc3w8Y+0xuD2Nm3bFf8E34gei/FcNNua+cO2P9DQ0cCDEx8kxjfGrfEI91BVldXZVUxMCMTPU6uu6nK5WPXAG1ibVS567BLMgb5ujlIIIURvOdPUSknkhBA9q7UOcj6DnOVaMZHL/t6t0bYuczlB0Z0oYHJc/ZtvkbPiIGUeqaRNiyLtmhnojV1IGq3VUJUFG57Ungcnw5I/9WjCeS5q22p5bNtjOFUnD01+iGDPYLfGI/pWm82Jp+nU4jcFn25h20dHGTUzgoyb5rohMiGEEH1F1sgJIXpXax1sfArevQ32vQkhqbDo9+DXC6NIR9fDq5dCU/kpLwVcew3D7rwOixccWF/OsvveomjNXjr90MoSAvZ27fHE74JXsDaC6GZBnkHcl3kfo0JG4Wfy0zZWH4HsT9wbmOh1B8sa+dEbu8mrsp60XbXb0X/xAUl+1aReO9NN0QkhhHA3aT8ghDh3Lhe01YF3sFa+v3wPpCyB9IvBL6p3rlm6Ezb8SXtss552l9CJGSzITKPos+0c+DyXnRvqaaCU0fM6SSq/Ol/SAki/5JTRPneJtETynRHfAaDF3gJ7/oN3+T4tzm83PxeDgrXDwd/X5eHrYSQm8OT1b6peT9A1VxHq79e10WYhhBCDkvwPIIToPlsr5K2CQx9rpfoveQ5M3nDVK707FdFp19oUfKW+SBv5Ow2dTkfCRZOJXTCeoux6fIM9afp8BS35hXgtuRi/YZGnHhSUBEYv7Z4UBZqPwfZ/wNQfgWdA79xTN7hUF0/ufBJj0yHuR8UkSdygpKoq/9p4lMY2O/dfmor5G70S21vsbH4/j9FzMwiKkp5xQggxlMnUSiFE1zVXatUn37lF++4ZAKOuOfF6b68n2/eG9j1pntZzbu//TkyHPAO92ciwMaEER/ug2m3k7q3n8z+uZeufPqS1ukHb6atpl5Fj4NK/faNxeLs2yvjlMyf2cSOdouOChAvIt5bwNxpwOO3uDkn0gi/zath6tJal42MYFnJysrbrr8uozynF7CWfwwohxFAniZwQ4uxUVSssAlB5QFubFT0RLnwaLnwSEmb0zRTE2nytLUDiXJh+D0z8HrTWwpHlXT6F38UXM+7X3yE61kThkRaWPfQp+19dh2PVo5DzubaTJeTEAQHxMOEOKNsNhz7s0dvB5dRG/LppQvAobsaXA3Tw8qZHOl/795WizdraOtHv5VdZSQv35ZLRJ48aV+/JpSSvhWh/K5YAGY0VQoihTj7SE0KcntMBhRvg0Eday4Dhl0PCLK15t7cbKid+VTSlaJOWBIWPhAm3Q9rF3TqNJTqUaQ8tpS6rgL3/3UzegUocpU7GhLejOJ0o+m9VCEy9AMr3wq5XIGwkBCed/72oKqz9PZRsh4xLYfytoDd27VhrFbMMgTQ7XLxfuIpwj0AumXjPafar1nriRWdqzc9bqmHHS1oVUUWvjTaaZWpef3TrtAQ6HE50uhMfkLhcLna/vh2jXmXMd+a5MTohhBD9hSRyQoiTtTdqrQMOfwptDeAXDV5B2mt6o3uSOJcTDCaY8wso3ATH9kHBeu21nOVw+YvdHhUMTAln7qyD1B4txzz5Rhr2VVL/39+hm30hcYvHo9Mdn7CgKDDtx/DRD2Hny7D4sfO/H0WB5EXa40MfQcU+mPWzrrVp8I+BG97hwroCDF/8nEmHVkLwWC3ZdnRA8VZt/WL5Xm1/nV5L5AKHgbUS9rym9fjL+UwrSjPp/87/fkSP2JRXQ0yAF7FBXietiwMoXb2L+gYYOSNCesYJIYQAJJETQnzbxqehbBdEjdNGiyLHubd6Y1W21ttt3sMQN1X7UlVoKoOK/Voz7+7G53TAyoehJoegxffBsNm0qrso29VA+QdHyPkilzFXjiF8Soa2v9kHljx+IqE9V611UH1Yu4fYSdpXyXbY9Bf45G645K9dq/bZ0YwSEM/iK16H7I9xxUziSG02aWuf1JI172AYfS0kzQefcO2Y8JGQshiyPtR65AEEJmrf7W3aOsD46RDfR1NlxUlK61t5YX0+Y2P8uXfhyQV8VFVFv+Z90n18SbvmajdFKIQQor+RhuBCDHWqCl8+rSVsiXO0tWh6Y+808e4uRwd8/COtWuWlfwOT1zdes2mjdPY2rcpkdx14F3wiIH7a15ucNjtH3t5I9qYybA4dkemhjL11Bj6BPbAeyVoFK34J7Q1w5Uvg8Y1RldY6OLpOm76qKNoIpO7UJtCAlrh+cjfETtH63QHLC5bz7pF3+X7wBCZETIaI0adPxmwt8MH3oa1ee37dG1qSWpsPa36rxTHiSsi87fzvV3SZ06Xyq48OUtnUwZNLR+HvZTrpdVVVsR09imqz4ZGe7qYohRBCuMuZGoLLiJwQQ52iaMnBxqeg5ghk3t771Se7averWtPvhY8CKhRvg4q92rRBkxdMuQuW/xzmPAiRYzs/X0ezdr7gFBh51Skv601G0m+cS+KlrRx8dS2l7X4U7q8hfbgZDAYM9fvgyEqt0Xl3Rq0aSmDlQ9q6tAWPnJzEAXgFwogrju9bDKt+BT6RWo84g6eWqKZdCIEJWusHnUGbkmkJhYxLmRc7j33V+/hH7S684mYw/Eyxmbxhyg/hi99pz61VWiIXlAhL/wNb/goH34PQDG20UPSJj/aWkV9t5e55KackcU67i3X/O0zK5HBi0gPdFKEQQoj+SKpWCiFgzkPaNMrsT2DFg9rIjLtV7NOSldQLtKmeb1yrJSBHVoB3kDYNcPu/QNFp/d+6ojYflt2nJSxnmY1g8vFi3F0XsujOsaRODqfu9dfJ/ulj7Hr9CB1FB2HZvbDsfvj0Xi25BKgrgC1/16Ztfvuayx8AlwMW/xFCuzCiEpSs7W+t0qZiFm+BjibttZojWjIKWhVPVcWkN/HjsT8m0hLJc3ue42jD0TOfO3YSXPkvGHsTmL+RUCoKTPw/bS3dxqfOqaKm6L7i2lbe213KtMRgpiSeOnX3wMsrqcsqwNPrDCO0QgghhiyZWimEOOHoem29lmeAVkDEnSNzH94FqgsuehpKd0J9oTZlMDRdm/pZvBW+eFQr1pHexcqVqgo7/qUliGNugDHXdekwe2Ule/65iqNFCkZnE6nR5aRPdKI36SDjcogeDzW58Ok9Wi+6Ob88Md1z/9taYZGFv+/a+rfOOO1aQRp7K5gs2mjecY0djTy27TGcqpM/zPgDRl0XK2F+U/Mx2P2a9uf67ZFD0ePsThcf7y1n0YhwLOaT/721VtWz7OFlBAXpmftY135WhRBCDD5nmlopiZwQ4mT1RdBcAbGTteeqikvVBmyUviyCUZkF3qEn93X7phW/hMqD2pRAT/+un/erNYH5a2HyndqUxa6GtOMw+97eTV2Dik9MMOOun0RE0jeunbsKNj+rjRDOflCb+qiqYLNqUxj7QHVrNU22JhL9E8//ZC4X6GTiRm9xulT0ujP/m9r6xAcU5ray8CfTCBwe33eBCSGE6FdkjZwQomsC4rQv0BKT8j38tHQm5S1g0CsYdAoGvY7xcQF8f5aWLLy2tQiDTiHEYibM14MwPzPB3uaT+mB1W9jws78+cqk27bA2T+uV1lWKAlPvhg4rbH0ewkdpJf27EtKENOaPT6Fk9W7ySs20NtlozzmCq60Vj1Gj0CUv0KYrrv09vPsduOx57dx9lMQBhHiFEOKlJb+byzczIngEvqZzGFlrb4TVv9US3STpW9bTjlZb+fPqXO5bmEJckPcpr1uLKijKayUm3ixJnBBCiNOSRE4IcWYdzVCwgXvUQ+xJu4smcyhOl4rdqRIbeKKC5M7COmqtNhwu19fbZqeG8v1Ziaiqyvoj1cQFeRMT4IlB30MjPJFjtOqPX43GqWrXC5DoDTD759rauy4mcV/R6XTELcwk1qWiqio1f32Doj2V1AZnMfryMURMm6S1KijYeNK0x75W317Pq1mvEmGJ4GcTfoanoZuVPU0W0Jtgy9+0Ecavkntx3hxOFy+uP4rd6SLYYj7tPu3LP2aE6whxt/y8j6MTQggxUMjUSiHE2ZXvgfVPaMU3Ztx3Ysrlt6iqSm2Ljcqmdiqb2gn39SQj0pfq5g5+9MZuAMwGPanhPmRE+DJpWCARfufQNuB0ijZrRVDm/AIMp39jfFaVh7RKkCEp3T5UdTg48vZ6sjZWYHMohAQrjL5+MsEjh3U/jh62v3o/z+55lpSAFO4Zdw9GfTfXzLXWwQf/BwkzYeqPeifIIejDPWW8uaOY+xemkhl/arKvqirOujpsBQV4ZXZjtFkIIcSgdKaplbL4QQhxdpFj4eJnwTdKKy5SX3Ta3RRFIdhiZnikH3PTwsiI1KbzBVtMPHPNGH40N5lZKSHUt9h4c0cxhTWtAFQ1tbMy6xhVze3nHqO9TRtdW/vYqVUjO+NyaaNOq36llf7vJsVgIPX6eVz0pytJGx9Ifb2L9W8XkL25vNvn6mmjQkZx+4jbOVx3mBf2v4DT5ezeCbwCISABGkt7J8AhqKyhjXd3lTJ5WNBpkziA7Z8UcDirXZI4IYQQZyVTK4UQnbOEwJInoGzniSl2Z2ta/Q2KohDh50mEnyfTkoIBaGq3Yzo+xXJvSQMvbyqATRDl78XYWH/GxvqTHu7b9TV2SfPAadMSsg1PwKwHuhQboBXzmPsQfHY/rHwYLnjyzAVWzsJk8WTM95eQWtNE/sEG/MO8aV63jrajxXgtWoIlqvvn7AlTIqfQYm/h9cOvc6j2ECNDRnbvBH5RULK9d4Ibgr44XIWHUcdtUxNO+/qxrdkUfXGEkRd3skZUCCHEkCdTK4UQ3VeVDRufhpk/PafpiN+kqioVje3sKW5gb0k92RXNALxw03gsZgN1LTb8PI1nre73tawPtfYCw2bBjPu717S77qjWXNwzAC54Ajz8zu2GvqHhvfc5+NkhyvUJJGRYGHnTHDyC3FPSv6ipiDjfc1jnVp0DrbUQO6V7f57itFRV5VhT+xmnFa/86f9otqpc/MQVmHy8TruPEEKIoUXaDwghek5tvtacu60eJv8AUhb12KnbbE4Ka1tIj9ASnl9/dJDyxnYmxgcyJTGIjIhORur2v61VXJxwR9cTj7YGLVmxt8G6P0D0BJj+k/O+F4D6I2Xsf30TFWUODHoXqdNjybhmOnqje2a259TlUNJcwvy4+W65/lBV32IDIMDbdMZ9Stft48v/HWT45BBG3r6wr0ITQgjRz0kiJ4ToWe2NsOFPUL4XkhfApDvBcOY3qedqR2EdW/Nr2VVcT7vdSaC3iSvGRjM/I+zMB31VwbKtQRtZO11C11oHK35xYv2Xdwgs/TdYq7S1ct1padAF1fvy2ffmDho9I4nLjGXcvAgUvR7F0Lcz3P914F9sLt/MbcNvY0b0DG2jrRWqDmnJbPLCk/+82hqgoQiCU040ORfd9vSqI+Qca+LZ68ZiNpw67VdVVVbe/z9a2+DiJ6/C4OXhhiiFEEL0R9JHTgjRszz8YP4jsOc1OPCOVqK+G821u2pCfCAT4gPpcDjZXdTAhtzqr/MMa4eD9TnVTEsKwt/rG0mkomiJ2ic/hqT5MO6WU5O5juaTi3h81bzbEqp99bCQ0YnMH51ITakVk4eexo8+om7rXtTpSxh22TR0fdR4+9bht9LY0cgrWa/gXbqLcbUlUHNEu3eAuKkn9707dgDWPw6XPAeBp1/XJc5uT3E92wtquXZC7GmTOID2/fuJr96I+bJrJIkTQgjRJZLICSHOnU4H42/RRq9CM7RttlYw9fzaHrNBz5TEIKYkBn297UBpI69tLeS/W4sYF+fP3LQwxsb4a1MvPQMgZjIceBeKtmgJXeJc8A4Cpx3yvwC9UXsMWvxHVkDFXm2qaMSYXlkTFhxtAaAtPZ3K7dUULy8mZ30hI5akEbNwfO8kdI4OrXl6xX4M9YXcNfN+ntz1FC8UL+decxxpI67SisXo9CeSOFsrZH8MWR9oz2U07px0OJz8e1MhkX6eXDgq4rT7qKqKOSWFiOsvx2f+zD6OUAghxEAlUyuFED2npRY+/QmkXwwjl/ZJcYzS+lY25tawLqeKxjY7Qd5mHr9qFBazQRtlylsNuau0qYOKDq75r5asLLsXvEMh41JtdNHkDUfXwb43wWbVSu8nzYfR12sNxHuBy+ki/6MtZH1RSHsHBMX6M/a2mQRH+3R+cFdUZcPe/2mjai6n9vfhFwOLfo9Vb+QP2/9Asn8yt4649eTj6grg8we1P4eYiTDmBghK7JmYhpi3d5bw/u5SHrowgxFRpy+gU3aknqIDtYxfEofZq5u9/oQQQgx6skZOCNH77O2w+TkoWA8xk2DGvVqC1AccThe7ixvIOdbETVPiAfh0fzkRfh6MiQlAb62AyixtPR9o/eZOl6A5bFC8GTY8qT2/8OnzrszZGWeHncPvfElRjTeRI8IZPtIDZ0M9Hqmp53AyB7js2gha4Zew7o9as3Ojl7aGsb1J+3uJn47VZsVLZ0R36GMYNltb9xicpJ1j698hdQkEJ/f4/Q4Vqqryr40F2Jwu7pqTdNp9XC4Xyx/8AENEBAt/PAWlqy03hBBCDBmSyAkh+oaqQvYnWhsASxjM/SUExPd5GA6ni3vf3kdVczsBXibmpIUyNy2UYIv55B33vqElm8kLoWiTFmtQIjSVQ/MxiBrXZzE77S6cThfNb/yXinV7OBY+iVFLxxOa2YWErqMZtr2ojSrGTIR5v9L6v615RHvdw09LyiJGQ9pF2rRSVYWcz6jd+lf+QxO3mqMJvPq/2muix7hc6hkrreZ9uJmdywrIvCCBpMun9nFkQgghBgJJ5IQQfasySyvlHz4aZv3ULSE4XSp7iutZnV3FvpIGAL43axhzUr9RzOSzn0JDCahOrf3AiCsg8ztuifcrLpuNvDe/YP/mGhxOhdBQHaOunkDw6NOP6gDwzq3QUqM9jhwLC38HHVZtWmVwMngFnTzV9eg6bcSuOoeitioeNzsICE7jwVmPYzFZevP2hoQDpY34eBiIDz7ziLTL5WLZT15HVeGiZ65Dd4ZCKEIIIYY2SeSEEH2vtQ4MZm3Eq60eTD69tt6sM9XNHaw9XMXs1BBCfT3IKm/kcEUzS9o+xatwFcTP0NbEhQ3vN42vOxqsZP1vPfn768E/iJSFIxg9L+b0Ox87COW7tZFFS1jn95DzOWz5KwTEwaQ7OWwy8PTOp4n1jeX+zPvxMEjlxHPVbndyz1t7CfQ28ehlI1DO8Hdx9JMtbP/4KOMXx5F85fQ+jlIIIcRAcaZEzj0daYUQQ4NXoJbEFW2Gt2/Wpi66SYiPmasnxBDqqyUoh8qbeGdXCXccGs0zIb9jf/ytqP0oiQMw+1sYd9eFXPT7i0lbkIa3v5n2w4epfv5FmvLLTt45fASMuxl8wrt2D6mLtUIv9UXQUExaYBrfH/19ChoL+Nvev+FwOXrnpoaAD/eUUd9q49ap8WdM4lRVxbBlOcMsx0i8dEofRyiEEGIwkPYDQojeVbpT60MWmAjRE9wdzdeWZsYwIzmENYcrWZdTzbaibDIi/PjVxRnuDu0UnsF+jJyvVTxsXptFye5icnevIybezKibZ2CJOUtz9LPJvF3rpbftefCNZFzkOG4dfitritfQ5mjDx9RD1TOHkMqmdj7dX8GM5BCSw87y56eqBC5ZSLC3t0ypFEIIcU5kaqUQoveU7daKbQTEwcLfg7l/rr2yO13sKKjDpcL05GAcThcvbypgWlIwGRG+ZxxVcZfmkioO/G8TJUdbUVCJz/Bn9PcWnVvpelurtk4wfjqMuQ4Ah8uBQWfA6XKiU3T97v77sydX5HCgrJE/XzOGAG/TafdRVZVdy4uISQ8kLMG3jyMUQggx0JxpaqWMyAkhekdjGXzxO/CLhoWP9tskDsCo1zE1Kfjr52UNbWw7WscXh6uI9PNkfkYYM1NCtN50/YBPTChTf345jQXHOPC/TRTXeGB9L4/Z1yWjtrai9+1GcmDyggufAuOJNXEGnQF7zgr+vvNJYtOv5PJx3++Fuxh8VFUlLsiLjEjfMyZxAMWr91K4uZaQqJGAJHJCCCHOjYzICSF6h6rCoQ9h2Bzw9Hd3NN3W4XCy9Wgdqw9VklvVjFGv449XjiLK39PdoZ2iua6djlYHHoX7qH7lVepGLCHj+ll4hgZ070TVR+DQR6AoqEfX8oqulY3B0Vw//Bbmx83vneCHGJfLxef3vo7dDhc9fQ16s7R6EEIIcXYyIieE6BvVOVrzaf8YGH65u6M5Z2aDnlkpIcxKCaGotoVtR+uI9NNGrZbtr8CoV5iRHIKnyf3rm3wCPfAJBLshkY70aeRmtXD0oU9JSPdh+A3dSOiaSrVm7oAy5kZuHnEV1gMv8Prh1/E2ejMlUopynMnG3Go8jXrGxwWcdSpqyerdNLUojJkTJUmcEEKI8yKJnBCi59TkwsqHtKbaS57oVxUgz0dckDdxQVo/MFXVetMdLG/kf9uKmZ4UzLz0UIaFuH/qqDE8nLT7bibkUBEH3t5O3qEWCh9dTcY100ibHI5O30mh4sS5oDeBdwiEpKKvO8r3gyfxtL2Vlw6+hLfRm1Eho/rmZgaQxjY7/95USFKohfFxZ0+as1fmYDa5SL5yWh9FJ4QQYrCSRE4I0TNq82Hlw2D2hZk/GzRJ3LcpisIvL0wnv7qF1dmVbMyrYc3hSq4cF83SzDP0eOtjQRlxzP5NHLWHijiyo5raMitqRzv1y5Zhmb8QY4DfmQ+O/0Y/swPvYqzO4ceX/ZXn9z4vVSzP4N1dpbTbndw8Je6so3Fth3MIqsnCd/4cGY0TQghx3iSRE0Kcv/pCbSTO6AGLHgNLiLsj6lWKopAUaiEp1MJNk+PYmFtDWriW5BTUtLD2cBXz08OIDfJya5xBGXFMyYjD5XTRvm8vNZ+u5otVrcSl+zP8hpmdT7m0hEHhRjx1Ju7NvPfrzVabFYvJ/SOQ/UFZQxtrsiuZnx5GdMDZ/751JiNxY0IJXjqzj6ITQggxmPVIQ3BFURYripKjKEqeoig/P83rZkVR3jr++jZFUeJ74rpCiH5i35ugM2hJnM859jQboLzNBhaPCCc+WJt6WVjTwtqcKn723j5+9dFBNhyppsPhdGuMOr0Or3HjCHv4IYIivcg71MKnD33Kzmc+oqOl48wH+kZqRWtaqr/e9HnB5zy86WGqW6vPfNwQ8vq2IswGHVeOjz7rfk01bZTUWwi668fozOY+ik4IIcRgdt6JnKIoeuBvwBIgA7hOUZRvd9S9HahXVTUJeAZ4/HyvK4ToR6bfAxf8SXvjP8TNSQvl7zeM46bJ8TS3O/j7ujx++s5++kOFYN+kGGb/ZikL7plGSKQHR4tg9SuHsbU7cNlspx5gOZ6UN1d8vWlkyEgcLgdP7nySxo7GPoq8f1JVlQnxgVw/KQ4/z7NPlTz47g4OrS/G6XT1UXRCCCEGu54YkZsI5KmqelRVVRvwJnDpt/a5FPjP8cfvAvMU6TArxMDWfAzWPa41lDaYwSfc3RH1Gz4eRi4cFcHTV4/moQszuGJcFIqioKoqz6/LZ3NeDXY3vqHX1tAtZeGD80ibGoGupZGyu39C9vPv0VZZ940bidC+N1d+vSnKEsVPxv+Exo5Gnt71NK321j6Ovv9QFIXZqaEsyDj7KHRjfhnFu0oIV8sweciKBiGEED2jJxK5KKDkG89Lj2877T6qqjqARiCoB64thHAHaxV8/iCU74HWGndH028pisKIKD9mp4YC0NBqJ7uiiWe/yOWu/+3mf9uKqGxqd1t8/mEWEsdqsSkZ49i3u51PH/6M7X96n5ayKvAK0qqPxp9cYTHRP5Efjv0h5dZyntvzXL8Ybexr2wvq+OxABU5X5/ee9c5WFAXSr5naB5EJIYQYKnpkjVxPUhTle4qi7FQUZWd1tazBEKLfaanVkjhbCyz6PfjHujuiASPA28Rfrh3Dg0vSSQv3Ydn+Cu5+cw8Hy9w7RdEQFET0j+5gwb0zCI02c/RIO8t+u4p9Kwvo8E0B86nVKkcEj+C7I7/LjOgZZ63UOBjZHC5e3VLIhiPVdHbnrRW1lB5tIzrWjE/04C4CJIQQom/1xByPMuCbNbejj2873T6liqIYAD+g9nQnU1X1H8A/ADIzM4fex7xC9GetdbDiQehogoWPQlCiuyMacBRFYXSMP6Nj/KlrsbHhSPXXFS/XZFfS3O5gbnoovh59X54+KD2WWb+KpT63jMNr8ijJt5KWepCmtZ9jXnwL5siIk/afGDHx68elzaVEWaKGRFK3IusYNdYO/m9mIjrd2e+3buVavF2NZCyd1UfRCSGEGCp6IpHbASQripKAlrBdC1z/rX0+Bm4BtgBXAV+oQ3EujhADnaMdFB3M/y2EpLo7mgEv0NvEZWNPzETPrbKyLqeK93aXMi0pmCUjwr9uRN6XApKjmJIchd3mRFn/Z+rfe4+Dm33xjQli+NKJBGYknLR/ubWc3275LfNi53FN6jWDOplrbrfz4Z4yRsf4MzL6LP34jvN0WZk0XkdAav/oMSiEEGLwOO9ETlVVh6IoPwRWAHrgZVVVsxRFeQTYqarqx8BLwGuKouQBdWjJnhBioLC1gtFTq0p56d9Ap3d3RIPS92clctGoCFZkVbL+SDXrcqq4eHQkN0yKc0s8RpMeIocRcVEoFc1hFOe3U/bMZiLCtzLqltkEJGkjdBHeEcyOmc3KopVYTBYuGnaRW+LtCx/uKaPV5uTGLvyd1JZb8b76BsxeUuBECCFEz+uR/11UVf0M+Oxb2371jcftwNKeuJYQoo91NMOKX0L4SJj4XUniell0gBe3T0/gmgkxrM+pJj5YazJdY+1gc34tc9NCsZj7MDGwhGPyVJi8dDSjXMFkvfElhTlW1n1QypSrvAgNN6Hz9OT6tOtpsbfwfu77+Bh9mBUzOKcSjo0NwNfTSEzg2Zt/u+wOdryXjSXMn+lXJ/dRdEIIIYYS+ZhQCHFmthZY9StoKIJxt7g7miHFYjZw4agTa9L2Fjfw+rYi3t1VyoykYBYNDyc26OzJRI/4qq1EcwVeCUlMuPsiRlo7KDpYR3C0hWO//TWNxnAC5s/ktsm30WJv4dVDrxLjE8Mw/2G9H18fGxHlx4iozqdUFny2g4b9R0m7dlQfRCWEEGIokkROCHF6tlZY9WuozYc5v4To8e6OaEibnxFGSpgPK7KOsSG3mjWHKxkV7c/PF6d1WnDjvFjCtFHY9hOVNT0sZlInR6A6HFimTWP3J+VYXzlA4Af7uWLJdEaljiTBL+EsJx14jjW2s/LQMS4fG4VPFwrRHFmfj6cZYmdl9EF0QgghhqJ+135ACNEPqCqsewxqcmDWAxA7yd0RCSA2yIvvzhzG328Yx/WT4ogJ9Po6iducV4O1w3H+F1FVKN0Jax8Dpx1MXrDkT5C84JRdFYMB3yVLWPjE9aRPCaO5VWHLm0dQPjZTV9FCdWs1hY2F5x9TP/DurhJWZ1d1qW/csS2HaGxWSJoQjs4gU5GFEEL0DhmRE0KcSlEg/RJImn9KM2jhfj4eRi4ZHfn188qmdp79IheTQc+slBAWjwgnyt+zeyd1uaBoExx4G+oKwDsYmiu0PoEhKWc91GTxZPR35jP8Ohs572+lxOpHR4udZZ8/Qr21nutvfIzowIHbb7CkrpVNebVcPDoSfy9Tp/uXr9mBUa8n+QppAC6EEKL3SCInhDjBaYeqQxAxGmImdr6/6BfCfD34wxWj+PzgMdYermLVoWOMjvbn9ukJhPp6dH6C1jr4/OfQVA6+UTDtbhg2B/Td+y/C4Gli+DWTSXcpKEYD898NYfdeldX7PmDU9ERGXbsAg6f5HO/Sfd7eWYKHUcclYyI73dfV0kJwyWZi5szH5NMHaxiFEEIMWUp/bueWmZmp7ty5091hCDE0OB3adMrSHXD5P8A3ovNjRL/T2GZn9aFKvsyr4dHLRuBtNlDW0EaQtwkP42mm+akqbP075CyHYbNh1NXgE9m9JM7eDqXboWCj9vNj8oZr/osK7H313+z6sha9IwBfk4608T6kLknDEDOyp265V+VVWXnowwMsHR/DleOjO93f1uZA7+wAQG/p+x6AQgghBh9FUXapqpr57e0yIieE0JK49Y9DyXaYfKckcQOYn6eRK8dHc8W4KBRFQVVVnluTS1VzB3PSQlk0PIxQn2+M0tnboHir9vjoOu1L0cG4m2HkVeDogMKN4BcDftFakvZN2Z/CzpfBaQNPf0hZrE3HVBQUYKz/bjym5PB2qSfJReM4nBVHW9sWxt4+DFxO9L6+ffMHc44sZgNzUkO5YGTn/yZsrTZWvHyI9CmRJI0P7YPohBBCDGUyIifEUOdywcYnoWCD1icu41J3RyR6kKqq5FZZWX7gGNsKalFVmBAfwCVjokgKtZzY0dYKjaXQWKJ9jxyjTbGtzYdP7j6xn2cAWEJh6o8gIB4q9kHhJkiYAaHDQfetGlqtdVBfwJGKncT4xtHaHo4xIBTn5+9QtXoTDeOvYfh107HEhPXFH0evOvDSCg7tbWHBT2cTGBvo7nCEEEIMEjIiJ4Q4vaJNWhI3/lZJ4gYhRVFICfMhJcyHGmssqw5Vsia7krFxASSFWuhwOFFQMJm8tKIm3y5sEhAPl79wPMk7nug1lUPzMe21iNHa15l4BYJXIClRWvsKxdHO6tyPWGBbgy0qlYK8Dgp/t5roGCMjlk7CLy2u1/4sumv5gQqGR/p1qV+fy+nk6O5q/DwUSeKEEEL0CUnkhBjq4qdroyzhI9wdiehlwRYz102M5YpxUegVrW3B6kNVfLS3jPnpYcxNDyXY8q1iJDq9NqXSr/P1YV2xt3ov7+//J7UWOzf/+h5i63Rkvb2VkvxWyv+5n8SFMHZhLIrSi73xuqC0vpVXtxRx2dhIYoM6r7hZvGo3bTaF4QsGV/88IYQQ/ZckckIMRaoKu/8DCbMgMEGSuCHG/I3eZkmhFpJCLXywp4wP9pQxLs6f+elhjI0N6JVrT1YslLfZ+dTijaVyC1emXMnUBy6jpaKO/D3V6DwN2MvKqX/9dZQZiwiZPNItSd1He8sxGnQs6cLaOIC8dUcwGVUSlkzo5ciEEEIIjSRyQgw1qgrb/wHZn4DerCVyYshKDffhZ4vTqGpu54vsKtbmVNFud32dyLXZnHiaeqiptcsF257ncq94mlNnsqxgGd4mbxbHL8Y7IpBREdqUxLZ9+6gtrmf/ywfwf2cf6QtTiVmYie7b6+96SWVTO1/m1nDBqAh8PYyd7m8vLye+fC3GhZeiN3e+vxBCCNETpNiJEEOJqmoVBrM+0NbDTbhDa/4txHEOp4vGNjtBFjP1LTbufmsv42K1Ubrhkb7nNzqmqtqaTIMHrqhxvLDvBfIa8nh02qN4GU9eh2ZvaSfn3U3kbj9Ghw18LDD6+0uISg5A0fXuz+zLXxbwxeEqnrtuLAHenTcAd9lstG7ZgueYMej9/Ho1NiGEEEPPmYqdSCInxFCy+1XY/zakXqC1GZAkTpxFQ6uNT/ZXsD6nCmuHgwg/T+anhzEnLQQv0/lP6LC77DR1NBHkGXTGfRw2O/kfbiX/SDtqYBgL78jAtnsXnuPGojN1nmSdi7d3ltDa4eDWaZ2PVtttTrZ/fJS0qREERVo63V8IIYToLknkhBjqnA5Y8xuwhMGUH0oSJ7qsw+Fk29E6VmdXkltp5bnrxxJsMdNqc+Bp1HdtlG7P/0BvhJFLT/nZU1WV93PfZ0TwCFIDU097uOpSaa5vx9xQTsVvH+GwZSqRY2JJu3YmJl/3Nd4+9OaXHDrYzty7JhMYIYmcEEKIniftB4QYypwO0Btg3q9BZ5AkTnSL2aBnZkoIM1NCqG7u+Lqy5V9W51LXYmN+RhgzkoPPPErXVA4H3oZhc077s9fubGd31W7WFK/hgYkPEOd7agsCRafgG+SJGjiMgHt+hvLGfg7tqCN31/vED/dl+E3z8Ag4v0TK4XRx+Fhzl6eQulwucjcV4GXUSRInhBCiz/XNynEhhPtkfQDLfwq2Fm1ERJI4cR5CfE60J5g8LAiDXuHfmwr4/n93848N+RTVtpx60I6XQGeEcTef9pyeBk/uy7wPL6MXT+98mmMtx854fUVR8Bs7nEVPXMfs72biH2ImL7udVa/l0ljdiupwnPO9bc6v5dFlhzhU0dSl/cvW7qOtXUfSlM7bEwghhBA9TRI5IQaz7E+0N9HeoWDwcHc0YpCZkxbKH64Yxe8vH8m0xCC+zKtlX2kjoI1utdudUL4XSrbBqGu05uBnEOgRyP2Z96Oi8vTOp2lob+j0+uETU5n36NUs+sVC4kaG4ONvovwXvyDnmVdpOFzUrXtRVZVl+yuI8vciI8K3S8fkrc3BqFcZdvHEbl1LCCGE6AkytVKIwSrnc9j2IsRMgpk/1Ro7C9ELEkMsJM6ycNOUOBS0Ed+tR+t4aWM+Dzr+TpQlGO+MSzs9T7h3OPeMv4endz1NcXMx/h7+Xbq+f0wA/jEBuNra8Bg5mu0bbew59CXh4ZsZfvk4Qsadft3dN+VUNlNU18J3Zwzr0rRKR3093uUHCRo7HoOHudP9hRBCiJ4miZwQg9HRdbDlrxA1Hmb/XFsfJ0Qv+3qNnKoS7asnMyGIf+VchK6+HfOyI8zPCGNaYjC6s7QPSPBL4ImZT+Bp8Dx+KrXLLQ90np4E3XQDC+fXcejtTRRmN7Hm+Z1ETW5k1CUj8QvxPOOxq7Iq8TIZmJYU3KVruVpaiUu2ELh0Spf2F0IIIXqavLsTYjAKSYfkBTDpTm1dnBB9pToHtr1AfNhw7ppzB01T4thwpJrVhyr5eG85048nStYOBxbz6f8L+iqJ21qxle0V2/nBmB9g0HX9vyvviEAm3H0xI+utHP5gO+UdHtg7nFg3bEA1e+I9YfxJzcUdThc5lc3MTA7Gw9j5yLXqUqlo9CDyvp9i7Klm6UIIIUQ3SfsBIQaTmlwISpKCJqLvtdbBrn9D/lrw9IcxN0JLFYSNgKhxqKpKQ6udAG8TbTYnP/jfLpLDfFiYEcbY2AD0pxml21C6gVeyXmFS+CS+N+p759yM3Gl3oeih8rePcLjEgxZLJKkzE0i8ZDI6k/ZBh8PposPhwvsMyeU3le08yvYvaph4aTJRqQHnFJMQQgjRVWdqPyDFToQYLIq2wLJ7Iet9d0cihqINf9KSuPBRcPk/IHkhHF4GhRsBrdpkgLfWwFtF5aJRkZTWt/Lkyhx+/MYe3ttVSmOb/aRTzoyeyZXJV7Lt2DZeP/w65/rBo96oQ6fTEf6rh4m+cDou9OxaUcwn97xBzkfbsdmdGPS6LiVxADnvb8NVmEt4YteKogghhBC9QRI5IQaDkh2w/o8QnAIpS9wdjRiKMm/X1mTO/zWYvKA6GzwDIXcVHDt40q5eJgNXjo/muevGcd/CVKICPHlnVwn1LTYA2mzOr5O2CxIuYEHcAtYUr2FZwbLzClHR60m+cjoXPXMdEy9Lxmg2sGN3O488s53iokpcra2dnqO5uJKqahexKT7oDTKtUgghhPvIGjkhBrqyXbD2UQiIh/m/1d5EC9HXgpNgwW+1x6oKW/4GjSXa801/gSv/ecohep3ChPhAJsQHUmM90Wj85U0F5FdZmZ8RxsyUEK5NvZZmWzMdjo4eCVWn1zHswokkXDCBP7+TRV1jKx6rPuXwjkO0Zcwk49oZeIadvlVC7kc7AEi5eHyPxCKEEEKcK0nkhBjIbC2w/gnwi4GFj4LZ4u6IhNDWaC58VEvgynZBcwV0WM/68/lVEgcwNsafY43tvLqlkDd3lDA1MYiFGdczLEQ73uFydKv4yZk0dzjY1dTCohFh+EfMo+SYidyDLeQ//BlxiZ5kXDMVn/iIr/dXnU7qDpcQFOSHX2LUeV9fCCGEOB+SyAkxkJm8Ye7D4B8DZh93RyPECV6BMP838Ob1EDm2Wx8yTE0KZmpSMIU1Law6VMmXeTV4mw0MC7FQ0lTCUzv/zJ1jvkdqYOf94c5mc14NDpeLWSmhmIK8GP9QPPHZxWS9u53C/HZKnt9N6pLRjJwdDYCtoIBk6zYCb77rvK4rhBBC9ASpWinEQFSVDU3lkDTP3ZEIcXaqet5VVFttDuxOFT9PI1sLS/nZ2kfw8bJx3/ifMScx/ZyrWT74/n4A/nDFqFNeayysJP9AA14hviQEWWn4dBlMW0xQYgh6Pz8Ug3wOKoQQom+cqWqlJHJCDDTVR2DlL8EzAC79m/SJE/1DeyPkrdH6F341OtzeBHoTGD167DIVjW28t+cwbxc+i8PlZLTnbSxKS2HJyHDM3Sg+oqoqe0saUBSFMTH+Z923ZcsWsv+zijxnIjpvb2ZcnUTE1BHneSdCCCFE10j7ASEGg9p8WPUwmH1h4e8liRP9x7GDsPNlaD6mPVdV7flHPwCn/ezHdkOEnyc/nD2W/1z2W+KDPSjmbZYdzMdwvMF3eUMbDqer0/MoisLY2IBOkzgA7ylT0I2eAICrrY31/97H6l++RW2Z9bzuRQghhDgfksgJMVDUFcDKh8DoCYseA0uIuyMS4oSAeO17XQFU7IN3boG81RA7pVc+cEjwj+WRGT/jwvSR/OmqTPQ6BadL5XefHuIH/9vNK5sKOFptPW3vOYfTxds7S6hqbu/StVwuF+WHa/G1qFzx1GWkTw6l1RzMzs8KcTqctB8+fM497oQQQohzJZP8hRgoKvaCzgCL/gA+Ye6ORoiT+UZqxXdqciB8JLTWaR86jLq61y6ZFJBEUkASAG2ONnTouWNGAhuO1LA6u4rPs44R5e/F9ZNiGB93op3AnpIG3t9dSlKohVCfzqd91uzOw9qqMGpmBCaLJ6NvX8AIuwtrQzsde/dQ+ezfKAidRfLikUTNHYtOJ5+RCiGE6H2SyAnR331VLGL45ZA0X6pTiv5JUSA4GWqOgG8ExE3VqlV6+PX6pR0uB0/seIJQr1C+P+r7jI8LxNrhYGt+LRtzq7+edlnR2EZupZUNR6oJ8DIxOtq/S+fPW30AnQKJF034epveqMMvxAvVfzTmq26i4fNyNr11GL9lh8hYlEbMwvGS0AkhhOhV8r+MEP1ZUwV8/EOoydOeSxIn+jOvYFCPr0+b8wtIXdInlzXoDEwMn8iOYzt4/fDrqKqKxWxgfkYYv710BKOPr4PberSWv6/L42B5I/WtNg6VN+FynX1KpMtmI7hwE8OTHZgDTv33pxiNRF4yl0uevppRs6PosCls/aSQDa/nYGtz9MbtCiGEEICMyAnRf1mrYMWDYG8HXder8QnhNtN/0qOFTbpjScISmjqaWFG0Al+TLxcnXnzKPpeNiaKisZ0NR6oB+P1nh4jw8+SppaPR6c7QwsDlIuyieXikppz1+gYPExk3zCb1KjuF24uobdSj1zmp/ONT6CbPJmjmBBmhE0II0aMkkROiP2qpgc8fBHubVtgkMMHdEQnRNW6spHp16tU025v5IO8DAj0CmRY17aTXFUUh3NeD+CBvHrl0BLuK6qlrsX2dxP1tbR7Dgr2ZmhSMn6d2H7l76wkaMxO/6K6NhuvNRhJnJJEI2MvKaKtrYvv/crB8eIQRF6QRPV+mXAohhOgZ0kdOiP6mrR6WP6B9X/h7CDn7SIAQ/UL5Hsj+BCbfBd5BbgvD4XLw5uE3WZywmGDP4NPuo6rqKU3E22xOHvn0EAU1VnSKwugYf6b66qhcUUbGvCQyZsacUzxOh5Pc9zZxeEMx7TYFfx8X4+5cRGhy6DmdTwghxNAjfeSEGCiM3hCUBAsekSRODByVWVCyHcwWt4Zh0Bm4MeNGgj2DUVWVYy3Hvn6t3e4EOCWJA/A06fnDFSN5culoLh4dSVFtC5vf2kxbSRkRYVrLgnP54FNv0JN2zUwufvoaRs6KpMPoy6ZPSrHWt+OoqTn3GxVCCDHkydRKIfqL9kZQdFpBk1k/c3c0QnRPTS74x4HB7O5IvvZ+7vusKV7DgxMfJMY3hsc+yybEYuZH85LPeEx0gBfXTYzlmswYPth8EA8/CEiNYfmBCpYfPMb05GBmJocQ7td524Jv0puNDL9xDml2F1XFzXjQRvkDP6c6ahIxF88gdELa+d6uEEKIIUZG5IToDzqatWbfq3+jtRsQYiBRVajNA/9zm37YW2bHzMbT4MnTu54mp7qMI5XNxAR6denYuqwC7DYDcaO1no3hfh6E+3nwwe4yfvLWHn7zcRYbc6u7PUqnN+qISPRD5+2N5fKrKKzy5It/7OKLX7xBzd7cbt+jEEKIoUsSOSHczdYCKx+GhmIYfb3Wj0uIgcbsA4Vfamvl+okgzyDuy7wPh8vBY1v/hJNWJg0L7PxAoGHrXsy0M2zJeADGxgbwiwvS+ev1Y7l2Qiz1rTbWHq7+eppmVVN7t2LTmUwEXrSYix6/nJRxgdTWuVj9tx1sf+8Q7Vb3VP4UQggxsMjUSiHcydYKq34F9QUw55cQPd7dEQnRfYoCF/0ZcldA+GhtW9lu8IsBS4hbQ4u0RHL3uLu5c/lvMHp9Srjv3C4d51Ofz9RUPV5hJyd+QRYzl42N4tIxkTR3aH3iGlvt3PP2XmIDvZidGsq0pGAs5q7992r282bcnUvIqG3i0LIDVFQ6UFGxbtyI5+jR6H19u3fDQgghhgypWimEO218Co6ug9kPQtxUd0cjRM9wueDtm7R1n6HpkDAL4qeBZ4BbwmmzObn+tbeZkRTOvbPmdLq/rd2B3qhDsXWg8/TsdP92u5MNR6pZm1NFQU0LJoOemcnBXDY2imBL99YMOu0u1OYGSu+7jyOmcYSMTmD4TXMx+XRtSqgQQojB50xVKyWRE8KdWmqgNh9iJ7k7EiF6VlMFFG6Eo+uhoUjbNuF2GH55n4fSbney5WgtSSEWYgK9yK7NJjUwFZ1y+tUFu1cWUVNiZcF3Mk5b4fJsCmpaWJl1jM35tTy5dDQhPmYa2+z4mA1nbjp+Gm0lZWx58UuqKh2YDC7Sp0aScs1M9Cb39ekTQgjhHpLICdFfOGyQ8xmkXwLSGFgMBfWFULABoidCaBrUHYUjKyHtAvCP7dNQChoL+N3W3zEvdh7Xp11/SqLmaG3nw/s/IGpEOFN+0Pno3Zm02514GPUA/GF5NhUN7SwaHs6ctBC8TF1f1VCx5RB739tLY7NC0Ph0Jl2Zim9Q56OEQgghBo8zJXKyRk6IvuR0wLrHoHQnBCZAxGh3RyRE7wuI176+UpsPR5bD4U8hOhMmfg98I3vl0k6XyprsSiYmBOLvZSLBL4GFcQtZWbSSQI9AliQsOWn/otV7cNhdRKf6n9d1v0riAOakhvL5wWO8trWQd3eVsGh4OEtGRuDn2fnoWsSUDMImpVG2q4iiQgeePkbqXn0V84TJeKdLn0khhBjKJJEToq84HbD+j1oSN+UuSeLE0JW8AKInQO5K2P2q9m9i6SvgHdzjl8qrsvLypgJ8PY1MHhYEwDWp11DfXs87R94hwCOAyRGTv96/ZEcRRoNK1KxRPRbD5GFBTB4WxNFqKx/vK+ejveWYjTouHxvdpeN1Oh0xExKImQCO2lqsO3azYUMHgdF7GPud2fjEhvVYrEIIIQYOSeSE6AsuJ2x8Eoq3aqMPqUs6P0aIwczTH0ZdDU3l2ho6D/9euczBskYUFEZE+X29TVEU7hh5B422Rl468BKJfomEeIVga26lqtJJ9DAvdAb9Wc56boaFWPjJ/BTKG9q+Ho3berSWXUX1XDomkuiAzguaGIKCiPjDY0S/tIb8rCaOPbqKpJF+jPzOAoze3WtSLoQQYmCTRE6IvtBQDCXbtWIPGZe4Oxoh+gdVhWl3ax906Hvnv6MDZY3EB3uf0g7AqDfyo7E/4kDNAUK8tBYJtoP7SevYSeS8O3ollq9E+p9Y41bfYmNbQR0bc6uZPCyIK8dFd9q03GjxIvPui0kuOMaeVzaQm91G1WtHmHFdKt5+3auSKYQQQ5KjA8r3QnU2ZFzqtqrK50uKnQjRV6zVbu+pJUS/kfM5HP4ELn4WdD0/+gVawZE7/rOTJSPDuWFS3Fn3LWgswKe2DcPuQ/hdcQVKHxYiamq389n+Cj7POkaH3cVFoyM6jfebqvOqKclvZczscGpf/Ace8xbiMzy1FyMWQogB7Oh62PFPaGuAkDS44E9aP9R+TIqdCNHXVBW2/h0Ch2lTKSWJE+IET3+oL9KmG8dP65VLFNa24HC5GBHpd9b9bE4bz23/GyElSXz3kmv7NIkD8PUwcu3EWC4YFcGy/RXEHR+R63A4qbXaThrBO52QpBBCksBWWoY1r4j1+7cQEb+fsd+dd0pDcyGEGPI2PwsefrDgtxA+ut8ncWcjtc+F6A2qCttehJzlYK1ydzRC9D/RE8ESCtkf99ol0sJ9efGmTDIifc+6n0lv4oq2RZiyLfxn52s4XI5ei+lsfD2MXDcxlqlJWtGX1YequO/tffxtbR7HGts7Pd4UHUXUo78lNiOQ0mIbn/36M7JfW4PL6ezt0IUQYuBwdGjvzSLHadP6Vz6kFd4agCSRE6KnqSrs+JdWWn34ZTDuZndHJET/o9NB2oVQmaX1leslfp5GjPrO/6uzba8hqqmDLHU3r2S9Qn9YdjA9OZgLRkWwraCO+97Zy782HqW+xXbWY0y+3ky89xIW3D0NHx8d+3c0suHNXBw2SeaEEILVvzl1m7UaijZr3wcYSeSE6Gm7/wOHPoL0iyHz9gE9ZC9Er0peCHoTZH/S46d2OF08tTKH/aUNne7b0WiluspFfGIgl6dcwebyzWwp39LjMXWXn6eRmybH8ey1Y5ibFsbanCr+tjavS8cGDo9nwePXMvm2yQSEe6O0t1D36mvYG5p6OWohhOjH9McLQs3/zYn3ZxPugNZa+ORuaKl1W2jnQtbICdHTTBZtTdzE70kSJ8TZmH1gyg8gIKHHT11c18qOwjqmJAZ1vu/qPagqxM1IYdKwDEK8QpgQNqHHYzpX/l4mbp+ewIUjI7A5XAA0ttrZmFfNwoxwTIbTfyar0+mIGxtJHNCybTuVX2xj3WaV4bNjSb56Jro+XgsohBBuparaTBCvQDB8o11LdKZWz6DuqDZbZACRRE6IntJap/1yGHmV9stCkjghOpc0v1dOm1tlBSA51KfTfZuzcvEy6QiblI6iKF83CG9ob6CytZLUwP5RATLc78Qbj60Ftfx3axHLDxzjqsxoZiWHoNOd+XeO96SJhFlCKPr3FvasKaNo9xtM+t4s/JK61pRcCCEGNFWF168Ge5v2vHgrLP239thpA+8QSJw34NoQDKy0U4j+6uB78MH/QUOJ9lySOCG6xunQplb28ELzvCor/p4mgi2ms+6nOp2E1u5n2iTDKSNUrx56lT/v/jMlzSU9GltPWDQ8nIcuzMDfy8iL6/P5+fv7OVjWeNZjAocnsPCJaxk1K4LGRpXVL+4hb5cUYxJCDAGKAmkXaY8tYTD2RqjJg5IdYDDDzPsheYF7YzwH0kdOiPN16GPY/g+In6H9IuilnlhCDDqODlj3RyjdARNuh+GX99ip739nH6E+Zn62OO2s+zkdLnSKitrRgc7r5Ebc9e31/G7r79ApOh6e/DB+5rO3MXAHVVXZerSO17cXkxxq4cfzkrt0XHNpNQc3V+Mb4UdKsh5XaxvmYT0/xVUIIfoNVYWOJnjzhhPbLKFw5Uv9/gP4M/WRkxE5Ic7H4WVaEhc7BWbcJ0mcEF1la4XVv9WSuMl39mgSp6oqFrOBlLDOp1XuXV3MxrfzTkniAAI8Arh73N1YbVae2/McNufZK0a6g6IoTEkM4qmlo7ltWjwAJXWtvLa1iFbbmdso+ESHMHlpOhnTIml4511yHnmO7U99iL2l8zYHQggx4LTWwWc/PTmJA7jomX6fxJ2NJHJCnKuy3bD1eYieALMe0HqRCCE6Z2uFlb+EygPaByBpF/bo6RVF4TeXDOeysVFn3c9ps1P42XaMzWeeXhjnG8f3Rn2PgsYCPsj9oEfj7Ekmgw4fDyMAWeWNfLa/4v/Zu+/wqKr0gePfOzW9h1RCQoAACRB6b4KAilix7c+6u65t3Wpb17KuuljWddUturZ11bWuDVERpPeW0GtIgBTSe5l2f38ckhBIIJDMTALv53nyZObOnXvODGHmvvec8778+sNM1hwoabOUgqZpaAaN8NtuxTZwLFm7a/jmwY85unanJ7suhBDuUVMMq1+GJU/DRzdB0e6Wj4+4TRUG78YkkBPibEUPhuG3wNTfSRAnxJkw+0JYMkx9GJKneq0b+at3Yq+zExVrPuV+w6KGcceQO5idPNtDPeuYWWkxPHVFGqH+Zv66eC/PfbeH4uqGNvc3+Psz4v65jL8hDadLY8kbW8h4fy0up8uDvRZCiA6y1cD+RbDlXXXfaYODy6HwhItTRjNEpcGAOZ7vYyfr0Bo5TdPCgA+BRCAbuEbX9bJW9nMC247dPaTrerveOVkjJ7qkw+shoh/4hni7J0KIVny44RC78qt47NKBaKeYMrP2mc84dKCWy1+4EkuAb7uObXfayarI6jKZLE/F6dL5dnsBH248zMVp0Vw3KuG0z2kor2bjv5ZQqMUw/OIkevYLRjPJhSohRBd2aC3s+x5yN4LLCUGxcNnf1UV2Wy2U58DqV6C+HGY8qX6HJYNPkLd73m5trZHr6Kfzg8BiXdfnaZr24LH7D7SyX52u6+kdbEsI78teBcvmqZTp43/h7d4I0X3ZamDD65A4AeKGd+qhDxTV0OBwnjKIc7lc5B+qJjzC1O4gDuDTfZ+y+NBi7ht5H/1C+3VGd93GaNC4ZHAMI5NCCfZVo44Hi2sI9jUT5t96Nk9rSADj77uU4iPVhEaayX/kUZyDRhN79cUYLaceuRRCCI8rOQA/PKnKBvSfDUmT1MV2TVNB3Jq/wcFl6uL78FshNLFbr4k7UUenVl4G/PvY7X8Dl3fweEJ0XYfWwbJnICIFRv7U270RonszWqBgO6x7TZUg6ET5FfXEBp86OLPn5JBQs50+o2LP6NiXJl9KhG8EL295mcLa7pG6v0egD1aTEV3X+duS/dz/SSZrs0pO+ZyI+AA0pxMtLoG1S8r57oEPqdh/xEM9FkKIdvIJgVG3w6V/hVE/hciU5kBt/auQvRyGXAdX/gv6Tj+ngjjoeCAXpet6/rHbBUBUG/v5aJq2UdO0tZqmXd7BNoXwvCMbYenTEJ4MF/4BLCdnuBNCnAGjWX35VubCoseg5tSBRXvZnS6KqxqIOq54dms0s5leY5KIv2DoGR3f3+zPL4f9EnR4ecvL1Du6T5ZHTdP4zYx+RAX58OKivfxz2QEaHM429zf4+RFzzx0Mnd2X2jpY+NxS9n68woM9FkKIU6grB/9wGDgH/MJaPuZyqhI3aVermnHm9s+86E5OG8hpmrZI07Ttrfxcdvx+ulps19aCu17H5nXeALyoaVryKdq7/VjQt7GoqOhMXosQ7uFywaa3IaQXXPgEWPy93SMhzg09R8LYe6BwF3xxN+RndviQRyvr0dGJOU0gl1/ui+/1t2IMOvM1ElH+UdyRfgd51Xm8u+vds+2qV8QE+/KHOalcOSyeZXuKeOTz7VTU2k/5nD6XjWPmQ9MJDNTIWFHExgUHJRGKEMK79n4Hn/5ETa1sjcEIUx6EYTd5tl8edto1crquT2/rMU3TjmqaFqPrer6maTFAq/NMdF3PPfY7S9O0pcBQoNV3Xtf114DXQCU7Oe0rEMLdDAYVwBlMYD19XSohxBlImQXRg2DdPyAwpsOH0zSNMb3D6RXW9gWX6vxyNn21j7RpifQbFX1W7aSGp3Jr2q0kB7d5XbLLMhkNXDOiJylRgSzbW0Sgz+mXywf2iubCedexe/khGuwG9NoaGoqKsPbu7YEeCyHEcfYvUmUF4oZDyAlJnOz1UF8Bu76EsCSV0+Ac1tGslc8BJcclOwnTdf3+E/YJBWp1XW/QNC0CWANcpuv6aQvVSNZK4VVHd8L+72HM3VJeQAhPqS5SGcbiT0rO1Wl2vfsDmcvymfHriYQNOH0mx9PRdZ3iumIi/SI7oXfeUVZj44fdhVwxNA6D4dRrSHRdp/SttylcsQn7uNmk/XgGBoNUMxJCuFl+ppohVbwPzH5w7btgssDWj+DoDogZrH4fXq/29wkGpx0GzYW0K9UoXTfVVtbKjn7yzgMu1DRtHzD92H00TRuhadrrx/YZAGzUNC0TWALMa08QJ4RXFe1R63aO7gBbtbd7I8T5Y++3sOhxcNjO6umOdkz5y991FB+LTkhK/Fm1caL5WfN5bPVjFNQUdMrxvGFtVgkfbzrMC9/vpd7e9ro5UKOeIddcQ0XiGHauL+GH331AXXGFh3oqhDgv1ZXBdw+rIA7AXgvZx63ZrcqHjW9B8d7mbfUVar/N/4Z3LoOM/8K6Vz3bbzfrUCCn63qJruvTdF3vq+v6dF3XS49t36jr+k+O3V6t6/ogXdeHHPv9Rmd0XAi3KTkACx9RV3Jm/knqxQnhSWFJ6nfF4bN6+jPf7uZPC3a1+bijvoGSYic94n07bRRpbOxYTAYTL295mTpHXacc09MuGhTDLeMS2ZRTxuNf7qDkFAXEAYwB/ox65AYGTYympFTnu8e+pDCjjbUqouuqOurtHgjRPpvePnlb7bEkWfUVUJmnbteVt32MjPdUgq1ziMwXE+J4pVnqio/FXwVx/uHe7pEQ55fQY4Hc/sUQ1vuMU0UXV9tICGs7q2zRht04XRoxQzo+pbJRhG8Ed6XfxfMbnudfW//Fz4f+/JQ17LqqWWkxRAX58NLiffz+8+08dNEAEsLbfi8NBgOpN00jYsBu1r63ldWLSpkQEUVEfIAHey1aVXJAZVuuyoOqAjWaMfG3EHms9mFDFWz+D+xZALP+pNapnqiqADI/AEc9jP8lmE+dQEgItxpxG/SaAMFxYPJpzlLpcsLOL5r3G3pj8xTLE4O/QXNhwKUe67InSCAnxPHsdaqo5PTHIKD7rncRotsKjoN+s9RCdZMVht/c7qfquk5JjY1hCSFt7mM9vINRzmXET7myEzrbrH9Yf67rfx3v736f77K/Y1bSrE49vqcMTQjlicvSeG/dIcICWi8afqKoIQnM7BXMvp1OQnpYqZj/NYEzLsRgad/zRSfJWgb15TDwMijcCVv+o77PAmNUILftY5j6O3WRZNNbahRjwKXqe++Le9TJscmqgrzSLHVMoxkMZjWKEd79EvuIc4Cuqwt6PsEq03FbEsaoLMiNs6jCklUgN+xmGDzXEz31CgnkhACw1aracFGpcNnfVKZKIYR3jL0b0M84S2x1gwObw0l4gLXNfUIuuwy/YcMw+3V+TaFpCdPIrc4l2v/sMmF2FT3D/Hjwov6Aqsu3p6CKtLjg5h0ak6RpmlpzkvlffDSNQbP/Qt2+vRR//BkbFhcw6t6LCEo6s4Lr4izlrIYVz0OPVOg/G/pcCH1nqMAMIHezKqGz6S3Y/j+I7K+yMYcnQ9FeCOiham7Za1sGcVe9oX53l4zNpQfV65Tv8K6pLAf2LVSjZo0jvEV7YeuHappkv1nQe4pKalJXpn5K9kFlPsx6Wl2YOJHz2HrqyP4tl8KYLHDLfHe/Iq+TQE6Iyjz49kEYfB30v1i+AITwNk1TV1YbpydWF4J/5GmnWZZUqy/0cP/WR4JqKhrY8l0eqZN64o5JYpqmcXNq8wiiruvdcoplE13ny4w8Ptl0hMcjl5DSw18lgirPgSteVaOn4X0g9XI1yrPiz/jO/it+t91N+X938P0zPzD2R0OIndjKtL32cjqgtlj9+xuMqq5n2UHQDCpIMVrVyVs3zkbXYWU5sOxZiEiBaY+q9+LE9yNumPqtu2D8L1RK9sa/zch+6nmNjmxSKd0bZ6XoOhxcDj0Ggn+E+1/P2XA6YN0/VbKkUberAtFdidOh/k268+fB8cpy1P/DhHEqYGqPhipY/ARUH1XTdcf9XGWXXDpPXUgP662Kdq/7Bxxc0RygWQIgMgV8Qlo/buN+prYv4J3LJJAT57eqo/Dd71R62qiB3u6NEKJR4wlPZR589QtIuQiG33rKEyE/i5FLBse2ua7r0OIMCjIrGXyBG0eJdB0KtrGwIZ/dpXu67Xo58rfCkqeZfflrZBWH8OVuKzcWLCLa91hW0OqjKpDrOVL99EiFH/4Iy+YRM/VhpgW4WPGPNax4J4PBR0oYcN3k9p/E7vpKneBV5ql2gmLhkj+rk8fVLzUnNWh05WvqObu+Um2EJkJAlApWEsZ06tvS5TgdsPIFlYr9gt+rE+JTGfmT0x8zfnjL+9VHVaA49h5V97GjXM6zC7ydjtZLAa1+GYp2q78PUFNEj5e7WY3sRKWq+44GCO115u2fSNfBVgPWNtaE1pXDkQ1waA3kZajXfN1/W76GqgI15XX0nV2/zJHLpV7Pri/ViJnBCD+aoB5b+aL6PztwDqReCc4GtW7NUa/+5nQdlj+vLsoMvEz93zy4HJY/p6ZATn+sebQtKg36X6ou0PiEnP59sdWo//fm0/ztn6O6+F+NEG5UXaSCOHs9zHxKffkLIbqWwBjoPVVNBwO14L0NPYJ8uHFM2ydoeRuzMFc6CQjr5GmVthqVXCJmsBqx+u53mIdfS0ZRBl8f/JrZvWd3bnvuZq+HVX8FgxGrAX59YQr/tJj41d7R3JpoZ0YCEDu05XMSRqsTNnsdaBqhA5KYOfwJVmwexq71Diz5n5M890cq6KsugqJdED1YjbZWFwAa9BqvUoevexWC4yGiHyRPVUV/rYHgE6Su2I//pfrtqFcX4XxC1GjRrq/Uvmjq38FRf+4Hcke3q7+9qb9zX4bliiPqt8VP/W2cadITp0P9u+ZnQsFWNf3x2nfVCfqOz9S0uR79wb/HsX9TGySMbQ78nQ5YNk8FZHPfVn8Hug4uR/P/vcp8mHQf9J7csu3KPDUK5HI0b/MJhuveU7cb11+dqfJDsPiPKuV9RF91TKMF0n+kgsRd89UIIahRzL4zju1jUkFl5odqlHTxE2qfoFg1JdZdo0q6DrWlzQnctn0Cg65u+fj8X0JFrvp3NvurpG8pF0GfaWr647JnVFDvF67WnSWMbg6yYgZDQyVseRf2fQ+J49VntslHfS4U7oL8DDVa2v8S9RyzLwTFqwsQx0+Z9A9vf6I5W41676568/QXMc5REsiJ85OjARY+rD54Zj4ti7iF6Ko0DcbcCY46dWIwYE6b07uq6u1YTAasppOv9jsb7JQUO4nv7de5xatrS9WIodOmTjI1dewpAUnsia7hs32fkRKaQt/Qvp3X5tmoLlIns0ExaorTgvtVn4PjYNy9ze9pdSEseVqdsM2aBz5BGIE7JyejafBhdhkjRw4htLWT39TLm29bg7D2SmFafyt7isKISxhNVWYOfqEDMe5ZoEYhjmeyQuIEFYykXKQCdvMJAXdIAlz619ZfX9JE9dPI5VTJPAAKtsGeb2Hib1qfOl9VoE7CG7PgORpUsBHR13NTNnNWqyC45+jWR3h0XQUlBdtU4hGXQ41qxKY3T3N1l/py9XvZs+r3/33a/oDDVgtf/7o5GAzrDX2nq4DNGKD+LrOWqOyZjcJ6Q69x6vaBH+DAEsjbou6XZqm/g2XPqiAWIGYIXP+Bek/ePnbRJDgervinet9ihqhi0JV5KnFL43q/kgOw8PcqYAmMhoBo9Vif6er9zM9UU/zihqtjVOaq56TMUiO+wfEqgCnLUZ8DLoe6sADQYwCk36D+PU/MvltdBLvnq59GG9+Cze+o0arTzDw4K1lLVPA2+y9qdHDHZypDcFiS+rt31Ktp0ppBrTG016kausc+zwiMUq95+C3q3+bE/xfJF6ifvC3qQkzjhTdNU+9NdQEMvBxSLm5+TlmOKjOTtwX6Xnjmr6kyD778uQoET7ywdB6RQE6cn0xW9aES1lt9WQshui5NUyNzcMqkC/9enc3eo9W8dP3JX+pHN+7BpWtEp3XyCW/BNnWFffrjUF8JO9QJjGav4ebUm8kqz+Jfa+fxh/BR+FoCVfDkaFBXkXuOVkkm3K2mGD79sRrdmvArteYktBdoRshZqU7wxtyhRhkW3A+6U50cRac1HcJg0LhzcjKFwxoIbWMNYgtGE1z8LBrQH7AXFlLw2kNkLC5kyN1zCJqUqAKEgGj1HhjNx/6do48lu+kgg7E5MKspgoPL1Gf98cEmqBPzb+5XJ/ypl0PSJFWCpr5C/Rul3wBJZzAttC22WnVy3DjSUHUUcjeq12+vUaOJhbtUv2PSVVCbMEb9vZcfVrNH6srUc41mFXhGD1IXId0ZxIEaEfcLV8ko6ivV96euqwsCPkGnfm5lnho1Gf8L9fd+4v6jb1cjNuU56j0/Pq280wFr/6Het1G3N5+sz/8lNFSr4MxWo7abLFBf33zcxsAxOA4u/IO6HTPk5P71m6UC+eoCyFmlXlNkinqe0QJZS9W6O80ABpNKqNFvpnoPpj/W9usOT277AnFkPzVyV35I/a0bTCrLaNaSds08aJOuHwuMMtSol8Go/m9bg6Bwh2qvrkwFbfUVsOhY/32CYfQdas1aW3yCVbKR04kdCnNegcz31e1vH1LTMfvNbLlfRa5KzmPxV//nTtRQ3faUVUeDes8Orjj2WRp/+n6dwySQE+eXunJ1pTkyRSU2EUJ0D2a/YydsbY8ElNfaCfEzt/pYw779BOllxI4dcPZ9sNepgrLDb1UnSY0nTqCuZhdsg+yV6iQjoh++Jl9+mjCLZ5c9wO7CQwzVT+h7fQUM/b+T23G5oPKIGj0Iijn7/ja2sWeBSnKRdGzamabBlAfV7RUGNTIw7CYIilOjWgMvbzU40DSNqCAfdF3n84xcdB2uHNa+kyhzjx743/lrjr6xhUV/XsqEH4+ix4jJp3/iWdCPZdXUNI16Rz0V0QOxRffHvulfOGvycCRNpHdYP6wNNRQsepgco44zpCeOnMU4fMw46/OYGDcBn9pSdi1/mp2Ve3D6huLSXejouHQXc/vNxWK0sLFgIztKdrR4DODHaT9G0zSWHVzI9h3/RS/ep/qUfj2m/K387KgqSryYWvZiQ4sfgdZvLJTn4Fe0gRtzN0H2Sr7vN4GcsgPgo6FFDkQLiiUwMI65KSqd+uKcxRTUFmDUjBg0A0bNSIg1hGm9pgGwOnc1FbYKDJpB/WAgxCeE4VFqHVxGYQZ1jjqMmhFN0zBoBkKsISSHqCAkqzwLl+7C4B+MKSAco8GIf305IRvfhqPbqZz+GEa/MIyaEaPBiEkztVwTGtFHZb48VUIMg0GNDJ3IaFLPdTQ0J15xOlQAm3aVuhB7PJ8guPkrtSbNepoAE1oPto6fatljgBrpK9wJuZtUsDnq9s4ZLUu/vuX9uGHq8y2sN8SPUtsKtqtA1WRRo2etTZ11NKiRrcZpjSX71fagWBW47vu+uQC20aJ+kiapUc36chXclWapZEKdxWhSnydOx7EMoieEGrYaWPKU+ny79EXV/wOL1We8o15NoXU2wJWvn7w+rqYYPvuZet2apgLr87xUlARy4vzRUKWmUdSVqS8HKW4qRPcx8DJ1YrLzSxUwFe6EnqNgzF1qhMJWg72qmMhgf7WOx2huMf0n2FjNsMQKfMLbcYLXlpIDKglIWbY6Ady/SBVMDu6pThwTxsKcl9VJaWU+AH2TLuBZw18I7TlWpX7f+YU6kYof1ZxJ8Hi6Dv+9VgWNAT3g6jfb3z+XU53gNK4VWf+v5kK5Yb1bn340YI4qHA3q/WrnaFh+RT3L9xbhZzEyK619wWbkyFQu8PNn+d9XsvS1DYwqriBx1kgcLgd1jjrqHfXUOeqoc9QRExBDkCWIotoiNhRsaNpe76yn3lHPZcmX0TOoJzuKd/D+7vexO+00OBuwuWzYnDYeHvMwvYN7s75gPW/veFtNe7PUwc43Ye97PHnJO8Ru+x/bbGX8NzQMDFXgq8Hh7yE4mGHDbsYnLJn9Ga/ybeFGDJoBDa0pILqizxVYjBbyavLILMpUb9+xfTRNw6W7MGpGKjLfJa98LxpAaC/06jzMBk0VJk6+gJK9n3CocDMYNHRnFXpAKAGh8dD3etB18grXsKfiAHpID8CFXn2YcGdt03u6o2QHe8r24NJdTT9xAXFNgdyiQ4vIrsxu8e/QN6RvUyD30Z6PKKgtaPH4oIhB/Gr4rwB4JeMVyhvKWzw+Mmokd/abBQeX8eDXN1EfltgiuJkYM55b+14FB37gF4VLAQ2TwYTRYMSoGZkQN4FLel+C3WnnuY3PYdSMmAympkB0dMxoRseMps5Rxwf7P20RJBoNRgalzSElrDc19hpW5K7ApDU/16AZ6BvRl2j/aGrttewq3aWerxkxGNQ+Mf4xBFuDqXPUUVRb1NSvxn/bIEsQFqMFh8uBXbdj7NEfQ9TApmDXbTQNUq9ovp/5XzW9E9TI1eT7j60BPc6KF9RI4vGC4tTnkdGsyilV5alReGug2gbNgbM7pyQaTXD531puc7lg/q/UBfXpf1CfcatfUqOIjaIHQ0MFfHA9XPRsyyDfJ1g9Lz9TXaTqPcV9/e8mJJAT54eGahXEVRxRaZYliBOie3E5YcF96optwLGkCPu+V/WI/MJgx+dcn/MqQT5mKDxubVVwT5wOJwFXvozZ7ywKVFfmwf9uV7cveUFdvc5ZpQK5hLEq8Oo9+djaK4O6Er7m72oUbOZTEDOE0F4qs9uO3DXEhycTPOm+5tfUmIkvf2tziQV7nXo87aqWfakpga3HRgjG3N2caXfhI2q9UOP6nCHXqZG+mHR14hM9CML7tj6SENFXZX08cT3aKWiaxs8mJVNvc/L26mx8LSYm94tE13Vq7DVU2arwM/sRbA2m2lbNytyVVNurmx6zza4kcUEUm9bWUBC2ndfKXjipjTuH3MnI6JEU1xXzyb5PMGDA1+SLr8kXq8lKnVO9Rz4mH2IDYrEarZgNZqxGKxajhSCLCthTQlP4SdpPMBvNmA1mzCX7MZYcJDwoHsbcxdjkC0gLjm06mTcZTJgMJvxMfqBpXDr0Di4FtcZu/yI15bShEvYtAr8w5iTPYU7SbPjoRvXvaatW73lDFfiGMGf875mjO9WUweCeJ40wXDPqV1xzivf65sh+p/y3uHfYvad8/KHRD7UI8py6E6PWfIHjNyN+g91lb3pM13WsxuaR4zuG3IHNacPhcuDUnbh0FyHWEAjtC2Pv4dqVT9EQNABnzCCcuhPn4fUkbP4v7FqKXl/ByEEzcFoD1WMuJw7doZ4P6OiYDCZcuos6R13TPjX2GgBsThvbirc1Pc/pcuLUnQRaAkkJS6HKVsVHez46+T0beDPR/tEcrT3K3zL+dtLjtw++nTExY8ipzOHZDc+e/J4OvZf0HulsL97OS1teavGYhsZ9I++jf1h/NhRs4J0d76gg8tjfj9Fg5J70e4gPjGfT0U0syFrQ/Nixx29OvZkwnzAyizJZn78eg2ZoCmQbLxL4mf3YNfAi9kUlYnDaMGYtxfj9b7H0n834UfdiNpgpqDxMWf4GrPFDMPeZgbWmGMuG1wmuK0erPqqmcBpNavTN22y1sP97dXEJ1FTb2HR1e8aT6nd+pspsWZqlgk57nZqaecMHzccxmtXaxOOmfZ/vtMYpCF3RiBEj9I0bN3q7G6K7s9WqIK70AFzwCMSP8HaPhBBnI3ezutocGKWu7BZsVamqjSacRft44b9fMy4piPENK1XSDoCYIRRUx7I2ezSTbhhAWIx/+9uz1cKnP1En7qASgtQUq9s3fKiukh+v5IBKp924PgfUNO5L/kylrZL7v/0JAwOT+PmUeWi6rtLGZy1t3jf5Aqgrbb46HdJLrS0JilMp4esrVH8MJjV6dulLKojd+YVav3RorQo8GwO5TmJ32ilvKKeioYIKWwUVDRXEBsSSHNyPp7/JYHHhGyRHGTCZ63HqTgCu7Hsls3vPpriumPuX349BMxBgDsDf7E+gJZALYy7EtTWM6MF+bD/0Lea4WHzMvk3BWnxgPEGWIBwuBy7dhdlg9m4Zh5zVKgnM8XpPgUm/VcH82r+rNYeNCSwm/Epl+zuX6bpau1e4U323RqbAV79Uoy0RfVWmxpSL3Ni8Tr2zvinAawxG/c3++Jp8sTltFNQUtHjMpbuaRuQqbZXsK9vX/JhL/R4YPpBw33AKawvZUrilxXOdLicT4iYQ6RfJwYqDrM5b3fQ32hiIXtXvKiJ8I8gsymRxzmIcuqPpuU7dyd3pdxPuG86yw8v4+uDXLR5z6k7+NPFPBFmC+GzfZ3yV9ZV6sS4X1BSCXxj/nPkmFqOF93e9z6Kc79W06WOzDzQ0Xp/xOpqm8c6Od1ibv1Zd5DCa8TH6EGgJ5L6R6kLS4pzFHK46jMVowWq0YjVZCbIEMSlerVnLqsiizlGH1aAesxqt+Jh8mi6StJvTDh/f0px8CNQU2BP/P+u6mhZ7aK1aV9dQpbZPe1TNvgAV3P33Opj425bJjc4DmqZt0nX9pBNYCeTEuS/zA8h4X6VnPtdTUQtxnrI7XSzccZS+UQH0iwpUi+l1J4QksO6ZDzi0v4IrxnyNKShMBWAmXzUyP/X36ipvyQF1RdhWraZxFu6CDa+rRBmhvZprVIEKpG76XGVjW/68SohRsg82va1GYyb8WhXL3fCG2n/Cr8BWwzdH1/Fx4XruNkUxvMGuArFPf6z26TdTJX1oXNMW2R/Wv6YCx17jYepDaj+nQ12U+vo36v7ct5szTuq6OtE5wzTc5fXllNSXUFpfqn7XlRIXEMfknpNx6S5u//72prVfjab2nMqNA2+kuqGBmz/7Ayk9ohgaH0uwJZhASyCJQYnEBMQ0nWz7GH1aDcRshw9T8Nhj1I+YSZ875nZuRtHOVrgLivepdZox6eqCwokOrlDrC09cv3WustXA0j/BsFvUerhVL6lELa1NGxZn7PgA0ak7sTvtBJsD0AoyKcJFidmKTVOjlzanDYfuaArENhRsYH/5fmxOG/WOehqcDZgNZu5MvxOAf+/4N5lFmdQ76rE5bbhwEeEbwbOT1CjlcxueY1fprhb9iQuI44/j/wjA0+ue5lDVITXSfewnOSSZ2wffrv6vBMY0r+vbu1BNoYwepIL+031GuZzwzmXq9uQHVNDmtKuLZF/+XJWLikqD+JEn1z08R0kgJ85fLqf68u3R39s9EUJ4msvJ/Dv+gcVZyoyJ65u3R/ZXV7IvfEJNi2uclghqqmTWslMf96rXVaB1eL26fXC5mhI09u7mzJpOh0qQkLsJNr+DE50/mqqpNPvwZNJV+A29Cey1KpBsLRuny6ky6gXFnnz1es+3Kngce89pky/YnDaK64oprC3kaO1RimqLCLIGMSd5DgC/XvrrFuugrEYr42LHcePAGwFYmL1QTZW0BBNsVT8B5gCMx0YBXC4dg0E76XZ76LpO1r8+ZsMGO7HxZsY/dAVGS+sJa4Q47+m6Kr2QvaJ52y3z296/3YfVVaDosuNrUtOsC2oKqGiowO5S608bHA1YTdam9ZWLDy2muLYYu8uO3WXH5rTRw68HVyRepC5QGYww6BqVgCVhDPxwbArlsSnnp+V0qLIzlgD1Gbf1I1WKwuKv1ryW7Ff15655p/PLNXRBbQVyskZOnJscNtjwLxhyvZp6JEGcEOe06gYHFXV2ogKtmIzNozq1676kusFC/9R4mDQZdn6uLuxU5qpEACarCuKMFrVOrL7i9EFccE91UnF4vQoIA6NV8orjTyZyVqupoJW5KjMcYAzvy02T7uWptU/xaYAvN5p9Tr1e12BsO7V8yixgVtNdXdcprS+loKaAvJo8GpwNTYXIn93wLFkVWU37+hh9GBQxqOn+Df1vwGQwEe4bTrhPOL4m3xajZzMSZ5zy7WgM3DYfKuOD9Yd4+JKBBPu2LxjTNI2kn1xNpfNb9mwuY9njnzDp91dg8pN1zEKcRNNg9M/UhZ89C9oXELXrsBomTa0PbRTtH020f3Sbz5mWcIqpw9Mfh1V/bS6Kvn9R82PtHUAymsB43AWu4Hg1zb0qX5VKCIpVn68L7lOf36N+2nr203OcBHLi3OO0q9S2uZtU9qPzbB61EOeFoztVgo+SA1BbQo51MM/u78kz148lOthHrW/bs4D8b74HbSTRE0ZD71SVMbJot5oW2VivqvcUtVbt+OmDaVep6Tu1JWrKZO8pULxXrUGb+hB8fpfar7EG0vFB3L5FsOrF5vtJk1T68ITR9A5O4JLelzQlfDhTjQFbfk0+aaH9QdP4aN+nLD34HfW6o2mtTIRPBJckXYKmaVyUdBE2p40ovygi/SIJMAe0CNRGRHfOuuEAq4n8inr+vHAPv79kIBZT+6ZJGgwGht55MZZ3FrNtxVGW/3M1k+6ZjMnioWLcQnQnviEw4la1buzELJZdRURfuPSv6mJX7kbY+50qf1KWrdZSno1e46DHQFj8BGx8U2W0XHlf8+Pf3A8/+rhTut+dyNRKcW5xOmDp0+rDY+w9x65aCyHOKS4XLH5cjXgFRIHFn5JDu9hQF82Ee98iwFEBn98B9jry9kRw+Gg8I/7xGEZrK6NER3eqK735mbDsmebtV72h1kB9ea+aMgkqpXf/i1UikcLd6oQqsJUr1llLVSHesmx132BSJyAXPnFyXaQTbXxLrbNLvaIpODxQfoANWd9yqGQXhzUXNU5V+PiV8PH4bf+MlcHh5FQcJCZhAnHpNxMTEEOgOdAryUHWZZXwl0V7mZLSg59N6n3GfchatI1t2x2MntOb6N7BbuqlEMJjbLXqIlhjlsqOOrAEVvy55baYITDmTvVZfo6SqZXi3OdywvJnVRA3+g4J4oTo7hqqVD22qny1VqwqT42y95kGUx9WNdN8VAa1dUtXsmzHIWZajGAJUxkg+0wnqG8hKQUFrQdxDVWw6DGVmCL1ypaPNSay6D8bNIMqDnz8WrVTTdfuPUX92Grg6A4VJNaWNAdx6/+FyxLIysAggnzDST+yFX3IDRTYKzmQu4oDZXs5eOQb7hh1H9EFuzm88wOWVO8j3uDDiCE30TMkmYTDm7Bs/x+gMcHmYsKQ29VrOMNEJ51tdO9wrhwWz/82HyEx3J9ZaW1PzWpN7+mDiB5lw1xbStHf3iXsttsw+ra/NIIQooux+HVeEAfqoljSpOaSKWlXQ1D7almeiySQE+cOe62aLz3yxzBgtrd7I4Q4G3Vl4BOibn94o1rU3sg3FAJj1W2TVf0cU2CKoyTA2jwCNOZO7DYnZeHhRA0b3Xpb1kCVnGT58yrgajTspubb/U69PuyULP5q+lNj6mxQ60MqjlCdu4EvtFKMBhMWpy9/K1xGndkCVh1fq5mkgp3Yvvw5YGZ8aCITRz6MMXaoyqCp67DkBUBT60JSLjn9SJ8HzR0eT05JDQUVdWf1fL8gC7X7csnbmMWWg58y+ZHLsQYHdHIvhRDdUmCUKo4uAAnkxLlA19VonDUQLvkLmM6i6K8QwvtcTvjsTug7XaXiH3OX+n8dFKNSWR8XuJ2opsGBn6XlV1rR7gI2fpvHhOv6E5nQSlZIUCNn1YWqTInTpop8D7y8817TMZW2SvaW7mVP2R72+OkciQwljWi2V+Wwp99FjAoIp3dwb5JDkonRjWiNZQnmvIT5xFT2x2fYNPs1rYvrKjRN41fT+7VIOnOm/IYPx/+yBkq/PsiSP37B1Ecuk2BOCCFOIIGc6N50HVa/rDLNTf2dBHFCdGdFe1Qdt8hj0xbPYDRs+sAoRiaFtdiWu3gjtv3VhIQPauNZxwy+BlIuVoVmw3p3yudIrb2W8oZyYgNisTlt/Gbpb3DqTiwGC31D+zKy/3UM7TGU+p3vsKo+j3kj722RMY64YeAX3no9MpNFbS/NUpnhdn8NyVO71MhcYxCXU1LDJ5uOcM8FfbCazizg7HP5OAA2fn2QJU98wQV/uBJLgEyzFEKIRl3jE1+Is6HrsPYfsG8hDL62y12VFkKcoWNp+gnvc8ZPHRATdNK2kiNVBAVomP3bkcreGgA3fKSKiJ8FXdfJqcxhW/E2thVvI6s8i76hfXlg1ANYjBZuGngTMf4xJAYntgjY5iTP4YVNL7AqdxWTe05uPuCFT5y6wemPq0xwviEqscr6f6k1eek3nFX/3aW0xsaG7FLeWZ3DTyedeZHsxmBu85KjrJ+fzdi5KRg7MNInhBDnEgnkRPek6+rEZc8CSLtSZZETQnQv5YdUpshx96qU1P4RantdWevZIE9h39Eqgn3N9AhSQZu9pp6KKo2k/mcwHe8ME4U4XI6moOzvmX9n09FNAPQK6sXFvS9mcMTgpn0nxrdeBiU1PJUJcROI8I04o7bxC4P069XtvjNg77cQP+rUz/GCoQmhzBkSx5eZuQyMDWJ8nzN8nahgzpRcTPnRWrDb0TUzmkGCOSGEkEBOdE8Z78OuL2HAHBh+a8saTkKIrk/X4dBaKMuBFS/Ala+qdXAANcVnfLjnvtvDqKQwfjJRjfoUbdmHrkPkgNjO7DV1jjoyizLZfHQzO0p2MG/iPAItgUyMm8jQyKGkRaYRZDl5dLAtmqZxW9ptZ96RIxtV8pfwZDUbof8lanv2SpUhs++M5qxuXnbNiHj2FFTyrxVZ9I70Jyb4zPuVOCgCfYCDo888ix4RTdRPb8EgwZwQ4jwngZzonuJHgrNBgjghuoPdX0P+Vhh2o6rzU18By55Vafk1DQbOUfsFx6lC3NGnWdN2Al3XqbU58bc2f6X5lR1iWMNy4sZe2ikvIb86n0/3fUpmUSZO3UmwJZjR0aOxu1TikcGRg09zhFOrtFWyr2wfw6PaWeB3xQuQMBqSp6mMm4OvUe/l4XWqztK2T2D4LaoMg5c/I01GAz+f1pcHP93K/Mz8s5piCaCZTBiSU1i5qJz46i8Z8as5EswJIc5rEsiJ7qVwt6rfFNlP/Qghui6nHQ78oNayAhxeC7NfhJBeKkPlqNtVkpHjE3SMuPWMm2lwuHC4XC0CuYAxozD3iMQS0v7RsePpuk5WRRYmg4leQb0wGUwcKD/A1J5TGRU9iuSQ5E4tuP3twW9ZmL2QF6a+0L4RPYsf7Pte/Wiayr4ZGAWj74R+F8H612DlX2DH/+CiZ1UphEYVueCoV6N5HhIRYOXxOalnNRrX4jjXXEFMzudk7a7B8tq3pN9xcSf1UAghuh+5lCW6j22fwILfwqF13u6JEKI9claprLKhveCq12HojRCaCAYDzPqTGonrhCyL1Q2q1py/RSU80nWd7Zl11ESfomh3G+od9Sw7vIzH1zzOU+ue4uusrwGI9Ivkz1P+zA0DbqBPaJ9ODeIAxsaMxYWLLUe3tO8JA+ZAv5kqW+/1HzQXMLf4QdRAmP0XmPArqK+Eg8tbPvezn8FXv+jU/rdHfKgfRoNGRZ2dQyW1Z3UMg8HA6PsuJzbOzO5NZez7dGUn91IIIboPGZET3cOOz2HT25A0SU2rFEJ0bXVlqtA2wNSHVfKSQVc3P96JgVBtg8o02TgiV5NfyoHlB/D37U2PXu0fkZufNZ8FWQuod9bTM7AnNw28idExzcXEDZr7rn3GB8YT4RvB5sLNLbNXtqVxOmpbNA36TFM/1UXN252O5sd13SvTLl9YuIeyWjvPXDUYX8uZZxs2GAyMf/ByfnhqPjv2mwjLrSY8TmrMCSHOPxLIia5v13zY8Dr0GgcTfq2u5gshPMflhKPbIWZI87b9iyFrCQy+DqLTWu6fuxm+f7T5fmMSEzcJD7Dw2xkpJEeqk/mC9Xuw5+cRZDl9opMjVUeI8Y/BaDBiMphI75HOBT0v6PSpk6ejaRpDewxlyaEl2F12zAZz5xx4y3uQ+V912zdUBdgA437htbVzN4zuxeNf7uDdtWdXkgDA6GNh8sNz2L48l4AwK87qaowBEswJIc4vEsiJrq38MKz7J/QcBZPu7zLFboU4r6z8C2QthSv+qZKVNG4DyMtQxanTroSEcZCfAWHH1l4F94TL/ub2gMHfamJEYnMx8NIDhWgaRAxuO0jIq87jk72fkFGUwU8H/ZSxsWOZlTjLrf08nX6h/fg+53tyq3JJDE7spIPOAs0ALjvUlsL+RWp7bHrnHP8spEQHcumQWL7MzGV07zAGx4ec1XEsPiaGzehF2UcfUb12AxEP/g7fHqHNOzgaVFbUxr+//YthyHWq9p4QQpwD5KxYdG0hPVXh25ghEsQJ4Um1pbD6JRh0jQriQJ0YN7rsFXWSXLIfinZDXTl8cRdUFcAFv1f7VBxWj7s5MVFxdQOHS2sZEBOEj9lIaV4NQQFgtJ48qlVeX84XB75g+ZHlWI1WLu9zeYczTnaWQRGDeHHqi2dUvuC0/MOb680B9BqvMlv6n3k9txaylkHBNhhzpyp/sH+xuj/hl+16+tXD49mQXcobKw/y7NWDsZqOTbG01aryCSE9290V32HDWPd9Ia55X3PhH69uLgC/7p8qGczxIvqC7oK44aoWnxBCdGMyR010TVnL1JV+gPgRYOykaUZCiPZZ909Vq8wvDKJS1Tb7sQQV1YXw3cNquuXIH8PFz6npz1UFEJIAsUObj9M4lc+NtuVW8My3u6mqd+Cy23FUVhEWe3Jxb13XeXHzi6zMXcm0hGk8M+kZ5iTPwd/s38pRPc9itHRuENeaniNh3D1n/3yXC96eDcufU0XIs5aqbSv/okb7yg+36zAWk4EfT0iiV5gftqpild0UYMXz8PmdLS8aHK9wl7qAcByf6GD6zRlOZZXGmue+xOVyqfV/taVqh0Fzm3cu2Q+r/gof3QTr/3WGL14IIboWGeIQXU/2SvVlHjtUjcRJnTghPKsyH3JWg8mqkpSMuxfW/UNNoQR18l5foTInAmx5t/m5UWnqebHpav+E0ScevdM12FWyEx+zAUdBAYMa1hA+/o6mx4tqiwjxCcFsMHNT6k34m/yJ8o9ye7/Oxg+HfqDeUc/Fvd2QVr8sB4wWCOrAmsXFf2h5vzxHlToA6D8bguLafai0uGDS4oJh2XNwcBlkfgiVuZA4Qf0NORpUmQSf4OYnLbhP/U69QtURdTngs5/R2xpAeUwqew/HsOO1rxjU42uozFP7Bhz3b73rq+bbYUln8MKFEKLrkUBOdC2H1qpCwZH9YcpDEsQJ4Q3WQPU7vI/6HRwHF/6x+f9j9VGwBjVPfwuIbn5uRF/1e8aTnukrUG93AeBjNmLu2ZP4V15Gs1gA2FiwkTe3v8mUnlO4JuUaegefXXINT8ksyqTSVtn5gZzToS6Q2Wrhyn+dfdKo3E3Nty95AQ6tht3zVTKqkT9Rx9V1VaR85xdqe/JUFZTZalqdzni09+VkFRoZE1KL5huq6gsCbH5HXTRIGHvyd8GOz6DPdHWhYfQdkLuR9D7rqTg6kH27exPttBMZZFZ1C0N7gX8kLHpMPXfq71S/hBCim5NATnQdRzbC0j+pk8fpfwBzxwrHCiHO0tHtYDDByJ82b6vMg+K96qS6pqjlGqs+09S0y7pS6D3V492ttzsxGjRMBo2dq/KoLK5j9JzefHngSz7f/zlJQUlckHCBx/t1NoyaEZfuOrsn67palxgY03I6utMB2z6G0oPqAtnZBHGlWeDfQyVPsQaq5DaWAAiIhIGXqwQiNcXwzQMqYLNVq/0apzXmZ8LiJ9TfTeJEGHGbCs7yM8k/cJB/lgzFP97B4JRwqMpXCVr6TFdTIQ+tae5HaC/wi4Dht6jbAP1mQL8ZGOz1TBi9hk0LsrGOvgF997/QCnepNhuDOGhOxiOEEN2cBHKi6zi0BkJ6wYVPqKK2QgjPahxJiR8Jc99S6eobHV4HG9+Ea95Rdckas1eCOiE/XV0zN6q3O/ExGdE0jbylGWh+fry/ey2LDy1mXOw4bkm9BZOhe3zduXQXGmc5E6F4H3z9azUVNqKvGrVqXHsGkDDmzEaiCnepAK78sBp1A7jhQ7Act6bw+L+Rwp1qtBZUEpQ+09UUSVCf7SN/AnlbVL9CE9UFgMwPGZKfyaNVNTiX67h6BGDQNPUa+s2Ai55pf3/NPphTpjImBap+WEL+rj70uHoqJtNx//bXvHPqJCfbPlF9ix/R/naFEMJLusc3mzi3NRalHXuPuqpv6RqJB4Q4rxTtga9/o25f+27LE3RQJ7igRnwm/lqNmHQRs9JiGN07HJfLRdmhUiJ7VbAydyUX9rqQ61Ku82g9uI4qbygn2Bp8+h1b07iGcdvHaoSscFfzYwMvg2E3tz1dXdePJRI5CP0vUdvW/0uNwpp81AyJpMlgtLbdfsJYmPUn6DFQZbI8XmAUpF6u+rHgPlj3KsQNg7KDaJpGdLAP2cU1bAmZwfBRE1XSnA4whoawuyCE/c9+yaRH56r+R/ZXU4JPlLNGJfepLWneNuQ6OLi8eZ1d/EgY/wspXSCE6FIkkBPedXQHrP07THsMAnpIECeEp+1bBH6hULC9eZtPK4HE4GtUZsrcTWpaXBcSHexDdLAPZXsO43RpRCfF8IdxfyDSN7JbBXEADpeDcJ/ws3uy0aQCkMwPYOzdsP1TKNgKE38LSRPbfp7LpZKY5G4Cs1/zSNr4X6jPZL/w9q1XNpohetCp99E0GH8vLPkTWALh6jeh+ij+Ib1YuGgfWw6X82JIGmH+ljN77SfwGzqUkPRc9m6tYM+Hy0kZfC1selsVqp/6UPM6UIAeA1oGcaDew+Md2aDeo9l/6VC/hBCiM0kgJ7yncDcsehx8w9R6HCGE5616Uf3+v/+pkZzWEkuAGkkxWSGu600523qkHIOmYd6RjcPlJLRfLD38eni7W2flj+P/iMPlOPsD5GWodcaVuWoaI8CyZ1RwHtNGvbwd/1NB3NAbYcClzdMhG9egdbaQBLj87+rvzGhSUxmBH41OICrIB1+z8dTPb6f0Oy+i+KEPyFx8mMheyYSBqnlYU9wykHM5T32g0EQ1ZbR4H5QcgHBZYyeE6Bq6ztwYcX4p3q+ujPoEw8ynpTCrEN6SMFb91gxqdGTKQ63vp2mQcpFKbtHF/G9zLp9tyeVI9iZoyKIk1u7tLp0VXdfRNA3z2dbNtNVAyT5V8HrtP1o+9t3v1NrGExXvU9khe41Xo66eWp/cysWCHkE+3DA6AV9L5wRyBpORcb+YgdGos/7TrThdRhVENpYjcNhUTbyPb279ANHHAt+ybLDXqdtf/QIW/h6WP6/qJgohhBdJICc8ryxbfRFaA2Dmn8D/LKcRCSFOz9GgasI52xjlieinftcUqenNZ5uW3osaHE6sJgPFtVsIDNzIgOiB3u7SGdN1nafWPcV32d+d/UEs/mp9o626edv0x9XvhLGqOHv5oZbPaahSI07j7uky5V62Hang7VUHO+VYAfGRjP/lTJy9RlIZf4XKgvn+NSqAe/fKljtPuk9NDW1MCOOoP3lUMjBajXpmLYX6yk7poxBCnC2ZzyY8zzcMolJh1E+75NV9Ic4pm99R9bx6jlKjbd89rLILjrgNek9R66iiUtUJajdlc+iYjRpLRvox+aqZ3W5dHMCGgg1kVWQxLWFaxw5kDYS+M2HLf2DMXSr74s3HimAvuE9dSJvwK0gcr7bFDYPYoV0miAM4VFrLtzsKGJEYpgqGd1B0cjAzwupxFIygDl988/6jHgiMUaUOwnrDxc+pKaW9J598gG2fqPV1oEbhgmKh74zmmolCCOElEsgJz6kqUIvmfYJg2iPe7o0Q5z5dh/oKdfvwevj2IbVGCFQpgcKdMOWB9iez6KIcLhcGp4NeKydinBQKA7zdozNT76jnk32fEOsfy+iY0R0/4JBr1TTJxn/Txt9THoQlT6t6nQljVBbHtKu63L/9hQOjWLAtnw82HOKPsWkdC8xX/BkOLMFk9qdonR+59mQG3vZz/IdOUYGbve70NUsHXg7BPcFpU/9Xegzocu+ZEOL81P3m0IjuqSJXXQ1e/ZK3eyLEuU/XobZUlfM4skFtC+3VHMSN/In63Wu8Go3pYKp3b3O4dAJzjxJW1ICt4oi3u3NGdF3n/d3vU1JXwo0Db8TQWWUdWgs0/CNUXbZ+s+DQWshZdfpEH15gMRm4ekQ8+wur2ZhT1rGDHZv+qNlr8L/jdxx0JrHmf6W4DMfWIZ4uiAOVkCVhtMr8GTVQgjghRJchgZxwv6oCtdDe5YS0q73dGyHOfQeXwUc3QenB5jVSKRer3wljob5c3Y4f6Y3edboHZ/VnsMFJoNPMRekzvd2dM5Jfk8+avDVcnHQxKWEp7m/QaFbr4WY+pf42jF1zYs6kvpHEhvjywfrDuFz62R9o0Fz1u+8Mggf2ZeDYHhQXu9j9/tJO6acQQnhT1/wEF+eO6iI1ncvRALOedl86ayGEUpELmR+q298+2Lw9epBaK6VpsHeh2vbVL+DqNzzfx07WM8yPvOJqTAaN0OR46h31+Jh8vN2tdokNiOXRMY8SHxjv2YZjhni2vTNkNGjcOKYXJdU22hXGNRa0T/8RVOWpwt+WANjztZoKOeZOAAbePI383R+wfXk+ceMKCO7dfdeGCiGEjMgJ99F1WPq0Sok940m1oFwI4R5OO+z8Ej77GVQcPvnxgOjmKWEZ76nf50iyhkU7j1JUUI2/n86BigPct+w+1uSt8Xa32lTvqOfN7W+yOm81AD2DenbLBC3uNjQhlOkDozAaTvPelBxQQRxAeQ4UbIN9C9Xfucupkr4cK+lgMBgYc890LHExbNtUia53YLRPCCG8TEbkhPtoGoy9B1wOiOjj7d4IcW5b8zfYv6j1x3pPAZNF3XY6oLZE3R5zl0e65m5vrDzIzSX7iR3Yl15BvYgPjOf1ba9TUFPAZX0u67x1Z51gR/EO3t31LoW1hUT4Rni7O12ey6WzbMsOrME9GNenlSLvS+dB9kp1e+rD0Gts82ONJTdOmD4a2LMHI68bitnHCC4XGDunbp0QQnha1/l2E+eOunLYvUDdDk+GSA+s+xDifJb5YdtB3HXvwaTfNt83mpqn1Rkt7u+bm7lcOric+EUZSRoZh8Vo4VfDf8XY2LF8lfUVT6x5gr1le73dTXKrc/nr5r/y501/xqk7uX/k/cxJnuPtbnV5Wu5Gev1wD/sXvYXD6VIbq4tUHbf1/1K1DxtV5bV8stHU5hrAuJRQAssPknff/dhLS93TeSGEcDMJ5ETnqq+EhQ/DhtdVkhMhhHuV5aiaYQCj7zj5cXs9fHYHfHSzKoL89mzIz1TBnLl7rCM7FbvLhaZrFF92MwFTpgBgMVr4yaCfcOeQO6m2V7O7VGXrdLgc2J12j/Wt3lFPla0KgPL6cvaW7WVuv7k8NeEpzyQ26W7qymDh79XfJ4Cuo616kchAK8NL55P3yf1qBG3hw7D8eVUfMem4um8b32out9EOpqgosivDWPnCQpliKYTolmRqpeg8DVXqS7giF6Y92q0LDAvRbTgaVFr5SfdDRD+V2OGrXzQ/fnidKmB8eH3L5/Weqk6KAQwGKNwFB1dAdBr0Gue5/neQw6kTVO2kbFE+1f2jCAxrDk5HRo9kSOQQnLpKsb+lcAtvbX+LoT2GMjhyMCmhKYT4hHRqf4pqi9hRsoMdJTvYVrSNqT2ncm3/axkYPpDnJz+Pr6kd6e7PV+tfh7wMsAarCw15m6G+ggAfEz5mIw05G3Du/AJj5XEjb8f/rQM0VINP+4qIm6OjCRg5nF0bSzj49Tp6zx7Tea9FCCE8QAI50TlstfD9o2qh+QWPQNwwb/dIiPNDZD+46g0wHFvnE54MM/4ICx9RUydTLoL8jJbPCegBBVth4xsw+X518rvsGZUQwidIBXJVBXBwOaRe2WVT1IMK5Prk7MNi88MvcMRJj1uOmz7aw68HI6JHsOXoFtbkq2Qokb6RPDH+CaxGK7nVudQ76gmyBBFkDcJqtJ50PJfuosHZQEVDBaX1pbh0F2kRaQA8vvpxDlUdAiDUGsq42HGMjFElHjRNkyAO1LTIg8ugMheqjqrvikHHytIMuFQ9dnAZTPgllKukPVpgLJH1hzhcWstG1wBaLZk+5yU1ovfZz9T9q99sOe2yDWm3Tefwzg/I/GYf8VPTsfh3/1FqIcT5o+t+O4vupWCbqlk15XcQf/LJlBDCjQwnJGuIHapqhMUMAYNJjcgNvAyCe6pMftWF0LAGeo4GSyBsels977r3VYHk8kNqenTuZpURcOpDnn5F7RbgY2JIiC9VpXaM5lN/pfUK6sVtabfhHOjkUNUh9pbt5VDloaaA7csDX7KhYEPT/kbNSLhvOPMmzgPgpc0vkVGU0eKYMf4xPDXhKQDGxo5lomEiqeGpRPlFnb+ZKAu2ww9Pqqm7FzyiLi6AymT8ya3N+0X0hb4zmu/36K/+bhc9rsoJpF6uAj2TL4F+4SxdeoCpwQGtt7nzCxh0TfP9T26DW+aftqtGs4lhc9NZ/u9Mdny6kaE3TTjTVyuEEF4jgZzoGF1X2SkTRqtRAf9wb/dICAEtL6iM/In6veVdVaZgwi8hcZLKZKnrKlgD0Izq/r6FKogDyFmlRk4Cozza/fYyGjQctU4C/NufedBoMJIUnERScFKL7Vf0uYLxseOpaKig0lZJraO2xajc8Kjh9ArqhdVoJcgSRJhvGOE+4SrFvcHIzMTuVYy8U9nrYemfVJbixjVu9lqw+Dfvo2mqtputWt2f/Rf121YLq19SJWo2v6O2BR+rqxeSoJ4K3H1BX1XOBtQoXtIk+PJedX//YnVB8SzETkijd66LI4UuUusdWHzk1EgI0T3Ip5U4e44G9cXdf7Y6aZQgToiure9MNV0tYVxzOQJNUyfU9RVqCmbmfyFygHpszJ3gF9GuKWreUlFnp6zSSVTPjmfgjPaPJtq/7bW94+PGt9xQWwq758Pe72D2ixAQ2eE+dHm6Dru+giPrVfAWNxzSrwfNAPXlYDAft68LFv9RjegGx6uMxo1B3PGJearyoWh3cxkBgMKdra7VrHBYWDPidWamRqEd3dH8gG+oGmlOmgTDbmoZQLbD4NkDsNU7MFSVoVsjzt/RVCFEtyKBnDg7TjsseUpdtU+c6O3eCCHaIyCy9WmSjYXBC7ZD8T71A+C0qdH2LqykuALr0Y1Yh04+/c6dxVYDWz9UBdhdx2qVlezr2oFc4+yJjqgrg5V/UZ/7ob3AJ0Sl/Nd1dWHg0r+q/UoPQu4mFejWFqti9C6nGt1NnAgWP+h/SfNxw5Nh7ttgr1MXCM2+YDp5fSLAxpxS3l59kJ5hvqTGpsFVr0NAlAoQ8zMh8eymRvr4m+FIFrnz5hF6588JGiXrvIUQXZ8EcuLMOR1qJC53M4z7OfSZ5u0eCSE6Q8Bx0yfjhqvR9i7OabdzKC6C5NRenmnQXgfvX6tux4+AIdfD4icg8wOIH9V1EsPYauHoDjXKmpehElEB3PRFc7B0poFdzmo1fXHMXSqJTlvPD0tSP8db+081ejnzqeY6hicy+6qfU5jQN4IPNxxm/tZ8UmODm7MjWwPPOohrZE1O5nDwMDL/s5NLhg4+7ZpLIYTwNqkjJ86My6my2x1er6Zd9TuP14QIca7Z+Xnz7QGXqiyWHqy71oKtVq3pc9hOuVut08K2/tMx9O7r3v44GmD3Apj/6+ZtaVdDZAqMvRtKs9S01K5i93xY/AfYswB8Q2DQXBh8jUqMs+wZ+O53bT+3qgCyV0FNScvtKRfD5f+A/hefPgh0NMCu+apw97e/U/0BCOlYwG01GZmZGs2WQ2UcKavt0LFOpJlMxE0fSW29xp4PlnfqsYUQwh3kcpM4M5pB1egZ9dOWU2OEEN3f8FuhtgSG36JGOrKWqeQTFz8HfmFnd0xbDax7VZU1iB0G0YPbN2q16ys1yhXRD3qOanO34j0lJB+xobmzoHNVAXx6LGFMeB91ESskQdXcA7WWq880Nd0SHYbe2PFpjGfD0QBl2Sq47D1F/Y4c0LweElTtwMKdLQtpH2//IlUiIOM9db/nKIjsDz0Gqtfb3vqgNcWw7p8tt8Wmt7vG26lcmBrF5xm5fL01n59NTu7w8Y7X6+JR7Fmyn12r80ieU421rSyZQgjRBUggJ9rH5VIL2f3C1NVnWQguxLnHaIIpDzbfD45X/++/fwTSf9T+qYNlOVCyXwU3Zj8VXJTnwI7P1Yl8VKqalm0NbPsYTVPzTv1ZY1u/maiiOnx9z6DsSUMVrP+XWgt48XNgbeNkvbZUBTaNmRSTJsOk37b++TfuXpX1c/unKohyNMCRDaqUw/HvaWeqLlK1AHM3wYV/hKPbVSmJy16B0MTWk9RU5anpoT36n/zY1o/Ua+03UyXAyVkF+75XMzAi+6v3qr2f/UGxkDwVDixRpS9CE1VQ2AnfHUE+Zqam9KC6wYGu652amMRgMDD0muEseWMLu7/ZwZDruvYaUSHE+a1DgZymaXOBx4EBwChd1ze2sd8s4K+AEXhd1/V5HWlXeJiuq9TQ+Rkw5+VTn3wJIc4d4cmqNuSqF2HJ0yoIG3Fb6+tiG5NplB+Cbx9UWQOTL1DbLv2rCmwOr1VFxnNWq2najnqY8Cvwj2h5nMpcCEtW21e/BJf/vc3PHbNDp0ewicTw02QpLMtRyVvC+6jpogd+UNuN5tb33zW/eUQpMAaSJqpgtq2gwWBUwemAS9VrWPWiahNU4NiZn5uOBtj+P9j2sbqfOAG+fUC9p3HDVNDUFt2lfmevguRp6vXoOmz+N2z7RAWhY+5SryeiL6T/n/rsD+t9ZkGYpsGEX8PE35zlizy1W8cnui2zZNToAfTO1cgrspPmdGE0yioUIUTX1NERue3AlcCrbe2gaZoR+BtwIXAE2KBp2pe6ru/sYNvCE3Qd1v5dXZUecp0EcUKcb+KHq4yCuZtVfTnf0ObHdF0lv9j1lZo6mTxNrb/SDGqEqPFEW9Ng/atqdGfApWqk6PA69djxa/DW/lONAtWVqSBu3L2qOPSGN1Ttu1Y01Drx8zdhautkW9fVKFlj0fMeA2Hm02qtVkOF6rtfuBpBrCuH9BsgOE6tB9MMKjFHcFz73itNUyOJR3c0B3GBMWf2uVlTApVH1GtuqFIZHnVdvSdz/60KvH/9a3X8xAkqsD68rjkwbawZ2Bb/SBXolexr/vdZ9Lga1es36+QZF0ZTy5qEZ8KNMzcag7iCinrC/C1YTJ0bbA2emYzBqKGXl0G4lNYRQnRNHQrkdF3fBZzuqtgoYL+u61nH9v0AuAyQQK6r03VY/xrs+QbSrlJXo4UQ5x+DEXqOVD+gpiUWbFWBRk2xClQaqlQ9NbMfXDQPgmKan+90qCAO1GhSdaG6Pe7e5v10HfZ9p1LVD7kOYtJVADX+XghqO5CqqdOptTipbnAQYD3hK03X4d2r1Ehco9TLVXAy/l74+jfNAZ7BpEoJHFoN1753LEX+xWf3fvkdO/E3WY/VSNsLkf3UNltt2xkjC3fBgvua70elqX6AmpbYuNYt9QoVkDVmf+w5RtVv6zP99FNfzb5q6mUjXVejdAljut20+YPFNTz0v63cNaUPk/p1bukHH38zlQsWUPbp/4ie9wzWSAnmhBBdjyfWyMUBh4+7fwSQSefdwa6v1M/Ay1Tyg270BS+EcCOzrwokgnuqgKv3ZKivVIFY/Cg1De94xXvU756jVLAw7ufq8+ToTlUwevwv1IheUJxah3t8IqU+09Xvoj1q9K//bDD7AKDb7cSVr2G5tT8NdicBpTtVMDnmDhVcrnu1OYiz+KuMi41JWyJT1Hq3sGQVwAXFqtFAW23HP+sCo1UClJzV6r5/hFpnvPxZVfQ6IEpN1Uyc2Dxlcc+3sO4fan+TFa59V40ItlZPrfE9aRQQCSmzzq6vmgYz/nh2z/WyxHA/YoN9+X7n0U4P5ACsQ4ez7X855L25nLEPXNHpxxdCiI46bSCnadoioLU0VQ/ruv5FZ3dI07TbgdsBEhISOvvw4kz0mQa6EwZeLkGcEKLZ0P87eVtAZOvbASJSVLAQk97ys6QqD3I3wlf3wpSH1EhTG4WgyduiyhEMmNO0Sc9aQUiqkTxTLCZXPax4XiUomfCrY+32VVM5+0xX6/1O1HtKy/vJF7T5ks/YsJshf6vKcNkYPFqDVP8rDqvpnts+UVMj065UI29xw8ESAPEjT1tPTajZQBcOjOLfa7I5WFxDUsRp1kmeIWtMFKFJERw6UEva4UICe7aSPEYIIbzotIGcruvTT7fPaeQCPY+7H39sW1vtvQa8BjBixAg35pMWp2XxVydWQgjREUYTxA49eXuf6RCcAMvmwTf3q6CmrWDKfGyKYX25Gg3c/G/qN38N1lBKA2Pw3faeCuIueaF5emGfaa0nZvGE4Di44YOW28be1Xy7vgIOrW2eNpo0SY3QuRzeq93XDU3qF8l/Nxzm+50F3D6pc0sRAAy6YTxH/rSEbe+vYpyMygkhuhhPpGLaAPTVNC1J0zQLcB3wpQfaFUII0dVF9lNZLeOGq7V3/74U3p6t1m4dLyBK/a4rVyNaOz6nuCGJHfljSKo7iGnvAjXtsnEtWlfnE6zS/DfWogM1Wmk0N6+LE6flbzUxPjmc9QdLsTtdnX78oKRY4hIsHMluoL6yvtOPL4QQHdGhQE7TtCs0TTsCjAW+1jTtu2PbYzVNWwCg67oDuAf4DtgFfKTr+o6OdVsIIcQ5wxoIFzyi1so1qi1tuY9PkPq9Z4Gqd+YTTMVRK44yJ5NqF6D5R8CwGz3XZ9FlzB3Rkz9fk47ZTWUC0v5vCubkPuTsKnfL8YUQ4mx1NGvlZ8BnrWzPAy4+7v4CYEFH2hJCCHEO0zToeyGs+qu637iurJFPsPpduBMq8wBoMI3EYPFjwr0voZlQ08HFeSfM3+LW44ckRjJ+rpXwOH90hwPN5Ik8cUIIcXpS5VIIIUTXMfNpmPzAyQmWAmPgxs/VYwC9xtNgisJs0vELjmhZVFycd46U1fLYF9s5VFLrluOHhxs5+vvfU7VkiVuOL4QQZ0MCOSGEEF1HzGCVmv9EmqaSmATFQspFMOFXNNQ60DUX767N8Xw/RZcS5GvmQFENy/YWuuX4xgB/SozRrPziME6H0y1tCCHEmZJATgghRPdh9lW16Mw+JJuyCTJls3DnUW/3SnhZkI+ZYb1CWbGvGIcbkp4ABIwZRWWDlZzvNrnl+EIIcaYkkBNCCNEtRV56IdUjRyJVLgXAlJRIKuvtbDlc7pbjJ8waidXsYv/yA245vhBCnCkJ5IQQQnRL+cYEKiOSMUgkJ4Ah8SGE+FpYtqfILcc3mk0kpoVSWuqibPcht7QhhBBnQlIvCSGE6HZcTheZC/bhirKgmSSSE2A0aFw5LA6jGyP7lCvHcrBoG4eO6IT2d1szQgjRLhLICSGE6HbsFdXYsrPxc1kwJCR6uzuii5iRGu3W4/tFhzPokgEER/q6tR0hhGgPmVophBCi27FVqTTz6X0ief3mkV7ujehKqhscbMwuPf2OZ6nPkDCsmUup3bzFbW0IIUR7SCAnhBCi27FVqkDO5OfeYtCi+/l+ZwHPL9xDcXWDexowmSheup69n691z/GFEKKdJJATQgjR7diq6gA4WG3nP2uyvdsZ0aWM7a2Kw685UOKW42uaRlWfsezOD6IiK88tbQghRHtIICeEEKLbCfK1M6xhGQWak9VuOmEX3VN0sA+9IwPc+neRPHsEAPsXyPRKIYT3SCAnhBCi27Em9iLurtuwR/RA173dG9HVjEsO52BxNQUV9W45fmBCFOGhcHhXGS6XewqQCyHE6UggJ4QQotuparBwhF44zT64JJITJxjTOxyAbbkVbmsjcUwSNksQRfuL3daGEEKcipQfEEII0e0U7c5n+4p8tHFRSBgnThQRYOXvPxpOmL/7kuEkzR7N3vzt1NRKHUMhhHfIiJwQQohup27vfuyHDuGj6fiajd7ujuiC3BnEAZgsRmb9bBAJCUZ0GRUWQniBBHJCCCG6HZfDCcD143vz0vVDvdwb0RXV25385fu9rNhX5LY2bDu2kfvLX1G/d6/b2hBCiLZIICeEEKLbcTlcgIYmo3GiDVaTgYPFNW7NXmnp25ed1lFkfrHTbW0IIURbJJATQgjR7TgdLjRNZ+X+Ev62ZL+3uyO6IE3TGN4rlO25FdTbnW5pw+jnh7VHGHnZNZK9UgjhcRLICSGE6HZ6R1QxSltNTmkt6w+Wers7oosa3isUu9PF1iPuy14ZPyQOm12jeItcUBBCeJYEckIIIbqd4GlTib7nTgwakrVStKl/dCC+ZiOZh8vd1kb8pDQAjqyTQE4I4VlSfkAIIUS3U1jtR1WNAU2zS8ZA0SaT0cC0AVEE+5rd1oZfdBiRaQlU+Aa7rQ0hhGiNBHJCCCG6nbyMHApzqjFMSUDiOHEq/zeml9vbSJmZiq3eia7raJrUlRNCeIZMrRRCCNHt2Pbtp+FgNn5WE6Furhcmuj+700V5rc1tx49LDiSydDsNUoZACOFBEsgJIYTofo4NeswZEsvLUkdOnMZD/9vGmysPuq8Bo5HCT74i95s17mtDCCFOIIGcEEIIIc5p/aIC2J5X6bb1lJrBwOGI0Wzda5I1m0IIj5FATgghRLekA6v2F/Onb3bJybM4pf7RQdTaHBwurXNbG5HJYTTYNSqz8t3WhhBCHE8COSGEEN1OSkw1Iw1rKaioJ/NwuSQ8EafUPzoQgN0FlW5rI3p4MgD5G6QMgRDCMySQE0II0e0EX3IRUb/+FYZj32IuieTEKUQGWgn1s7CnoMptbYQOTMRs1Ck54r5gUQghjiflB4QQQnQ7RZUWig5b0EJV1hOnrssXmmiTpmncOj6JMDdmODUYDMTNHE1lqfuyYwohxPHke08IIUS3U7wrlwObizBfkQKAy+XlDokub1RSmNvbSJ3cE4NR6sgJITxDplYKIYTodhyHc7Dl5hFkMRAX4oeOTK0Up2Z3utiUU0ZuufsSnvjqNdS8/gp1O3a4rQ0hhGgkgZwQQohux2g2AjA2Pog/XzMEP4tMMBGn5tJ1nv9uD6v2F7utDYOfHwe3l3N4XZbb2hBCiEYSyAkhhOh2jBYVyDnqGrzcE9FdWE1GYkN8yS6ucVsbBl9fCgP7cWi/+9oQQohGEsgJIYTodsxWM0ac7DxYzGNfbKe0RhJMiNPrHenPQTcGcgAh4VYqypxS21AI4XYSyAkhhOh24hOtjGpYjN1lY8/RKhocTm93SXQDieH+lNXaqKizu62N0IRQ7A6oPlLktjaEEAIkkBNCCNEN+QwaTMxTT0JYBAAOp4x+iNPrFe4HwOHSWre1ET4gHoOfP5X5Uk9OCOFesjpcCCFEt1NnN7FrqwNHjPoac7gkkBOn16dHAM/PHUJ0kI/b2ggf0hfLmjpsPiFua0MIIUACOSGEEN2Qo7qGnLVZRIxUtcGcEsiJdvAxG4kP9XNrGxZfE5fek47BJPXkhBDuJVMrhRBCdDsmzYmjoABT/lGSIwOwGOXrTLTP6gPF/LD7qFvbKHvzdQqfe96tbQghhIzICSGE6HasoYEABOg6T10xyMu9Ed3JmgMl5JXXc0H/KLe1UVTnT262jR66jqbJyJwQwj3kEqYQQohux+RnxaDp2Gql7IA4M9HBPhytrHdreQBHYATF9lDqiiXhiRDCfSSQE0II0e1omoavyU5dTS33f5LJ9twKb3dJdBPh/hYcLhdVDQ63tREYEwJA9SH3TuEUQpzfJJATQgjRLQ0P2ku8JY9DpbVU1bvvpFycW4J9LQBU1Lqvlpx/dCgA1fllbmtDCCFkjZwQQohuqcdvf0OJywif75aC4KLdQvzMAFTU2enppjYCEqIxBofQgPvKHAghhARyQgghuqVDh3WOZBUDYHO4vNwb0V307RHA27eOwsdsdFsbPuGBhAxMxBwT6bY2hBBCAjkhhBDdUlXWEY5uOAqJgdicEsiJ9jEZDZjcF8MBag3n9FsHotvdN31TCCFkjZwQQohuyVhZjKOkhPQgM2F+Fm93R3Qj767NYW1WiVvbKHrhBQrnPePWNoQQ5zcJ5IQQQnRLfhEBaMCPB4Qyrk+Et7sjupGlewrZmefe0gCHG3qwvdh9teqEEEKmVgohhOiW/HqEAFBbKKUHxJmxmo3UuzlBjt3gR7lNrpcLIdxHPmGEEEJ0SwGxYfjpVXy9Zi/vrcvxdndEN+JjMtJgd++6Sou/BYdTwynrN4UQbiKBnBBCiG7JPzaCIY51uOqLKHdjTTBx7rGaDTS4OdOpxd8K6DSUVrm1HSHE+UsCOSGEEN2SZjTS8x9/5/CIqdTapCC4aL8Aqwmny72BnF+vWEwREThsMiInhHAPWSMnhBCi29q5voTIg3VUp7g5n7w4p/zu4gFubyO4fyKRR81glaLgQgj3kBE5IYQQ3VZd9mFCso9Q2yBTK0XXEhbty4TZMQQGyzVzIYR7SCAnhBCi2/KxV2KtqyM1QEbkRPt9viWXz7YccWsbDfv3c+Sen1O/Z69b2xFCnL8kkBNCCNFtBcSEYDZqzIowe7srohvJPFJO5mH3lq2oqdPItIyjOL/Ore0IIc5fEsgJIYTotoITVcHl8oNFXu6J6E7sThcWk5tPgQxGarUAbHWSiEcI4R4SyAkhhOi2gvvE4WMr5JsVmVTUyTo50T4Ndhc+bg7kjBa1Ns4pGVWFEG4igZwQQohuy2g1kxheiNNRTqUEcqKd6h0urGb3rqvUzGq6r0sKggsh3EQCOSGEEN2a9bcPsjH9AikKLtrNx2QgyMe92SQ1X19MPXpgiY5yaztCiPOX5MQVQgjRrdVmV5O230ZJRT3EB3u7O6IbeG7uELe3YQ7yJ2ZYEoFJEsgJIdyjQyNymqbN1TRth6ZpLk3TRpxiv2xN07ZpmpahadrGjrQphBBCHC/EXkl4aQEV27O83RUhmvhYYNSEIMKjrd7uihDiHNXRqZXbgSuB5e3Yd6qu6+m6rrcZ8AkhhBBnKjIhGF+XnYCiUm93RXQDRyvrmffNbvYXVru1HduRXPLuu4/67Tvc2o4Q4vzVoUBO1/Vduq7v6azOCCGEEGcqoFc0AVYNa7nU6xKnV1TVQMbhMhocTre2U1lSz2bLJErLdLe2I4Q4f3kq2YkOLNQ0bZOmabd7qE0hhBDnAYPBQFCwgbJCCeTE6TWWqQjxs7i1HXudjQbNB93g3uyYQojz12mTnWiatgiIbuWhh3Vd/6Kd7UzQdT1X07QewPeapu3Wdb3V6ZjHAr3bARISEtp5eCGEEOezSl877N2Gy3YtBot7T9BF91ZWawMgxNfs1nYaqtSFBWuwn1vbEUKcv04byOm6Pr2jjei6nnvsd6GmaZ8Bo2hjXZ2u668BrwGMGDFC5iMIIYQ4raAhvcg6uJ2qskqCoyK83R3RhR2tbCDAasLf6t7E3bbqegCswf5ubUcIcf5y+9RKTdP8NU0LbLwNzEAlSRFCCCE6RcjQwawcdQVHqqQ8qjg1i8lASnSg29vRwqMxRUdjiQhxe1tCiPNThy5HaZp2BfAyEAl8rWlahq7rMzVNiwVe13X9YiAK+EzTtMb23td1/duzbdNut3PkyBHq6+s70nUhTsnHx4f4+HjMZvdOvRFCdI6oQB+SD9vZveIwqX3CvN0d0YXdOKaXR9oJTIohvsaMNdDXI+0JIc4/HQrkdF3/DPisle15wMXHbmcBnVZ588iRIwQGBpKYmMix4FCITqXrOiUlJRw5coSkpCRvd0cI0Q7RwT6EVBykfpWG6+ZBGAwyMie8KyKwgYgRPhgMcq4ihHCPbvdNV19fT3h4uARxwm00TSM8PFxGfYXoRiwmA4nJIaAbqTyQ7+3uiC4qr7yO33yUyc68Sre3VfbhxxS++Fe3tyOEOH91u0AOkCBOuJ38jQnR/UycNQSjQePo5v3e7oroonJKasktr8XX4v6SAFuyg9hjGOT2doQQ5y/3pmw6x5SUlDBt2jQACgoKMBqNREZGArB+/XoskvJaCCG8xq9fPJrm4uieQlK83RnRJWWX1GAyGOgZ6v51a3X1Oj49ZH2cEMJ9JJA7A+Hh4WRkZADw+OOPExAQwG9/+9umxx0OByaTvKVCCOENGYcrKbMdZODRamCut7sjuqCDxTX0DPPFZHTvhCSX00mD3YhvkNWt7Qghzm8SdXTQLbfcgo+PD1u2bGH8+PEEBQW1CPDS0tKYP38+iYmJvPvuu7z00kvYbDZGjx7N3//+d4xG90/vEEKI80FihB8f9e5DWt9AdF2XKdKiBV3XOVhcw/BeoW5vq+5oKboOfmFSQ04I4T7dPpD7w1c7Tto2tnc4M1KjaXA4mffN7pMen9wvkikpPaist/OX7/e2eOyxS1PPuA9Hjhxh9erVGI1GHn/88Vb32bVrFx9++CGrVq3CbDZz11138d5773HTTTedcXtCCCFOFh3kQ1VCH3bofvTJriQ6KdjbXRJdiM3pYmhCCEPiQ9zeVoPTjDk+nqABCW5vSwhx/ur2gVxXMHfu3NOOrC1evJhNmzYxcuRIAOrq6ujRo4cnuieEEOcFTdNI7hFI9ap89lVXEX37WG93SXQhVpORu6b08UhblgAf+kzoTVhf+Z4XQrhPtw/kTjWCZjUZT/l4kI/5rEbgTuTv3zx1wmQy4XK5mu43prDXdZ2bb76ZP/3pTx1uTwghROuSewRQUHyA/GJfnA0jMFrN3u6S6CKq6u0EWE0emXJrLjlCSqwdn9Cebm9LCHH+6pblB7qyxMRENm/eDMDmzZs5ePAgANOmTeOTTz6hsLAQgNLSUnJycrzWTyGEOBdN6hvJ8Il9cbo0jm7Y4+3uiC5k3je7ee47z/xNFH/1LSVvve2RtoQQ5y8J5DrZVVddRWlpKampqbzyyiv069cPgIEDB/Lkk08yY8YMBg8ezIUXXkh+vhStFUKIzhQd7MOI2cMxaHBk3QFvd0d0EfV2J9klNSSE+XmkvS0Hg9ljHuqRtoQQ569uP7XSW9pKauLr68vChQtbfezaa6/l2muvdWOvhBBC5DSA2ddBVU6BZK8UAOwvrMbp0kmJDnR7Wy6bnZp6jR5JngkahRDnLxmRE0IIcU5Zc6CE7X71JJevxlFU5O3uiC5gd0EVGhr9otwfyFUcyEXXNYJiQ9zelhDi/CYjckIIIc4paXFBfB+fQu2tszFFRnq7O6IL2HaknKRIf/yt7j/tKd5+GIDIgZLoRAjhXhLICSGEOKcMiAnCafZhw7oyijdVM+n6FG93SXjZ5UPjcOmeaasutCe+yUZCpIacEMLNJJATQghxTvGzmOgd6U/xvgJ886qpGBVAcHKct7slvGhoQqjH2oobGEFQVABG06nrywohREfJGjkhhBDnnGEJoeT5aDhrasj6LsPb3RFetPlQGQeLazzSlm63Y1g+n9jgWo+0J4Q4v0kgJ4QQ4pxzyeAYnr3nAkKD4fCuMlwul7e7JLxA13XeWHmQzzYf8Uh7lXuzOfzNOupzpbyQEML9JJA7C0ajkfT09Kaf7Oxsxo0bB0B2djZpaWkAZGRksGDBAm92VQghzks+ZiNGg0av9Bhq6zVKMqSm3Pkop6SWkuoG0hNCPNLe4XVZ7LSMgNhEj7QnhDi/yRq5s+Dr60tGRkaLbatXrz5pv4yMDDZu3MjFF1/c7mM7HA5MJvlnEUKIjtqUU8rHdh8ucWbh2GaAYX293SXhYesPlqKhMbxXmEfaK9xXgq9VJ6hXlEfaE0Kc32RErpMEBAS0uG+z2Xj00Uf58MMPSU9P58MPP6SmpobbbruNUaNGMXToUL744gsA3n77bebMmcMFF1zAtGnTvNF9IYQ45/hbTWTbNXyHxhKYJMlOzkfrDpYyICaIYF+z29ty2uyUlDqJiPV1e1tCCAHnwojcNw+evC1pIvS/BBwN8P1jJz/eZzr0nQ71FbDkTy0fu2jeaZusq6sjPT1dNZWUxGeffXbSPhaLhSeeeIKNGzfyyiuvAPC73/2OCy64gDfffJPy8nJGjRrF9OnTAdi8eTNbt24lLMwzVw2FEOJc169HIGH+Fhb3HEdsTAwNedWExwac/oninFBWY6OouoELB3pmdKx0Tx5Og4Wo1BiPtCeEEN0/kPOC1qZWtsfChQv58ssvef755wGor6/n0KFDAFx44YUSxAkhRCcyGDQm9o3kq4xcBuyvJ9RSx8SfjfN2t4SHhPpb+NdNw9E9VD+uvN4Ha9++xF+Q6pkGhRDnve4fyJ1qBM1kPfXjPsHtGoHrLLqu8+mnn5KS0rI47bp16/D39/dYP4QQ4nwxqV8kX2Tk4izZR26unapZvQnsFe3tbgk3049Fb1YP1nLrPSySHomB+ARaPdamEOL8Jmvk3CgwMJCqqqqm+zNnzuTll19u+oLZsmWLt7omhBDnhbgQXy5Pj6P3JcMA2PvlRi/3SHhCdkktD3y6lZwSz9SPc1ZWUnDffVjy9nmkPSGEAAnk3Grq1Kns3LmzKdnJI488gt1uZ/DgwaSmpvLII494u4tCCHHOu25UAkNG9iEiXOPQrnKcdoe3uyTcbMW+IvLK6wkP8MzoWPbCzewrj0D3D/RIe0IIAefC1EovqK6ubnNbYmIi27dvByAsLIwNGza02O/VV1896bm33HILt9xyS+d3VAghBABHK+vRBkTDiixKV24kcuoYb3dJuInD6WLlvmKG9QolwOqZ05ycTXlU+PbEp3eiR9oTQgiQETkhhBDngflb83nb5kN6wC6Mh/Z4uzvCjTKPVFBZb2dyvwiPtGerrKG4xEVMYgAGg5xWCSE8Rz5xhBBCnPNmpkZh1zW2zf0ZwTfcSH213dtdEm6yZHchQT5mhsSHeKS9I0syceka8WOSPdKeEEI0kqmVQgghznnxoX4Mjg/hm6xKfPbsIijExJgrU07/RNHtjOsTzrBeIZiMnrlWfbTKF5+IYGLGDvRIe0II0UhG5IQQQpwXZg+OoazOTm1VPtnfbqZ8X663uyTcYFxyBBf090wRcF3X8YuJoN/sYRg9WOpACCFAAjkhhBDniUFxwfSPDsLZPxoNFzs+WuPtLolO1OBw8lVmHpX1nps227BvHwNSNAaOj/VYm0II0UgCOSGEEOcFTdN4dPZArpqZRs/ePhzJaaAyO9/b3RKdZM2BEt5bl8OhklqPtZn7n08p+sc/PdaeEEIcTwK5s/DUU0+RmprK4MGDSU9PZ926dW5pJzs7m/fff7/p/ttvv80999zjlraEEOJ8YDBoAATNSAdg/8crvNgb0Vl0XWfBtnziQvxIjQ3ySJvFGfvYUJBARd9JHmlPCCFOJIHcGVqzZg3z589n8+bNbN26lUWLFtGzZ0+3tHViINdRTqez044lhBDd1f7Cap7cWEhUryrCdy/EWV3j7S6JDtp8qIxDpbVclh6LpmkeaXPvgkw0DZJmj/ZIe0IIcSIJ5M5Qfn4+ERERWK1WACIiIoiNjSUxMZGHHnqI9PR0RowYwebNm5k5cybJycn8859q2oWu69x3332kpaUxaNAgPvzww1Nuf/DBB1mxYgXp6en85S9/ASAvL49Zs2bRt29f7r///qZ+LVy4kLFjxzJs2DDmzp3bokD5Aw88wLBhw/j444899j4JIURXlRzpz4CYIP4XnkLYY39At/p4u0uiA3Rd57MtuUQG+jAuOdwjbTaUVZKbXU9sghXfiGCPtCmEECfq9uUHnln/zEnbRkaP5IKEC7A5bfxl019Oenx83HgmxE2gylbF3zP+3uKxB0Y9cMr2ZsyYwRNPPEG/fv2YPn061157LZMnTwYgISGBjIwMfvWrX3HLLbewatUq6uvrSUtL44477uB///sfGRkZZGZmUlxczMiRI5k0aRKrV69udfu8efN4/vnnmT9/PqCmVmZkZLBlyxasVispKSn8/Oc/x9fXlyeffJJFixbh7+/PM888wwsvvMCjjz4KQHh4OJs3bz6r91cIIc41mqbxo9EJ/P7zSr7cWkPwl9uZfG0fAiP9vd01cRbq7S4CrGam9OvhsZIDB1fuw4mRlIsGe6Q9IYRoTbcP5DwtICCATZs2sWLFCpYsWcK1117LvHnzAJgzZw4AgwYNorq6msDAQAIDA7FarZSXl7Ny5Uquv/56jEYjUVFRTJ48mQ0bNrS5PSjo5Hn+06ZNIzhYXf0bOHAgOTk5lJeXs3PnTsaPHw+AzWZj7NixTc+59tpr3f22CCFEt9KnRyCjk8JZfLCMi7KPsvGvu5j65HXe7pY4C74WIw9e1B9d1z3WZmF1AFHjBxM5rK/H2hRCiBN1+0DuVCNoFqPllI8HWgJPOwLXGqPRyJQpU5gyZQqDBg3i3//+N0DTdEuDwdB0u/G+w+E443Zac/xxjUYjDocDXde58MIL+e9//9vqc/z95SqzEEKc6LpRPdmVX0loDydH9znJW7md2Alp3u6WOAM5JTX4mo30CPLx2No4Z3UN467qQ32Nw2NtCiFEa2SN3Bnas2cP+/bta7qfkZFBr1692vXciRMn8uGHH+J0OikqKmL58uWMGjWqze2BgYFUVVWd9rhjxoxh1apV7N+/H4Camhr27t17di9QCCHOEzHBvrxywzAm330RVpOLjE8zcLlc3u6WaCdd1/nnsgP86ZvdHhuNc7lcHJ33DOVvvEZAqPX0TxBCCDfq9iNynlZdXc3Pf/5zysvLMZlM9OnTh9dee61pHdupXHHFFaxZs4YhQ4agaRrPPvss0dHRbW4PDw/HaDQyZMgQbrnlFkJDQ1s9bmRkJG+//TbXX389DQ0NADz55JP069evU1+7EEKcaywmA7rRSsCgCEq2lJK/cA1xs8Z7u1uiHVYfKOFgcQ13TenjsZGxvKWZbChIYOy4Ph5pTwghTkXz5JzyMzVixAh948aNLbbt2rWLAQMGeKlH4nwif2tCnB925FXwxy+3cVfmN/QJg5gnn5Qpc12c3eniNx9l4ms2Mu+qQR7593K5XCz87XvU1mvMeX4uJj8ZkRNCeIamaZt0XR9x4naZWimEEOK8lhobzPCkCD5KmYB29y+prbR5u0viNBbuOEphVT03jE7wWNCdu2gz5VUGUsbGShAnhOgSJJATQghx3rttfBK1IRG8+c1hvn9jB6VZhd7ukjiF6gY7wxJCGRzvmRpuuq6zffEBfCw6KddM8EibQghxOhLICSGEOO+FB1i5ZVwiO231VB88wNpXFkriky7s2pEJ/HZGisdG44oOVVEf2YdBc0dislo80qYQQpyOBHJCCCEEMLFvBBcMiiEhPYTKKo1d7y7xdpfECXbkVbArvxIAg8EzQZxutxMRbWX0ZckkTZIkYkKIrkMCOSGEEALQNI0fT0hiwq3TCAvV2LEqn4oDud7uljimusHBKz/s5/UVB3G5PJeorXzBt+Q/9DuiYsweCx6FEKI9JJATQgghjmMwGKickorN1sCRf3+CLlMsu4Q3Vx6kst7O3VOTPRZQ1eQVsXRBCeWR/TEG+HukTSGEaC8J5M5CQEAAANnZ2bz//vudeuynn376jJ/z9ttvc88995y0/ejRo8yePZshQ4YwcOBALr744s7oYps+//xzdu7c2XR/ypQpnFg+QgghugMtKpxd0S7qjh7AWV7u7e6c91btL2b1gWKuGhZP78gAj7W76bXF2HQzsVfO8libQgjRXhLIdcDZBHIOh+OUj59NINeWRx99lAsvvJDMzEx27tzJvHnzOu3YrTkxkOuI071PQgjhTlcOiyNw4gT+OnQuq7dVU1lc5+0unbdKqht4fUUWfXsEcll6nMfaPbx4M3m5TvoMDiE0pafH2hVCiPaSQK4DHnzwQVasWEF6ejp/+ctfcDqd3HfffYwcOZLBgwfz6quvArB06VImTpzInDlzGDhwIACXX345w4cPJzU1lddee63peHV1daSnp/OjH/0IgHfffZdRo0aRnp7Oz372M5xOJwBvvfUW/fr1Y9SoUaxatarV/uXn5xMfH990f/DgwU39mTx5Mpdddhm9e/fmwQcf5L333mPUqFEMGjSIAwcOACpQveCCCxg8eDDTpk3j0KFDbW5fvXo1X375Jffddx/p6elNx/j4448ZNWoU/fr1Y8WKFQBn9D4JIYQ3aJrGvdP7EhUUyMpvslj68hLsNfXe7tZ5KczfwtXDe/LzaX0wemhKpdPhInPRIfx8dIbcNt0jbQohxJkyebsDHVXQygiW/+jRBE6bhstmo/D55096PGDiRAImTsRZVUXRyy+3eCz6d79rd9vz5s3j+eefZ/78+QC89tprBAcHs2HDBhoaGhg/fjwzZswAYPPmzWzfvp2kpCQA3nzzTcLCwqirq2PkyJFcddVVzJs3j1deeYWMjAwAdu3axYcffsiqVaswm83cddddvPfee1x44YU89thjbNq0ieDgYKZOncrQoUNP6t/dd9/NtddeyyuvvML06dO59dZbiY2NBSAzM5Ndu3YRFhZG7969+clPfsL69ev561//yssvv8yLL77Iz3/+c26++WZuvvlm3nzzTe69914+//zzNrfPmTOH2bNnc/XVVzf1weFwsH79ehYsWMAf/vAHFi1axBtvvNHu90kIIbzFz2Livtn9efXAt1RmV7P+xfmMf/jq0z9RdApd16msdxDsa+aSwTEebbswpwp7ZAJjfhQlxb+FEF2WjMh1ooULF/LOO++Qnp7O6NGjKSkpYd++fQCMGjWqRXDy0ksvMWTIEMaMGcPhw4eb9jve4sWL2bRpEyNHjiQ9PZ3FixeTlZXFunXrmDJlCpGRkVgsFq699tpW+zNz5kyysrL46U9/yu7duxk6dChFRUUAjBw5kpiYGKxWK8nJyU2B1KBBg8jOzgZgzZo13HDDDQDceOONrFy58pTbW3PllVcCMHz48Kbjnsn7JIQQ3hQV5MP9D15KyqAgDmc3sOejFd7u0nnj2+0F/OajDHLLPTut1ZadTWSYi2k3DSAmzXNTOYUQ4kx1+xG5U42gGSyWUz5uDAw8oxG409F1nZdffpmZM2e22L506VL8/f1b3F+0aBFr1qzBz8+PKVOmUF9/8pQdXde5+eab+dOf/tRi++eff97uPoWFhXHDDTdwww03MHv2bJYvX054eDhWa/MVRoPB0HTfYDB06vq0xuMajcam47b3fRJCiK7Ax2xk2N0Xc+Q377PumyxihiUQ1KeXt7t1TtuUU8Y7a3IYmRhKbLCPx9q1VVST9eIbBIeaiH70UY+1K4QQZ0NG5DogMDCQqqqqpvszZ87kH//4B3a7HYC9e/dSU1Nz0vMqKioIDQ3Fz8+P3bt3s3bt2qbHzGZz0/OnTZvGJ598QmFhIQClpaXk5OQwevRoli1bRklJCXa7nY8//rjV/v3www/U1tYCUFVVxYEDB0hISGj36xs3bhwffPABAO+99x4TJ0485fYT34+2tPd9EkKIrsJoNsGlw7CUb2HzC3/DJSUJ3CanpIaXFu8jMcKfu6b2QdM8V7tt/csLyKxJwffyaz3arhBCnI1uPyLnTYMHD8ZoNDJkyBBuueUWfvGLX5Cdnc2wYcPQdZ3IyMhWR89mzZrFP//5TwYMGEBKSgpjxoxpeuz2229n8ODBDBs2jPfee48nn3ySGTNm4HK5MJvN/O1vf2PMmDE8/vjjjB07lpCQENLT01vt36ZNm7jnnnswmUy4XC5+8pOfMHLkSJYuXdqu1/fyyy9z66238txzzxEZGclbb711yu3XXXcdP/3pT3nppZf45JNP2jzuT37yk3a9T0II0ZVcPj2Vj8su573DNRQty+GqyYlSILqTFVc3MO+b3fhbjdw/MwUfs9Fjbe/7eDlHcuwkpwUTMqS/x9oVQoizpem67u0+tGnEiBH6iXXIdu3axYABA7zUI3E+kb81IcSJdF3nb1/vpnzpUfr31Ln6F1O93aVzit3p4s2VB7l4UAw9w/w81m7Zzmy+f3ElwcEGpj91DUaLXOcWQnQdmqZt0nV9xInbZWqlEEII0U6apnHHRSlEBVZSm3GEHe8s9naXzglV9Xaq6u2YjQZ+NjnZo0Gc0+5i89oqLGEhjPvFhRLECSG6DQnkhBBCiDNgMhq45aGLiYsxsW1FAXu/Xu/tLnVrFbV2nvhqJ08v2I2nZwnpug4uO3EDIxh7xyQC4yM92r4QQnSEBHJCCCHEGTJbTEx44FL8fF2s/Ggnn7631ONByLmgpLqBx7/awdGqBn40OsHjCUaKP/yEwiefpN+QEKISgzzathBCdJQEckIIIcRZsAYHMPWhi4kw5qG9/xr/XLgDh1OyWbZXfkUdf/hqJ+W1Nn53cX/S4oI92v6+T1ewbFENtrh+aD6eK3EghBCdpUOBnKZpz2matlvTtK2apn2maVpIG/vN0jRtj6Zp+zVNe7AjbQohhBBdRWBMOBc9ezs+t/yYtfsqeO7zndTZnN7uVrfw2vIs6u1OHr5kIP2jPTsadmRJBpu/yyEw1ELcrVJqQAjRPXV0RO57IE3X9cHAXuChE3fQNM0I/A24CBgIXK9p2sAOtiuEEEJ0CeaICC66+gJm6L4YV2Tz2fzN3u5Sl+ZyqSmod0/twx8vT6NPjwCPtl+4cS9rPtiOny9MfnA2JqvFo+0LIURn6VAgp+v6Ql3XHcfurgXiW9ltFLBf1/UsXddtwAfAZR1p19ueeuopUlNTGTx4MOnp6axbt86t7b399tvcc889AHz++efs3Lmz046dkZHBggULzvh5U6ZM4cTSEADz589n6NChDBkyhIEDB/Lqq692Rjfb9PTTTzfdzs7OJi0tza3tCSFEazRN4+LLk+hVW4b1+92U7sjG6ZI1c8dzOF38Z20OLy7eh67rRARYiQry7JTG2kobG1ZVYgn0ZcpvZ+ATLuvihBDdV2eukbsN+KaV7XHA4ePuHzm2rVtas2YN8+fPZ/PmzWzdupVFixbRs2dPj7V/NoGcw+Fo87GzDeRaY7fbuf322/nqq6/IzMxky5YtTJkypVOO3ZbjA7mOOtX7JIQQpxPaM5Tp905EQ+OHl1fw3N8X8VVmniRBAcpqbDz59S6+3ppHiK8Zb8S4jpISLL4GolMiuOB3lxDYs4fnOyGEEJ3otIGcpmmLNE3b3srPZcft8zDgAN7raIc0Tbtd07SNmqZtLCoq6ujhOl1+fj4RERFYrVYAIiIiiI2NBWDTpk1MnjyZ4cOHM3PmTPLz85u2DxkyhCFDhnDfffed9ajR6tWr+fLLL7nvvvtIT0/nwIEDHDhwgFmzZjF8+HAmTpzI7t27Abjlllu44447GD16NPfffz/r169n7NixDB06lHHjxrFnzx5sNhuPPvooH374Ienp6Xz44YfU1NRw2223MWrUKIYOHcoXX3wBQF1dHddddx0DBgzgiiuuoK6u7qT+VVVV4XA4CA8PB8BqtZKSktLUnzvvvJMxY8bQu3dvli5dym233caAAQO45ZZbmo7x3//+l0GDBpGWlsYDDzxwyu0PPvggdXV1pKen86Mf/QgAp9PJT3/6U1JTU5kxY0ZTP9v7PgkhREeEpyUy9ZeTMRqg97qtfLdgDc98u4eKOru3u+Y1G7NLeeDTrWQV1/DzC/py24QkjAbPrkkr35PD4ceeovKDDxg+K5HAMEluIoQ4B+i63qEf4BZgDeDXxuNjge+Ou/8Q8FB7jj18+HD9RDt37mxxf/mHe076ObClUNd1XXfYnK0+nr29WNd1Xa+vtZ/02OlUVVXpQ4YM0fv27avfeeed+tKlS3Vd13WbzaaPHTtWLyxUbX/wwQf6rbfequu6rg8aNEhftmyZruu6/tvf/lZPTU09bTvHe+utt/S7775b13Vdv/nmm/WPP/646bELLrhA37t3r67rur527Vp96tSpTftdcsklusPh0HVd1ysqKnS73a7ruq5///33+pVXXnnSsXVd1x966CH9P//5j67rul5WVqb37dtXr66u1v/85z83vZ7MzEzdaDTqGzZsOKmvP/7xj/XIyEj9uuuu0999913d6XQ29efaa6/VXS6X/vnnn+uBgYH61q1bdafTqQ8bNkzfsmWLnpubq/fs2VMvLCzU7Xa7PnXqVP2zzz5rc7uu67q/v39T2wcPHtSNRqO+ZcsWXdd1fe7cuU2vpb3v0/FO/FsTQoj2qjyYr+f8+j59+d/e0f/vX2v0O/6zUd96uNzb3fK4OptD/+m/N+gPfpqpHy6t8UofSrZn6Z/e8Y7+7Z3/1G1HjnilD0II0RHARr2VWMnUkSBQ07RZwP3AZF3Xa9vYbQPQV9O0JCAXuA64oSPtelNAQACbNm1ixYoVLFmyhGuvvZZ58+YxYsQItm/fzoUXXgiokaGYmBjKy8spLy9n0qRJANx44418801rM1DPXHV1NatXr2bu3LlN2xoaGppuz507F6PRCEBFRQU333wz+/btQ9M07PbWrw4vXLiQL7/8kueffx6A+vp6Dh06xPLly7n33nsBGDx4MIMHD271+a+//jrbtm1j0aJFPP/883z//fe8/fbbAFx66aVomsagQYOIiopi0KBBAKSmppKdnU1OTg5TpkwhMlIVZP3Rj37E8uXL0TSt1e2XX375Se0nJSWRnp4OwPDhw8nOzj6j90kIITpDYGI0fn94jBiLlYr3drPpaCnvr8/h6bhB53yGRF3XyThczqC4YHzMRh6ZPZCYYB9MRs9XPMpfvZ1V72SgaQZG3DUDc1y3XdkhhBAn6VAgB7wCWIHvj30xrdV1/Q5N02KB13Vdv1jXdYemafcA3wFG4E1d13d0sN0mE6/p1+ZjRrPhlI9bfU2nfLzN4xqNTJkyhSlTpjBo0CD+/e9/M3z4cFJTU1mzZk2LfcvLy9t1zIcffpivv/4aUOvW2sPlchESEtLm/v7+/k23H3nkEaZOncpnn31GdnZ2m2vXdF3n008/bZoSeTYGDRrEoEGDuPHGG0lKSmoK5BqnoxoMhqbbjfcdDgdms/ms22x0/HGNRiN1dXVn9D4JIURnMQb4o9udhPpAyr48Ym1FuBwDqXPB/sJq0nuGeLuLne5oZT1vrcom43AZP5uczNSUHvQM8/NKX/L2FLPyP9uwmmDKr6cS3DvWK/0QQgh36WjWyj66rvfUdT392M8dx7bn6bp+8XH7LdB1vZ+u68m6rj/V0U570549e9i3b1/T/YyMDHr16kVKSgpFRUVNgZzdbmfHjh2EhIQQEhLCypUrAXjvvdaXET711FNkZGScNogLDAykqqoKgKCgIJKSkvj4448BFYRlZma2+ryKigrijl2JbAysTjwewMyZM3n55ZebFudv2bIFgEmTJvH+++8DsH37drZu3XpSG9XV1SxduvSk96a9Ro0axbJlyyguLsbpdPLf//6XyZMnt7kdwGw2tzm62OhM3ichhOhMJrORMXMH0CvJSl5WPYsf/oj5y/cw75tdPPvtbgoq6r3dxU5RVW/nP2tz+O3HmewpqOSmsYlM6hvplb7ouo7L6WLXuiLC0npz4aOXSBAnhDgneX6eQzdXXV3NzTffzMCBAxk8eDA7d+7k8ccfx2Kx8Mknn/DAAw8wZMgQ0tPTWb16NQBvvfUWd999N+np6R3OXnbdddfx3HPPMXToUA4cOMB7773HG2+8wZAhQ0hNTW1KTnKi+++/n4ceeoihQ4e2yM44depUdu7c2ZTs5JFHHsFutzN48GBSU1N55JFHALjzzjuprq5mwIABPProowwfPvykNnRd59lnnyUlJYX09HQee+yxFkHj6cTExDBv3jymTp3KkCFDGD58OJdddlmb2wFuv/12Bg8e3JTspC3tfZ+EEKKzGU1Gxj5wBenT4ikr1wn9fCU39zSwM7+S336cyXvrcqhu6N5Zc19ctI8FW/MZ3yeCF65J5+JBMR5PaALgrLdR+NrrVC9axJjLejP5pyPw6xHq8X4IIYQnaB0NLNxpxIgR+om1ynbt2sWAAQO81KOOy87OZvbs2Wzfvt3bXRGn0d3/1oQQXc/RDXuofO9tQlN7wzU38uHmw6zcV8KkfpHcOSXZ291rt4o6O9/tKGBmajTBvmayiqoxGw1em0YJUH2kkJV/WYiprIARc/oQ0so6aiGE6I40Tduk6/qIE7d3dI2cEEIIIdopamQKEf1/B0YjW5bmk15Wz8xJCQTFhgFwsLiGNQdKmJEaRUSA9TRH87yDxTUs3nWU5fuKsTtcxAb7MqFvBL0jA7zar5xvN7Dx8904dQPDLhtPyGVjvdofIYTwBAnkPCwxMVFG44QQ4jxmDAxE13VCov3Z8u1mjq6sZeil/eDi0ewpqOSrzDzmb81jVFIY0wdEkRob5PVMlw6niyfm72Tv0SrMRgPj+0Rw6ZBY4kJ8vdovl9PFtm/2suuLfQT4Gxh3x0TC+id4tU9CCOEpEsgJIYQQHqZpGn2G9SDElM7aN9ew7rP9HFp3kEl3zWD49UP5bkcBP+wuZG1WCUkR/jx9hWfLFtQ0OMg4XM6RslquHZmAyWggOTKAccnhTOgbSYDV+6cPjpISHNYgDuc0kDyxN0OvHorJr+uNYgohhLt4/5PYAxwOBzU1NQQEBEi9MCGEEF1GxOBkLnomga1vLCQro4j8PzxJz4d+xf+N6cXcEfFsyi6j1uZE0zR0XeeRL7aTEObH4PgQUqICCfW3dFpf8srrWHOghMwj5ewvrMal64QHWLl8aBxWk5GbxyV2WlsdYa+rZ8+b3xCw8UuiH3yQaTcPwDeg894HIYToLs7ZQK6hoYGPP/6YZ555hh07djSlqU9NTeWBBx5g7ty5LWqOCSGEEN5gtJoZetclpGQfpvY7G6boaDIWHaJnnwDG9Ylo2q/e7iLc38rarFJ+2F0IQKifhWtG9mRqSg/q7U72Hq0iPMBKgMWE1WzAajI0jeQ5nC6q6h2U1tooqmqgqKqB/Io6rhwWT0SAla1Hyvlk0xGSIv25PD2O9IQQ+vYI8Pq0zka6rnPouw1kfLWHOpuB4UOnYenZE4OfBHFCiPPTOZm1cv369Vx00UXYbDaqq6tPejwgIACLxcK3337LyJEjO7XP4twhWSuFEN5QU9HAsnd3UbVjLzFRMOT/xhOcHNf0uNOlc6ComgOF1WQV+Co4tAAAEYVJREFU1zChTwRDeoawv7CK339+8hrs38xIYWRiGBmHy5n3za4WjwVYTfzqwn6kxgZT3eDA6dIJ9jW7/TWeqeqyBtb9+QuKjtrx89EZeuUgek4d4u1uCSGER7SVtfKcC+Q2bNjA1KlTqampOe3x/f39WbJkyRkHc0899RTvv/8+RqMRg8HAq6++yujRo8/oGJ3tlVde4cUXX+TAgQMUFRURERFx+ieJU5JATgjhLQ2VNWS+vpjs3VXoQEJvH9JvnYJvVHibz6m3OzlYXENJtY1am4N6h4t6u5NRiWEkRvhTUt3A5kPlhPiZ6RFoJSLAin8XWOt2Ko6SEgwhISz5zx6qsgtIineQduNUjNauF2wKIYS7nBflBxoaGpg1a1a7gjiAmpoaZs2aRV5eXrunWa5Zs4b58+ezefNmrFYrxcXF2Gy2jnT7jOm6jq7rGAzN9dzHjx/P7NmzmTJlikf7IoQQovNZg/wZ9es5DDhSxNb/LOfogTKOPvQAcU8/iSG8B0az4aTn+JiNDIgJavOY4QFWLhwY5c5ud5qqnAJ2f7SKiF3f0OOnP2bYrMH4BvTFJ0ACOCGEaHTyN0E39vHHH59xUGWz2fjkk0/avX9+fj4RERFNgV9ERASxsbEAbNq0icmTJzN8+HBmzpxJfn4+AFOmTOGBBx5g1KhR9OvXjxUrVgCwY8cORo0aRXp6OoMHD2bfvn0AvPDCC6SlpZGWlsaLL74IqELiKSkp3HTTTaSlpXH48OEW/Ro6dCiJiYln9NqFEEJ0bYHxkYx/6Cou+sOlhF4+By08ku/f2smqvy8jf9UOXC6Xt7vYqYoz9rH88Q9Y8ORiDuxrwJY+FWtKCqHR/hLECSHECc6pqZWDBg06qxptaWlpbNu2rV37VldXM2HCBGpra5k+/f/bu/+gqus9j+PP9zkc0Msxk9SkMNRSboKKgSDTqtS9lsvea1emxq7sGrXOTrmr5OzM1tY/2Y4zbjP5o5k7A+XmOrq1Oe5au9no1sYUrpiBkYFmlvmDMKW4mlRXzo/P/oGalSgq+uV8fT1mHOFwvofXl48/ePH9fD+fXzNz5kymTJlCJBJhypQpvPbaawwaNIhXXnmFTZs28eKLL1JcXExeXh7PPvssb7zxBkuWLOGtt95i3rx5TJw4kbKyMjo6OojFYuzcuZPy8nK2bt2Kc47CwkLWrFnDgAEDGDFiBFu2bGHixIld5hs2bBh1dXWaWtkDNLVSRHqbSEeM3Vu+4ONXNhONOsJ9HcPzh3DLbwtIGdD11bjezMXjxGOwZf2ntLy7A4uc4KaRYUbfW8g1w4Z4HU9ExHO+n1oZi8Voamq6qGObmpqIxWLd2pogHA5TX19PTU0N1dXVzJw5k8WLF5Ofn09jYyNTp049nSc9Pf30caWlpQDk5eWxb98+AIqKili0aBHNzc2UlpYycuRINm/ezIwZM0hNTT19XE1NDdOnTyczM/OcJU5ERPwtlBwkp/gmRuX9js/+ext73/+Cj2oO495ZyLDy35GcX0QwFCCY1Lsn3MQjUVr+r5E/bt/FgAPbuOGfF9M3HGLMb0YzomAofdL6eR1RRKTX802Ra29vJxQKXdT9aklJSbS3t9O/f/9uPT8YDFJcXExxcTFjxoxh1apV5OXlkZ2dTW1t7VmPOTUVMxgMEo1GAZg1axaFhYVs2LCBkpISqqqqzvl5T5U7ERG5uiX3+wW3zirml793tG7fQ3AnJA8bxsdbD/H51v1c8+1B0kcPIWNKDuEbB3kdF4BoJMbhD/ezf9N2Dh38nkgsQJLFmTT+VuLffUd+yXCvI4qIJBTfFLlwOEwkErmoY6PRKOFwuFvP3b17N4FAgJEjRwLQ0NBAZmYmWVlZtLa2UltbS1FREZFIhE8++YTs7OwuX2vv3r2MGDGC+fPnc+DAAXbs2MHkyZMpLy/n8ccfxznH+vXrWb169UWdl4iI+JuZMThvFOSNAuAGa+f7z/bRcjDG4eoWGqpbSEs9Qf74INfefz8xkkhKDlyRveH+9PUxWj/4jL5t+wmPG83+o/1p+t8viR44weAhKQzNu4mhd44jOdz3smcREfEj3xS5YDBIdnb2Rd0jl52d3a1pldB55W/evHkcPXqUpKQkbrnlFp5//nmSk5NZt24d8+fP59ixY0SjUR599NFzFrm1a9eyevVqQqEQQ4YM4YknniAtLY3y8nIKCgoAmDNnDuPHjz89HbMrzz33HM888wxffvklY8eOpaSkhBUrVnT7ayAiIolvYEaYgXMmEY/fTlvj5zRv+YRISwvf7/iIAbNn8/aKJv70+X76Ro5yzaC+9L/hWtIy00gblU7o+gtf0TIejRI9+g2BoNERTGX/jiMc3rSZb/4Y4bsTndM7fxn9gOF9k8iYfBfXXp/NdYPzSPpF91aKFhGRrvlqsZM1a9bwyCOPnHUT8K6Ew2EqKyspKyu7pKziP1rsRET8wjkHDvZ+2Mrhd7Zz9MDXtH8L8TgMiR1gVHo71z/9T2ysaiS+/1OCJ74llBwgEDAGp7YzdPR19Jk2nfqN+2l/fzuR70/Q0QEd0QDDo7sYkZ9OysyHeHv1Lmz/J/QLw4CMaxmYlc7g224mpX/3Zr2IiMjP+X6xE4D77ruPioqKCzomOTmZe++99zIlEhER8Z6ZgcHN4wdz8/hpAMRjcY4fOEL8aBt9U8DFHRlZAzj2TRIdx5KInIgTd45I/Hti3xwnEDCiJ2IEU5JICsa5tm8Sffr14fqb7qDf6AxS0vrw23m5BJNu8/hsRUSuDr4qcikpKWzcuJE77rijW5uCp6amsnHjxm5vBi4iIuIXgWCA/sOHAD8s8T/uV0PhV0O7PGbKrCwgq8uPBwOX/947ERHp1LvXJ74IEyZMoLq6mrS0tC4XMAmHw6SlpVFdXc2ECROucEIREREREZFL46siF4vFcM4xYcIEWlpaqKysJCcnBzMjFAphZuTk5FBZWUlLSwsTJkzAOUcsFvM6uoiIiIiISLf5ZmplLBbjwQcfJC0tjaVLl5KSkkJZWRllZWXEYjHa29sJh8M/Wp3SOceCBQtoa2tj5cqV3V65UkRERERExEu+uSIXCARIS0tj+fLlLFiwgDNX4wwGg/Tv3/+sJW758uWkpaURCPjmSyEiIiIiIj7nmytyZsbSpUsBWL58OQBLly4966anZ5a4ioqKLp/XlUWLFvHSSy8RDAYJBAJUVVVRWFjYMydykcrKyqirqyMUClFQUEBVVRWhUMjTTCIiIiIicnn4pshB98rcpZa42tpaXn/9dbZv305KSgpfffUVHR0dPXsi5+Gcwzn3o6uIZWVlrFmzBoBZs2axYsUKHnnkkSuaS0RERERErgzfzSc8VeYqKip+Ns3yUkscwKFDhxg4cODpLQsGDhzIDTfcAEB9fT1TpkwhLy+Pu+++m0OHDgFQXFzMY489RkFBAaNGjaKmpgaApqYmCgoKyM3NZezYsezZsweAJUuWkJOTQ05ODsuWLQNg3759ZGVlMXv2bHJycjh48OCPcpWUlGBmmBkFBQU0Nzdf3BdQRERERER6Pd8VOei6zF1qiQO46667OHjwIKNGjWLu3Lm88847AEQiEebNm8e6deuor6/noYce4sknnzx9XDQaZdu2bSxbtoyFCxcCUFlZSUVFBQ0NDdTV1ZGRkUF9fT0rV67kvffeY+vWrbzwwgt88MEHAOzZs4e5c+fS1NREZmbmWfNFIhFWr17NtGnTLvjcREREREQkMfhqauWZfjrN8tRUy0spcdC5B119fT01NTVUV1czc+ZMFi9eTH5+Po2NjUydOhXoXEUzPT399HGlpaUA5OXlsW/fPgCKiopYtGgRzc3NlJaWMnLkSDZv3syMGTNITU09fVxNTQ3Tp08nMzOTiRMnnjPf3LlzmTx5MpMmTbqo8xMRERERkd7Pt0UOfihzp0ocdL0AyoUIBoMUFxdTXFzMmDFjWLVqFXl5eWRnZ1NbW3vWY05NxQwGg0SjUaDzXrbCwkI2bNhASUkJVVVV5/y8p8pdVxYuXEhra+t5X0dERERERBKbL6dWnnJqOuWZfro1wYXavXv36XvZABoaGsjMzCQrK4vW1tbTRS4SidDU1HTO19q7dy8jRoxg/vz53HPPPezYsYNJkybx6quv8t133/Htt9+yfv36bl1dW7FiBZs2beLll1/WVgoiIiIiIj7n2+/4f3pPXDweP+sCKBeqvb2dBx54gNGjRzN27Fh27tzJU089RXJyMuvWreOxxx5j3Lhx5ObmsmXLlnO+1tq1a8nJySE3N5fGxkZmz57NbbfdRnl5OQUFBRQWFjJnzhzGjx9/3lwPP/wwhw8fpqioiNzcXJ5++umLOj8REREREen97FKuTl1u+fn5rq6u7keP7dq1i1tvvfWcx3W1sElPLXgiV4fu/FkTEREREbmczKzeOZf/08d9d4/cucrahWwaLiIiIiIi0lv5qsh154qbypyIiIiIiCQ63xS5C5k2qTInIiIiIiKJLCGLnHPuZ6UrHo/T1tbW7XvfzixzbW1txONxgsHgZcssiaU33zsqIiIiIpJwi518/vnn9OvXj+uuu+5nZS0WixEIBC7oyppzTiVOfsQ5x9dff83x48cZPny413FERERE5Crmm8VOMjIyaG5uprW11eso4mN9+vQhIyPD6xgiIiIiImeVcEUuFArpKomIiIiIiFzVfLshuIiIiIiIiF+pyImIiIiIiCQYFTkREREREZEE06tXrTSzVmC/1zmucgOBr7wOIT1O4+pfGlt/0rj6l8bWnzSu/uXF2GY65wb99MFeXeTEe2ZWd7blTiWxaVz9S2PrTxpX/9LY+pPG1b9609hqaqWIiIiIiEiCUZETERERERFJMCpycj7Pex1ALguNq39pbP1J4+pfGlt/0rj6V68ZW90jJyIiIiIikmB0RU5ERERERCTBqMjJeZnZfWbWZGZxM+sVq/TIxTOzaWa228w+NbPHvc4jPcPMXjSzI2bW6HUW6TlmNtTMqs1s58l/hyu8ziSXzsz6mNk2M/vw5Lgu9DqT9BwzC5rZB2b2utdZpOeY2T4z+8jMGsyszus8oCIn3dMIlALveh1ELo2ZBYE/AH8OjAZ+b2ajvU0lPeRfgWleh5AeFwX+3jk3GpgI/K3+zvrCCeBO59w4IBeYZmYTvY0kPagC2OV1CLks7nDO5Wr7AUkYzrldzrndXueQHlEAfOqc2+uc6wD+HbjH40zSA5xz7wJtXueQnuWcO+Sc237y7eN0fnN4o7ep5FK5Tu0n3w2d/KVFC3zAzDKAvwBWeJ1F/E9FTuTqciNw8Iz3m9E3hSIJwcyGAeOB9zyOIj3g5PS7BuAI8KZzTuPqD8uAfwDiHueQnueA/zGzejP7G6/DACR5HUB6BzN7Cxhylg896Zx77UrnERGRH5hZGPgP4FHn3Dde55FL55yLAblmdi2w3sxynHO6xzWBmdlvgCPOuXozK/Y4jvS8P3POfWFmg4E3zezjk7NhPKMiJwA4537tdQa5Ir4Ahp7xfsbJx0SklzKzEJ0l7t+cc//pdR7pWc65o2ZWTec9ripyie12YLqZlQB9gGvMbI1z7i89ziU9wDn3xcnfj5jZejpvV/G0yGlqpcjV5X1gpJkNN7Nk4H7gvzzOJCJdMDMD/gXY5Zxb4nUe6RlmNujklTjMrC8wFfjY01ByyZxz/+icy3DODaPz/9e3VeL8wcxSzazfqbeBu+gFP3hRkZPzMrMZZtYMFAEbzGyT15nk4jjnosDfAZvoXDRhrXOuydtU0hPM7GWgFsgys2Yz+2uvM0mPuB34K+DOk0teN5z8ab8ktnSg2sx20PkDtjedc1qqXqT3uh7YbGYfAtuADc65jR5nwpzTIkkiIiIiIiKJRFfkREREREREEoyKnIiIiIiISIJRkRMREREREUkwKnIiIiIiIiIJRkVOREREREQkwajIiYiIiIiIJBgVORERERERkQSjIiciIiIiIpJg/h+8W09m9x5yUAAAAABJRU5ErkJggg==\n" 217 | }, 218 | "metadata": { 219 | "needs_background": "light" 220 | }, 221 | "output_type": "display_data" 222 | } 223 | ], 224 | "source": [ 225 | "plot_bearings([true_states, filtered.mean, smoothed.mean, iterated_smoothed_trajectories.mean,\n", 226 | " seq_iterated_smoothed_trajectories.mean],\n", 227 | " [\"True\", \"Filter\", \"Smoother\", \"Iterated Smoother\", \"Seq - Iterated Smoother\"],\n", 228 | " s1, s2, figsize=(15, 10), quiver=False)" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 12, 234 | "metadata": {}, 235 | "outputs": [ 236 | { 237 | "data": { 238 | "text/plain": "(1001, 5)" 239 | }, 240 | "execution_count": 12, 241 | "metadata": {}, 242 | "output_type": "execute_result" 243 | } 244 | ], 245 | "source": [ 246 | "iterated_smoothed_trajectories.mean.shape" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 13, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "data": { 256 | "text/plain": "(1000, 5)" 257 | }, 258 | "execution_count": 13, 259 | "metadata": {}, 260 | "output_type": "execute_result" 261 | } 262 | ], 263 | "source": [ 264 | "seq_iterated_smoothed_trajectories.mean.shape\n" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 13, 270 | "metadata": {}, 271 | "outputs": [], 272 | "source": [] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": 13, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": 13, 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 13, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 13, 298 | "outputs": [], 299 | "source": [], 300 | "metadata": { 301 | "collapsed": false, 302 | "pycharm": { 303 | "name": "#%%\n" 304 | } 305 | } 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 13, 310 | "outputs": [], 311 | "source": [], 312 | "metadata": { 313 | "collapsed": false, 314 | "pycharm": { 315 | "name": "#%%\n" 316 | } 317 | } 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 13, 322 | "outputs": [], 323 | "source": [], 324 | "metadata": { 325 | "collapsed": false, 326 | "pycharm": { 327 | "name": "#%%\n" 328 | } 329 | } 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 13, 334 | "outputs": [], 335 | "source": [], 336 | "metadata": { 337 | "collapsed": false, 338 | "pycharm": { 339 | "name": "#%%\n" 340 | } 341 | } 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": 13, 346 | "outputs": [], 347 | "source": [], 348 | "metadata": { 349 | "collapsed": false, 350 | "pycharm": { 351 | "name": "#%%\n" 352 | } 353 | } 354 | } 355 | ], 356 | "metadata": { 357 | "kernelspec": { 358 | "name": "python3", 359 | "language": "python", 360 | "display_name": "Python 3 (ipykernel)" 361 | }, 362 | "language_info": { 363 | "codemirror_mode": { 364 | "name": "ipython", 365 | "version": 3 366 | }, 367 | "file_extension": ".py", 368 | "mimetype": "text/x-python", 369 | "name": "python", 370 | "nbconvert_exporter": "python", 371 | "pygments_lexer": "ipython3", 372 | "version": "3.8.5" 373 | } 374 | }, 375 | "nbformat": 4, 376 | "nbformat_minor": 4 377 | } -------------------------------------------------------------------------------- /notebooks/linear_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Imports" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "metadata": { 13 | "ExecuteTime": { 14 | "end_time": "2024-08-09T19:57:28.160509Z", 15 | "start_time": "2024-08-09T19:57:27.779511Z" 16 | } 17 | }, 18 | "source": [ 19 | "import jax.numpy as jnp\n", 20 | "from parsmooth.parallel import ekf, eks\n", 21 | "from parsmooth.sequential import ekf as seq_ekf, eks as seq_eks, ckf as seq_ckf, cks as seq_cks\n", 22 | "from parsmooth.models.linear import get_data, make_parameters\n", 23 | "from parsmooth.utils import MVNormalParameters" 24 | ], 25 | "outputs": [], 26 | "execution_count": 1 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "### Input parameters" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "metadata": { 38 | "ExecuteTime": { 39 | "end_time": "2024-08-09T19:57:28.478071Z", 40 | "start_time": "2024-08-09T19:57:28.162240Z" 41 | } 42 | }, 43 | "source": [ 44 | "r = 0.5\n", 45 | "q = 0.1\n", 46 | "x0 = jnp.array([0., 0.]) # initial true location\n", 47 | "\n", 48 | "T = 1000 # number of observations" 49 | ], 50 | "outputs": [ 51 | { 52 | "name": "stderr", 53 | "output_type": "stream", 54 | "text": [ 55 | "2024-08-09 20:57:28.400845: W external/xla/xla/service/gpu/nvptx_compiler.cc:836] The NVIDIA driver's CUDA version is 12.5 which is older than the PTX compiler version (12.6.20). Because the driver is older than the PTX compiler version, XLA is disabling parallel compilation, which may slow down compilation. You should update your NVIDIA driver or use the NVIDIA-provided CUDA forward compatibility packages.\n" 56 | ] 57 | } 58 | ], 59 | "execution_count": 2 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### Get parameters" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "metadata": { 71 | "ExecuteTime": { 72 | "end_time": "2024-08-09T19:57:28.871458Z", 73 | "start_time": "2024-08-09T19:57:28.479254Z" 74 | } 75 | }, 76 | "source": [ 77 | "A, H, Q, R, observation_function, transition_function = make_parameters(r, q)" 78 | ], 79 | "outputs": [], 80 | "execution_count": 3 81 | }, 82 | { 83 | "cell_type": "code", 84 | "metadata": { 85 | "ExecuteTime": { 86 | "end_time": "2024-08-09T19:57:28.875619Z", 87 | "start_time": "2024-08-09T19:57:28.872932Z" 88 | } 89 | }, 90 | "source": [ 91 | "observation_function = jnp.vectorize(observation_function, signature=\"(m)->(d)\")\n", 92 | "transition_function = jnp.vectorize(transition_function, signature=\"(m)->(m)\")" 93 | ], 94 | "outputs": [], 95 | "execution_count": 4 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "### Get data" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "metadata": { 107 | "ExecuteTime": { 108 | "end_time": "2024-08-09T19:57:29.444821Z", 109 | "start_time": "2024-08-09T19:57:28.876503Z" 110 | } 111 | }, 112 | "source": [ 113 | "ts, true_states, observations = get_data(x0, A, H, R, Q, T, 42)" 114 | ], 115 | "outputs": [], 116 | "execution_count": 5 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "### We can now run the filter" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "Initial state guess" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "metadata": { 135 | "ExecuteTime": { 136 | "end_time": "2024-08-09T19:57:29.449773Z", 137 | "start_time": "2024-08-09T19:57:29.445689Z" 138 | } 139 | }, 140 | "source": [ 141 | "m = jnp.array([0., 0.])\n", 142 | "P = jnp.eye(2)\n", 143 | "\n", 144 | "initial_guess = MVNormalParameters(m, P)" 145 | ], 146 | "outputs": [], 147 | "execution_count": 6 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "### We can now run the smoother" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "Run the filters" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "metadata": { 166 | "ExecuteTime": { 167 | "end_time": "2024-08-09T19:57:42.814995Z", 168 | "start_time": "2024-08-09T19:57:29.451638Z" 169 | } 170 | }, 171 | "source": [ 172 | "par_ekf_filtered = ekf(initial_guess, observations, transition_function, Q, observation_function, R)\n", 173 | "seq_ekf_ll, seq_ekf_filtered = seq_ekf(initial_guess, observations, transition_function, Q, observation_function, R)\n", 174 | "par_ckf_ll, seq_ckf_filtered = seq_ckf(initial_guess, observations, transition_function, Q, observation_function, R)" 175 | ], 176 | "outputs": [], 177 | "execution_count": 7 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "Compare:" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "metadata": { 189 | "ExecuteTime": { 190 | "end_time": "2024-08-09T19:57:43.061201Z", 191 | "start_time": "2024-08-09T19:57:42.816350Z" 192 | } 193 | }, 194 | "source": [ 195 | "print(seq_ekf_ll, par_ckf_ll)\n", 196 | "\n", 197 | "print(jnp.max(jnp.abs(par_ekf_filtered.mean - seq_ekf_filtered.mean)))\n", 198 | "print(jnp.max(jnp.abs(par_ekf_filtered.mean - seq_ckf_filtered.mean)))\n", 199 | "\n", 200 | "print(jnp.max(jnp.abs(par_ekf_filtered.cov - seq_ekf_filtered.cov)))\n", 201 | "print(jnp.max(jnp.abs(par_ekf_filtered.cov - seq_ckf_filtered.cov)))" 202 | ], 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "-1178.5336 -1178.5336\n", 209 | "1.7851591e-05\n", 210 | "1.7851591e-05\n", 211 | "2.346933e-06\n", 212 | "2.346933e-06\n" 213 | ] 214 | } 215 | ], 216 | "execution_count": 8 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "Run the smoothers" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "metadata": { 228 | "ExecuteTime": { 229 | "end_time": "2024-08-09T19:57:46.092218Z", 230 | "start_time": "2024-08-09T19:57:43.063031Z" 231 | } 232 | }, 233 | "source": [ 234 | "par_eks_smoothed = eks(transition_function, Q, par_ekf_filtered, par_ekf_filtered.mean)\n", 235 | "seq_eks_smoothed = seq_eks(transition_function, Q, par_ekf_filtered)\n", 236 | "seq_cks_smoothed = seq_cks(transition_function, Q, par_ekf_filtered)" 237 | ], 238 | "outputs": [], 239 | "execution_count": 9 240 | }, 241 | { 242 | "cell_type": "code", 243 | "metadata": { 244 | "ExecuteTime": { 245 | "end_time": "2024-08-09T19:57:46.100782Z", 246 | "start_time": "2024-08-09T19:57:46.093902Z" 247 | } 248 | }, 249 | "source": [ 250 | "print(jnp.max(jnp.abs(par_eks_smoothed.mean - seq_eks_smoothed.mean)))\n", 251 | "print(jnp.max(jnp.abs(par_eks_smoothed.mean - seq_cks_smoothed.mean)))\n", 252 | "\n", 253 | "print(jnp.max(jnp.abs(par_eks_smoothed.cov - seq_eks_smoothed.cov)))\n", 254 | "print(jnp.max(jnp.abs(par_eks_smoothed.cov - seq_cks_smoothed.cov)))" 255 | ], 256 | "outputs": [ 257 | { 258 | "name": "stdout", 259 | "output_type": "stream", 260 | "text": [ 261 | "6.023445e-05\n", 262 | "6.01972e-05\n", 263 | "2.7239323e-05\n", 264 | "2.7239323e-05\n" 265 | ] 266 | } 267 | ], 268 | "execution_count": 10 269 | }, 270 | { 271 | "cell_type": "code", 272 | "metadata": { 273 | "ExecuteTime": { 274 | "end_time": "2024-08-09T19:57:46.111310Z", 275 | "start_time": "2024-08-09T19:57:46.105079Z" 276 | } 277 | }, 278 | "source": [], 279 | "outputs": [], 280 | "execution_count": 10 281 | } 282 | ], 283 | "metadata": { 284 | "kernelspec": { 285 | "name": "python3", 286 | "language": "python", 287 | "display_name": "Python 3 (ipykernel)" 288 | }, 289 | "language_info": { 290 | "codemirror_mode": { 291 | "name": "ipython", 292 | "version": 3 293 | }, 294 | "file_extension": ".py", 295 | "mimetype": "text/x-python", 296 | "name": "python", 297 | "nbconvert_exporter": "python", 298 | "pygments_lexer": "ipython3", 299 | "version": "3.8.5" 300 | } 301 | }, 302 | "nbformat": 4, 303 | "nbformat_minor": 4 304 | } 305 | -------------------------------------------------------------------------------- /notebooks/runtime.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Runtime experiments for CPU and GPU benchmarking of our algorithms" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "id": "6-Car3VDUC1b" 15 | }, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "Model name: Intel(R) Xeon(R) W-2133 CPU @ 3.60GHz\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "!lscpu |grep 'Model name'" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": { 33 | "colab": { 34 | "base_uri": "https://localhost:8080/", 35 | "height": 51 36 | }, 37 | "id": "AQzvVUCESkGP", 38 | "outputId": "e998c8f0-3abe-48de-bbfe-6f4f09662af2" 39 | }, 40 | "outputs": [ 41 | { 42 | "name": "stdout", 43 | "output_type": "stream", 44 | "text": [ 45 | "Mon Oct 19 21:43:43 2020 \n", 46 | "+-----------------------------------------------------------------------------+\n", 47 | "| NVIDIA-SMI 455.23.05 Driver Version: 455.23.05 CUDA Version: 11.1 |\n", 48 | "|-------------------------------+----------------------+----------------------+\n", 49 | "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", 50 | "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", 51 | "| | | MIG M. |\n", 52 | "|===============================+======================+======================|\n", 53 | "| 0 Quadro P2000 On | 00000000:91:00.0 On | N/A |\n", 54 | "| 47% 36C P5 8W / 75W | 4722MiB / 5050MiB | 3% Default |\n", 55 | "| | | N/A |\n", 56 | "+-------------------------------+----------------------+----------------------+\n", 57 | " \n", 58 | "+-----------------------------------------------------------------------------+\n", 59 | "| Processes: |\n", 60 | "| GPU GI CI PID Type Process name GPU Memory |\n", 61 | "| ID ID Usage |\n", 62 | "|=============================================================================|\n", 63 | "| 0 N/A N/A 1177 G /usr/lib/xorg/Xorg 65MiB |\n", 64 | "| 0 N/A N/A 1217 G /usr/bin/gnome-shell 80MiB |\n", 65 | "| 0 N/A N/A 1797 G /usr/lib/xorg/Xorg 385MiB |\n", 66 | "| 0 N/A N/A 2014 G /usr/bin/gnome-shell 34MiB |\n", 67 | "| 0 N/A N/A 2333 G ...oken=12133755331783826913 10MiB |\n", 68 | "| 0 N/A N/A 3158 G .../debug.log --shared-files 43MiB |\n", 69 | "| 0 N/A N/A 4660 G ...AAAAAAAAA= --shared-files 142MiB |\n", 70 | "| 0 N/A N/A 30637 C ...rallelEKF/venv/bin/python 3951MiB |\n", 71 | "+-----------------------------------------------------------------------------+\n" 72 | ] 73 | } 74 | ], 75 | "source": [ 76 | "!nvidia-smi" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": { 82 | "id": "D66bXe-p0wdC" 83 | }, 84 | "source": [ 85 | "### Imports" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 36, 91 | "metadata": { 92 | "id": "cfLgDa4d0wdD" 93 | }, 94 | "outputs": [], 95 | "source": [ 96 | "from jax import jit, devices, make_jaxpr\n", 97 | "from jax.config import config\n", 98 | "import jax.numpy as jnp\n", 99 | "from matplotlib import rcParams\n", 100 | "import matplotlib.pyplot as plt\n", 101 | "import numpy as np\n", 102 | "import pandas as pd\n", 103 | "import time\n", 104 | "import tqdm\n", 105 | "\n", 106 | "from parsmooth.parallel import ieks, icks\n", 107 | "from parsmooth.sequential import ieks as seq_ieks, icks as seq_icks\n", 108 | "from parsmooth.models.bearings import get_data, make_parameters, plot_bearings\n", 109 | "from parsmooth.utils import MVNormalParameters" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 30, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "# rcParams['font.family'] = 'sans-serif'\n", 119 | "rcParams['font.sans-serif'] = ['Computer Modern Sans serif']" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": { 125 | "id": "7n5s5J1S0wdH" 126 | }, 127 | "source": [ 128 | "### Input parameters" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 5, 134 | "metadata": { 135 | "colab": { 136 | "base_uri": "https://localhost:8080/", 137 | "height": 51 138 | }, 139 | "id": "I7udHQWm0wdH", 140 | "outputId": "112b06e5-55f9-4d22-f4db-27d7df41cb1b" 141 | }, 142 | "outputs": [], 143 | "source": [ 144 | "s1 = jnp.array([-1.5, 0.5]) # First sensor location\n", 145 | "s2 = jnp.array([1., 1.]) # Second sensor location\n", 146 | "r = 5. # Observation noise (stddev) - Large because IEKS is not very stable\n", 147 | "dt = 0.01 # discretization time step\n", 148 | "x0 = jnp.array([0.1, 0.2, 1, 0]) # initial true location\n", 149 | "qc = 0.1 # noise - Large because IEKS is not very stable\n", 150 | "qw = 0.1 # noise - Small because IEKS is not very stable\n", 151 | "\n", 152 | "T = 100 # number of observations" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": { 158 | "id": "rjRijP8D0wdL" 159 | }, 160 | "source": [ 161 | "### Get parameters" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 6, 167 | "metadata": { 168 | "id": "MnBGuowI0wdL" 169 | }, 170 | "outputs": [], 171 | "source": [ 172 | "Q, R, observation_function, transition_function = make_parameters(qc, qw, r, dt, s1, s2)" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 7, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "transition_function = jnp.vectorize(transition_function, signature=\"(m)->(m)\")\n", 182 | "observation_function = jnp.vectorize(observation_function, signature=\"(m)->(d)\")" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": { 188 | "id": "EEWK8aah0wdO" 189 | }, 190 | "source": [ 191 | "### Get data" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 8, 197 | "metadata": { 198 | "id": "tMnoVBYc0wdP" 199 | }, 200 | "outputs": [], 201 | "source": [ 202 | "ts, true_states, observations = get_data(x0, dt, r, T, s1, s2, qw, random_state=42)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": { 208 | "id": "wKIP9WPu0wdS" 209 | }, 210 | "source": [ 211 | "### We can now run the filter" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": { 217 | "id": "MNhOFYON0wdS" 218 | }, 219 | "source": [ 220 | "Initial state guess" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 9, 226 | "metadata": { 227 | "id": "0Z9b5Hpe0wdT" 228 | }, 229 | "outputs": [], 230 | "source": [ 231 | "m = jnp.array([-1., -1., 0., 0., 0.])\n", 232 | "P = jnp.eye(5)\n", 233 | "\n", 234 | "initial_guess = MVNormalParameters(m, P)\n", 235 | "initial_linearization_points = jnp.zeros((T, 5), dtype=m.dtype)\n", 236 | "initial_linearization_covariances = jnp.repeat(jnp.eye(5).reshape(1, 5, 5), T, axis=0)" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": { 242 | "id": "SzlrSEnn0wdi" 243 | }, 244 | "source": [ 245 | "### Sequential vs Parallel computation time comparison" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": 10, 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [ 254 | "gpu_par_ieks = jit(ieks, static_argnums=(2, 4, 7), backend=\"gpu\")\n", 255 | "cpu_par_ieks = jit(ieks, static_argnums=(2, 4, 7), backend=\"cpu\")\n", 256 | "\n", 257 | "gpu_seq_ieks = jit(seq_ieks, static_argnums=(2, 4, 7), backend=\"gpu\")\n", 258 | "cpu_seq_ieks = jit(seq_ieks, static_argnums=(2, 4, 7), backend=\"cpu\")\n", 259 | "\n", 260 | "gpu_par_icks = jit(icks, static_argnums=(2, 4, 7), backend=\"gpu\")\n", 261 | "cpu_par_icks = jit(icks, static_argnums=(2, 4, 7), backend=\"cpu\")\n", 262 | "\n", 263 | "gpu_seq_icks = jit(seq_icks, static_argnums=(2, 4, 7), backend=\"gpu\")\n", 264 | "cpu_seq_icks = jit(seq_icks, static_argnums=(2, 4, 7), backend=\"cpu\")" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 11, 270 | "metadata": { 271 | "id": "8ihUWWOniFgy" 272 | }, 273 | "outputs": [], 274 | "source": [ 275 | "def profile_smoother(s_method, lengths, n_runs=1, n_iter=10):\n", 276 | " res_mean = []\n", 277 | " for j in tqdm.tqdm(lengths):\n", 278 | " observations_slice = observations[:j]\n", 279 | " init_linearizations_points_slice = initial_linearization_points[:j]\n", 280 | " init_linearizations_covs_slice = initial_linearization_covariances[:j]\n", 281 | " init_linearizations_states = MVNormalParameters(init_linearizations_points_slice, init_linearizations_covs_slice)\n", 282 | " args = initial_guess, observations_slice, transition_function, Q, observation_function, R, init_linearizations_states, n_iter\n", 283 | " s = s_method(*args) # this is a call used for compiling the function, this is a bit slow at the moment in JAX and shouldn't be taken into account for benchmarking.\n", 284 | " # they are currently working on AOT compilation, which would then reduce the overhead substantially. \n", 285 | " s.mean.block_until_ready()\n", 286 | " run_times = []\n", 287 | " for _ in range(n_runs):\n", 288 | " tic = time.time()\n", 289 | " s_states = s_method(*args)\n", 290 | " s_states.mean.block_until_ready()\n", 291 | " toc = time.time()\n", 292 | " run_times.append(toc - tic)\n", 293 | " res_mean.append(np.mean(run_times))\n", 294 | " return np.array(res_mean)" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": { 300 | "id": "VrId5I8-0wdj" 301 | }, 302 | "source": [ 303 | "Let's now run the sequential vs the parallel implementation to see the performance gain coming from such parallelisation" 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": 12, 309 | "metadata": { 310 | "id": "L4aio98JjBvs" 311 | }, 312 | "outputs": [], 313 | "source": [ 314 | "lengths_space = np.logspace(1, np.log10(T), num=20).astype(np.int32)" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": 13, 320 | "metadata": {}, 321 | "outputs": [ 322 | { 323 | "name": "stderr", 324 | "output_type": "stream", 325 | "text": [ 326 | "100%|██████████| 3/3 [01:58<00:00, 39.46s/it]\n", 327 | "100%|██████████| 3/3 [00:07<00:00, 2.49s/it]\n", 328 | "100%|██████████| 3/3 [02:21<00:00, 47.26s/it]\n", 329 | "100%|██████████| 3/3 [00:08<00:00, 2.96s/it]\n" 330 | ] 331 | } 332 | ], 333 | "source": [ 334 | "gpu_par_ieks_time = profile_smoother(gpu_par_ieks, lengths_space)\n", 335 | "cpu_par_ieks_time = profile_smoother(cpu_par_ieks, lengths_space)\n", 336 | "\n", 337 | "gpu_seq_ieks_time = profile_smoother(gpu_seq_ieks, lengths_space)\n", 338 | "cpu_seq_ieks_time = profile_smoother(cpu_seq_ieks, lengths_space)\n", 339 | "\n", 340 | "gpu_par_icks_time = profile_smoother(gpu_par_icks, lengths_space)\n", 341 | "cpu_par_icks_time = profile_smoother(cpu_par_icks, lengths_space)\n", 342 | "\n", 343 | "gpu_seq_icks_time = profile_smoother(gpu_seq_icks, lengths_space)\n", 344 | "cpu_seq_icks_time = profile_smoother(cpu_seq_icks, lengths_space)\n" 345 | ] 346 | }, 347 | { 348 | "cell_type": "code", 349 | "execution_count": 41, 350 | "metadata": {}, 351 | "outputs": [], 352 | "source": [ 353 | "data = np.stack([\n", 354 | " gpu_par_ieks_time,\n", 355 | " cpu_par_ieks_time,\n", 356 | " gpu_seq_ieks_time,\n", 357 | " cpu_seq_ieks_time,\n", 358 | " gpu_par_icks_time,\n", 359 | " cpu_par_icks_time,\n", 360 | " gpu_seq_icks_time,\n", 361 | " cpu_seq_icks_time],\n", 362 | " axis=1)\n", 363 | "\n", 364 | "columns = [\"GPU_par_IEKS\",\n", 365 | " \"CPU_par_IEKS\",\n", 366 | " \"GPU_seq_IEKS\",\n", 367 | " \"CPU_seq_IEKS\",\n", 368 | " \"GPU_par_ICKS\",\n", 369 | " \"CPU_par_ICKS\",\n", 370 | " \"GPU_seq_ICKS\",\n", 371 | " \"CPU_seq_ICKS\"]\n", 372 | "\n", 373 | "df = pd.DataFrame(index=lengths_space, data=data, colums=columns)\n", 374 | "df.to_csv(\"...\")" 375 | ] 376 | }, 377 | { 378 | "cell_type": "code", 379 | "execution_count": null, 380 | "metadata": {}, 381 | "outputs": [], 382 | "source": [] 383 | } 384 | ], 385 | "metadata": { 386 | "colab": { 387 | "name": "ekf_example.ipynb", 388 | "provenance": [], 389 | "toc_visible": true 390 | }, 391 | "kernelspec": { 392 | "display_name": "parsmooth", 393 | "language": "python", 394 | "name": "parsmooth" 395 | }, 396 | "language_info": { 397 | "codemirror_mode": { 398 | "name": "ipython", 399 | "version": 3 400 | }, 401 | "file_extension": ".py", 402 | "mimetype": "text/x-python", 403 | "name": "python", 404 | "nbconvert_exporter": "python", 405 | "pygments_lexer": "ipython3", 406 | "version": "3.8.5" 407 | } 408 | }, 409 | "nbformat": 4, 410 | "nbformat_minor": 4 411 | } -------------------------------------------------------------------------------- /parsmooth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EEA-sensors/parallel-non-linear-gaussian-smoothers/4fb19a189f1338e8cec9dafd2d8088c233875b5d/parsmooth/__init__.py -------------------------------------------------------------------------------- /parsmooth/cubature_common/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import Tuple 3 | 4 | import jax.numpy as jnp 5 | import numpy as np 6 | 7 | from ..utils import MVNormalParameters 8 | 9 | SigmaPoints = namedtuple( 10 | 'SigmaPoints', ['points', 'wm', 'wc'] 11 | ) 12 | 13 | 14 | def mean_sigma_points(points): 15 | """ 16 | Computes the mean of sigma points 17 | 18 | Parameters 19 | ---------- 20 | points: SigmaPoints 21 | The sigma points 22 | 23 | Returns 24 | ------- 25 | mean: array_like 26 | the mean of the sigma points 27 | """ 28 | return jnp.dot(points.wm, points.points) 29 | 30 | 31 | def covariance_sigma_points(points_1, mean_1, points_2, mean_2): 32 | """ 33 | Computes the covariance between two sets of sigma points 34 | 35 | Parameters 36 | ---------- 37 | points_1: SigmaPoints 38 | first set of sigma points 39 | mean_1: array_like 40 | assumed mean of the first set of points 41 | points_2: SigmaPoints 42 | second set of sigma points 43 | points_1: SigmaPoints 44 | assumed mean of the second set of points 45 | 46 | Returns 47 | ------- 48 | cov: array_like 49 | the covariance of the two sets 50 | """ 51 | one = (points_1.points - mean_1.reshape(1, -1)).T * points_1.wc.reshape(1, -1) 52 | two = points_2.points - mean_2.reshape(1, -1) 53 | return jnp.dot(one, two) 54 | 55 | 56 | def cubature_weights(n_dim: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 57 | """ Computes the weights associated with the spherical cubature method. 58 | The number of sigma-points is 2 * n_dim 59 | 60 | Parameters 61 | ---------- 62 | n_dim: int 63 | Dimensionality of the problem 64 | 65 | Returns 66 | ------- 67 | wm: np.ndarray 68 | Weights means 69 | wc: np.ndarray 70 | Weights covariances 71 | xi: np.ndarray 72 | Orthogonal vectors 73 | """ 74 | wm = np.ones(shape=(2 * n_dim,)) / (2 * n_dim) 75 | wc = wm 76 | xi = np.concatenate([np.eye(n_dim), -np.eye(n_dim)], axis=0) * np.sqrt(n_dim) 77 | 78 | return wm, wc, xi 79 | 80 | 81 | def get_sigma_points(mv_normal_parameters: MVNormalParameters) -> SigmaPoints: 82 | """ Computes the sigma-points for a given mv normal distribution 83 | The number of sigma-points is 2*n_dim 84 | 85 | Parameters 86 | ---------- 87 | mv_normal_parameters: MVNormalParameters 88 | Mean and Covariance of the distribution 89 | 90 | Returns 91 | ------- 92 | out: SigmaPoints 93 | sigma points for the spherical cubature transform 94 | 95 | """ 96 | mean = mv_normal_parameters.mean 97 | n_dim = mean.shape[0] 98 | 99 | wm, wc, xi = cubature_weights(n_dim) 100 | 101 | sigma_points = jnp.repeat(mean.reshape(1, -1), wm.shape[0], axis=0) \ 102 | + jnp.dot(jnp.linalg.cholesky(mv_normal_parameters.cov), xi.T).T 103 | 104 | return SigmaPoints(sigma_points, wm, wc) 105 | 106 | 107 | def get_mv_normal_parameters(sigma_points: SigmaPoints) -> MVNormalParameters: 108 | """ Computes the MV Normal distribution parameters associated with the sigma points 109 | 110 | Parameters 111 | ---------- 112 | sigma_points: SigmaPoints 113 | shape of sigma_points.points is (n_dim, 2*n_dim) 114 | Returns 115 | ------- 116 | out: MVNormalParameters 117 | Mean and covariance of RV of dimension K computed from sigma-points 118 | """ 119 | m = mean_sigma_points(sigma_points) 120 | cov = covariance_sigma_points(sigma_points, m, sigma_points, m) 121 | return MVNormalParameters(m, cov=cov) 122 | -------------------------------------------------------------------------------- /parsmooth/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EEA-sensors/parallel-non-linear-gaussian-smoothers/4fb19a189f1338e8cec9dafd2d8088c233875b5d/parsmooth/models/__init__.py -------------------------------------------------------------------------------- /parsmooth/models/bearings.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import jax.numpy as jnp 4 | import matplotlib.pyplot as plt 5 | import numba as nb 6 | import numpy as np 7 | import scipy.linalg as linalg 8 | from jax import lax, jit 9 | 10 | __all__ = ["make_parameters", "get_data"] 11 | 12 | 13 | def _transition_function(x, dt): 14 | """ Deterministic transition function used in the state space model 15 | 16 | Parameters 17 | ---------- 18 | x: array_like 19 | The current state 20 | dt: float 21 | Time step between observations 22 | 23 | Returns 24 | ------- 25 | out: array_like 26 | The transitioned state 27 | """ 28 | w = x[-1] 29 | predicate = jnp.abs(w) < 1e-6 30 | 31 | coswt = jnp.cos(w * dt) 32 | sinwt = jnp.sin(w * dt) 33 | 34 | def true_fun(_): 35 | return coswt, 0., sinwt, dt 36 | 37 | def false_fun(_): 38 | coswto = coswt - 1 39 | return coswt, coswto / w, sinwt, sinwt / w 40 | 41 | coswt, coswtopw, sinwt, sinwtpw = lax.cond(predicate, true_fun, false_fun, None) 42 | 43 | F = jnp.array([[1, 0, sinwtpw, -coswtopw, 0], 44 | [0, 1, coswtopw, sinwtpw, 0], 45 | [0, 0, coswt, sinwt, 0], 46 | [0, 0, -sinwt, coswt, 0], 47 | [0, 0, 0, 0, 1]]) 48 | return F @ x 49 | 50 | 51 | def _observation_function(x, s1, s2): 52 | """ 53 | Returns the observed angles as function of the state and the sensors locations 54 | 55 | Parameters 56 | ---------- 57 | x: array_like 58 | The current state 59 | s1: array_like 60 | The first sensor location 61 | s2: array_like 62 | The second sensor location 63 | 64 | Returns 65 | ------- 66 | y: array_like 67 | The observed angles, the first component is the angle w.r.t. the first sensor, the second w.r.t the second. 68 | """ 69 | return jnp.array([jnp.arctan2(x[1] - s1[1], x[0] - s1[0]), 70 | jnp.arctan2(x[1] - s2[1], x[0] - s2[0])]) 71 | 72 | 73 | @partial(jnp.vectorize, excluded=(1, 2), signature="(m)->(d)") 74 | def inverse_bearings(observation, s1, s2): 75 | """ 76 | Inverse the bearings observation to the location as if there was no noise, 77 | This is only used to provide an initial point for the linearization of the IEKS and ICKS. 78 | 79 | Parameters 80 | ---------- 81 | observation: (2) array 82 | The bearings observation 83 | s1: (2) array 84 | The first sensor position 85 | s2: (2) array 86 | The second sensor position 87 | 88 | Returns 89 | ------- 90 | out: (2) array 91 | The inversed position of the state 92 | """ 93 | tan_theta = jnp.tan(observation) 94 | A = jnp.array([[tan_theta[0], -1], 95 | [tan_theta[1], -1]]) 96 | b = jnp.array([s1[0] * tan_theta[0] - s1[1], 97 | s2[0] * tan_theta[1] - s2[1]]) 98 | return jnp.linalg.solve(A, b) 99 | 100 | 101 | def make_parameters(qc, qw, r, dt, s1, s2): 102 | """ Discretizes the model with continuous transition noise qc, for step-size dt. 103 | The model is described in "Multitarget-multisensor tracking: principles and techniques" by 104 | Bar-Shalom, Yaakov and Li, Xiao-Rong 105 | 106 | Parameters 107 | ---------- 108 | qc: float 109 | Transition covariance of the continuous SSM 110 | qw: float 111 | Transition covariance of the continuous SSM 112 | r: float 113 | Observation error standard deviation 114 | dt: float 115 | Discretization time step 116 | s1: array_like 117 | The location of the first sensor 118 | s2: array_like 119 | The location of the second sensor 120 | 121 | Returns 122 | ------- 123 | Q: array_like 124 | The transition covariance matrix for the discrete SSM 125 | R: array_like 126 | The observation covariance matrix 127 | observation_function: callable 128 | The observation function 129 | transition_function: callable 130 | The transition function 131 | """ 132 | 133 | Q = jnp.array([[qc * dt ** 3 / 3, 0, qc * dt ** 2 / 2, 0, 0], 134 | [0, qc * dt ** 3 / 3, 0, qc * dt ** 2 / 2, 0], 135 | [qc * dt ** 2 / 2, 0, qc * dt, 0, 0], 136 | [0, qc * dt ** 2 / 2, 0, qc * dt, 0], 137 | [0, 0, 0, 0, dt * qw]]) 138 | 139 | R = r ** 2 * jnp.eye(2) 140 | 141 | observation_function = jit(partial(_observation_function, s1=s1, s2=s2)) 142 | transition_function = jit(partial(_transition_function, dt=dt)) 143 | 144 | return Q, R, observation_function, transition_function 145 | 146 | 147 | @nb.njit 148 | def _get_data(x, dt, a_s, s1, s2, r, normals, observations, true_states): 149 | for i, a in enumerate(a_s): 150 | with nb.objmode(x='float32[::1]'): 151 | F = np.array([[0, 0, 1, 0], 152 | [0, 0, 0, 1], 153 | [0, 0, 0, a], 154 | [0, 0, -a, 0]], dtype=np.float32) 155 | x = linalg.expm(F * dt) @ x 156 | y1 = np.arctan2(x[1] - s1[1], x[0] - s1[0]) + r * normals[i, 0] 157 | y2 = np.arctan2(x[1] - s2[1], x[0] - s2[0]) + r * normals[i, 1] 158 | 159 | observations[i] = [y1, y2] 160 | observations[i] = [y1, y2] 161 | true_states[i] = np.concatenate((x, np.array([a]))) 162 | # return true_states, observations 163 | 164 | 165 | def get_data(x0, dt, r, T, s1, s2, q=10., random_state=None): 166 | """ 167 | 168 | Parameters 169 | ---------- 170 | x0: array_like 171 | true initial state 172 | dt: float 173 | time step for observations 174 | r: float 175 | observation model standard deviation 176 | T: int 177 | number of time steps 178 | s1: array_like 179 | The location of the first sensor 180 | s2: array_like 181 | The location of the second sensor 182 | q: float 183 | noise of the angular momentum 184 | random_state: np.random.RandomState or int, optional 185 | numpy random state 186 | 187 | Returns 188 | ------- 189 | ts: array_like 190 | array of time steps 191 | true_states: array_like 192 | array of true states 193 | observations: array_like 194 | array of observations 195 | """ 196 | if random_state is None or isinstance(random_state, int): 197 | random_state = np.random.RandomState(random_state) 198 | a_s = 1 + q * dt * np.cumsum(random_state.randn(T)) 199 | a_s = a_s.astype(np.float32) 200 | s1 = np.asarray(s1, dtype=np.float32) 201 | s2 = np.asarray(s2, dtype=np.float32) 202 | 203 | x = np.copy(x0).astype(np.float32) 204 | observations = np.empty((T, 2), dtype=np.float32) 205 | true_states = np.zeros((T+1, 5), dtype=np.float32) 206 | ts = np.linspace(dt, (T + 1) * dt, T).astype(np.float32) 207 | true_states[0, :4] = x 208 | normals = random_state.randn(T, 2).astype(np.float32) 209 | 210 | _get_data(x, dt, a_s, s1, s2, r, normals, observations, true_states[1:]) 211 | return ts, true_states, observations 212 | 213 | 214 | def plot_bearings(states, labels, s1, s2, figsize=(10, 10), quiver=False): 215 | """ 216 | 217 | Parameters 218 | ---------- 219 | states: list of array_like 220 | list of states to plot 221 | labels: list of str 222 | list of lables for the states 223 | s1: array_like 224 | first sensor 225 | s2: array_like 226 | second sensor 227 | figsize: tuple of int 228 | figure size in inches 229 | quiver: bool 230 | show the velocity field 231 | 232 | Returns 233 | ------- 234 | 235 | """ 236 | fig, ax = plt.subplots(figsize=figsize) 237 | 238 | if not isinstance(states, list): 239 | states = [states] 240 | 241 | if not isinstance(labels, list): 242 | labels = [labels] 243 | 244 | for label, state in zip(labels, states): 245 | ax.plot(*state[:, :2].T, linestyle='--', label=label, alpha=0.75) 246 | if quiver: 247 | ax.quiver(*state[::10].T, units='xy', scale=4, width=0.01) 248 | ax.scatter(*s1, marker="o", s=200, label="Sensor 1", color='k') 249 | ax.scatter(*s2, marker="x", s=200, label="Sensor 2", color='k') 250 | 251 | ax.legend(loc="lower left") 252 | -------------------------------------------------------------------------------- /parsmooth/models/linear.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import jax.numpy as jnp 4 | import numpy as np 5 | from jax import jit 6 | 7 | __all__ = ["make_parameters", "get_data"] 8 | 9 | 10 | def _transition_function(x, A): 11 | """ Deterministic transition function used in the state space model 12 | 13 | Parameters 14 | ---------- 15 | x: array_like 16 | The current state 17 | A: array_like 18 | transition matrix 19 | 20 | Returns 21 | ------- 22 | out: array_like 23 | The transitioned state 24 | """ 25 | return jnp.dot(A, x) 26 | 27 | 28 | def _observation_function(x, H): 29 | """ 30 | Returns the observed angles as function of the state and the sensors locations 31 | 32 | Parameters 33 | ---------- 34 | x: array_like 35 | The current state 36 | H: array_like 37 | observation matrix 38 | 39 | Returns 40 | ------- 41 | y: array_like 42 | The observed data 43 | """ 44 | return jnp.dot(H, x) 45 | 46 | 47 | def make_parameters(r, q): 48 | A = 0.5 * jnp.eye(2) 49 | Q = q * jnp.eye(2) 50 | R = r * jnp.eye(1) 51 | H = jnp.array([[1., 0.5]]) 52 | 53 | observation_function = jit(partial(_observation_function, H=H)) 54 | transition_function = jit(partial(_transition_function, A=A)) 55 | 56 | return A, H, Q, R, observation_function, transition_function 57 | 58 | 59 | def get_data(x0, A, H, R, Q, T, random_state=None): 60 | """ 61 | 62 | Parameters 63 | ---------- 64 | x0: array_like 65 | true initial state 66 | A: array_like 67 | transition matrix 68 | H: array_like 69 | transition matrix 70 | R: array_like 71 | observation model covariance 72 | Q: array_like 73 | noise covariance 74 | s1: array_like 75 | The location of the first sensor 76 | s2: array_like 77 | The location of the second sensor 78 | random_state: np.random.RandomState or int, optional 79 | numpy random state 80 | 81 | Returns 82 | ------- 83 | ts: array_like 84 | array of time steps 85 | true_states: array_like 86 | array of true states 87 | observations: array_like 88 | array of observations 89 | """ 90 | if random_state is None or isinstance(random_state, int): 91 | random_state = np.random.RandomState(random_state) 92 | 93 | R_shape = R.shape[0] 94 | Q_shape = Q.shape[0] 95 | normals = random_state.randn(T, Q_shape + R_shape).astype(np.float32) 96 | chol_R = np.linalg.cholesky(R) 97 | chol_Q = np.linalg.cholesky(Q) 98 | 99 | x = np.copy(x0).astype(np.float32) 100 | observations = np.empty((T, R_shape), dtype=np.float32) 101 | true_states = np.empty((T, Q_shape), dtype=np.float32) 102 | 103 | for i in range(T): 104 | x = A @ x + chol_Q @ normals[i, :Q_shape] 105 | y = H @ x + chol_R @ normals[i, Q_shape:] 106 | true_states[i] = x 107 | observations[i] = y 108 | 109 | ts = np.linspace(1, T, T).astype(np.float32) 110 | 111 | return ts, true_states, observations 112 | -------------------------------------------------------------------------------- /parsmooth/parallel/__init__.py: -------------------------------------------------------------------------------- 1 | from .ckf import filter_routine as ckf 2 | from .cks import smoother_routine as cks, iterated_smoother_routine as icks 3 | from .ekf import filter_routine as ekf 4 | from .eks import smoother_routine as eks, iterated_smoother_routine as ieks 5 | -------------------------------------------------------------------------------- /parsmooth/parallel/ckf.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import jax 4 | import jax.numpy as jnp 5 | import jax.scipy.linalg as jlinalg 6 | from jax import lax, vmap 7 | 8 | from parsmooth.utils import MVNormalParameters 9 | from .operators import filtering_operator 10 | from ..cubature_common import get_sigma_points, get_mv_normal_parameters, covariance_sigma_points, SigmaPoints 11 | 12 | 13 | def make_associative_filtering_params(observation_function, Rk, transition_function, Qk_1, yk, i, initial_state, 14 | prev_linearization_state, linearization_state, propagate_first): 15 | predicate = i == 0 16 | 17 | def _first(_): 18 | return _make_associative_filtering_params_first(observation_function, Rk, transition_function, Qk_1, 19 | initial_state, prev_linearization_state, linearization_state, 20 | yk, propagate_first) 21 | 22 | def _generic(_): 23 | return _make_associative_filtering_params_generic(observation_function, Rk, transition_function, Qk_1, 24 | prev_linearization_state, linearization_state, yk) 25 | 26 | return lax.cond(predicate, 27 | _first, # take initial 28 | _generic, # take generic 29 | None) 30 | 31 | 32 | def _make_associative_filtering_params_first(observation_function, R, transition_function, Q, initial_state, 33 | prev_linearization_state, linearization_state, y, propagate_first): 34 | # Prediction part 35 | 36 | if propagate_first: 37 | initial_sigma_points = get_sigma_points(prev_linearization_state) 38 | propagated_points = transition_function(initial_sigma_points.points) 39 | propagated_sigma_points = SigmaPoints(propagated_points, initial_sigma_points.wm, initial_sigma_points.wc) 40 | propagated_state = get_mv_normal_parameters(propagated_sigma_points) 41 | 42 | pred_cross_covariance = covariance_sigma_points(initial_sigma_points, prev_linearization_state.mean, 43 | propagated_sigma_points, 44 | propagated_state.mean) 45 | 46 | F = jlinalg.solve(prev_linearization_state.cov, pred_cross_covariance, 47 | assume_a="pos").T # Linearized transition function 48 | 49 | m = propagated_state.mean + F @ (initial_state.mean - prev_linearization_state.mean) 50 | P = propagated_state.cov + Q + F @ (initial_state.cov - prev_linearization_state.cov) @ F.T 51 | linearization_points = get_sigma_points(linearization_state) 52 | obs_points = observation_function(linearization_points.points) 53 | obs_sigma_points = SigmaPoints(obs_points, linearization_points.wm, linearization_points.wc) 54 | obs_mvn = get_mv_normal_parameters(obs_sigma_points) 55 | update_cross_covariance = covariance_sigma_points(linearization_points, linearization_state.mean, 56 | obs_sigma_points, obs_mvn.mean) 57 | 58 | H = jlinalg.solve(linearization_state.cov, update_cross_covariance, assume_a="pos").T 59 | d = obs_mvn.mean - jnp.dot(H, linearization_state.mean) 60 | predicted_observation = H @ m + d 61 | 62 | S = H @ (P - linearization_state.cov) @ H.T + R + obs_mvn.cov 63 | else: 64 | m = initial_state.mean 65 | P = initial_state.cov 66 | linearization_points = get_sigma_points(prev_linearization_state) 67 | obs_points = observation_function(linearization_points.points) 68 | obs_sigma_points = SigmaPoints(obs_points, linearization_points.wm, linearization_points.wc) 69 | obs_mvn = get_mv_normal_parameters(obs_sigma_points) 70 | update_cross_covariance = covariance_sigma_points(linearization_points, linearization_state.mean, 71 | obs_sigma_points, obs_mvn.mean) 72 | 73 | H = jlinalg.solve(prev_linearization_state.cov, update_cross_covariance, assume_a="pos").T 74 | d = obs_mvn.mean - jnp.dot(H, prev_linearization_state.mean) 75 | predicted_observation = H @ m + d 76 | 77 | S = H @ (P - prev_linearization_state.cov) @ H.T + R + obs_mvn.cov 78 | 79 | K = jlinalg.solve(S, H @ P, assume_a="pos").T 80 | A = jnp.zeros_like(initial_state.cov) 81 | b = m + K @ (y - predicted_observation) 82 | C = P - K @ S @ K.T 83 | 84 | eta = jnp.zeros_like(initial_state.mean) 85 | J = jnp.zeros_like(initial_state.cov) 86 | 87 | return A, b, 0.5 * (C + C.T), eta, 0.5 * (J + J.T) 88 | 89 | 90 | def _make_associative_filtering_params_generic(observation_function, Rk, transition_function, Qk_1, 91 | prev_linearization_state, linearization_state, yk): 92 | # Prediction part 93 | sigma_points = get_sigma_points(prev_linearization_state) 94 | 95 | propagated_points = transition_function(sigma_points.points) 96 | propagated_sigma_points = SigmaPoints(propagated_points, sigma_points.wm, sigma_points.wc) 97 | propagated_state = get_mv_normal_parameters(propagated_sigma_points) 98 | 99 | pred_cross_covariance = covariance_sigma_points(sigma_points, prev_linearization_state.mean, 100 | propagated_sigma_points, 101 | propagated_state.mean) 102 | 103 | F = jlinalg.solve(prev_linearization_state.cov, pred_cross_covariance, 104 | assume_a="pos").T # Linearized transition function 105 | pred_mean_residual = propagated_state.mean - F @ prev_linearization_state.mean 106 | pred_cov_residual = propagated_state.cov - F @ prev_linearization_state.cov @ F.T + Qk_1 107 | 108 | # Update part 109 | linearization_points = get_sigma_points(linearization_state) 110 | obs_points = observation_function(linearization_points.points) 111 | obs_sigma_points = SigmaPoints(obs_points, linearization_points.wm, linearization_points.wc) 112 | obs_mvn = get_mv_normal_parameters(obs_sigma_points) 113 | update_cross_covariance = covariance_sigma_points(linearization_points, 114 | linearization_state.mean, 115 | obs_sigma_points, 116 | obs_mvn.mean) 117 | 118 | H = jlinalg.solve(linearization_state.cov, update_cross_covariance, assume_a="pos").T 119 | obs_mean_residual = obs_mvn.mean - jnp.dot(H, linearization_state.mean) 120 | obs_cov_residual = obs_mvn.cov - H @ linearization_state.cov @ H.T 121 | 122 | S = H @ pred_cov_residual @ H.T + Rk + obs_cov_residual # total residual covariance 123 | total_obs_residual = (yk - H @ pred_mean_residual - obs_mean_residual) 124 | S_invH = jlinalg.solve(S, H, assume_a="pos") 125 | 126 | K = (S_invH @ pred_cov_residual).T 127 | A = F - K @ H @ F 128 | b = pred_mean_residual + K @ total_obs_residual 129 | C = pred_cov_residual - K @ S @ K.T 130 | 131 | temp = (S_invH @ F).T 132 | HF = H @ F 133 | 134 | eta = temp @ total_obs_residual 135 | J = temp @ HF 136 | return A, b, 0.5 * (C + C.T), eta, 0.5 * (J + J.T) 137 | 138 | 139 | def filter_routine(initial_state: MVNormalParameters, 140 | observations: jnp.ndarray, 141 | transition_function: Callable, 142 | transition_covariance: jnp.ndarray, 143 | observation_function: Callable, 144 | observation_covariance: jnp.ndarray, 145 | linearization_states: MVNormalParameters = None, 146 | propagate_first: bool = True): 147 | """ Computes the predict-update routine of the Cubature Kalman Filter equations 148 | using temporal parallelization and returns a series of filtered_states TODO:reference 149 | 150 | Parameters 151 | ---------- 152 | initial_state: MVNormalParameters 153 | prior belief on the initial state distribution 154 | observations: (n, K) array 155 | array of n observations of dimension K 156 | transition_function: callable 157 | transition function of the state space model 158 | transition_covariance: (D, D) array 159 | transition covariance for each time step 160 | observation_function: callable 161 | observation function of the state space model 162 | observation_covariance: (K, K) array 163 | observation error covariances for each time step 164 | linearization_states: MVNormalParameters, optional 165 | in the case of Sigma-Point . 166 | propagate_first: bool, optional 167 | Is the first step a transition or an update? i.e. False if the initial time step has 168 | an associated observation. Default is True. 169 | 170 | Returns 171 | ------- 172 | filtered_states: MVNormalParameters 173 | list of filtered states 174 | 175 | """ 176 | n_observations = observations.shape[0] 177 | x_dim = initial_state.mean.shape[0] 178 | dtype = initial_state.mean.dtype 179 | 180 | if linearization_states is not None: 181 | if propagate_first: 182 | x_k_1_s = jax.tree_map(lambda z: z[:-1], linearization_states) 183 | x_k_s = jax.tree_map(lambda z: z[1:], linearization_states) 184 | else: 185 | x_k_1_s = jax.tree_map(lambda z: jnp.concatenate([z[None, 0], z[:-1]], 0), linearization_states) 186 | x_k_s = linearization_states 187 | else: 188 | 189 | m_k_s = jnp.zeros((n_observations, x_dim), dtype=dtype) 190 | P_k_s = jnp.repeat(jnp.eye(x_dim)[None, ...], n_observations, axis=0) 191 | x_k_1_s = x_k_s = MVNormalParameters(m_k_s, P_k_s) 192 | 193 | @vmap 194 | def make_params(obs, i, prev_linearization_state, linearisation_state): 195 | return make_associative_filtering_params(observation_function, observation_covariance, transition_function, 196 | transition_covariance, obs, i, initial_state, 197 | prev_linearization_state, linearisation_state, propagate_first) 198 | 199 | As, bs, Cs, etas, Js = make_params(observations, jnp.arange(n_observations), x_k_1_s, 200 | x_k_s) 201 | _, filtered_means, filtered_covariances, _, _ = lax.associative_scan(filtering_operator, (As, bs, Cs, etas, Js)) 202 | 203 | filtered_states = MVNormalParameters(filtered_means, filtered_covariances) 204 | if propagate_first: 205 | filtered_states = jax.tree_map(lambda x, y: jnp.concatenate([x[None, ...], y], 0), 206 | initial_state, filtered_states) 207 | return filtered_states 208 | -------------------------------------------------------------------------------- /parsmooth/parallel/cks.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import jax.numpy as jnp 4 | import jax.scipy.linalg as jlinalg 5 | from jax import lax, vmap 6 | 7 | from parsmooth.parallel.ckf import filter_routine 8 | from parsmooth.utils import MVNormalParameters 9 | from .operators import smoothing_operator 10 | from ..cubature_common import get_sigma_points, SigmaPoints, get_mv_normal_parameters, covariance_sigma_points 11 | 12 | 13 | def make_associative_smoothing_params(transition_function, Qk, i, n, filtered_state, linearization_state): 14 | predicate = i == n - 1 15 | 16 | def _last(_): 17 | return filtered_state.mean, jnp.zeros_like(filtered_state.cov), filtered_state.cov 18 | 19 | def _generic(_): 20 | return _make_associative_smoothing_params_generic(transition_function, Qk, filtered_state, linearization_state) 21 | 22 | return lax.cond(predicate, 23 | _last, # take initial 24 | _generic, # take generic 25 | None) 26 | 27 | 28 | def _make_associative_smoothing_params_generic(transition_function, Qk, filtered_state, linearization_state): 29 | # Prediction part 30 | sigma_points = get_sigma_points(linearization_state) 31 | 32 | propagated_points = transition_function(sigma_points.points) 33 | propagated_sigma_points = SigmaPoints(propagated_points, sigma_points.wm, sigma_points.wc) 34 | propagated_state = get_mv_normal_parameters(propagated_sigma_points) 35 | 36 | pred_cross_covariance = covariance_sigma_points(sigma_points, 37 | linearization_state.mean, 38 | propagated_sigma_points, 39 | propagated_state.mean) 40 | 41 | F = jlinalg.solve(linearization_state.cov, pred_cross_covariance, 42 | assume_a="pos").T # Linearized transition function 43 | 44 | Pp = Qk + propagated_state.cov + F @ (filtered_state.cov - linearization_state.cov) @ F.T 45 | 46 | E = jlinalg.solve(Pp, F @ filtered_state.cov, assume_a="pos").T 47 | 48 | g = filtered_state.mean - E @ (propagated_state.mean + F @ (filtered_state.mean - linearization_state.mean)) 49 | L = filtered_state.cov - E @ F @ filtered_state.cov 50 | 51 | return g, E, 0.5 * (L + L.T) 52 | 53 | 54 | def smoother_routine(transition_function: Callable, 55 | transition_covariance: jnp.ndarray, 56 | filtered_states: MVNormalParameters, 57 | linearization_states: MVNormalParameters = None): 58 | """ Computes the predict-update routine of the Extended Kalman Filter equations 59 | using temporal parallelization and returns a series of filtered_states TODO:reference 60 | 61 | Parameters 62 | ---------- 63 | transition_function: callable 64 | transition function of the state space model 65 | transition_covariance: (D, D) array 66 | transition covariance for each time step 67 | observation error covariances for each time step 68 | filtered_states: MVNormalParameters 69 | states resulting from (iterated) EKF 70 | linearization_states: MVNormalParameters, optional 71 | states at which to compute the cubature linearized functions 72 | 73 | Returns 74 | ------- 75 | filtered_states: MVNormalParameters 76 | list of filtered states 77 | 78 | """ 79 | n_observations = filtered_states.mean.shape[0] 80 | 81 | @vmap 82 | def make_params(i, filtered_state, linearization_state): 83 | if linearization_state is None: 84 | linearization_state = filtered_state 85 | return make_associative_smoothing_params(transition_function, transition_covariance, 86 | i, n_observations, filtered_state, linearization_state) 87 | 88 | gs, Es, Ls = make_params(jnp.arange(n_observations), filtered_states, linearization_states) 89 | 90 | smoothed_means, _, smoothed_covariances = lax.associative_scan(smoothing_operator, (gs, Es, Ls), reverse=True) 91 | 92 | return vmap(MVNormalParameters)(smoothed_means, smoothed_covariances) 93 | 94 | 95 | def iterated_smoother_routine(initial_state: MVNormalParameters, 96 | observations: jnp.ndarray, 97 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 98 | transition_covariance: jnp.ndarray, 99 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 100 | observation_covariance: jnp.ndarray, 101 | initial_linearization_states: MVNormalParameters = None, 102 | n_iter: int = 100, 103 | propagate_first: bool = True): 104 | """ 105 | Computes the Gauss-Newton iterated cubature Kalman smoother 106 | 107 | Parameters 108 | ---------- 109 | initial_state: MVNormalParameters 110 | prior belief on the initial state distribution 111 | observations: (n, K) array 112 | array of n observations of dimension K 113 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 114 | transition function of the state space model 115 | transition_covariance: (D, D) array 116 | transition covariances for each time step, if passed only one, it is repeated n times 117 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 118 | observation function of the state space model 119 | observation_covariance: (K, K) array 120 | observation error covariances for each time step, if passed only one, it is repeated n times 121 | initial_linearization_states: MVNormalParameters, optional 122 | points at which to compute the jacobians durning the first pass. 123 | n_iter: int 124 | number of times the filter-smoother routine is computed 125 | propagate_first: bool, optional 126 | Is the first step a transition or an update? i.e. False if the initial time step has 127 | an associated observation. Default is True. 128 | Returns 129 | ------- 130 | iterated_smoothed_trajectories: MVNormalParameters 131 | The result of the smoothing routine 132 | 133 | """ 134 | 135 | def body(linearization_points, _): 136 | filtered_states = filter_routine(initial_state, observations, transition_function, transition_covariance, 137 | observation_function, observation_covariance, linearization_points, 138 | propagate_first) 139 | return smoother_routine(transition_function, transition_covariance, filtered_states, 140 | linearization_points), None 141 | 142 | if initial_linearization_states is None: 143 | initial_linearization_states = body(None, None)[0] 144 | 145 | iterated_smoothed_trajectories, _ = lax.scan(body, initial_linearization_states, jnp.arange(n_iter)) 146 | return iterated_smoothed_trajectories 147 | -------------------------------------------------------------------------------- /parsmooth/parallel/ekf.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import jax 4 | import jax.numpy as jnp 5 | import jax.scipy.linalg as jlinalg 6 | from jax import lax, vmap, jacfwd 7 | 8 | from parsmooth.utils import MVNormalParameters 9 | from .operators import filtering_operator 10 | 11 | 12 | def make_associative_filtering_params(observation_function, Rk, transition_function, Qk_1, yk, i, m0, P0, x_k_1, x_k, 13 | propagate_first): 14 | predicate = i == 0 15 | 16 | jac_obs = jacfwd(observation_function, 0) 17 | jac_trans = jacfwd(transition_function, 0) 18 | 19 | def _first(_): 20 | return _make_associative_filtering_params_first(observation_function, jac_obs, Rk, transition_function, 21 | jac_trans, Qk_1, m0, P0, x_k_1, x_k, yk, propagate_first) 22 | 23 | def _generic(_): 24 | return _make_associative_filtering_params_generic(observation_function, jac_obs, Rk, transition_function, 25 | jac_trans, x_k_1, x_k, Qk_1, yk) 26 | 27 | return lax.cond(predicate, 28 | _first, # take initial 29 | _generic, # take generic 30 | None) 31 | 32 | 33 | def _make_associative_filtering_params_first(observation_function, jac_observation_function, R, transition_function, 34 | jac_transition_function, Q, m0, P0, x_k_1, x_k, y, propagate_first): 35 | if propagate_first: 36 | F = jac_transition_function(x_k_1) 37 | m = F @ (m0 - x_k_1) + transition_function(x_k_1) 38 | P = F @ P0 @ F.T + Q 39 | H = jac_observation_function(x_k) 40 | alpha = observation_function(x_k) + H @ (m - x_k) 41 | else: 42 | P = P0 43 | m = m0 44 | H = jac_observation_function(x_k_1) 45 | alpha = observation_function(x_k_1) + H @ (m0 - x_k_1) 46 | 47 | S = H @ P @ H.T + R 48 | K = jlinalg.solve(S, H @ P, assume_a="pos").T 49 | A = jnp.zeros_like(P0) 50 | 51 | b = m + K @ (y - alpha) 52 | C = P - (K @ S @ K.T) 53 | 54 | eta = jnp.zeros_like(m0) 55 | J = jnp.zeros_like(P0) 56 | 57 | return A, b, C, eta, J 58 | 59 | 60 | def _make_associative_filtering_params_generic(observation_function, jac_observation_function, Rk, transition_function, 61 | jac_transition_function, x_k_1, x_k, Qk_1, yk): 62 | F = jac_transition_function(x_k_1) 63 | H = jac_observation_function(x_k) 64 | 65 | F_x_k_1 = F @ x_k_1 66 | x_k_hat = transition_function(x_k_1) 67 | 68 | alpha = observation_function(x_k) + H @ (x_k_hat - F_x_k_1 - x_k) 69 | residual = yk - alpha 70 | HQ = H @ Qk_1 71 | 72 | S = HQ @ H.T + Rk 73 | S_invH = jlinalg.solve(S, H, assume_a="pos") 74 | K = (S_invH @ Qk_1).T 75 | A = F - K @ H @ F 76 | b = K @ residual + x_k_hat - F_x_k_1 77 | C = Qk_1 - K @ H @ Qk_1 78 | 79 | HF = H @ F 80 | 81 | temp = (S_invH @ F).T 82 | eta = temp @ residual 83 | J = temp @ HF 84 | 85 | return A, b, C, eta, J 86 | 87 | 88 | def filter_routine(initial_state: MVNormalParameters, 89 | observations: jnp.ndarray, 90 | transition_function: Callable, 91 | transition_covariance: jnp.ndarray, 92 | observation_function: Callable, 93 | observation_covariance: jnp.ndarray, 94 | linearization_points: jnp.ndarray = None, 95 | propagate_first: bool = True 96 | ): 97 | """ Computes the predict-update routine of the Extended Kalman Filter equations 98 | using temporal parallelization and returns a series of filtered_states TODO:reference 99 | 100 | Parameters 101 | ---------- 102 | initial_state: MVNormalParameters 103 | prior belief on the initial state distribution 104 | observations: (n, K) array 105 | array of n observations of dimension K 106 | transition_function: callable 107 | transition function of the state space model 108 | transition_covariance: (D, D) array 109 | transition covariance for each time step 110 | observation_function: callable 111 | observation function of the state space model 112 | observation_covariance: (K, K) array 113 | observation error covariances for each time step 114 | linearization_points: (n, D) array, optional 115 | points at which to compute the jacobians. 116 | propagate_first: bool, optional 117 | Is the first step a transition or an update? i.e. False if the initial time step has 118 | an associated observation. Default is True. 119 | Returns 120 | ------- 121 | filtered_states: MVNormalParameters 122 | list of filtered states 123 | 124 | """ 125 | n_observations = observations.shape[0] 126 | x_dim = initial_state.mean.shape[0] 127 | dtype = initial_state.mean.dtype 128 | 129 | @vmap 130 | def make_params(obs, i, x_k_1, x_k): 131 | return make_associative_filtering_params(observation_function, observation_covariance, 132 | transition_function, transition_covariance, obs, 133 | i, initial_state.mean, 134 | initial_state.cov, x_k_1, x_k, propagate_first) 135 | 136 | if linearization_points is not None: 137 | if propagate_first: 138 | x_k_1_s = linearization_points[:-1] 139 | x_k_s = linearization_points[1:] 140 | else: 141 | x_k_1_s = jnp.concatenate([linearization_points[None, 0], linearization_points[:-1]]) 142 | x_k_s = linearization_points 143 | else: 144 | x_k_1_s = x_k_s = jnp.zeros((n_observations, x_dim), dtype=dtype) 145 | 146 | As, bs, Cs, etas, Js = make_params(observations, jnp.arange(n_observations), x_k_1_s, x_k_s) 147 | _, filtered_means, filtered_covariances, _, _ = lax.associative_scan(filtering_operator, (As, bs, Cs, etas, Js)) 148 | filtered_states = MVNormalParameters(filtered_means, filtered_covariances) 149 | if propagate_first: 150 | filtered_states = jax.tree_map(lambda x, y: jnp.concatenate([x[None, ...], y], 0), 151 | initial_state, filtered_states) 152 | return filtered_states 153 | -------------------------------------------------------------------------------- /parsmooth/parallel/eks.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import jax.numpy as jnp 4 | import jax.scipy.linalg as jlinalg 5 | from jax import lax, vmap, jacfwd 6 | 7 | from parsmooth.utils import MVNormalParameters 8 | from .ekf import filter_routine 9 | from .operators import smoothing_operator 10 | 11 | 12 | def make_associative_smoothing_params(transition_function, Qk, i, n, mk, Pk, xk): 13 | predicate = i == n - 1 14 | 15 | jac_trans = jacfwd(transition_function, 0) 16 | 17 | def _last(_): 18 | return mk, jnp.zeros_like(Pk), Pk 19 | 20 | def _generic(_): 21 | return _make_associative_smoothing_params_generic(transition_function, jac_trans, Qk, mk, Pk, xk) 22 | 23 | return lax.cond(predicate, 24 | _last, # take initial 25 | _generic, # take generic 26 | None) 27 | 28 | 29 | def _make_associative_smoothing_params_generic(transition_function, jac_transition_function, Qk, mk, Pk, xk): 30 | F = jac_transition_function(xk) 31 | Pp = F @ Pk @ F.T + Qk 32 | 33 | E = jlinalg.solve(Pp, F @ Pk, assume_a="pos").T 34 | 35 | g = mk - E @ (transition_function(xk) + F @ (mk - xk)) 36 | L = Pk - E @ Pp @ E.T 37 | 38 | return g, E, L 39 | 40 | 41 | def smoother_routine(transition_function: Callable, 42 | transition_covariance: jnp.ndarray, 43 | filtered_states: MVNormalParameters, 44 | linearisation_points: jnp.ndarray = None): 45 | """ Computes the predict-update routine of the Extended Kalman Filter equations 46 | using temporal parallelization and returns a series of filtered_states TODO:reference 47 | 48 | Parameters 49 | ---------- 50 | transition_function: callable 51 | transition function of the state space model 52 | transition_covariance: (D, D) array 53 | transition covariance for each time step 54 | observation error covariances for each time step 55 | filtered_states: MVNormalParameters 56 | states resulting from (iterated) EKF 57 | linearisation_points: (n, D) array, optional 58 | points at which to compute the jacobians, typically previous run. 59 | 60 | Returns 61 | ------- 62 | filtered_states: MVNormalParameters 63 | list of filtered states 64 | 65 | """ 66 | n_observations = filtered_states.mean.shape[0] 67 | 68 | if linearisation_points is None: 69 | linearisation_points = filtered_states.mean 70 | 71 | @vmap 72 | def make_params(i, mk, Pk, xk): 73 | return make_associative_smoothing_params(transition_function, transition_covariance, 74 | i, n_observations, mk, Pk, xk) 75 | 76 | gs, Es, Ls = make_params(jnp.arange(n_observations), filtered_states.mean, 77 | filtered_states.cov, linearisation_points) 78 | 79 | smoothed_means, _, smoothed_covariances = lax.associative_scan(smoothing_operator, (gs, Es, Ls), reverse=True) 80 | return MVNormalParameters(smoothed_means, smoothed_covariances) 81 | 82 | 83 | def iterated_smoother_routine(initial_state: MVNormalParameters, 84 | observations: jnp.ndarray, 85 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 86 | transition_covariance: jnp.ndarray, 87 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 88 | observation_covariance: jnp.ndarray, 89 | initial_linearization_points: jnp.ndarray = None, 90 | n_iter: int = 100, 91 | propagate_first: bool = True): 92 | """ 93 | Computes the Gauss-Newton iterated extended Kalman smoother 94 | 95 | Parameters 96 | ---------- 97 | initial_state: MVNormalParameters 98 | prior belief on the initial state distribution 99 | observations: (n, K) array 100 | array of n observations of dimension K 101 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 102 | transition function of the state space model 103 | transition_covariance: (D, D) array 104 | transition covariances for each time step, if passed only one, it is repeated n times 105 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 106 | observation function of the state space model 107 | observation_covariance: (K, K) array 108 | observation error covariances for each time step, if passed only one, it is repeated n times 109 | initial_linearization_points: (N, D) array, optional 110 | points at which to compute the jacobians durning the first pass. 111 | n_iter: int 112 | number of times the filter-smoother routine is computed 113 | propagate_first: bool, optional 114 | Is the first step a transition or an update? i.e. False if the initial time step has 115 | an associated observation. Default is True. 116 | Returns 117 | ------- 118 | iterated_smoothed_trajectories: MVNormalParameters 119 | The result of the smoothing routine 120 | 121 | """ 122 | 123 | def body(linearization_points, _): 124 | if linearization_points is not None: 125 | linearization_points = linearization_points.mean if isinstance(linearization_points, 126 | MVNormalParameters) else linearization_points 127 | filtered_states = filter_routine(initial_state, observations, transition_function, transition_covariance, 128 | observation_function, observation_covariance, linearization_points, 129 | propagate_first) 130 | return smoother_routine(transition_function, transition_covariance, filtered_states, 131 | linearization_points), None 132 | 133 | if initial_linearization_points is None: 134 | initial_linearization_points = body(None, None)[0] 135 | 136 | iterated_smoothed_trajectories, _ = lax.scan(body, initial_linearization_points, jnp.arange(n_iter)) 137 | return iterated_smoothed_trajectories 138 | -------------------------------------------------------------------------------- /parsmooth/parallel/operators.py: -------------------------------------------------------------------------------- 1 | import jax.numpy as jnp 2 | import jax.scipy.linalg as jlinalg 3 | from jax import vmap 4 | 5 | 6 | @vmap 7 | def filtering_operator(elem1, elem2): 8 | """ 9 | Associative operator described in TODO: put the reference 10 | 11 | Parameters 12 | ---------- 13 | elem1: tuple of array 14 | a_i, b_i, C_i, eta_i, J_i 15 | elem2: tuple of array 16 | a_j, b_j, C_j, eta_j, J_j 17 | 18 | Returns 19 | ------- 20 | 21 | """ 22 | A1, b1, C1, eta1, J1 = elem1 23 | A2, b2, C2, eta2, J2 = elem2 24 | dim = b1.shape[0] 25 | 26 | I_dim = jnp.eye(dim) 27 | 28 | IpCJ = I_dim + jnp.dot(C1, J2) 29 | IpJC = I_dim + jnp.dot(J2, C1) 30 | 31 | AIpCJ_inv = jlinalg.solve(IpCJ.T, A2.T, assume_a="gen").T 32 | AIpJC_inv = jlinalg.solve(IpJC.T, A1, assume_a="gen").T 33 | 34 | A = jnp.dot(AIpCJ_inv, A1) 35 | b = jnp.dot(AIpCJ_inv, b1 + jnp.dot(C1, eta2)) + b2 36 | C = jnp.dot(AIpCJ_inv, jnp.dot(C1, A2.T)) + C2 37 | eta = jnp.dot(AIpJC_inv, eta2 - jnp.dot(J2, b1)) + eta1 38 | J = jnp.dot(AIpJC_inv, jnp.dot(J2, A1)) + J1 39 | return A, b, 0.5 * (C + C.T), eta, 0.5 * (J + J.T) 40 | 41 | 42 | @vmap 43 | def smoothing_operator(elem1, elem2): 44 | """ 45 | Associative operator described in TODO: put the reference 46 | 47 | Parameters 48 | ---------- 49 | elem1: tuple of array 50 | g_i, E_i, L_i 51 | elem2: tuple of array 52 | g_j, E_j, L_j 53 | 54 | Returns 55 | ------- 56 | 57 | """ 58 | g1, E1, L1 = elem1 59 | g2, E2, L2 = elem2 60 | 61 | g = E2 @ g1 + g2 62 | E = E2 @ E1 63 | L = E2 @ L1 @ E2.T + L2 64 | return g, E, 0.5 * (L + L.T) 65 | -------------------------------------------------------------------------------- /parsmooth/sequential/__init__.py: -------------------------------------------------------------------------------- 1 | from .cubature import filter_routine as ckf, smoother_routine as cks, iterated_smoother_routine as icks 2 | from .extended import filter_routine as ekf, smoother_routine as eks, iterated_smoother_routine as ieks 3 | -------------------------------------------------------------------------------- /parsmooth/sequential/cubature.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Callable 2 | 3 | import jax 4 | import jax.numpy as jnp 5 | import jax.scipy.linalg as jlinalg 6 | from jax import lax 7 | from jax.scipy.stats import multivariate_normal 8 | 9 | from ..cubature_common import SigmaPoints, get_sigma_points, get_mv_normal_parameters, covariance_sigma_points 10 | from ..utils import MVNormalParameters, make_matrices_parameters 11 | 12 | 13 | def predict(transition_function: Callable, 14 | transition_covariance: jnp.ndarray, 15 | previous_state: MVNormalParameters, 16 | linearization_state: MVNormalParameters, 17 | return_linearized_transition: bool = False 18 | ) -> MVNormalParameters: 19 | """ Computes the cubature Kalman filter linearization of :math:`x_{t+1} = f(x_t, \mathcal{N}(0, \Sigma))` 20 | 21 | Parameters 22 | ---------- 23 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 24 | transition function of the state space model 25 | transition_covariance: (D,D) array 26 | covariance :math:`\Sigma` of the noise fed to transition_function 27 | previous_state: MVNormalParameters 28 | previous state for the filter x 29 | linearization_state: MVNormalParameters 30 | state for the linearization of the prediction 31 | return_linearized_transition: bool, optional 32 | Returns the linearized transition matrix A 33 | 34 | Returns 35 | ------- 36 | mvn_parameters: MVNormalParameters 37 | Propagated approximate Normal distribution 38 | 39 | F: array_like 40 | returned if return_linearized_transition is True 41 | """ 42 | if linearization_state is None: 43 | linearization_state = previous_state 44 | 45 | sigma_points = get_sigma_points(linearization_state) 46 | propagated_points = transition_function(sigma_points.points) 47 | propagated_sigma_points = SigmaPoints(propagated_points, 48 | sigma_points.wm, 49 | sigma_points.wc) 50 | 51 | propagated_state = get_mv_normal_parameters(propagated_sigma_points) 52 | cross_covariance = covariance_sigma_points(sigma_points, linearization_state.mean, propagated_sigma_points, 53 | propagated_state.mean) 54 | 55 | F = jlinalg.solve(linearization_state.cov, cross_covariance, assume_a="pos").T # Linearized transition function 56 | b = propagated_state.mean - jnp.dot(F, linearization_state.mean) # Linearized offset 57 | 58 | mean = F @ previous_state.mean + b 59 | cov = transition_covariance + propagated_state.cov + F @ (previous_state.cov - linearization_state.cov) @ F.T 60 | if return_linearized_transition: 61 | return MVNormalParameters(mean, cov), F 62 | return MVNormalParameters(mean, 0.5 * (cov + cov.T)) 63 | 64 | 65 | def update(observation_function: Callable, 66 | observation_covariance: jnp.ndarray, 67 | predicted_state: MVNormalParameters, 68 | observation: jnp.ndarray, 69 | linearization_state: MVNormalParameters) -> MVNormalParameters: 70 | """ Computes the extended kalman filter linearization of :math:`x_t \mid y_t` 71 | 72 | Parameters 73 | ---------- 74 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 75 | observation function of the state space model 76 | observation_covariance: (K,K) array 77 | observation_error :math:`\Sigma` fed to observation_function 78 | predicted_state: MVNormalParameters 79 | predicted approximate mv normal parameters of the filter :math:`x` 80 | observation: (K) array 81 | Observation :math:`y` 82 | linearization_state: MVNormalParameters 83 | state for the linearization of the update 84 | 85 | Returns 86 | ------- 87 | updated_mvn_parameters: MVNormalParameters 88 | filtered state 89 | """ 90 | if linearization_state is None: 91 | linearization_state = predicted_state 92 | sigma_points = get_sigma_points(linearization_state) 93 | obs_points = observation_function(sigma_points.points) 94 | obs_sigma_points = SigmaPoints(obs_points, sigma_points.wm, sigma_points.wc) 95 | 96 | obs_state = get_mv_normal_parameters(obs_sigma_points) 97 | cross_covariance = covariance_sigma_points(sigma_points, linearization_state.mean, obs_sigma_points, 98 | obs_state.mean) 99 | 100 | H = jlinalg.solve(linearization_state.cov, cross_covariance, assume_a="pos").T # linearized observation function 101 | 102 | d = obs_state.mean - jnp.dot(H, linearization_state.mean) # linearized observation offset 103 | 104 | residual_cov = H @ (predicted_state.cov - linearization_state.cov) @ H.T + \ 105 | observation_covariance + obs_state.cov 106 | 107 | gain = jlinalg.solve(residual_cov, H @ predicted_state.cov).T 108 | 109 | predicted_observation = H @ predicted_state.mean + d 110 | 111 | residual = observation - predicted_observation 112 | mean = predicted_state.mean + gain @ residual 113 | cov = predicted_state.cov - gain @ residual_cov @ gain.T 114 | loglikelihood = multivariate_normal.logpdf(residual, jnp.zeros_like(residual), residual_cov) 115 | 116 | return loglikelihood, MVNormalParameters(mean, 0.5 * (cov + cov.T)) 117 | 118 | 119 | def filter_routine(initial_state: MVNormalParameters, 120 | observations: jnp.ndarray, 121 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 122 | transition_covariances: jnp.ndarray, 123 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 124 | observation_covariances: jnp.ndarray, 125 | linearization_states: MVNormalParameters = None, 126 | propagate_first: bool = True) -> Tuple[float, MVNormalParameters]: 127 | """ Computes the predict-update routine of the cubature Kalman Filter equations and returns a series of filtered_states 128 | 129 | Parameters 130 | ---------- 131 | initial_state: MVNormalParameters 132 | prior belief on the initial state distribution 133 | observations: (n, K) array 134 | array of n observations of dimension K 135 | transition_function: callable :math:`f(x_t, \epsilon_t) \mapsto x_{t-1}` 136 | transition function of the state space model 137 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 138 | transition covariances for each time step, if passed only one, it is repeated n times 139 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 140 | observation function of the state space model 141 | observation_covariances: (K, K) or (1, K, K) or (n, K, K) array 142 | observation error covariances for each time step, if passed only one, it is repeated n times 143 | linearization_states: MVNormalParameters, optional 144 | states for the cubature linearization 145 | propagate_first: bool, optional 146 | Is the first step a transition or an update? i.e. False if the initial time step has 147 | an associated observation. Default is True. 148 | 149 | Returns 150 | ------- 151 | loglikelihood: float 152 | Marginal loglikelihood of the observations given the parameters 153 | filtered_states: MVNormalParameters 154 | list of filtered states 155 | """ 156 | 157 | n_observations = observations.shape[0] 158 | 159 | transition_covariances, observation_covariances = list(map( 160 | lambda z: make_matrices_parameters(z, n_observations), 161 | [transition_covariances, 162 | observation_covariances])) 163 | 164 | def prop_first_body(carry, inputs): 165 | running_ell, state, prev_linearization_state = carry 166 | observation, transition_covariance, observation_covariance, linearization_state = inputs 167 | predicted_state = predict(transition_function, transition_covariance, state, prev_linearization_state) 168 | loglikelihood, updated_state = update(observation_function, observation_covariance, predicted_state, 169 | observation, linearization_state) 170 | 171 | return (running_ell + loglikelihood, updated_state, linearization_state), updated_state 172 | 173 | def update_first_body(carry, inputs): 174 | running_ell, state, _ = carry 175 | observation, transition_covariance, observation_covariance, linearization_point = inputs 176 | loglikelihood, updated_state = update(observation_function, observation_covariance, state, 177 | observation, linearization_point) 178 | predicted_state = predict(transition_function, transition_covariance, updated_state, linearization_point) 179 | return (running_ell + loglikelihood, predicted_state, linearization_point), updated_state 180 | 181 | body = prop_first_body if propagate_first else update_first_body 182 | 183 | initial_linearization_state = jax.tree_map(lambda z: z[0], linearization_states) 184 | if propagate_first: 185 | linearization_states = jax.tree_map(lambda z: z[1:], linearization_states) 186 | 187 | (ell, *_), filtered_states = lax.scan(body, 188 | (0., initial_state, initial_linearization_state), 189 | [observations, 190 | transition_covariances, 191 | observation_covariances, 192 | linearization_states], 193 | length=n_observations) 194 | 195 | if propagate_first: 196 | filtered_states = jax.tree_map(lambda y, z: jnp.concatenate([y[None, ...], z], 0), initial_state, 197 | filtered_states) 198 | return ell, filtered_states 199 | 200 | 201 | def smooth(transition_function: Callable[[jnp.ndarray], jnp.ndarray], 202 | transition_covariance: jnp.array, 203 | filtered_state: MVNormalParameters, 204 | previous_smoothed: MVNormalParameters, 205 | linearization_state: MVNormalParameters) -> MVNormalParameters: 206 | """ 207 | One step cubature kalman smoother 208 | 209 | Parameters 210 | ---------- 211 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 212 | transition function of the state space model 213 | transition_covariance: (D,D) array 214 | covariance :math:`\Sigma` of the noise fed to transition_function 215 | filtered_state: MVNormalParameters 216 | mean and cov computed by Kalman Filtering 217 | previous_smoothed: MVNormalParameters 218 | smoothed state of the previous step 219 | linearization_state: MVNormalParameters 220 | state for the cubature linearization 221 | 222 | Returns 223 | ------- 224 | smoothed_state: MVNormalParameters 225 | smoothed state 226 | """ 227 | predicted_state, F = predict(transition_function, transition_covariance, filtered_state, linearization_state, True) 228 | smoothing_gain = jnp.linalg.solve(predicted_state.cov, F @ filtered_state.cov).T 229 | mean = filtered_state.mean + smoothing_gain @ (previous_smoothed.mean - predicted_state.mean) 230 | cov = filtered_state.cov + smoothing_gain @ (previous_smoothed.cov - predicted_state.cov) @ smoothing_gain.T 231 | return MVNormalParameters(mean, 0.5 * (cov + cov.T)) 232 | 233 | 234 | def smoother_routine(transition_function: Callable[[jnp.ndarray], jnp.ndarray], 235 | transition_covariances: jnp.ndarray, 236 | filtered_states: MVNormalParameters, 237 | linearization_states: MVNormalParameters = None, 238 | propagate_first: bool = True 239 | ) -> MVNormalParameters: 240 | """ Computes the cubature Rauch-Tung-Striebel smoother routine and returns a series of smoothed_states 241 | 242 | Parameters 243 | ---------- 244 | filtered_states: MVNormalParameters 245 | Filtered states obtained from Kalman Filter 246 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 247 | transition function of the state space model 248 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 249 | transition covariances for each time step, if passed only one, it is repeated n times 250 | linearization_states: MVNormalParameters, optional 251 | states for the cubature linearization 252 | propagate_first: bool, optional 253 | Is the first step a transition or an update? i.e. False if the initial time step has 254 | an associated observation. Default is True. 255 | 256 | Returns 257 | ------- 258 | smoothed_states: MVNormalParameters 259 | list of smoothed states 260 | """ 261 | n_observations = filtered_states.mean.shape[0] 262 | if propagate_first: 263 | transition_covariances = make_matrices_parameters(transition_covariances, n_observations - 1) 264 | else: 265 | transition_covariances = make_matrices_parameters(transition_covariances, n_observations) 266 | transition_covariances = transition_covariances[:-1] 267 | 268 | def body(state, inputs): 269 | filtered, transition_covariance, linearization_state = inputs 270 | if linearization_state is None: 271 | linearization_state = filtered 272 | smoothed_state = smooth(transition_function, transition_covariance, filtered, state, linearization_state) 273 | return smoothed_state, smoothed_state 274 | 275 | last_state = MVNormalParameters(filtered_states.mean[-1], filtered_states.cov[-1]) 276 | filtered_states, linearization_states = jax.tree_map(lambda x: x[:-1], 277 | [filtered_states, linearization_states]) 278 | _, smoothed_states = lax.scan(body, 279 | last_state, 280 | [filtered_states, transition_covariances, linearization_states], 281 | reverse=True) 282 | 283 | smoothed_states = jax.tree_map(lambda y, z: jnp.concatenate([y, z[None, ...]], 0), smoothed_states, 284 | last_state) 285 | 286 | return smoothed_states 287 | 288 | 289 | def iterated_smoother_routine(initial_state: MVNormalParameters, 290 | observations: jnp.ndarray, 291 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 292 | transition_covariances: jnp.ndarray, 293 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 294 | observation_covariances: jnp.ndarray, 295 | initial_linearization_states: jnp.ndarray = None, 296 | n_iter: int = 100, 297 | propagate_first: bool = False) -> MVNormalParameters: 298 | """ 299 | Computes the Gauss-Newton iterated extended Kalman smoother 300 | 301 | Parameters 302 | ---------- 303 | initial_state: MVNormalParameters 304 | prior belief on the initial state distribution 305 | observations: (n, K) array 306 | array of n observations of dimension K 307 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 308 | transition function of the state space model 309 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 310 | transition covariances for each time step, if passed only one, it is repeated n times 311 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 312 | observation function of the state space model 313 | observation_covariances: (K, K) or (1, K, K) or (n, K, K) array 314 | observation error covariances for each time step, if passed only one, it is repeated n times 315 | initial_linearization_states: MVNormalParameters, optional 316 | states for linearization of the first pass. 317 | n_iter: int 318 | number of times the filter-smoother routine is computed 319 | propagate_first: bool, optional 320 | Is the first step a transition or an update? i.e. False if the initial time step has 321 | an associated observation. Default is True. 322 | 323 | Returns 324 | ------- 325 | iterated_smoothed_trajectories: MVNormalParameters 326 | The result of the smoothing routine 327 | 328 | """ 329 | n_observations = observations.shape[0] 330 | transition_covariances, observation_covariances = list(map( 331 | lambda z: make_matrices_parameters(z, n_observations), 332 | [transition_covariances, 333 | observation_covariances])) 334 | 335 | def body(linearization_points, _): 336 | _, filtered_states = filter_routine(initial_state, observations, transition_function, transition_covariances, 337 | observation_function, observation_covariances, linearization_points, 338 | propagate_first) 339 | return smoother_routine(transition_function, transition_covariances, filtered_states, 340 | linearization_points, propagate_first), None 341 | 342 | if initial_linearization_states is None: 343 | initial_linearization_states = body(None, None)[0] 344 | 345 | iterated_smoothed_trajectories, _ = lax.scan(body, initial_linearization_states, jnp.arange(n_iter)) 346 | return iterated_smoothed_trajectories 347 | -------------------------------------------------------------------------------- /parsmooth/sequential/extended.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Tuple 2 | 3 | import jax 4 | import jax.numpy as jnp 5 | import jax.scipy.linalg as jlag 6 | from jax import lax, jacfwd 7 | from jax.scipy.stats import multivariate_normal 8 | 9 | from ..utils import MVNormalParameters, make_matrices_parameters 10 | 11 | __all__ = ["filter_routine", "smoother_routine"] 12 | 13 | 14 | def predict(transition_function: Callable[[jnp.ndarray], jnp.ndarray], 15 | transition_covariance: jnp.ndarray, 16 | prior: MVNormalParameters, 17 | linearization_point: jnp.ndarray) -> MVNormalParameters: 18 | """ Computes the extended kalman filter linearization of :math:`x_{t+1} = f(x_t, \mathcal{N}(0, \Sigma))` 19 | 20 | Parameters 21 | ---------- 22 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 23 | transition function of the state space model 24 | transition_covariance: (D,D) array 25 | covariance :math:`\Sigma` of the noise fed to transition_function 26 | prior: MVNormalParameters 27 | prior state of the filter x 28 | linearization_point: jnp.ndarray 29 | Where to compute the Jacobian 30 | 31 | Returns 32 | ------- 33 | out: MVNormalParameters 34 | Predicted state 35 | """ 36 | if linearization_point is None: 37 | linearization_point = prior.mean 38 | jac_x = jacfwd(transition_function, 0)(linearization_point) 39 | cov = jnp.dot(jac_x, jnp.dot(prior.cov, jac_x.T)) + transition_covariance 40 | mean = transition_function(linearization_point) 41 | mean = mean + jnp.dot(jac_x, prior.mean - linearization_point) 42 | return MVNormalParameters(mean, 0.5 * (cov + cov.T)) 43 | 44 | 45 | def update(observation_function: Callable[[jnp.ndarray], jnp.ndarray], 46 | observation_covariance: jnp.ndarray, 47 | predicted: MVNormalParameters, 48 | observation: jnp.ndarray, 49 | linearization_point: jnp.ndarray) -> Tuple[float, MVNormalParameters]: 50 | """ Computes the extended kalman filter linearization of :math:`x_t \mid y_t` 51 | 52 | Parameters 53 | ---------- 54 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 55 | observation function of the state space model 56 | observation_covariance: (K,K) array 57 | observation_error :math:`\Sigma` fed to observation_function 58 | predicted: MVNormalParameters 59 | predicted state of the filter :math:`x` 60 | observation: (K) array 61 | Observation :math:`y` 62 | linearization_point: jnp.ndarray 63 | Where to compute the Jacobian 64 | 65 | Returns 66 | ------- 67 | loglikelihood: float 68 | Log-likelihood increment for observation 69 | updated_state: MVNormalParameters 70 | filtered state 71 | """ 72 | if linearization_point is None: 73 | linearization_point = predicted.mean 74 | jac_x = jacfwd(observation_function, 0)(linearization_point) 75 | 76 | obs_mean = observation_function(linearization_point) + jnp.dot(jac_x, predicted.mean - linearization_point) 77 | 78 | residual = observation - obs_mean 79 | residual_covariance = jnp.dot(jac_x, jnp.dot(predicted.cov, jac_x.T)) 80 | residual_covariance = residual_covariance + observation_covariance 81 | 82 | gain = jnp.dot(predicted.cov, jlag.solve(residual_covariance, jac_x, assume_a="pos").T) 83 | 84 | mean = predicted.mean + jnp.dot(gain, residual) 85 | cov = predicted.cov - jnp.dot(gain, jnp.dot(residual_covariance, gain.T)) 86 | updated_state = MVNormalParameters(mean, 0.5 * (cov + cov.T)) 87 | 88 | loglikelihood = multivariate_normal.logpdf(residual, jnp.zeros_like(residual), residual_covariance) 89 | return loglikelihood, updated_state 90 | 91 | 92 | def filter_routine(initial_state: MVNormalParameters, 93 | observations: jnp.ndarray, 94 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 95 | transition_covariances: jnp.ndarray, 96 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 97 | observation_covariances: jnp.ndarray, 98 | linearization_points: jnp.ndarray = None, 99 | propagate_first: bool = True) -> Tuple[float, MVNormalParameters]: 100 | """ Computes the predict-update routine of the Kalman Filter equations and returns a series of filtered_states 101 | 102 | Parameters 103 | ---------- 104 | initial_state: MVNormalParameters 105 | prior belief on the initial state distribution 106 | observations: (n, K) array 107 | array of n observations of dimension K 108 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 109 | transition function of the state space model 110 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 111 | transition covariances for each time step, if passed only one, it is repeated n times 112 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 113 | observation function of the state space model 114 | observation_covariances: (K, K) or (1, K, K) or (n, K, K) array 115 | observation error covariances for each time step, if passed only one, it is repeated n times 116 | linearization_points: (n, D) array, optional 117 | points at which to compute the jacobians. 118 | propagate_first: bool, optional 119 | Is the first step a transition or an update? i.e. False if the initial time step has 120 | an associated observation. Default is True. 121 | 122 | Returns 123 | ------- 124 | loglikelihood: float 125 | Marginal loglikelihood of the observations given the parameters 126 | filtered_states: MVNormalParameters 127 | list of filtered states 128 | """ 129 | n_observations = observations.shape[0] 130 | 131 | transition_covariances, observation_covariances = list(map( 132 | lambda z: make_matrices_parameters(z, n_observations), 133 | [transition_covariances, 134 | observation_covariances])) 135 | 136 | def prop_first_body(carry, inputs): 137 | running_ell, state, prev_linearization_point = carry 138 | observation, transition_covariance, observation_covariance, linearization_point = inputs 139 | predicted_state = predict(transition_function, transition_covariance, state, prev_linearization_point) 140 | loglikelihood, updated_state = update(observation_function, observation_covariance, predicted_state, 141 | observation, linearization_point) 142 | 143 | return (running_ell + loglikelihood, updated_state, linearization_point), updated_state 144 | 145 | def update_first_body(carry, inputs): 146 | running_ell, state, _ = carry 147 | observation, transition_covariance, observation_covariance, linearization_point = inputs 148 | loglikelihood, updated_state = update(observation_function, observation_covariance, state, 149 | observation, linearization_point) 150 | predicted_state = predict(transition_function, transition_covariance, updated_state, linearization_point) 151 | return (running_ell + loglikelihood, predicted_state, linearization_point), updated_state 152 | 153 | body = prop_first_body if propagate_first else update_first_body 154 | 155 | if linearization_points is not None: 156 | initial_linearization_point = linearization_points[0] if linearization_points is not None else None 157 | linearization_points = linearization_points[1:] if propagate_first else linearization_points 158 | else: 159 | initial_linearization_point = linearization_points = None 160 | 161 | (ell, *_), filtered_states = lax.scan(body, 162 | (0., initial_state, initial_linearization_point), 163 | [observations, 164 | transition_covariances, 165 | observation_covariances, 166 | linearization_points], 167 | length=n_observations) 168 | 169 | if propagate_first: 170 | filtered_states = jax.tree_map(lambda y, z: jnp.concatenate([y[None, ...], z], 0), initial_state, 171 | filtered_states) 172 | 173 | return ell, filtered_states 174 | 175 | 176 | def smooth(transition_function: Callable[[jnp.ndarray], jnp.ndarray], 177 | transition_covariance: jnp.array, 178 | filtered_state: MVNormalParameters, 179 | previous_smoothed: MVNormalParameters, 180 | linearization_point: jnp.ndarray) -> MVNormalParameters: 181 | """ 182 | One step extended kalman smoother 183 | 184 | Parameters 185 | ---------- 186 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 187 | transition function of the state space model 188 | transition_covariance: (D,D) array 189 | covariance :math:`\Sigma` of the noise fed to transition_function 190 | filtered_state: MVNormalParameters 191 | mean and cov computed by Kalman Filtering 192 | previous_smoothed: MVNormalParameters, 193 | smoothed state of the previous step 194 | linearization_point: jnp.ndarray 195 | Where to compute the Jacobian 196 | 197 | Returns 198 | ------- 199 | smoothed_state: MVNormalParameters 200 | smoothed state 201 | """ 202 | 203 | jac_x = jacfwd(transition_function, 0)(linearization_point) 204 | 205 | mean = transition_function(linearization_point) + jnp.dot(jac_x, filtered_state.mean - linearization_point) 206 | mean_diff = previous_smoothed.mean - mean 207 | 208 | cov = jnp.dot(jac_x, jnp.dot(filtered_state.cov, jac_x.T)) + transition_covariance 209 | cov_diff = previous_smoothed.cov - cov 210 | 211 | gain = jnp.dot(filtered_state.cov, jlag.solve(cov, jac_x, assume_a="pos").T) 212 | 213 | mean = filtered_state.mean + jnp.dot(gain, mean_diff) 214 | cov = filtered_state.cov + jnp.dot(gain, jnp.dot(cov_diff, gain.T)) 215 | return MVNormalParameters(mean, cov) 216 | 217 | 218 | def smoother_routine(transition_function: Callable[[jnp.ndarray], jnp.ndarray], 219 | transition_covariances: jnp.ndarray, 220 | filtered_states: MVNormalParameters, 221 | linearization_points: jnp.ndarray = None, 222 | propagate_first: bool = True, 223 | ) -> MVNormalParameters: 224 | """ Computes the extended Rauch-Tung-Striebel (a.k.a extended Kalman) smoother routine and returns a series of smoothed_states 225 | 226 | Parameters 227 | ---------- 228 | filtered_states: MVNormalParameters 229 | Filtered states obtained from Kalman Filter 230 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 231 | transition function of the state space model 232 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 233 | transition covariances for each time step, if passed only one, it is repeated n times 234 | linearization_points: (n, D) array, optional 235 | points at which to compute the jacobians. 236 | propagate_first: bool, optional 237 | Is the first step a transition or an update? i.e. False if the initial time step has 238 | an associated observation. Default is True. 239 | 240 | Returns 241 | ------- 242 | smoothed_states: MVNormalParameters 243 | list of smoothed states 244 | """ 245 | n_observations = filtered_states.mean.shape[0] 246 | if propagate_first: 247 | transition_covariances = make_matrices_parameters(transition_covariances, n_observations - 1) 248 | else: 249 | transition_covariances = make_matrices_parameters(transition_covariances, n_observations) 250 | transition_covariances = transition_covariances[:-1] 251 | 252 | def body(state, inputs): 253 | filtered, transition_covariance, linearization_point = inputs 254 | if linearization_point is None: 255 | linearization_point = filtered.mean 256 | smoothed_state = smooth(transition_function, transition_covariance, filtered, state, linearization_point) 257 | return smoothed_state, smoothed_state 258 | 259 | last_state = MVNormalParameters(filtered_states.mean[-1], filtered_states.cov[-1]) 260 | filtered_states, linearization_points = jax.tree_map(lambda x: x[:-1], 261 | [filtered_states, linearization_points]) 262 | _, smoothed_states = lax.scan(body, 263 | last_state, 264 | [filtered_states, transition_covariances, linearization_points], 265 | reverse=True) 266 | 267 | smoothed_states = jax.tree_map(lambda y, z: jnp.concatenate([y, z[None, ...]], 0), smoothed_states, 268 | last_state) 269 | 270 | return smoothed_states 271 | 272 | 273 | def iterated_smoother_routine(initial_state: MVNormalParameters, 274 | observations: jnp.ndarray, 275 | transition_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 276 | transition_covariances: jnp.ndarray, 277 | observation_function: Callable[[jnp.ndarray, jnp.ndarray], jnp.ndarray], 278 | observation_covariances: jnp.ndarray, 279 | initial_linearization_points: jnp.ndarray = None, 280 | n_iter: int = 100, 281 | propagate_first: bool = True): 282 | """ 283 | Computes the Gauss-Newton iterated extended Kalman smoother 284 | 285 | Parameters 286 | ---------- 287 | initial_state: MVNormalParameters 288 | prior belief on the initial state distribution 289 | observations: (n, K) array 290 | array of n observations of dimension K 291 | transition_function: callable :math:`f(x_t,\epsilon_t)\mapsto x_{t-1}` 292 | transition function of the state space model 293 | transition_covariances: (D, D) or (1, D, D) or (n, D, D) array 294 | transition covariances for each time step, if passed only one, it is repeated n times 295 | observation_function: callable :math:`h(x_t,\epsilon_t)\mapsto y_t` 296 | observation function of the state space model 297 | observation_covariances: (K, K) or (1, K, K) or (n, K, K) array 298 | observation error covariances for each time step, if passed only one, it is repeated n times 299 | initial_linearization_points: jnp.ndarray , optional 300 | points at which to compute the jacobians durning the first pass. 301 | n_iter: int 302 | number of times the filter-smoother routine is computed 303 | propagate_first: bool, optional 304 | Is the first step a transition or an update? i.e. False if the initial time step has 305 | an associated observation. Default is True. 306 | 307 | Returns 308 | ------- 309 | iterated_smoothed_trajectories: MVNormalParameters 310 | The result of the smoothing routine 311 | 312 | """ 313 | n_observations = observations.shape[0] 314 | 315 | transition_covariances, observation_covariances = list(map( 316 | lambda z: make_matrices_parameters(z, n_observations), 317 | [transition_covariances, 318 | observation_covariances])) 319 | 320 | def body(curr_smoother, _): 321 | if curr_smoother is not None: 322 | linearization_points = curr_smoother.mean if isinstance(curr_smoother, 323 | MVNormalParameters) else curr_smoother 324 | else: 325 | linearization_points = None 326 | 327 | _, filtered_states = filter_routine(initial_state, observations, transition_function, transition_covariances, 328 | observation_function, observation_covariances, linearization_points, 329 | propagate_first) 330 | return smoother_routine(transition_function, transition_covariances, filtered_states, 331 | linearization_points, propagate_first), None 332 | 333 | if initial_linearization_points is None: 334 | initial_linearization_points = body(None, None)[0] 335 | 336 | iterated_smoothed_trajectories, _ = lax.scan(body, initial_linearization_points, jnp.arange(n_iter)) 337 | return iterated_smoothed_trajectories 338 | -------------------------------------------------------------------------------- /parsmooth/utils.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import jax.numpy as jnp 4 | import jax.scipy.linalg as jlnialg 5 | import numpy as np 6 | 7 | __all__ = ["MVNormalParameters", "make_matrices_parameters"] 8 | 9 | MVNormalParameters = namedtuple("MVNormalParameters", ["mean", "cov"]) 10 | 11 | 12 | def make_matrices_parameters(matrix: jnp.ndarray or np.array, n_observations: int) -> jnp.array: 13 | """ Processes a matrix (or "list" thereof) to be able to be iterated over n_observations times 14 | 15 | Parameters 16 | ---------- 17 | matrix: array 18 | Matrix to be processed 19 | n_observations: int 20 | First dimension of the returned array 21 | 22 | Returns 23 | ------- 24 | 25 | """ 26 | if jnp.ndim(matrix) <= 2: 27 | return jnp.tile(matrix, (n_observations, 1, 1)) 28 | elif jnp.ndim(matrix) == 3: 29 | if matrix.shape[0] == 1: 30 | return jnp.repeat(matrix, n_observations, 0) 31 | if matrix.shape[0] == n_observations: 32 | return matrix 33 | raise ValueError("if matrix has 3 dimensions, its first dimension must be of size 1 or n_observations") 34 | raise ValueError("matrix has more than 3 dimensions") 35 | 36 | 37 | # The real logic 38 | def _make_associative_filtering_params(args): 39 | Hk, Rk, Fk_1, Qk_1, uk_1, yk, dk, I_dim = args 40 | 41 | # FIRST TERM 42 | ############ 43 | 44 | # temp variable 45 | HQ = jnp.dot(Hk, Qk_1) # Hk @ Qk_1 46 | 47 | Sk = jnp.dot(HQ, Hk.T) + Rk 48 | Kk = jlnialg.solve(Sk, HQ, assume_a="pos").T # using the fact that S and Q are symmetric 49 | 50 | # temp variable: 51 | I_KH = I_dim - jnp.dot(Kk, Hk) # I - Kk @ Hk 52 | 53 | Ck = jnp.dot(I_KH, Qk_1) 54 | 55 | residual = (yk - jnp.dot(Hk, uk_1) - dk) 56 | 57 | bk = uk_1 + jnp.dot(Kk, residual) 58 | Ak = jnp.dot(I_KH, Fk_1) 59 | 60 | # SECOND TERM 61 | ############# 62 | HF = jnp.dot(Hk, Fk_1) 63 | FHS_inv = jsolve(Sk, HF).T 64 | 65 | etak = jnp.dot(FHS_inv, residual) 66 | Jk = jnp.dot(FHS_inv, HF) 67 | 68 | return Ak, bk, Ck, etak, Jk 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jupyter 2 | matplotlib 3 | numba -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Author: Adrien Corenflos 2 | 3 | """Install parsmooth.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('requirements.txt') as f: 8 | requirements = f.read().splitlines() 9 | 10 | setup( 11 | name='parsmooth', 12 | version='0.1', 13 | description='Parallel Extended Kalman Filter.', 14 | author='Adrien Corenflos', 15 | author_email='adrien.corenflos@gmail.com', 16 | url='https://github.com/AdrienCorenflos/parallelEKF', 17 | packages=find_packages(), 18 | install_requires=requirements, 19 | ) 20 | --------------------------------------------------------------------------------