diff --git a/.github/workflows/alltests.yml b/.github/workflows/alltests.yml index b94c8cfab..8d957a711 100644 --- a/.github/workflows/alltests.yml +++ b/.github/workflows/alltests.yml @@ -297,12 +297,12 @@ jobs: - name: Validate MPMC dependencies (Unix) if: runner.os != 'Windows' shell: bash -el {0} - run: python -c "import torch, pyg_lib, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" + run: python -c "import torch, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" - name: Validate MPMC dependencies (Windows) if: runner.os == 'Windows' shell: pwsh - run: python -c "import torch, pyg_lib, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" + run: python -c "import torch, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" # ----------------------------------------------------------- # Colab readiness tests (Linux only) # ----------------------------------------------------------- diff --git a/.gitignore b/.gitignore index 70ed13be3..befd562b5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ demos/prob_failure_gp_ci_plots/ demos/fgpr_figs/ demos/GBM/images/*.png demos/GBM/outputs/*.* +*.tmp_colab* .tmp_* # Generated notebook/demo images diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c0fe3ae5..fd910136c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ While `dev` contains the most complete set of install dependencies, a number of pip install -e ".[dev]" ~~~ -The `dev` extra includes QMCPy's PyPI-hosted MPMC dependencies. MPMC additionally requires a platform-specific `pyg_lib` wheel that is not available from PyPI. After installing `dev`, let the QMCPy installer select the wheel page matching the installed PyTorch build: +The `dev` extra includes QMCPy's PyPI-hosted MPMC dependencies. The optional `pyg_lib` accelerator uses platform-specific wheels that are not available from PyPI. MPMC can run without it using a native PyTorch radius-graph fallback. After installing `dev`, the QMCPy installer can select the wheel page matching the installed PyTorch build; it warns and continues if neither a wheel nor a source build is available: ~~~bash qmcpy-install-mpmc diff --git a/demos/GBM/gbm_demo.ipynb b/demos/GBM/gbm_demo.ipynb index ddb48ac39..4f076b001 100644 --- a/demos/GBM/gbm_demo.ipynb +++ b/demos/GBM/gbm_demo.ipynb @@ -858,6 +858,8 @@ "results_data = []\n", "params_ql = {'initial_value': 100, 'mu': 0.05, 'sigma': 0.2, 'maturity': 1.0, 'n_steps': 252, 'n_paths': 2**14, 'seed': cf.QUANTLIB_SEED}\n", "params_qp = {'initial_value': 100, 'mu': 0.05, 'diffusion': 0.2**2, 'maturity': 1.0, 'n_steps': 252, 'n_paths': 2**14, 'replications': 8}\n", + "if IN_COLAB:\n", + " params_ql['n_paths'] = params_qp['n_paths'] = 2**10\n", "theoretical_mean, theoretical_std = calculate_theoretical_statistics(params_ql)\n", "\n", "# Add theoretical values once\n", @@ -1140,6 +1142,8 @@ "# Generate specific data for visualization (ensure we have data for both libraries)\n", "params_vis_ql = {'initial_value': 100, 'mu': 0.05, 'sigma': 0.2, 'maturity': 1.0, 'n_steps': 252, 'n_paths': 2**14, 'sampler_type': 'Sobol'}\n", "params_vis_qp = {'initial_value': 100, 'mu': 0.05, 'diffusion': 0.2**2, 'maturity': 1.0, 'n_steps': 252, 'n_paths': 2**14, 'sampler_type': 'Sobol'}\n", + "if IN_COLAB:\n", + " params_vis_ql['n_paths'] = params_vis_qp['n_paths'] = 2**10\n", "\n", "# Generate paths for visualization\n", "vis_quantlib_paths, _ = qlu.generate_quantlib_paths(**params_vis_ql)\n", @@ -1201,7 +1205,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We benchmark path generation with `%timeit`, using ten loops and three repeats.\n", + "We benchmark path generation with `%timeit`, using ten loops and three repeats. On Colab, the comparison and benchmark use 1024 paths per library, with one timing loop and one repeat.\n", "\n", "QMCPy Halton is slowest here because its work scales with an $(n,d,t)$ array of base-$b$ digits. We use `t = cf.HALTON_DIGITS` ($32$ rather than $63$) with the default linear-matrix scramble (LMS) and digital permutation (DP), `'LMS DP'`. QMCPy paths use consecutive batches from the same sampler, limiting the two temporary digit buffers to 64 MiB without changing path counts, time steps, or randomizations. This limit excludes the returned paths and the sampler's permutation tables; the latter use about 3.6 GiB at 512 time steps with eight replications.\n", "\n", @@ -1222,7 +1226,10 @@ " for sampler_type in samplers_to_test:\n", " print(f\"QuantLib ({sampler_type}) timing:\")\n", " benchmark_func = lambda st=sampler_type: qlu.generate_quantlib_paths(**base_params, sampler_type=st)\n", - " timing_result = %timeit -n 10 -r 3 -o benchmark_func()\n", + " if IN_COLAB:\n", + " timing_result = %timeit -n 1 -r 1 -o benchmark_func()\n", + " else:\n", + " timing_result = %timeit -n 10 -r 3 -o benchmark_func()\n", " timing_results[sampler_type] = {\n", " 'average': timing_result.average,\n", " 'stdev': timing_result.stdev,\n", @@ -1243,7 +1250,10 @@ " for sampler_type in samplers_to_test:\n", " print(f\"QMCPy ({sampler_type}) timing:\")\n", " benchmark_func = lambda st=sampler_type: qpu.generate_qmcpy_paths(**qp_params, sampler_type=st)\n", - " timing_result = %timeit -n 10 -r 3 -o benchmark_func()\n", + " if IN_COLAB:\n", + " timing_result = %timeit -n 1 -r 1 -o benchmark_func()\n", + " else:\n", + " timing_result = %timeit -n 10 -r 3 -o benchmark_func()\n", " timing_results[sampler_type] = {\n", " 'average': timing_result.average,\n", " 'stdev': timing_result.stdev,\n", @@ -1400,6 +1410,8 @@ " 'n_steps': 252, \n", " 'n_paths': 2**14\n", "}\n", + "if IN_COLAB:\n", + " base_ql_params['n_paths'] = base_qp_params['n_paths'] = 2**10\n", "# Run benchmarks\n", "quantlib_timing_results = benchmark_quantlib_samplers(quantlib_samplers_to_benchmark, base_ql_params)\n", "qmcpy_timing_results = benchmark_qmcpy_samplers(qmcpy_samplers_to_benchmark, base_qp_params)\n", diff --git a/demos/lattice_kronecker_methods.ipynb b/demos/lattice_kronecker_methods.ipynb new file mode 100644 index 000000000..2951e79de --- /dev/null +++ b/demos/lattice_kronecker_methods.ipynb @@ -0,0 +1,295 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "492a51ed", + "metadata": {}, + "source": [ + "# Lattice and Kronecker Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/QMCSoftware/QMCSoftware/blob/develop/demos/lattice_kronecker_methods.ipynb)" + ], + "id": "31badecd" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# @title Execute this cell to install dependencies\n", + "try:\n", + " import google.colab\n", + " IN_COLAB = True\n", + "except ImportError:\n", + " IN_COLAB = False\n", + "if IN_COLAB:\n", + " !pip install -q qmcpy\n" + ], + "id": "f534a4d7" + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2e06ea48", + "metadata": {}, + "outputs": [], + "source": [ + "from qmcpy import *\n", + "import numpy as np\n", + "from time import time\n", + "from matplotlib import pyplot" + ] + }, + { + "cell_type": "markdown", + "id": "39b265e4", + "metadata": {}, + "source": [ + "## Discrepancy Values" + ] + }, + { + "cell_type": "markdown", + "id": "2c1e69d8", + "metadata": {}, + "source": [ + "#### Lattice" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "958e16e1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "42.48726056679934\n" + ] + } + ], + "source": [ + "dim = 20\n", + "n = 2**15\n", + "lat = Lattice(dimension=dim, order=\"RADICAL_INVERSE\", seed=12) # initialize a lattice as usual\n", + "coord_weights = np.array([j**(-2) for j in range(1, dim + 1)]) # define some coordinate weights\n", + "\n", + "lat_discs = lat.expected_squared_periodic_discrepancies(n_max=n, coord_weights=coord_weights) # compute the expected squared periodic discrepancies for n = 1, 2, ...\n", + "\n", + "sample_weights = np.arange(1, n+1) # define some sample weights\n", + "lat_wssd = lat.wssd(n_max=n, coord_weights=coord_weights, sample_weights=sample_weights) # compute the wssd\n", + "print(lat_wssd)" + ] + }, + { + "cell_type": "markdown", + "id": "f174209f", + "metadata": {}, + "source": [ + "#### Kronecker" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c8eec507", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[34.65168403]\n" + ] + } + ], + "source": [ + "dim = 20\n", + "n = 2**15\n", + "coord_weights = np.array([j**(-2) for j in range(1, dim + 1)])\n", + "\n", + "kron = Kronecker(dimension=dim, seed=12, generating_vector=\"CBC_MT\") # initialize a Kronecker sequence as usual\n", + "kron_k_tilde = (lambda x, gamma: np.prod(1 + (x * (x - 1) + 1/6) * gamma, axis=-1), 1) # define the kernel function (in this case, the second Bernoulli polynomial)\n", + "\n", + "kron_discs = kron._square_periodic_discrepancies(n = n, k_tilde = kron_k_tilde, gamma = coord_weights).reshape(-1) # compute the expected squared periodic discrepancies for n = 1, 2, ...\n", + "\n", + "sample_weights = np.arange(1, n+1) # define some sample weights\n", + "kron_wssd = kron.wssd_discrepancy(n = n, sample_weights = sample_weights, k_tilde = kron_k_tilde, gamma = coord_weights) # compute the wssd\n", + "print(kron_wssd)" + ] + }, + { + "cell_type": "markdown", + "id": "f1c20186", + "metadata": {}, + "source": [ + "## Searches" + ] + }, + { + "cell_type": "markdown", + "id": "01664bbb", + "metadata": {}, + "source": [ + "#### Lattice" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "524e3b99", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time taken for lattice vector wssd search: 0.1818382740020752\n", + "Searched lattice vector: [ 1 4825 13541 15249 15405 9909 7493 11407 14819 10089 3683 3347\n", + " 13789 8837 5309 6307 6447 12103 9097 2767]\n" + ] + } + ], + "source": [ + "# note that the search method requires that the sample weights be w_n = n, so they are not customizable\n", + "n = 2**15\n", + "dim = 20\n", + "coord_weights = np.array([j**(-2) for j in range(1, dim + 1)])\n", + "\n", + "bernoulli2 = lambda x: x * (x - 1) + 1 / 6\n", + "\n", + "time_start = time()\n", + "searched_lattice_vector = lattice_vector_wssd_search(n_max=n, d_max=dim, kernel=bernoulli2, coord_weights=coord_weights) # search for a lattice vector with low wssd\n", + "time_end = time()\n", + "print(\"Time taken for lattice vector wssd search: \", time_end - time_start)\n", + "print(\"Searched lattice vector: \", searched_lattice_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "9efeb0fa", + "metadata": {}, + "source": [ + "#### Kronecker" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "09388fbc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time taken for kronecker vector wssd search: 6.114294052124023\n", + "Searched Kronecker vector: [0.61803399 0.26774665 0.91444648 0.22708655 0.12137476 0.71267465\n", + " 0.69787961 0.10230792 0.18609503 0.31195642 0.41561801 0.13176115\n", + " 0.22004561 0.56882224 0.8920797 0.79690426 0.54748361 0.74175085\n", + " 0.42012299 0.41261152]\n" + ] + } + ], + "source": [ + "# note that the search method requires that the sample weights be w_n = n, so they are not customizable\n", + "n = 2**15\n", + "dim = 20\n", + "coord_weights = np.array([j**(-2) for j in range(1, dim + 1)])\n", + "searchsize = 20 # the time cost is O(dim * n * searchsize^2), so searchsize should be chosen with care. The largest search I have run was in MATLAB with searchsize = 300, N = 2^20, d = 100, which took about 24 hours \n", + "\n", + "time_start = time()\n", + "searched_kron_vector, wssd, new_kron_discs, _ = kronecker_vector_search_mobius_transform(n_max = n, d_max = dim, kernel = lambda x: x * (x - 1) + 1 / 6, searchsize = searchsize, coord_weights = coord_weights) # search for a Kronecker vector with low wssd\n", + "time_end = time()\n", + "\n", + "print(\"Time taken for kronecker vector wssd search: \", time_end - time_start)\n", + "print(\"Searched Kronecker vector: \", searched_kron_vector)" + ] + }, + { + "cell_type": "markdown", + "id": "26a634f7", + "metadata": {}, + "source": [ + "## Plotting" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f66d72b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Periodic Discrepancy')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/QAAANDCAYAAAAD6lSvAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QV4k1cXB/B/UnehFCvFrbi72wZsYzBgDHcGGzYYMNiYA9+ACYPBhsPwseHu7u7e0mKl7m2S7zk3pLSlpQVKC+3/9zx5krx582qQc++552oMBoMBRERERERERPRG0Wb2ARARERERERHR82NAT0RERERERPQGYkBPRERERERE9AZiQE9ERERERET0BmJAT0RERERERPQGYkBPRERERERE9AZiQE9ERERERET0BjLP7AN43en1evj5+cHBwQEajSazD4eIiIiIiIiyOIPBgNDQUOTNmxdabcr98AzoUyHBfP78+dP7/hARERERERE9k4+PDzw8PFL8nAF9KqRn3nQhHR0dU1udiIiIiIiI6KWEhISojmVTPJoSBvSpMKXZSzDPgJ6IiIiIiIgySmrDvlkUj4iIiIiIiOgNxIA+BdOmTYOXlxeqVq2asXeEiIiIiIiIKA00BimfR88cu+Dk5ITg4GCm3BMREREREdFrE4dyDD0RERERUQp0Oh1iY2N5fYgoXVlYWMDMzOylt8OAnoiIiIgoCUlivXfvHoKCgnhtiOiVcHZ2Ru7cuVMtfPcsDOiJiIiIiJIwBfPu7u6wtbV9qf9wExElbTCMiIjAgwcP1Ps8efLgRTGgJyIiIiJKkmZvCuZz5MjBa0NE6c7GxkY9S1Avf9e8aPo9q9wTERERESVgGjMvPfNERK+K6e+Yl6nTwYCeiIiIiCgZTLMnotf97xgG9ERERERE9Ea4f/8+Jk2ahOjoaLwOXrfjoeyHAT0RERERURZw7NgxTJ8+/anly5cvx2+//YbIyMhXsl+pNyBBra+v7wtv4+7du2obCVOPk1vm4+ODESNGvLJzSXg+8pg8eTJmzJiB9evX49GjR0+tmxHHQ/QsDOiJiIiIiLKAXbt24auvvkq0bNSoUejRoweKFSsWX4Qrvfn7+6ug9ubNmy+8jdu3b6ttJOzpTm6ZTPH12WefwdraGq+K6XyOHz+uGhXOnDmjgnsPDw98/PHHiYL3jDgeomdhlfsUTJs2TT2kyikRERER0ZtE/g/bv39//PPPP9i6dStq1aqFK1euYMuWLfjkk0/i15Ne57lz52LAgAGJigDu378fJ0+ehJOTE95++224ubm98LGEhYWpXm5hYWGBwoULo2nTpvFBsPSIL168WL2WTAJLS0t4enpi3759iZZ5eXmhcuXKyc7bffnyZezZs0dt86233kLOnDkTfX7hwgXs3btXfV67dm0ULVo01eMeOHAg6tSpE/9eAvtmzZqp81m4cGH8+SQ9HmkE2LZtm2qIqFmzJkqXLp2mY5Xe/mXLlmHQoEEqI0AaNNq1a4d8+fJBr9dj+/bt6rt58+ZF48aN1b0x2blzp7qOsr/du3cjMDBQbbtgwYLx65w6dUodl3BwcEC5cuXU+gmZtlO3bl3VQCSv69evrxqEkk67JuvKdS1UqBCaN28Oc3NzXLt2DWvXrsXgwYOh1T7pOz537hx27Nihzo3SF3von/EHWH6gR48eTedLTkRERET06kgg2b59e2zYsEEFjhLMmwLSsWPHJlpXgk/pjQ4JCYlf1rlzZ7z33ns4ceIE5syZgyJFiuDAgQMv1bhw79499ZCA7+uvv1bB5MOHD+M/N6Wzy5h0WU8CyaTLgoODk01xlyyEChUqqMYKabyQAFQCSJPRo0erZYcOHVJBZZUqVeIbGJ6HHPP48ePx999/x2cjJD0eud7SWPDvv/+qIRByLSdOnJimY71+/braljQiyHWXbctwAzlvWSaZAOfPn8fMmTNV48bZs2fjt7t69Wr1efXq1dV9l/elSpWKD+BFeHh4/H2QY2vdujW6d++e6BxN25H9yXbWrVuHsmXLqutmIo0F0ijSrVs31egj17JBgwbqM5nm8YsvvsDmzZsTbffbb79VDQT0ChjomYKDgw1ymeSZiIiIiLK+yMhIw4ULF9Sz0Ov1hvDo2Ex5yL7T6qeffjI4OjoaGjVqZChatKjh5s2biT5fsWKFwcnJKdGys2fPqv/r3r17V71fvXq1wcLCwnDx4sX4dXr06GEoU6aMQafTJbvfq1evqm3s3bs3zcfapEkTw+effx7//uDBg2oboaGhz1x29OhRtSwwMFC9X7NmjUGr1RoOHDgQv87Dhw8N165dU683btxoyJEjh8HPzy/+czlOa2vr+HN+nvO5c+eO+mzx4sXJHk/nzp0NvXv3jl9f7t+hQ4fSdKw7d+5U25o4cWKiffbr18/QsmXLRL+Fzz77zFC/fv3494MHD1bf3bNnT6JlJUuWNMTFxSV7nnL+9vb2iY5HvmNjY2O4fv16/LKuXbsaWrRoEf++Z8+eartBQUHxy0znKDp16mT44IMP4t8/evTIYGVlZVi7dm2yx5GdRSb5u+ZF4lCm3BMRERERPUNkrA5eXyXuccwoF75tDlvLtP+XPSIiQvUSS1p9wnTrtJJ0aUnnLlmyZPwySZOuWLEibt26pdLlXzRrQHqlvb29VW+2mZmZSgF/WVLwr1GjRolSx2V4gGmIwNKlS1WKuqSyS5q46SEp7JKB0KJFi+fan/RAm3qpk+Pu7q5S0S9evKh6yCUVX3rN03KsJl26dEn0Xo5djvPXX3+NP/7Q0FAcPnxYnYcptV167SVV3kTG+8t3JCuiRIkSapkULpTfh/TSS2aEpO3LfUh4TJLBkPA+y/s//vhDvZZ9r1y5Ej/++GOilH/TOYrevXurFHzJsJDrtWjRIvUsQzco/THlnoiIiIgoi5AgSwJAqQUlqebP686dO6r4W0L58+eP/+xFmBoCJNX+9OnTKpiUhoeAgAC8LNlWgQIFUvzcVHlfjl1e+/n5qWEGn376qRr7/rxkKIBwdHRM9vMvv/xSBcCSki4NKlLHQM4/LcdqknD8v6TJyz6joqISnYOMgZchwnFxcfHrSsNFQqb7aLpvMlRAAnt5lvR+U1Cf9D4kPTepExATE6Ney32T4RnPOg9Jv5fPJZAXUqOha9euqhGH0h976ImIiIiInsHGwkz1lGfWvp9XmzZtVC+qFFSTgO+nn35Sy6VoWdKCzxIwJpQnTx48ePAg0TIJ/EyfvYipU6eqQDLhOOwhQ4bEF717GdK7LQF6SqRnWHqxpUp9ejCNA69atWqynzs7O+PPP/9U48plfLmMHW/YsKEac5/asSZHChXK7AQSJEsjxLOYahKYSO2BhPdNZkD44YcfVME6E/mdSK/78xyPPFI7j169eqlAvl69eioDQLIT6NVgDz0RERER0TNI2rSkvWfGI2k197R69913VWE2CaaHDh2qlknleKnQbuoxFlL0LCGpPi/F2qQX2GTevHmqh/1F0+2lR9fFxSX+vaSLSyCZXK9wwgaG5JYld55yvFL93UR6kU0981L4TYLwI0eOJPrepUuX4nud00p6useMGYOWLVvGp7AnJRkIQtLgpSK/DH2QYQZyTKkda3Lk/sv3JONC1k1uXyZS9FAeJgsWLFDZFaYK9XIfXF1d4z+XBhappP885HhatWqlGi0SXr+rV68mWk+K7UkBP2m4kQJ7SavkU/phDz0RERERURYk467XrFmjglrpmZfx1JIKLgGpVMGXwFLGkSf04YcfqgBeKuN36tRJ9SzL1HdS/Ty1lGkZry6V5JOOB5ftSEOB9NpKb7H01iZtqJDK8JIyLinqsm+Z6q1JkyZPLZMx6kmPVxolatSoofYlvdmbNm3C7Nmz1XRv8rlUepdecqk4L/uXoFfGlcsY9LScj4z5lwYAuZaynfnz56f4HemVlhkBZD2Zak/SzmW/0qud2rGm5JdfflHXQqrst23bVmUcyDh4Gfcun5nIEAIJ/mU/0jsvU+stWbJEpcwLqUo/bNgwdS7SqCJTBZpqAjyPKVOmqIwBGVog+5N9HT9+PNFvKVeuXHjnnXdUo5JcE3p12ENPRERERJQFSBq4jKtOSOZN37hxowouJQVcglvpsZc0a0nNl7nNZZoyOzs7tb4E2jJd2eTJk9V7mWJNelplTvOUSO+7bEPmVTdNi2Z6yLRrEvzJVNAyX7mk/Us6uvQeJyz+Jsd38OBB1ZsrqeMyVVtyyyRoNe3LdLwyJlwaHaT3WQJJCSIl2DSZNWuWOk/JUJCec+k9lmshAXVq5yPBqgxbkABdpnqToD5htkHS45EAWwrISaBsZWWlzlUaSNJyrNKbLttKOH+7aR8SLMuUeXJN5P306dMTBfOiUqVKaro4+VzOVRosZNiFiQy9kJ512b7p83HjxiUqiCdF++R3kZD8BqTQnYk0PkijyPDhw9V7aWxJbviENOLY29snOgZKfxopdf8KtptlSGqKFBeRv0BSKn5BRERERFmHFCCTnmkJQE2BGtHrTFLbJesg6RCKzCSNA8WLF1eNGvT8f9ekNQ5lyj0RERERERGlC8lAkCkKpW6BZEfQq8WAPgVSeEIeSSuBEhERERERvU6kN1zG2L8OAgMDVa0DGef/ooUUKe2Ycp8KptwTERERZS9MuSeiNyXlnkXxiIiIiIiIiN5ADOiJiIiIiIiI3kAM6ImIiIiIiIjeQAzoiYiIiIiIiN5ADOiJiIiIiIiI3kAM6LOIgxvmIizoUWYfBhERERFRhnnw4IGqEv66eN2Oh7I+BvRZwMXDW6Ad+z/se78uTh5cm9mHQ0RERESZ4NGjRzh37px6Tkiv16vl4eHhGXYcly5deqlt3L9/H7du3Up12W+//YYePXogI66rPC5evAg/Pz8YDIZk182I4yFKiAF9FnDj1G6Y64ACdw3Q9f8cC37ui1hdbGYfFhERERFloLlz56Js2bJ47733nprPWpYfPHgww46jTp06L7WNn3/+Gb179051Wa5cudQc3q/6fCpUqIAPP/wQH3zwAapUqaLmB5f3V69ezfDjIUqIAX0W0LLfD9BM+hp3cwF20UDVmXsxr0ctnPc7mdmHRkREREQZyN7eHseOHcPq1atTXTc6OhrXrl17quc+MjJS9UYn7IW+cuUKfHx84t9HRETg/PnzL3SMAQEB8T3et2/fVhkESXvE/f391XGZ1pOe+aTLpKGiQ4cO+Oqrr57aR0xMjDo3OZfnOfeUODs7q33KOUsP/ZkzZ6DT6VC5cmW13CS545H1vL29ERQUlOy2UzrWe/fuqe+ZXl++fDnR54GBgbhx44baflJyv+Rz4evrq65dUnJNTeeU3OdJtyPnbXqdnODgYDXcIOHvRo7/zp07T60ry0znRi+HAX0WUalpB5RZtA5+5czU+zpHwnC740f4c93XiNZFZ/bhEREREVEGkJ7jAQMGYPTo0ckGekICrq+//hru7u5o2rQpcufOjbZt28YHnBKslytXTgWtQgJN6eF/991347exePHipzIB0mrz5s2qd1se0pOfI0cOLFy4MP7ztWvXqgaJs2fPxq83bdq0p5YdPnz4qRR307nJNuvXr488efJg7NixaT73tCpYsCCWLl2qeuNHjhwZvzzp8axfvx4eHh6oVasWSpQoofZpCnBTO9YJEyagXbt2aNKkCSpWrBifnSDB9zvvvANPT0/1mYuLC/73v/8lOr5mzZph8ODByJ8/P2rWrKm2Ld9P+Jv44Ycf1HVs3749ihYtivLlyz/VSCPbGTp0qPq8du3a6np17tw5UdD+8OFD9dvImTMnGjRooPa5adMm9dmyZcvUsoTry2tZ9vfffz/XNafkMaDPQtzzF0GN2fsQ3MwVkdYGFLoPVPliGX74pjlOPmBvPREREdELkWAkJjxzHimM1X6WMWPGqN7UOXPmJPu5BJ1LlizB6dOnVY/q3bt3VW/3sGHD1OcSYJYpUwa7du1S7/fv368CuQsXLsT30MpnEpS9iI4dO8b3skuvvzQO9OvXT/U2i+7du6NXr16oUaNG/Ho//fTTU8skOE5q8uTJ+PXXX1VAKT3TEmza2Nik+dyfh5mZmTqX7du3IzY2+eGuffv2VQG/BPFSA+CLL76I72lP7VjFkSNH8NFHH6nj3Lt3r1omQb7cIynAJ9dMGjYk+JeGkIT+/fdf1QgiPeFyvvJ+9uzZ8Z//9ddfiXro5X7KtU9q27ZtqhFGrpc0qPzzzz9Ys2ZN/Odt2rRRWRVyjpJxIRki8iy6deum9r9nz5749Xfv3q2yA+Qzennm6bANeo3YOTijyuSdODG5G6I3HYXNPQt0XH4fOy91wrZPO2Jgjc9ga2Gb2YdJRERE9OaIjQB+zJs5+/7CD7C0e66vSLAnQaT0/nbq1Ompz3/55RcVTEVFRakib9Jj+v7776veYVMjgAR3O3fuVL28Ery3aNECx48fV8FY69at1fP48eNf6tSkcUBSyaVHN1++fCpgLVy48EttUwL2QYMGqd5kYWFhoRo4nufcn4f0kkv6vgTE0guelKT0Sw+6ScOGDdN8rEJ69Xv27Bn/XgJzuR9yrSRoluOXR6NGjVTALj33JtKTXqlSJfXay8tL9dBLEC+NDCYy3EHugdyL5s2bq2OS1HnJ9DCR9YsUKaJeFy9eXNUQOHXqlMrQOHHiBPbt26eeJetBSOOPNNAIWSa993JtJQtByGvZV968mfRnKothQJ8CSeuRR0qpSq8zcwtLVP38bxzO/RXc1s9D8Dl7NDwD3Bm1GAM67UT/939EjTw1MvswiYiIiOgVGTJkCH7//XcVwEoKvokEstI7On/+fKxcuTLRdyTAkvR66SWWgF4CQAn4JID89NNP4eDgoIJ86b2X3tgX7aGXXmHpCZaAWoJga2tr1UMtj5chxy49/pKenpy0nvvzkPHvwsrKKtnPJbPg448/xsyZM1XQLen9cnypHatJ0gJ7cs20Wi369+//1LqS1p+QBPEJlS5dGn/++Wf8+3nz5qmGH4l3JF3elBYv9yFhQJ808Lazs0NoaKh6LdkGkqkgQzRSIr8jOW/5Pco+pId/wYIFzzxvSjsG9CkYOHCgekgKTsIf9JtCo9WiRvfvcTxnQRRdNxZ3DjnB45EWQ6ffxdyrvbDpgw/wWdXhcLB0yOxDJSIiInq9SXaj9JRn1r5fgASm0kM/fPhwNUbaxNzcXAWE33zzDbp27Zri96U3VcaVS2V8SaGW4F0CehmbL+PppcdWxoa/CAnwJJCVVH5TICzBZ9LieM9Lerjl3KQGQHLSeu7PQ3qqpRfa1dU12c/79OmjxqlLo4ik1ktvvDSySK/7s47VRILlhCwtLdV1kmuXWoyStMie7EuCcSENMnIfli9frlLmTQXwJCPgee6DNMZIg4A0lpi2nZSMw5cGA6k5IAG9ra1tonoM9HI4hj6Lq9yyN3y7zUOe5mGwzhMFyzig30Y9PCatwIdL38Vun92ZfYhERERErzeNxpj2nhkP2fcLkqBRUtm///77REFt9erVVSCXUm9zwnH0UjhN0uBlOra6deuqHmLpYX3R3nkhc9S3bNkyPpiX1HGp8p40UEw6Lj25ZQmZzm3dunWJlpu+k9ZzTysJiqWnOblhDUIC47i4ONUQIqnwkv0rBfMkOyC1Y02J1BCQ765YsSLVc5BsioR27NgRnxEg11uC64SFDaUWwPOSc5BGByn+l9J5SMOFnLek2sujS5cuqvGF0gd76LMBrxpv4bbLelhat4fjpWDcP+2I2heBInfvYYrfQGys3Qojq46Ei/WT8T1ERERE9GaTQOvHH39U6c5J08ClOroEWdJTLYGnjImXYD1hoChB+9SpU+NT9iUwlfnYpac5YVX65EivbcLp3IQEcdIDLL3UUpVdGg0kdVt6/RNWQRelSpVS47klCJUecBmrntyypGRcv4zPliEHMme8FKKbNGmSyjR4nnNP6XzkOCVz4dChQ5gyZYpKNf/uu++S/Y6Mn5cK8zJcQa6bFL3bsGFDfOG51I41OZL+LtdLviNj3eVaSsE8aaSQ+2Uau24K0EeNGqV6w7du3arG2JuK00mWhUxxKBkcUthPxsAnrLCfVnI8sg0ZAiDTEcp5SiE/KbQnwwwSNi7JdZLrOGvWrOfeD6WMAX02UaBEBfj3345Hs9qgUM6b8DngitxBZvh+gQ6LfNaite8BjK7xBZoXaA7NS7QEExEREVHmcHNzQ8mSJRMtkwJ28pCxzhLACQkCJY1eAtLPP/9czbEuxdrmzp2b6LtSCE+CZ+lNN5HAU1K3n9VDL8chmQGSap6QFEuTiumyny+//FJVlpe0cSm8J8GnqaiakEYIKQAn60ng+vPPPye7TDIHEo4zl6ECko4uFeQl6JWhAdOnT4//PK3nntL5SCOJo6OjKg4nPe5ybaUH2iTh8UgDyKpVq9S+ZPvyPTlnUwNJascq9QWS67H/9ttvVUOCZAfIDAEyhZ4cm1S/T0gq38t4eDlPGYIhlemlgUFIY8rGjRtVo4IE47JvmUZOgnPJhDCRBpikwwlkfwkLAMp+ZMiEfF/G5cs+Jk6cmOg7UviwXr16CAsLU5kflH40hqTNYZSIaQy9/KUhfwjfdBFhwbg8/UOUCzqIu0ecEXrHWPjjWFENprfUonrJJhhTfQxy2ubM7EMlIiIiyhQyHlim6JLALGFwQ/SmkKBbetxNc9dnNhkOIEG99NInrLKf3UU94++atMahHEOfzdjaO6HcsLU4lrcN8tUORK7KQTCYaVDlmgE/zdHhzv5teG/1e/jv2n9PpT4RERERERGllcQTMne9TMcnWcAyfp7SFwP6bMjM3BzVBszC4eLD4FosAoWb3Ife0Rw5QoGvF+vQZGcwvto7Fh9v+xh3w+5m9uESEREREdFzSC5VPjPIkIGPPvoIx48fV8MPnndaQEodU+6zWcp9Uic2zkXpQyNgEReHKyfyw3AzTi0/W0iL31ppEOtsh2GVh6FdiXbQatj+Q0RERFkfU+6JKCMw5Z5eWqW3e+Bmi8UItrBHyeresKmuB6ysUPamHr/M1aDI1TB8f/h79NzcE94h3rziRERERERErwl2uRJKVm+GsE7r4avJhYKF7iFnI38YPPLALiwOY5fp0XmPFifvHkPbNW0x//x86PQ6XjUiIiIiIqJMxoCelPzFysPm4524bF4Sbk6hKFz9NGJrloPGALy7PwaTV9jCLjASk45NQpeNXXAt8BqvHBERERERUSZiQE/xXN3zocCw7ThpVwfWFnEoV2ATwt+vAa29PfLdCMXUBZaoc8MKZ/3Pot26dph5eiZi9U/PjUlERERERESvHgN6SsTa1h7lhq7GIfcO6n0Vq1UIbl0YVmVKwzw0EoOWhWPMoTxATCx+P/U7Oq7riAuPLvAqEhERERERZTAG9JTstHY1BvyJQ8VHQG/QoFrcJoRWCoZDp47q8/I7fTD339woFuaAy4GX8dH6j/DriV8RrYvm1SQiIiIiIsogDOgpRTU+GotTtaYiymCB8jFHEIX/4Dj+W5g5O8Pqmi9+nBONjx+Whc6gw6yzs9BubTucenCKV5SIiIiI6DncuXMHHh4euHXrVobND1+zZk2cPHkyftnvv/+OihUrIn/+/Ni8eXOGHEdWNn78eHzxxRevfD8M6OmZKjXvglvvLEMgHFFUdx3a06Nh8dM3sKlSGYbwCDScdRJzT1ZDHrMcuBl8E103dsXEIxMRERvBK0tERESUwfR6PebPn48WLVqgWLFiqFSpEjp37oyDBw/GrzNz5kwVPJoepUqVQps2bXDo0KGnthcSEoIff/wRderUQZEiRVC7dm0MGTIEt2/fTtPxdO/eXe1jwoQJT302evRo9dmgQYPiA9pnPX766acU99GvX79Ey/bt24eiRYvi+++/x5sgLi4Ovr6+6jkjTJ06FU5OTiqAF+fOnVP39X//+5/6rdSvXz9d9yf3Tu7hmDFjEi033fczZ8680Hb9/f3x1VdfqcaJ0qVL46OPPsL58+dTXP/atWvw8vJCuXLl4OPjk+J6e/fuVduSxo3p06c/9fmKFSuS/Y1GRDyJgXr16oVp06bhypUreJUY0KdALr7c7KpVqyK7K1mlMcI7b4SPJi/y4CFybO+O8L7t4TbgY0Cjgd2mA5i2xA5dbBrCAAMWXVyENmva4NDdp/9RICIiIqJXQ4LB1q1bq15BCdDXrl2LefPmoVWrVvj8889V0CZCQ0NV4CEBvDxWrlwJNzc3NG7cOFGQ4+fnh8qVK2P16tUYOXIktmzZgsmTJ6uGgg8++CDNAZcclwRF0thgEhUVpRoW5LOAgADkyZMn/njk0b59e+h0ukTLkgbtCffx6NGj+Pfr1q1Ds2bN0KFDB4wdO/YlrmjWJNf8559/xscffxy/7MKFC3B1dUXTpk1VYGptbZ2u+wwODsb9+/fV7ydhY5CpISMmJuaFttu9e3dYWlpiypQpWLZsmTruWrVq4fr160+tK/uR85Pfr7u7u3r98OHDp9aT7cifIWkUk+OTRq2kwsPDn/p9ysPGxiZ+HdlHy5Yt8euvv+JVYkCfgoEDB6of9tGjR1/pDXhTeBQtA/sBO3DJwguOCEex7d1xu6AlPOfOgVlON8Rdv4F3f9yLObGdkNs2F3zDfNFnSx98feBrhMaEZvbhExEREWV5v/zyCzZs2ICtW7eid+/eKFmypOqJ/PDDD1WPo/TEm2i12vheRenZlF54CfKPHz8ev07fvn1V0LJ792688847qoe+Ro0a6v/JyfXmp6Rhw4YqmJfjMvnnn3/UvitUqKDem5mZJerpdHR0THZZahYuXIi2bduqdOcffvghfnnz5s1VL/GAAQPUdTE1DkiQ16NHDxQvXhxly5ZVgZw0NiTtQZZGAmkkkWsogWDSGEGCvhEjRqhtyEOu/927d59aRxpGypcvr+6L9Cw/K5CVY6lWrVp8I4tcPzkPyTyQa7p48eJE6w8bNkyd33fffae2X7du3WS3u3PnTtUI8vbbb6v3X3/9Nfr3768aReRcZfsmCxYsUAGy3Pu33npLZT68yD5FiRIlUKVKlXRtZFm9erXanvTQlylTBrNmzYKVlZXqQU9Izk0aeWQ9+e2tX79eHY+cU9KAXRqr5M+LZLbIbzAlSX+f8tBoNInWef/999V9kj9HrwoDekozl5x5UHDoVpywrwdLjQ5VTozE2UtrUGjVKtjVrg1DVBTsJ83HrINl0MmzjfrOP1f/QevVrbHbZzevNBEREb2RDAaDGk6YGQ/Zd1rNnTtXBRCSZZqcZwUnq1atgrm5uQrKxIMHD1TQ89lnnyXbW/usbSW3rvSkzp49O36ZBF6SkpyepCdUAmnZ9uDBgxN9Jr3DEiAXLFgQa9asUQG/9L42adIE9+7dw5IlS9T3JRCUbSTtQZZhAXK8//77LwoVKoR33303Pr1axqNLsHjz5k3MmTMHS5cuVcslbd3UOCCBu2RAbN++XfUm//3337CwsFAZFElJ8NenTx8sX75c9RZL2vd///2Hjh07qiBTGm0kPV6CadmfiWQ6SNaDHK8EkUmDWhNpoJFGBenZFkOHDsU333yDHDlyqIYa+VwsWrRI9eLL9ZBrJtkajRo1wsWLF597nyYTJ05U650+fTrZz2WfqQ29kHuQ0u9Qr9er6yfX1iQsLEz1tstwEdm+/M4l6JfAXhp3pLEqYSNOWn/b0igijQjSgNOlSxdcvXr1qXWkASwoKCjF800P5q9sy5Rlp7WrMPQ/HPrrU9S49zdq3JqGI0u8UXH6LAQvWIiHv/yKiA2b8cF5TzQd8xW+8p8H71BvfLLjE7Qs3BKjqo6Cs7VzZp8GERERUZpFxkWi+uLqmXLFDn90GLYWtqmuJ4HMpUuX0K5duzRtNzAwUAVHphR8CYIkyCxcuLBaJpmqQnrv00PPnj3VtqSnVAIcGactqf4bN25Ml+1L77UEaBLsybCD5EjgJkMPTCS4k95vORZnZ+P/T6UxQHq/x40bp1KzTaR3X9KnTa//+usvnD17FtWrV1eNId7e3qr3WoLFhHUKJPiWnn0JdGVst6SCy/ACIYFgwmEIIjo6Wg03kPVke7lz546vN/Dtt9+qwFFIRoEE0ZKVIdfWRBobZHiDZGCkRFLeTccgZCy9i4tLfNaGifTcS7Bv2r5kPBw4cEBlc0gmxPPs00SCarkPkqmwadOmpz6XBqkGDRo8cxsyNCAlP/30k2poke0kDNDlHuXLly/RunKv5DxkaMnzksYQyciQeyuNNTKUQOoRSOAu2QwmpussxQ6lnsWrwICenpvWzAw1+k/H4aX5UeXiRFQLWIszP7dC4QErUKByFfgO/wyxt71hN/A7zBk+FAtL+WPhpUVYf2M9DvodxBfVv0CzAs2eSkkhIiIiohcjAbkEh9LzmBYSxJnS5qUHU1KXpQdaerAlzdtUnC2t20uNNBRIurME0dJb/t5776ke4fRSoEABFeCaCgKaep8TShpQSfAlGQmmYF5I8T8JACVYTxjQS09swmsn10V6aIUEudJTLanqklFhyqqQxgspwiak0UD2lTCQFkmDYBn3b2dnh127dsUflzS+SGONBPRStM60j8jISDWWOyEZwpBaYC331tTwkBLZrjQq1KtXL9FyCbal4eR595mQZEdIY4ZkKyQMfoWcuzxexKpVq9QwBskEMTVMCRnXnjSYN0naiJFWki2RMJaRDA/5jUiDwowZMxJtXx6vstghA3p6YdU/HI1TWwugxL7BKBd1FNd/bQTHXv+i8KpV8Bs7FmHbtiNg/P/QoUljNBv6B746+z9cD76O4buHo7FnY4ytMRZuNm68A0RERPRaszG3UT3lmbXvtJAUYwlMTAFkapIGMpJ6LGnd0tMoad7S6ypkexKIpwdpMJBq9xL8yvCA9CQ91tLbKin0MoZegs6kQX3SoQPSG560wUKCeQl25bOky5MyBe6Sri0BasJU8ITBv5CALi2NI9KIIMG/jL83BfSmdHApZJd0jHrSDrK0FLOTRoWEtRKSYzr/pMcs75Nem+ctoCd1CKRugWRLSJZGQtLgM2rUqFQr9CfsgRcyJECCbPmsa9eueNWSXnf5fUh9gITDEUyNOtLQlrQhJz1xDD29lApNP4LPeyvwCE4oorsB3Z+N4e13FR5TpyLXmDHQWFiowN6mz1gs8PgC/cr1g7nGHNu9t+O9/97DmutrnmtsGBEREVFGk/+8S9p7ZjyeJ6NRAlkJkJKr3J0W9vb28QXCpOdUxlkn7G18WXJ8UmROAmYJvNObHK8UfDty5IhKu08aeCYlRdFkaIGMgTeRgEwCaGkgSCsZSnD58mU4ODg8Nd5blgmpayCzDEiv+rNID68UMZS0f1NwmCtXLpXNIO+Tbj+lnudnkXHdp06deirdP2lau8x8kHTst8xb/zzXJiUyZl/Ox1RvwEQC9aSV45M+pDBgQmvXrlXDFKTBQ4r7ZZYbN248NRxAGk6kEUSC/VeFAT29tOKVGiCq62Z4a/MhN/zhtuwdnN+/Fq5dOqPA0iWwKOCJOL+78O3WEx8etcKSFotRyrUUQmJCMGbfGHy8/WPcDUtcBZSIiIiIno+kG8tUWTKu1zQXt3ScyPhd6Q2VtO2USCE0qewtFdxNJJiXAE6CJCmSZ+ppliAlYeG4tJKeXJmTW7b5PCnaz0PSniVdXfYhaf0Ji50l1alTJ3U+X375pXqWWgJSbE6quksBuLSSce3SGCKF/0zT58n1kvthuuZSzE4aMqTInAxxkPuybds2lSaelIyLl9R7UwE6uVbDhw9XxfQkeJXvyvFK40XCSv5pJZXdJZiXoQLP8umnn6oGBtM5SJFEOV4pEPiy8ubNqwoXSpG8hCTdPrWieLa2T2pKbNiwQdWNkGsj1fYzihRYlAYa072QoRByPZMWepQ6ATIEJOF0dumNAT2li3yFS8Fp4E5csCgDB00kim/tjqP/TYNN6dIo9M8qOLZqJYO78HDKFNiOnIwF1X/D4EqDYam1xH7f/aoS/vLLy6E3pNxSSEREREQpk95BSdeW3mBJzZZCZzLVm/SGS3E1Uxp9wqJ48pD1pEdbqqZLQJuwJ1d6RKVXXcbWS2OBpJBL4GQqEPe8cubM+cyiZulBUrqlgUICLinAllKvuKS0yzADqR8g10l6waXAWdLp4FIj10+CawnU5TrLe8kWkJ5Z0zWXc5YAXiqhy35lHQmWq1atmuw2peK+BKoS1EtALUXkpFCfVL+XxgP5vhSnk+D8ecm5SuApBQCfRVLf5fpJsTf5jqSyT5o0KVGjz8uQ7T/PbAnJGTJkiKofIdciYdCfsPjh85LaAabtyAwIMuZfXpsKEgrJoJBigXIv5X7IEBLJjmklMc9jkiEiv6WEf6ZeBY2B+c7PJGlH8hdXcHBwmua+zO6iIsNxfnpnVA7dod4f9OyHGt0nSK4agletwr3vvlfT28nc9fl++gn3S7lj3P5xOPXwlFq/Sq4q+KbWN/B09MzkMyEiIqLsSnp1ZQoyCcaed3zw60L+iy9j1aXHM+k5SOApleZNpMcztSBbeiGlEUDWe54gTHqspYdZAtDkSGE52V7Sz+X/4FKt3FTlPbV9yNCEpOcgPe7yf3gJ1OW1nKcEX8mRayV1CEwp8iYSLMp4dhkDnfC8pQCfbDfptZXGA/n9pHS+puOSa5Kw+Nuz9iPHbdqe3Fc5X4lPEk7NJuT+iGft20S2IUMFJCtDxu3LtZZrldxYb2nkkN+LpOAnzaxI6z7lfsq1keEDSY/DtDzp+aTFvXv3ki04J9c2LdchObI92W5Scq/lGiQtHmiaBi8pyRqQ65tcbYW0/F2T1jiUAX0qGNA/P71Oh8OzhqDm3QXq/RHnFqg4YB4sLK0Qfe0afIcOQ7TM06jRwO3j/nDp3w/Lrq/Eryd+VdPCWJtZ45OKn6Bzqc4w075cqx0RERFRdgzoiVIjjRgSiL5oVXl6NmmsksaYhEMEXkVAz5R7eiXT2tXsNxWHvcZCZ9CgWtAGXJr8FkKDA2BVtCgKLl8GZ5kj1WCA//Q/cKdnL7R3aYx/3v0H1XNXR5QuCpOOTULXjV1xLTBtlVqJiIiIKGPJOPVnjXU2Fdij15NkNDCYf3WkN/9ZwXx6YQ99KthD/3JObV+K4nsGwVYTjRvagrDruQq5PIzzTQavX497X42DPjwcZs7OyDNhPOzr18c/V//B5GOTERYbBguthaqM37NsT/WaiIiI6FVjD33aSCq2qVhecqQC+/NU6SfKbqKYcv/qMaB/eVdP7oHL6i5wQxAewBVhHyxF4TLV1Wcxt2/Dd9hniHpcidW1e3e4DxuK+7EB+O7Qd9hzZ49aXtK1JL6t9S1K5SiVDkdERERElDIG9ESUEZhyT2+EYhXrIab7FtzW5oc7AuC+4j2c3WMsDmFZoAAKLFkM125d1fuAefNwq1NnuAbE4vdGv2N83fFwsnLCpYBL6Li+I+afn89564mIiIiIiDiGnjJK3oIl4PzJTpy3LAd7TSRKbu+FI//+pj7TWloi1+jR8Jg+DVonJ0SdPYub77dB6KZNaFW4Ff577z80LdAUOoNOja0fvns4wmPDefOIiIiIiChbY1E8yjBOrjlRdNhmHHNsAguNDtVOf4mDsz+DQW+ce96hUSMU/ncVbCpVgj4sTFXDvzvua7hq7DG5/mSMrjYa5hpzbLm9RfXWXw+6zrtHRERERETZFgN6ylBW1raoPGQFDubrod7X9JmFY79+iJjoKPXeIm9eFFgwHzn69VPT2gUtW4Zb7Tsg5uZNfFTqI8x9ay7cbd1xM/imCuo33dzEO0hERERERNkSA3rKcBqtFjX7/IIjZcYhzqBF1eDNuDKlOUKCHhk/NzeH+9Ah8Jw9C2Zuboi+cgU3236AoFX/ooJ7BSxvtVxNbydz1o/YMwITj0xErC6Wd5KIiIiIiLIVBvSUaap9MAznG/yJcIM1ykSfwqPfGuKez5N55+1q1VIp+Ha1asIQGYm7X3yB++PHw9XSGTObzkTvsr3VeosuLkLPzT1xP/w+7yYRERERvXEePXqE3r174+HDhxm2z2+++QZnz56Nf3/mzBmMHj0affr0walTpzLsOLKqBQsWYM2aNa98P5yHPhWctu7Vu3Z6P5z+7YScCFTT2oW2+RtFytWK/1zG2PvPmAH/36aq9/aNGyPfT/+D1tYWO7x3YOy+sQiNDYWrtSt+qvcTquWplgFHTURERFnVmz5tnY+PD/79919cvXoVDg4O8PLyQtu2bWFjY6M+37JlC5YvXx6/vqxTqlQpfPTRR7C3t39qeydOnMCGDRtw9+5d5M6dG1WrVkXz5s3TNMf81KlTcfr0abRp0wYtWrRI9NnKlSuxadMm1KxZE61bt8bIkSOfua1WrVqp9ZLbh4WFBfr37x+/zN/fH19//TVKlCiBTz/9FK+7W7duqd+b3LOiRYu+8v3J/Rw2bBjOnz8PMzMz3Lt3D0WKFFHXUH4LTZs2RYECBdJtf//99x/WrVuHd955B++9916ihgy5719++eUL7+/AgQPYunWritvKly+Pjh07qt9DSn+2v/jiC5ibm+OHH35Icb20bDMsLAyzZ8/GlStX4OnpiR49esDd3T3+85MnT+Ltt9/G5cuX4eTk9Nx/16Q1DmUPPWW6ouVrQ9dzK25pPdW0drn/eR9ndq5MlKKfc8AA5Pt5CjSWlgjbvh23u3ZD3MOHaOTZCEtbLUVxl+IIiApAn619MOfcHE5tR0RERNmSBLfFixfHnj17VIDm7OyMbdu2oUyZMrh9+3Z8T+yyZctQo0YN9fDw8MC0adNQpUoVhIaGxm8rNjYW3bp1Q/369VUwLw0DOp1OrVu7du00Hc/mzZvx999/q97gpMaMGaM+27lzJ6ysrOKPRx4PHjzAqlWrEi3Lnz9/ivuQc0zYoFG3bl3VkNC1q3FqZEpMGjukoUOCeXH48GF1DyZPnqwyBdIzmBfHjh1Twe/HH3+MiIiI+OXye5PlL5qZ0KVLF3z++efQ6/XIkycPJkyYgGrVqiE8/OkZseLi4tCuXTv1u1q4cCE6d+6svvci25Tjlt+k/H4LFiyogv8KFSrgzp078etUrFhRNSjNmDEDr5L5K906URrl9iyG4E934tyMtir93mtXHxwJ8EG1tkPj13F8+22Y58qNOwMHIurcOdzs0AGeM2fCs1gxLGqxCN8f+h5rrq/Bz8d/xukHp/F9ne/hYOnAe0BERETZgvS6Dho0SAUZ0tuekATIlpaW8e8leJPAzUR68KWXcO/evfE96WPHjsU///yjeuilkSAh6ZVMq2bNmmH79u2qN7h06dJq2e7duxEUFIRatYxZmZIZkPB4JDCS/SZclhbSGyq9y9KrKlkIpqyEb7/9VgVg0iO6Y8cOlCxZUvVGS9C2YsUK7N+/X10TyQKQBoykPchfffWVyia4ePGiagDp16/fU9kMco6yjqhevbq6pkmzGKTxQtYxGAx49913UadOnRTPRRopJItBGj6kMUOORdK4r1+/rt5/+OGHiQLvuXPnqvORayxBq/QoSy90UpJOLwG26VilcUeCTrk2cr3le3/88Yf67MaNG1i0aBHu37+PYsWKqV7ohL3Nad2nkAam6OhoTJkyRf220sOYMWPUvTSR45PecgnYE2ZsyPXu3r27+t3Kb1yOo0mTJqqBYebMmc+9zd9++001Qly7dk1luAwZMkQF8PI7+/PPP+O/Kz37kyZNSjX75GWwhz4F0vIorZCSUkQZw8nFDcWHbcZRp+Yw1+hR7ezXOPjX4Php7YRtpYoouGwpLAsWRJzfXdzq+BHCDxyAjbkNvq/9Pb6s8SUstBbY4bNDVcG/Epj2f2yIiIiI3mT/+9//VICcNJgXkgosvfUpkRR14erqqp6lF1V6+wcPHvxUMC+SW5YSCXg++OAD1RNrMmvWLNV7nlLK84uQIFUC5AYNGqghB6ZgXkiwKYGZBGKFCxdWwamQayWp57ly5VLBtwwl+PXXX5/qQZZtynhzCaClwUSCQQkSTYYOHaqCYUmNlmEJ0gPeoUOHRMcnjS0SxEsAnDNnTpX6LY0JyZHGCBmmII0TErxLQ0XZsmXVOcq1lxR96RE+cuRI/HekkUQCZQk65X7L58mR3mRJqzfdazknCbblXkijhzRGiOPHj6NcuXI4d+6cumZyTBK0SkPM8+5TSEq5BLzyOzX93pKS7cl1fNbj0KFD8euXTBB4ixw5cqiHNEAkvfZy/ySYl+spwxrk9a5duzBq1KhE66Zlm2vXrlWNP/LbFnLtpPdflickmSLSACMNI6+MgZ4pODhY/qSqZ8oYep3OcGDWMINhnKN6HJ3cxhAVGZ5ondiAAMPNTp0MF0qUNFwoXcYQuHJl/GdnH541NF3R1FBmXhlDlYVVDGuureGtIyIiojSLjIw0XLhwQT2r/5vo9QZdeHimPGTfaWVlZWUYMWJEquv99NNPat1evXqpR7t27Qz58uUz/PDDD/HrHDhwQP0feP369S/1y2nZsqWhU6dOhj179hhy5sxpiImJMQQFBRlsbGzUNW7evLn6PKlx48apY0rrPooXL25wcHAwDBkyJNlrVr58eUPlypUTfbZz506DVqs1nD9/Pn7ZX3/9ZbC3tzc8fPhQvb9586a6DlOmTIlf5/r162rZmTNn1Pt9+/YZ7OzsDH5+fvHr+Pv7q+3s2rUrfl/yHbmuJnIsvr6+ifZz9epVwx9//GFwcnIybNu2LX7dJk2aGIYNG5bonEaNGmVo0KBB/Ptu3boZXFxc1PV9lp49e6prltDChQsNuXLlSrSsfv36hg4dOsS/j46ONhQuXNgwcuTI597nmDFjDKVLlzbodDr1PGjQoETnffToUfX+4sWL6h4863Ht2rUU97Nx40a1Pfm9mdy/f98wa9YsQ0BAwFPrP+uzZ20zR44chm+//TbRevPnz1frhYWFxS+LiIhQy9atW5emv2teJA5lyj29ntPa9ZqMo6s8UeH0N6gSsg3npzSHR/9VcHLNqdYxd3GB55w5uPvFGISsW4e7Y8YixtsHOQcPQhm3MljWahlG7R2FA34H8MW+L3D64Wl8XvVzWJo9STUjIiIiSguZbedypcqZcrFKnDgOja1tqutJCrE8EhblehZTb6ypuFdAQIDq1e7Vq5fqrZaCXCKt20uN9FS6uLioqt/S0yk9vdJLnF4CAwPVeciwgZSK9b311luJPpPeWemBlqxck06dOqkq79I7Lb31JtIjbyK91XL9ZFiA9JpLsTc7OztVJ0B67U099zLEQdLbJYVfhkNIHQMpAGgix5I3b95Exzh+/Hh1jaQX3ZQpLNkSkqov25OecNM+pJha0mr0kqGRUgG2hFkHyRU/TDreXIYhSMq/iexfeqGlF/1592mi1WrVOUrGhmR/yPukveNJe8jT6vr16yrro2/fvur3ZiK/YfldJ+dZnz1rm5GRkfG98yam9/KZ/B6EZIlInQLTn6dXgQE9vbaqthmMsznyo9COASgdcwa3f2+AW3XGoVyDD1TQr7W0RN6f/gdLz/zwn/4HHs2ciVgfH+QZ/yNcrF0wvfF0zDgzAzNOz8Cyy8tw8dFFTG4wGbntcmf2qRERERGlKxn/Lene3t7eaV4/4fh0KZAmgdT333+vUu1NgbxsT4rlpYeePXuq9HWpqP7JJ58gPdWrV08F4BLwSuE+SYFPKumQA6kr4ObmlmiZBGASjCVN2U6Yvi8kEJX9CEkfl/T1pNdJCqmZlkmDgzSUpEYCd0mpN9UaMH1X9iUNAqahAqbtyxjtZ51jciR9PLUUcGngkaA+6fWR90mvTVr2mZBUupe0fhmrLsF9QtJYIGPVn0V+t6bGKBMZgtCoUSPVeCJDp9PDs7Ypf9bkviS9ZtJIkzDQl8YTuXdyzV8VBvT0Witbvw2uu+ZFxD8dUUB/BwX29MG1/RMQUuVTVGjWFVozM+QcNAgWHvlx96uvELJhA2Lv34fH71NVL/7ACgNR1q0sRu8djTP+Z9B+bXtMrDcRNfM+aR0lIiIiehaNjY3qKc+sfaeV9CJLoTMJxGRaruchvYgSLF64cEG9l55rCeplTLCM5U4PUjFfpieTxoT27dsjvUnPupy3BHxyDUaMGPHM9WUsdcLq+EIKz0k1cymCllb58uVTU4tJg0XSHmcTKaQnRfNSI2PqBwwYgJYtW2L9+vWwtbVVQbT0jkv2wfMWCUyOFAyUrIJnkTH+MuZdZkZIOKOBvH+ea5NavQfpqU9IGj2SBuvJHVtCt2/fRsOGDVVGw5IlS577t5+c1LYpjSumPysmUvRRxubL7zvhMgnyn1Vb4GWxKB699oqUrQGzj/fiUO5OiDBYoajuOiodHgKfH8rh6H/TEBsTDec278Nz1l/QOjgg8vhx3PrwQ8TcuqW+X8+jnkrBL+VaCoHRgei3tR/+PPMn9Ianp6kgIiIiSkr+Q661tc2UR1rmejcZN26cmrJt9OjR8b3HJlLZ3c/PL8XvSqEzSTOXQMUU4H/33Xeqqrqk4ickwbIEOc9LisVJRXUprpZayveLksJ38+fPV9dApht7FpnTXnqqEwa3UpFcgm9TYbi0kGrzUvFcpnxLSFLWTddcGjAk20GqwptID69c84QkdX3Lli2qUUGKrkm6vQSIkur+008/JeodlyEGGzduxPOSoQdyXFKhPSXyu5OGHOmZlqEcQr6zePFiVb3/ZUnQLtdfeukTkiyR1IriSQE/E29vb1WwsHLlyli6dGm6BPNp2aZkRsi1l5R8Ifdf1k2aMSHDOqRRIL2GriSHPfT0RsiRywM5+k9HkP9XOL36J5T2WWLssT/1BfxO/wKfUn1R/p0BKLhkMXz69UfsbW/c+rAjPKb9DtvKleHh4IGFLRbix8M/YtXVVZh6cirOPjyrprZzskrbmB8iIiKi15n0qkuQIWOCpadXxmvHxMSonkTpjZbq7AmDQVNvrwSPEnjIOtIoYCLjhiWYkyBZUvElDVwCF5m6TXqQkwYvafH+++/jVZP5xSUIk/nEpfEhpSnSZAy/TLEmwbZkN0gvu0yVJ9cuaYr9s0gQKo0IMpWdfFcCTqlKL40WMu2faV8yndnAgQNVUC8BnlRdT1j5P2lQLxXuJaiXBofff/9dBdIy3l9SwGWKObkPUin/ecm86XL/pLFGqs4/qxe9cePG6nclvfqSDi8NHQmng3sZkm6fcGjBi2jXrh18fX3VNUl4XDLeXTJCXtU25VmyYaRhQoZ7yGwD0jsv89cnJFP+pZYp8rI0Uhnvle7hDScFDOQPlfwBl7ES9HoIDQ7Auf8mo8TNBXCFscjEQ7jgerEeKF2nC/yHj0LU2bPQWFggz4TxcGrZMv67EtD/cOgHxOhj4GHvgZ8b/oySri9WfIOIiIiyHgmWpOCYpDhL2vGbRnrnjx49qnoPJaiUoEmCDRMJJA8fPhz/XtK6JQiVMdnJZQRIwH/gwAE19l1SoiW4S8t4cCGBqaSLS49ncjZv3qx6oJN+LoG1BMVpaTSQfUhGgQSfCe3ZswdXr15VPcHyWq6BFLFLSnrpZSo003EkHO8sDR/S8ypBf8JYYN68eWp/0giSMMtB9iPXSwJvuU5Jybh9WUcaHCRANO0ruf3I9qRBQMbhm7Yl10UaaCQNX4JrKTSY8HxlSryUrnVCZ86cQbNmzdS87LI/+a3IbyLplIfSICSNPXLvZWx/0nT4tO5TMhEkjT3p8A1prJBtyz1KOl4/LVasWKHitOQaWWQKwxfxPNuUaya/UxmGIIF9wiEXcm4y//zp06dTzBx41t81aY1DGdCnggH96y0yPBSn1/yGQpdnIxceqWWBcMSlfB2Q+1goonbvUctyDhmCHP36xv8jdeHRBQzbNQy+Yb6wMrPC2Bpj0bpo60w9FyIiIno9vOkBPVFayBh9yRyQqv2U/mSYi2RimIaxJIcBfQZgQP9miImOwql1fyDvuRnwMNxTy0J0Nrh+owKsj99W750+aIs848apXnsRHB2siuXt9d2r3rct1hajq49WAT4RERFlXwzo00Z6tn/88ccUP5dq+c+Tuk6U3USlQw89x9BTlmBpZY1qbYci7t2BOLZpLtxO/o6C8EbFYgdx3+CIRyftEbzyH8T6+sLj119h5uioxs7/3vh3/HXmL0w7NQ3/XP0HFwMuYkqDKchnny+zT4mIiIjotSbTcz2rIrmkwRPRq8WU+1Swh/7NpNfpcHr7Etgf+QXF4q4i1M8KvgdcYIjTQlsgPwrPmQuLfE+C9gO+BzBy70gERQepQH9C3Qmok+/Fxt0QERHRm4099ET0pvTQc9o6ypJkfvqKzTqj6BdHcLbhXPgULI6Cjf1hbqOD/rYPrrR6CzfWrohfv1a+WljeajnK5CijUvEHbBuAP079wantiIiIiIjotcWAnrI0jVaLsvXbwOuLfbjR8W9EtsoHK+dYaCPjEDNqLE590hDXTu9T6+axz4P5b89HhxIdYIAB009Px4DtAxAUFZTZp0FERERERPQUBvSUbXjVeAvlvtuJuCnTofOwgUGnhdW2u3Cd0B5nJjTBxcObYWlmqSre/1jnR1ibWWO/7350WNcB5x+dz+zDJyIiogwm03EREb3Of8dwDH0qOIY+azLExeHqsMHQbdmh3rsUC0OuiiG4YF0W+jqfoUyd93Al6Kqa2s471BsWWgt8Uf0LVQk/uflZiYiIKGv9J1vmL5eibjlz5lTzqPPffyJKLwaDATExMXj48CF0Oh2KFSuWaA57wXno0wkD+qxL/iAFzJmDBz9NUu9t80Yjf80AaC0MuGJeHOHVBqNQ3Zb46uA47PTZqdZ5t8i7GFFlBJytnTP56ImIiOhVkv9s3717FxEREbzQRPRK2NraIk+ePKrRMCkG9OmEAX3WF7JpM/xGjoQhOhq6nHYoUNMHDrZR6rOb2gJ4WHEgznhqMfX076pInoOFA3qX642PSn4Ea/PE1SiJiIgoazX+x8XFqR40IqL0JBlA5ubmKWb/MKBPJwzos4fIU6fgM2AgdAEB0LrnRFDzIvCK2QgHTaT6/I4mD3aWeg//Wl3F1aCralku21z4pOIneKfwOzDTcp5VIiIiIiJKHwzoM/hC0psvxscHPn37IebmTWjt7OD8w7e4cXc3St7+Gy4IVev4aHJhR/PPsOjOStwLv6eWFXMphqGVhqp56zm+joiIiIiIXhbnoSd6Tpb586PgksWwrVoV+vBwBHz2OUraVYblZ+dwqNgw+MMZ+Q33UXPbLKxqsRKfVf4MDpYOuBp4VU1v13tLb5z3ZzV8IiIiIiLKGKxynwr20Gc/+pgY3PvySwSvXqPe5+jTGzmHDsU9n6uwndsQTgjHIfcOqDHgTwRHB2PW2Vn4++LfiNXHqvXfLvg2Pq30KfI75M/kMyEiIiIiojcRU+4z+EJS1iuC4//7NPhPm6beO7z9FvKOH48z+/5FhX391bKTtaahYrPO6rVfmB9+P/k71t1YBwMMMNea48MSH6Jvub5wsXbJ1HMhIiIiIqI3CwP6DL6QlDUF/fcf7n75FRAbC5sKFeAxfRqOLfsCNe4vQQjsENZ9J/IWLBG//qWAS/j5+M844HdAvbe3sEfPMj3R2aszbMxtMvFMiIiIiIjoTcGAPoMvJGVd4YeP4M6nn0IfEgKLvHnh1K0rHl3/HSW1V9R89QVH7IWlVeLp6ySgl8BeAnzhbuOOgRUH4r0i77EiPhERERERPRMD+nTCgJ5E9I0bqgJ+7J07xgtiZQXbvKFwLxqM06XaqPH0Scmc9RtubsDUE1PhF+6nlhV1LoohlYagnkc9VsQnIiIiIqJkMaBPJwzoyUQXFobgf/9D0PJliL56LX65lXMswpq+g9KffwszB4enLliMLgZLLi3Bn2f+REhMiFpWOVdlDKs8DOVyluMFJiIiIiKiRBjQpxMG9JRcwbzIkycRtGwZgtathUZnMH5gZQWnd1rBpUMHWJcp81QPvATzs8/OxqILixCjj1HLmhVohsGVBsPT0ZMXmoiIiIiIFAb06YQBPT1L5IP7uD2sGSyuhCMmxCJ+uVWpUnDp0B6OrVrBzN4+0Xfuhd9TFfHXXF9jrIivMUe7Eu3Qr1w/5LDJwQtORERERJTNhaSxlhvnoU+nC0nZl9+ty7Cb2xAW/rG46usFqxtBMMQYe+A1trZwatkCzu07wKZsmUTfuxxwGb+c+AX7fPep97bmthhdfTRaF22dKedBRERERESvBwb0GXwhKXs7uWURKh4YqF6fqjAFnsHmCFy2HDE3bsSvY+3lBef2pl57u/jlh+8expTjU3Dh0QX1XlLwe5XpxaJ5RERERETZVAh76F/OtGnT1EOn0+HKlSsM6ClVh6b3RY0HyxAMO0T02IncnsUReewYApevQOjmzUl67VvCuUMH2JQpHV8R/9cTv2LOuTnqfRevLhheZTi0Gi2vPBERERFRNhPCgD5jLyRRTHQUbv1UF8XjruCyeQkUGrEnfn76uMBABP+3GkHLlyPm5s34i2VdurSx175lS9VrP//8fEw6Nkl91qpwK3xb+1tYaJ+MzSciIiIioqwvhAF9xl5IIuF38xLs5zeCI8JxKFdH1Ph4xlMV8iOOHkXQsuUI3bIFhthYtVxrawvHd96B28f9sSn8KL7a/xXiDHGok68OJtefDFsLW15gIiIiIqJsIoQBfcZeSKJkx9PXmYEKTTome3FUr72a1345Ym7dUsss8uZFgUULcVB/DZ/t+gxRuig1V/30xtPhZOXEi0xERERElA2EpDEO5QBdonRWsVlnHHJvr14X2vcZ7nlfTXY9cxcX5OjZA4U3boDn/PmwLFgQsX5+uN29B2palMBfzf6Co6Ujzjw8g24bu6np7oiIiIiIiEwY0BO9ApV6TcUV8+JwQjiCF3RGbEx0iutqNBrYVa8Gz/nzYJE/P2K9veHdvQfKaPJh/lvz4W7rjuvB19FlYxfcCH5SNZ+IiIiIiLI3BvREr4AUw7PvtBAhsEWJuEs4Pmdoqt+xyJULBebNhXnePKpwnnfPnihocMWitxehoGNB1UMvPfVnH57lPSMiIiIiIgb0RK9K3kIlca3m/9TrGvf+xqntS1P9jkW+fCgwbx7Mc+VC9NVr8O7ZC+5xNljw9gKUyVEGQdFB6LWlFw74HuCNIyIiIiLK5thDT/QKVWreJX48fcG9KY+nT8jS0xOec+fCzM0N0Zcuwbt3HzjGmmN289momacmIuMiMXDHQGy4sYH3joiIiIgoG2NAT5QB4+mvmheDM8IQtKDLM8fTm1gVLoQCc+fAzMUFUefOwad3H1hFGzCt8TS8VfAtxOnjMGrvKPx98W/ePyIiIiKibIoBPVEGjKe3/cg4nr5k3MU0jacXVsWKwXPuHGidnBB5+jR8+veDWXQsJtabiI4lO8IAAyYcmYDfT/6u5rcnIiIiIqLshQE9UQbIV7gUrtecED+e/vSO1MfTC+uSJeE5eza0Dg6IPHYcPgMGAtExGF1tNAZWMM51P/PMTHx36Dvo9LpXeg5ERERERPR6YUBPlEEqNu+GQznbqdcF9nyGez7X0vQ9mzKl4fnXn9Da2iLi0CHc+XQQDLGx6F++P76s8SU00GDFlRUYsWcEonWpp/MTEREREVHWwICeKANV7PUbrpoVfa7x9MKmQgXk/3MmNDY2CN+7F76Dh8AQE4P2JdpjUv1JsNBaYOvtrRiwbQDCYsJe+XkQEREREVHmY0BPlIGsrG1h22kRQg02KBl7AcfmDkvzd22rVEH+P6ZDY2WFsJ074Tt8BAxxcWhWsBn+aPIHbM1tceTeEfTc3BP+kf6v9DyIiIiIiCjzMaAnyoTx9Fcfj6eveXcRzuxYlubv2tWoAY/ff4fGwgKhW7bA7/ORMOh0qJ6nOua8NQeu1q64GHAR7/77Lqafmo6QmJBXeCZERERERJSZGNATZYJKb3XH4ZwfqNf5n2M8vbCvWwf5fv0VMDdHyIYNuDtmLAx6PUrnKI0Fby9AcZfiCI0NxR+n/8BbK99iYE9ERERElEVpDJzv6plCQkLg5OSE4OBgODo6ZtR9oWwgOioC3j/VRTHdNVyy8EKREbtgYWmV5u+HbNkC36HDAJ0Ozu3aIfc3X0Oj1UJv0GPb7W0qoL8WZGwocLBwQGevzurhaMnfMRERERFRVohDGdCn04UkehG+N87DcX5jOGgicTBPV9TsNzX5FeNiACl2FxOe4BGG4O374Dflb8BggEujMsjVrhI0uhigdGvoc5dNNrDv4tUFnbw6MbAnIiIiInpNMaDP4AtJ9KJObJyLSoeHqNeXrMrBEjGw1kfC2hAJK73xYY64FL8fdNMGdw87S/scXEuEwb1CCPTm1jDrvhbIXy3FHnsG9kRERERErycG9Bl8IYlexuHfe6K6/z+prhdtsEA4rBABa4QbrB8/W0FzQw/X48FqnZiSFihX/jZ0Vk4w770ZcC+llktgL1PbzTg940lgb/k4sC/FHnsiIiIiotcFA/oMvpBEL0MfF4vzO/5GaEQ0Ys1s1SPGzEY9x5nZqNcxGmvotRaSXQ8DJMve8Pi18Tn3znUovnSG2l5cfjMUKvIAVoVcYdFvC+Ds+WRfzwjsO5fqrF4TEREREVHmYUCfwReS6HXwaN48PJgwMf69mZUO9kUt4TT4Z9jWbgSNmVmiwH7L7S2YcWoGrgdfV8skmP+q5ld4q+BbmXL8REREREQEBvTphQE9vWkiTpzEvZWr8Gj9BthHR8QvN8/pBoe334bj22/DpkIFaDSaZAN7M40ZJtWfhCYFmmTiWRARERERZV8hrHKfsReS6HVz8U4AZn77Mwb7LEHMHTPoY7Xxn1nkzQuHt9+CY4sWsPbyUsG9Tq/DuAPjsPr6aphrzfFrw19Rz6Nepp4DEREREVF2FMKAPmMvJNHraNflB/hlwTIs1HwP3DcgJLgYwq5FQR/xpOfeooCnCuyl596iaBGM2jsKm25tgqXWEtObTEf1PNUz9RyIiIiIiLKbEAb0GXshiV5XCw/dxsY1SzHX4n+w0sRBX64rwuxaIWTjJoTt2gVDVFT8ulbFisK5V0+Mc9iOXXd2w8bcBjObzkRF94qZeg5ERERERNlJCAP6jL2QRK+zH9ZfwJ39SzHN4jdoNQag3gig0Vjow8MRunMXQjZuRPiePTDExqr1ratWxp9NgXWG07C3sMesZrNQ2q10Zp8GEREREVG2EMKAPmMvJNHrTK834OO/jyPHpcX40WK2ceFbE4Ea/ePX0YWEIHDJUvj/8Yex197cHIfq58S0Cg9g7eCMOc3noLhL8cw7CSIiIiKibCIkjXHokypZRJRlabUa/NKhIs7naYNJse2MCzeNBM4sj1/HzNERbv36ovC6dbBv1AiIi0ON7Xfx+xwzFDsbiD6be+Nm8M3MOwkiIiIiIkqEAT1RNmFjaYa/ulXBv/YdMTeuuVpm+O9j4OrWROtZeuRD/unT4DF9mqqG7xQYg8//0aPPIn+MXNYdPqE+mXQGRERERESUEAN6omzE3cEac3pUw89mPfCfrhY0+jgYlnUBfI48ta5Do0YovH4dcvTtq9LvK18zYMxvD7BsdHvcDfTOlOMnIiIiIqInGNATZTMlcjtgWucqGKX7GLt05aGJiwT+bgc8uPjUulobG7gPG4rCq/+DRZWKsIwDWm4LwpV3W8Fv16ZMOX4iIiIiIjJiQE+UDdUtlhPjWlfAx7GDcUJfFIgKAha2AYKS73m3KlIERRb+DdvvxyDEXgv3h7EI7j8UN4cMQuyDBxl+/ERERERExICeKNvqWM0T3eqXRo+Yz3HVkA8I9QMWvg+E3E12fY1GgwIfdEae1Suxu7od9BogatNWXG/1DqIuXMjw4yciIiIiyu7YQ0+UjX3evARqly2KLtGj4Ac34NE14LcKwMaRQLBvst/xzFcKzX5djgl9XXAzF2AICcHt3r0RfeNGhh8/EREREVF2xoCeKJtPZzelfQXkzl8EH0WPxnltcSAuCjg8wxjYrx0MBDw9VV1h58IY220upnR3xo3cgD4gEN49eiLmTvKNAERERERElP4Y0BNlc9YWZpjVrQrinAujZcQ49MFY+DpVBnQxwPF5wNTKwL/9gYdXEn2vhGsJjGz0LX7oYIY7OTSIu38f3j17ckw9EREREVEGYUBPRHCzt8L8ntVQMrcjtkZ5ofb9z9DX7Dv4udUCDDrg9BJgWjVgRXfg3rn4K9a0QFPULNUc33XUIsDVArHe3vDp1Ru6oCBeVSIiIiKiV0xjMBgMr3onb7KQkBA4OTkhODgYjo6OmX04RK+UTm/AmtO+mLzlCu4ERqplzZ198bXLJuS5u/3JiiVaAHWHAx6V8SjyEVqvbg3Le4GYvMwaVoHhsC5bFp5z58LM3o53jIiIiIjoFcWhDOjT6UISZSUxcXosOeKNqTuuwj8sRi1r4R6gAvuct9dDg8ftgIUbAg3HYEPsQ4zcOxIFHmnx0zIrIDgUtlWrIv9ff0JrbZ25J0NERERElEXjUKbcE9FTLM216FarIHaPaIhhTYvD3socGx64otrljzDM/U88KvYBoDEDbuwE5r6Nt2090SB/A9zOocefPXJDa2eHiKNH4Tt4CAwxxgYBIiIiIiJKXwzoiShFdlbmGNS4GPZ83hC96xSCpZkW/3rbofLZNhjtMR8ReWsC+lhodk/ElzW+hIOlA7bZ3MSZke9CY22NsN274TdqFAw6Ha8yEREREVE6Y0BPRKlytbPE2FZe2DmiAdpV9oBWAyy5qkXrW+9DDw1wcQ3cg+/h86qfq/UnRK+G+YTRgIUFQjZsxL2vvwbLdRARERERpS8G9ESUZvmcbfBTu/LYPKQempfOhSt6D6zR1TR+uGsC3ivyHurkq4MYfQy+1q9Bnv9NlMnuEbRiJR5M/B+DeiIiIiKidMSAnoieW7FcDpjZpQo+aVgUv8W1gU7+Krm8Hpq7pzCu5jjYWdjh9MPTWFvAH3m++059J2DePNz75huEHz4CfaSxgj4REREREb24LB/QR0REoHv37uoxbdq0zD4coiylR+2C8DXzwH+62sYFO8cjt11ufFblM/X2txO/IbRpVeT64gv1PmjpMnh364bLVavhZvsOuD9+AkK2bEGcv39mngYRERER0Rspy09bFxMTg8WLF+P06dPw8fHBypUrn+v7nLaO6NnG/ncWew8fwQ6r4TCDHui9HYZ8ldFnax8cvnsYVXJVwezmsxG2ZRtCNm1E5PETiHvw4KntWHh6wrZSJdhWqQzHFi2gtbXlpSciIiKibCmE89Antm7dOsybN48BPVE6u+UfjkaTd2G82Z/oYL4LKNIY6LIKd0LvoM2aNoiMi8TY6mPRoWQHtb60Icb6+iHy5AlEnDihAvzoq1flg/htWpcpo+awN3dx4f0iIiIiomwnJI0BvTkyUWxsLFavXo0ZM2bg0qVLWLFiBWrWfFxg6zH5z/9vv/2GBQsWIDQ0FLVr18b48eORO3du9fnVq1fxww8/JLv9OXPmQKvN8qMKiDJVQTc7vF0mD6aea40PzPfC7Pp2wPsQPDxrYEilIRh/ZDymHJ+Cuh51kdc+LzQaDSw98qmH0zvvqG3oQkIQefo0Io4fR9Cy5Yg6dw7eXbvBc85smOfMyTtMRERERJSMTI12v/jiCyxZsgRdu3aFr68voqOjn1pn4sSJ+OqrrzBmzBi1rre3N5o2baoaA4S0WjRo0CDZhwQORPTq9a1XGHcM7liua2BcsPNH9fRhyQ9Ryb0SIuIi8PWBlKeuM3N0hH3dunAfMgQFFi5QQbz02t/u3AWxd+/yFhIRERERvW5j6HU6HczMzHDnzh3kz58fO3fuVIF4wvHv7u7uGDt2LIYPH66W+fn5qXUluG/fvn2a98WUe6JXq+Ofh3D7xmXstRkGM0Mc0H09ULAObofcRts1bRGti8aoaqPQvnh7WJhZPHNbMd7e8O7eA7F+frDImxeec+fAskAB3kIiIiIiyhZC0phyn6k99BLMP4sUspMTkB55k7x586Js2bLYs2dPmvfz6aef4pdffsGxY8dUtfuzZ8+muK5kCcjFS/ggotT1q18YfnDDcn2jJ730BgMKOBbApxU/VYsmHJmAaourof3a9qrHftmlZTjz8Ayi4qISbcvS0xMF/l4Ey4IFVVAvPfXR167xNhARERERvS5j6FMjPffCNF7eJFeuXCpFP61k3H3lypXj37u6uqa4rozP/+abb17oeImys/rFc6Jkbgf8eu8dtLPZBfPb+4Gbe4DC9dG5VGf4hPpg482NCIkJwcWAi+phYqYxQyGnQvDK4YXSOUrjvaLvwS5PHhRYtBDePXsh+soV3O7SFfln/QWb0qUz9TyJiIiIiF4Xr8W0dSml3P/zzz/44IMP4O/vjxw5csQvb9WqlSp2t2bNmnQ/FumhTziWX3ro5dhSS3UgIuDfk3cwdNlpjLdZhI6GDUD+GkDPTcDjehby141fuB8uPrqIC48uqKBengOiAhJdvgo5K6ip7izNLKELCoJ3n76IOnsWWgcH5J85E7aVKvJyExEREVGW9Uak3Kcm5+Pq1hLQJyTvTZ+lNysrK3XBEj6IKG1alcuLvE7WmBLZEnFaK8DnEHB9R/znUqgyn30+NCnQBIMqDcIfTf7Arva7sL3ddvze6HcMKD8ADhYOOPXwFL45+I1qADBzdlZj6G2qVIY+NBTevXsj/NAh3hIiIiIiyvZe64C+QoUKsLS0xP79++OXydR1Mra+WrVqmXpsRPQ0CzMtetYphIdwwSqztxKNpU+JBPnutu6o71oGH984iUne12EGDdZcX4P55+erdczs7eH511+wq10bhogIePfug3vffoe4gMQ9+0RERERE2clrHdBL73jnzp0xYcIENWZepqqT6evs7OzQoUOHzD48IkrGh9U84Whtjv+FvgWdmTXgewy4ujXlayXB/tmVwLRqwJllqBUVhRGPHqmPZP76PXeMBTC1Njbw+GM6HFu2BOLiELh4Ma43bQb/GTOhj4zkvSAiIiKibCdTA/oVK1bAw8MDVatWVe/btWun3k+ZMiV+nd9++w2VKlVCoUKF1BiCrVu3qinonJ2dM/HIiSgl9lbm6FKzAPzhhDWWLY0Ld/6QfC99sC+w5EPgn15AhD/i3ErhZMHe+CgkDB+EhMIAAz7f8zmuBRor3GstLZFv8iR4zpsHay8v6MPD8fCXX3D9rbcR9M8qGHQ63hgiIiIiyjYytSheREQEApJJmU1u7LqsGxkZmag43utUjICInngQGoU6E3fCPi4IR+2HwSwuAvhwCVCyhXEFvR44MQ/Y8hUQEwpoLfCo8mC0PVsNt4LiMMRuKwbq5qJvbnccs7GGh70HFrdcDBdrl/h9GPR6hKxfjwc//4w4v7tqmVWJEnAfPhz2devwdhARERHRGyutcehrUeX+dcaAnujFjF51FkuOeGNarrVoGbwEyFUW6LcHCLwJrBkE3N5nXNGjKs5W/h6d14QgODL2yfdtV6MDVqBj3ly4Y2GBKrmq4M+mf8LCzCLRfvTR0Qhc9Df8Z86EPiRELbNv1Ah5f/xBFdQjIiIiInrTZIkq95lp2rRp8PLyih8OQETPp0/dQmq2ujH3G0BnYQ/cPwus6gP8UcsYzFvYAm9NwIZq89D2n0AVzFfI74ytQ+uhRC4HjI94F2vRClPv+8NOr8ex+8fw45EfVeX7hLRWVsjRqyeKbN4E127dAAsLhO3YgZttP0DUhQu8bURERESUZbGHPhXsoSd6cf0XHsem8/fwV/7NaPrQWLFeKdwQeOcXzD5vwPfrL6jh9U1K5cLUjhVhY2mGR2HR6DTrMC7dC8Ekm/lwt96DT3LlhEGjwehqo/FRqY9S3GfUxYu48+kgxN65A42lJXKPGwfntm14G4mIiIjojcEeeiLKdP3qF1bPI/3qQufgAVg7Ae9Nh77TKny3PwLfrTMG811qFMDMLpVVMC9y2FthcZ8aKJXHCSMiuyIsqhqGBQSpz/53ZCIO+B1IcZ/WpUqh0D8rYV+/PgwxMbg7ZgzufvmVSs0nIiIiIspKmHJPRK9MRU8XVCvkigCdLX4uvhAYfg1RZT7Ep0tPYfa+m2qdUW+XxLfvlYaZVpPou652lljcuzpK5XHGp5F94BlSEu+FhkEHPYbvHIKLjy6muF8zJyc1xV3OwYNkonsErViB2x91QqyvL+82EREREWUZDOiJ6JXqV8/YSz/v2EP4hMShy+zDWH/2LizMNPj1wwroX78INDLYPhkuEtT3qY5S+VzQP+oTNPP3QKWoKITGRaLP2g64vHEIcG0bEB321Hc1Wi3cPv4Y+f/8UwX4UefP42abtgjbv593nIiIiIiyBI6hTwXH0BO9HL3egOa/7MHVB2GwsTBDZKwODtbmKsW+VhG3NG0jOCIWnWcfxlXfB/jd5hfMzX0PZ6yt4KLTYfbdByimMwB5KwEF6zx5mFvFf1965u8MHoKoc+egsbJC4fXrYOnhwVtLRERERK8ljqEnoteCVqtB38e99BLM53Wyxsr+tdIczAsnWwss6l0dJTzc0TtyJG57fwbXGGcEmpmhZ57cuC5D7+8cAfZNARa1ARa8Z5zr/jGLfPlQYPHfsK1SBYboaNwfP+GVnCsRERERUUZiyj0RvXLvVciHusXcULtoDqwaUBslcjs89zacbCywsHd11Cjsijs6D9y+OQi6yHwIMtPg/dxF0R+dsFJXDxEGK8D7IHBxTaLva6Xi/dfjAHNzhG3fjrDdu9PxDImIiIiIMh5T7lPBlHui14vMQ/8wNBo3/MNx4f49zL0xCkFxt6DVOSLSuy8Gxu3CYPNViHApCdtPD0qKQKLv35/4PwTMnQsLT08UXrtGzWNPRERERPQ6Ycr9S5o2bRq8vLxQtWrV9LkjRJQupICeu6M1ahTOgZ41S2Nt24Uo7lIcerMQeHjNx02vlgg12MA28BJwZeNT33cbOBDm7u6I9fZGwJw5vCtERERE9MZiD30q2ENP9PoLiApAr829cC3oGtys3dH8khtGGTYhPEcZ2H2yT01dl1DwuvXwGz78cYG89bD0yJdpx05ERERElBR76Iko23C1dsVfzf5CYafC8I96gKUFr+EbVzc8Cr4EXN361PqOLVvAtmpVY4G8CeMz5ZiJiIiIiF4Wi+IRUZbgZuOG2c1no5xbOegQg5VOtmjlkQef7B2Fk/dPqLH3CdP2c3/1JWBmhrBt2xG2Z0+mHjsRERER0YtgQE9EWSqoX9RiEWY1m4U8Wi8YNBrsNo9F103d0HlDZ2y+tRlx+ji1rlWxYnDt0kW9vvfDD9DHxGTy0RMRERERPR8G9ESUpUjve/U81TG75Vx0u10EbUPDYGkAzvifwfDdw9FtYzfE6IzBu9snA2GW0w2xt1MukGdIMJ89EREREdHrhAE9EWVJ+V1tEVNiEEY/DMVm7zvo79kC9hb2KrCfcXqGWsfM3h65Ph+pXvvPmAnfEZ/Du1dv3GzTFlcbNcKlipVwuXIV+P/5V6KUfSIiIiKi1wEDeiLKsro2q4EV+gZw0+vR5fJpfFf7O7V8zrk5OP/ovHrt2KqlsUBeVBRC1q5F+P79iLpwAXF+d2GIjFSPh1Om4N6338Kg02XyGRERERERPWGe4DURUZbi4WKLO6X7IebiDjjeO4gmGns0L9hcjaX/cv+XWNZyGSzMLJB3dH8E/W0FbcEqMHPLBTMXZ5i7usLMxQVhO3fi/vgJCFqyFHEPHyLfpEnQWltn9qkREREREXEe+tRwHnqiN9udwAjs+7kTPtTuQFDeetB3nY/W/7VGYHQg+udthAF+N6C5aaxyH12qLaw6PD2WPmTzFviNGAFDTAxsKlaEx/RpMHdxyYSzISIiIqLsICQkBE5OTggODoajo2OK6zHlPgXTpk2Dl5cXqlat+qruERFlUC+9j1d/xBm0cPbbA1fvY/jCubz6bJbvdlz2PQidQQO9QQOri/8A13c8tQ3H5s3gOWc2tI6OiDx5Erc7dUasry/vHxERERFlKo2BlZ6eiT30RG8+36BIHJrSAW21u9V7KW/3mbsbttrZwi3KBlrfAehs2IgeZpsQYZcftkOOAhY2T20n+upVePfpi7h792Du7o6CSxbDIl++TDgjIiIiIsrK2ENPRPRYPmcb3Pbqj2iDsWzIOX1BxN5rBY3eFv7WkXj3vQgE1fgcdw2usA33QezOiQiPDcfii4sxaMcg7PDeoarcy9z1BZcthWXRIoh78AA+/T+GLiyM15mIiIiIMgV76FPBHnqirMEvKBIDf16E2Jgo5CxWDd1qF0K4+VGM2jcK5lpzzG++GIv/+g/9MAmLHR2x1iUnwnWR8d+vm68uRlUbBU9HT8TevYub7dtD99AfdvXqIv/06dCYs8YoEREREWVsHMqAPp0uJBG9GUG94XGPvZBe98E7B2Onz04UcykGjc4BV0JOxK9fyLEQKuWqhNXXVyNOHwcLrQV6lumJXmV7ARev43aXLmq6O5fOnZF77JhMPDMiIiIiykoY0GfwhSSiN9PDiIdovbo1QmJCjAsMGtSLiELnkCBUbzQe2mq9cCv4FsYfGY8DfgfUKvns86ne+koXY+A7aLBalmvsWLh27pSZp0JEREREWQTH0BMRpUFO25z4oc4PKO5SHD1K98CcxquQ5+5bqBkVDd3WcUDofRR0KogZTWZgSoMpyGWbC75hvvh0x6eYaLsbjoMHqu3c//FHhO0xTn9HRERERJQRmHKfCvbQE2U/v229hPp7O6K89gZiS7WGRYf58Z9FxEZgxukZmH9hPvQGPXLb5sKk/YVhuWkvNLa2sKtRA5YFChgfBQvAqkQJzllPRERERM+FKffphAE9UfYTFavDJ5PnYWbkZzDTGIAm3wA1BwJmFvHrnHxwEl/s/QJ3wu7ATGfAb2tzIufFe09tS2Njg/wzZ8CuWrUMPgsiIiIielMxoM/gC0lEWcvuKw9xccFQ9Ddfa1yQoxjQ/AegWDNAo4nvrZ90bBJWXFkBrd6AOved4RGohcP9MLg+jEb+h3q4hQD6vO4otW4jtLa2mXtSRERERPRGYECfwReSiLKegYuOwuHiUgw3Xw43zeOieUUaA81/BNxLxq+3584ejDswDv6R/om+bxNtwORZOhXUO3TuCI+xX2X0KRARERHRG4gBfQZfSCLKekKjYvH9uovYcOwyBpr/h15mG2Gh0cGgMYOm9iCg8bj43nqpkn/y/knYmNvAycoJzlbOsDCzwDdTWqP/godSPB8F/14M20oVM/u0iIiIiOg1x4D+JU2bNk09dDodrly5woCeKBs75ROEr1afQ7DvZXxhvhjNzY4ZP+i1Dchf9ZnfPXL3CI4N6o6GZw3Q5c8Nr7UbobW2zpgDJyIiIqI3Eqete0kDBw7EhQsXcPTo0fS5I0T0xqqQ3xn/DaiN/u83xSiLkVinq6GWh53+L9XvVstTDZEDOiDQDjDzuQffX6dkwBETERERUXagzewDICJ6E2i1GnSs5omdwxvgnGNdtUx3cX2avjug7udY3Sa3eh0ybyEiz559pcdKRERERNkDA3oioufgbGuJnBVbIcZgBqfwm8DDK6l+R8bVd+zzM/Z7aaE1ANeGfoLYe09PcUdERERE9DwY0BMRPae6ZYvgoL60eh1z4fG0dqmo4F4B0YM6w98BML/zAGfbvoPAy+d47YmIiIjohTGgJyJ6TsXc7XHEuqZ6HXEmbQG96FP3M2wYUQt+roDNozBc69ge69b9DJ1ex3tARERERM+NAT0R0XPSaDTQlHhbvXZ8dAoITVv6vKWZJX5sNwsWM3+Ct4cV7CMMyDf6T3wxuQWuBV7jfSAiIiKi58KAnojoBVQrVwan9EWghQH6SxuTXefYql9wfnx9nDm+P1FjQL2yrdDg390IKV8I1rFA57neWDT2A+y8uZ33goiIiIjSjAE9EdELqF7YFTthnIM+7Myapz4PvXcN5U9/i9LRp+Cxpj3+Wv4vomKfpNZbOTih5KyVuFu1Fsz1QIft0Qjo+ynmb58Eg8HAe0JEREREqWJAT0T0AqzMzRBUoJl6bXtnLxAdmujz+/+OhYVGB51BA1dNGNqfH4BhP8/BKZ8gPAiJwviNF1Fn0l70zPs+fqnQFtEWZvDyMaDMsNmYObEjImMjeV+IiIiI6JkY0BMRvaDS5arihj43zA2xwLVt8cvj7pxE0fvGNPw9NWYh2K0SnDQRmBD+Jb77Yy7qTNyJmbtvICo6Cm8530FUYVt82mgEQop5wDYaqD/vNDZMGcT7QkRERETPxICeiOgFNSyZC1v0VdTryLNPqt0Hrhmtnjdq6qBmk/fh1GcN4jxqwlETifkWEzBcsxDrHCfgkl1fzIgeifmWE1HF7iJWdZ6IoLYN1HfzrjiA6Mgw3hsiIiIiShEDeiKiF5TTwQo33YwBuNm1LYBOeuq3I+eDg4g2mMO30nBYW5jJgHmYd/0HKFQP9poo9DVfjzIxZ2CmiwLMrdX3e5lvxMrTd5Hj428R6KiFc5geR+b8D5fvhWLCxku4FxzF+0REREREiZgnfktERM8jf5m6eLjXETnjQoCbexCxYSxsASzWN8V7DWo9WdHSDvhoObDlSyAmHPCsAXjWBOzcgCleKBN3CxUNlzBtX140al0bLgv2Qrt4DT58UBOB0XocvPEIK/vXhIUZ22GJiIiIyIj/M0zBtGnT4OXlhapVjVWsiYiS09ArD7bpKqvX+vWfwTbgAkIMNrjl9bHqwU/EwgZoOQl4/w+gcjcgZ3HA1hUo10593N18C9ac9oPb+4MQYgO4PopG+ds71WenfYIwdftV3gQiIiIiiseAPgUDBw7EhQsXcPTo0ZRWISKCVx5HHLOuafwLNfCmep4R9y46NqyY9qtTrZ96etvsCHIZHuHnXSHYWCGXWtbp2k5MeL+Mev37zms4fjuAV52IiIiIFAb0REQvQaPRwL5UY4QbjL3xdw2uuFjgI5TM7Zj2jeQuAxSoAzPo0dl8O47dDsQqj6aItAQ8/MPQbNNAfOwVC70BGLLsFEKjYnnPiIiIiIgBPRHRy6pfOj/W62qo1z/FtkfXel7Pv5HqfdVTd6udsEIMCuSqi31VbNSy29su47PQ/6GsZRQcr17AH7+uQGRkNG8cERERUTanMRgMhsw+iNdZSEgInJycEBwcDEfH5+hxI6JsIypWhxrfrkXOuPvQuZXAtqH1odVqnm8jujjg1/JAyB0cq/ADSpWthKUruqDKQktY6gCNuR6GuCdJVQ9tnHCmfAPo33oHLRqVQ1F3h/Q/MSIiIiJ6reNQptwTEb0kmZqudilPXDV4oF+9ws8fzAszc6BqL/Wyivds2C3/EK1iA7G9onFbEswbNAaEuNgh1MoKOSOD0fjQajT6pi929vgUMQGBvI9ERERE2Qx76FPBHnoiSovgiFicvxuMmoVzqHH1LyT8ETClFKB7nE7vWQtD8xWC75E9CLYDHjgBceYamMcZ0O1BadQ+pIf95XNqVb2zKzx/+BYOjRvzhhERERFlkziUAX06XUgionSx+hPg5EIgb0Wg6xr4xYXh74t/IyrkDvQX1yAGwAZHJ8QZdKjvUR+eZ+qg/OI/4Bn6QH3d8Z13kHPQp7DMn583hIiIiOgNxYA+gy8kEVG6iAoGLq4FSr0DWDsl/mzD58CRmdjrXghDHbSI1seihFNFnD7YEr2u7sV7l3YCej2g1cKhWTO4du8G2woVeGOIiIiI3jAM6DP4QhIRZUiwP7UKEP4AR62t8Gkud4RrNbCMdUHwg+aYXbomCq1bivC9e+O/cqlJW7Se+t2LDwMgIiIiogzHonhERFmN9Nh3XQ2UbY+qBivMunsfrjodYiwCYZNvKT4PnoBjn7+FmDmTcLBYJfWVktv+wazfVoATmhARERFlPRxDnwr20BPRaykuGrixG2Ere2CxjRbTnHJBb5Z4bvo+GwxoelqH+zbOuPDdHxjQqgIiY3S4+iAU+ZxtkMPeKtMOn4iIiIhePg41f8Y2iIjodWVuBRRvBvtC9dH38nrERDTHb5bO8Mx/A/fC7wNmoVjQxIByt7XIFRSEM79ORoOLPRFwzx+V7l2CIZ8HZo/vBnMzzl5KRERE9KZiQE9E9CYr3gy4vB5trC5gkv8YXA9oCIMBqFzAAXF5JuP3d67jm0UaNPU5htz/PULJAG9YGHSIO6HFwQJmqDuwa2afARERERG9IHbNEBG9yYo1U095wi7AFSEqmNdqgG/fK4++5frisocGm2tbq3XKPrqpgvlge0uYG/Rwmzoe677vC98w30w+CSIiIiJ6EQzoiYjeZI55gdzloIEBHZwvqUUfVfdE6bxOeMulNAo4eGJhzRjcaVsTLp8MwJwvKqDvJzqsq2qsel9k0V4s/uxdBEcHZ/KJEBEREdHzYkBPRPSmK95cPfXNfRX96hfGyLdKAmeWw+zX8ugdZ404cw2+L3cd/ytzE5sM52BjYYedDbtgXrUS6nstdkdg4Zof8N9JX9wLjsrkkyEiIiKitGJAn4Jp06bBy8sLVatWTfPFJCLKFMWMAb3L3b0Y3awoHAxhwKbRalnLC9uQz8oVAVEB2Hp7K8y15vil4S8YWqMnluXtg4MFi6h/CHLMWYdh/25B59mHodMbeCOJiIiI3gAM6FMwcOBAXLhwAUePHs3YO0JE9LzyVQJscwDRIYD3IWDnj0CEP6AxgwWAXgEBajUNNBhfZzxq5q2JRiXdkdfJGn8Va4c4rQYVbhpQPWoZrj0Iw7rtpxH073/QR7G3noiIiOh1xoCeiOhNpzWLL46H/b8AR2cZX3dYBDjlR+sH3uhjXxw/1f8JbxV6S31kptWgYzVP3LVzw9qildWy7gdvo4X/fHh81g13R4/G3S+/yrRTIiIiIqLUMaAnIsoKTAH9tW2AQQ+Ufh8o2QJoOUX10g86twPNdZZAiB8QEQAph/9hNU/kcbLG5cZdEWNnCc+HwKf7zsI2JkZtKmTtWoQfOJC550VEREREKdIYDDLJEaUkJCQETk5OCA4OhqOjIy8UEb2eIoOA/xUGDDrAwhb45BjglM/42YoewPlVidcv0hjosgp6vQEaDXB/3mwETpyMWHMNltSyQ67QcDQ/aYAmf14UX7cBWiurTDktIiIiouwoJI1xKHvoiYiyAhtnoFA94+t6I54E8+LtiUDusoClA6CV/noA17cDQd7QajXQaDTI1b0XPKb9jiJrNmBPkQlYWLU4AuwBg48fHsyckTnnRERERETPxICeiCiraD0daL8AqD0k8XJ7d6D/PuCLO8BX/oBnTePyq1vjV5Gg3qFxY9gVLojvW5dFZGhHzGtkrT57OPNPhOzclaGnQkRERESpY0BPRJRVOOYFvN4DtKn81V60yZPx9sloWMIdu4a9h9jK/XCwpAbmOj18PhmIkA0bXsFBExEREdGLYkBPRJTdFGtqfL6xG4iLTnYVdwdrzOvQHyvfb4i9XhpodXrc+ewz+P81C4a4uKfWl3IscTr9qz5yIiIiIkqAAT0RUXaTuxxgnwuIDQe8Dz5z1R6lR2FyrYbYXFEDjQF4OHkyTrZqg++mrMIFvxC1zpGbAagzcSfe/X0/omJ1GXQSRERERMSAnogou5Gy9qa0+wTj6JNTp5gbov1bYHqF9zDjbS3CrAGbW1fx7qxx6Df+X3y86Dg6/nUIvkGRuHA3BP+cuKO+p9MbcPx2gHomIiIioleDAT0RUXaUdBz9vbPAnw2Ao7MSrZbHyQaFc9ohJrAOAhs1wdA+ZriS2xKW+jg08D6GjefuqaC9qLu9Wv/PPTfU+y9Xn0PbPw7iq9XnMvzUiIiIiLILBvRERNlRkYaARgs8vATc2gcsagv4nQR2/ADExSRatU5RN/WcJ649wuzssamqcQx9u/Cr6rMf3i+DNZ/UhrOtBW4/isCIlaex+LC3Wufvw97Yf80/E06QiIiIKOtjQE9ElB3ZuAAe1YyvF74PhN03vo4MAK5uTrRq7ccB/c4LkQi/2wLHi2oQawZY3LmN2Q1yoFP1ArC1NEf3WgXVeqtO+KrnfM426vnzlWcQFv10IT0iIiIiejkM6ImIsqtij9PudTGAowdQ7kPj+1NLEq1Wo3AOaDXAg9BoxAZXgsGyMM4U0qjPQjYbg/+I48fRIfYWHLTGMfPlPJywYVBdFdTL+Pqa47fjrV/2YNlRY889EREREb08BvRERNlViZZSIQ+wdgI6rwTqDDEulx768Cdp8k42Fijr4fz4nQa1czfEoRLGgD508xaEbNmC2527IGjoYCze/A2+vLUJv7UpBSdbC0xuXx4O1uYIjYrDpXuh+H79RcTEcXo7IiIiovTAgJ6IKLvK5QX03AT02wu4lzI+8lQA9HHA2ZWJVq1TNId6tjLXokelt3GsmAZxWiD6yhX4fTZcJqKH1tYW5uFhqHVqG2wWzYnv3T/8RWNsGVoPbvZWKrA/dOOR+uz47UD4BERkwokTERERZQ0M6ImIsjPPGoBLgSfvK3xkfD69ONFqrSvkg42FGbrVKohy7iXg6JYXZwsae+kNsbEIrlocy39tiXP9G6plAfPnI+rKFUTERuDf68tgaf0IzUrnUp9tOn9PBfMfzDiAFr/txXm/4Aw7XSIiIqKsRGMwGDhJ8DOEhITAyckJwcHBcHR0zLg7Q0SUGcIfAZNLAPpY4OMDQK7S8R+Z/rnQaDT47uB38P13KQav0SOuVGH0aHEb0ZbGAP+zf3SofsUAbfnSGNfJDOcDLyCHdQ4M8ZqOIX/fRE4HK5TM7YC9V41p/TnsLFWwv+eKPz6q7omBDYvy3hMREVG2FpLGOJQBfTpdSCKiLGNpJ+DSOsClENBxiTEVP4ldPrvw6fZPUCvADf75HXAl4hZq560NDwcP7DvxH8b/EQ7rWCDYFgi1Af6rqcWjBmVx/nhnhEbpAIM5zLQaFMlphyv3w+K3K1kAR8c2gb2VeQafNBEREdHrgwF9Bl9IIqIs49F1YGFrIMgbsLQH3p8JlGqVaBVJpa+ztA5ipScfgIuVC9a1WQdHS0dcCriEFd93xwcbnqTSR1lq8Gk/LULszWCAHnFhJdDcbSS+bFkOY/49B3trcxy5GQDvgAhMbFsWHap6ZvhpExEREb1pcSjH0BMRUWI5igB9dgEF6wIxYcCK7kCIX6JVbC1sUSVXlfj3gysNVsG8KOlaEh//sA4XZg+Dy9I5sC5XDtYxBny0ByqYF+b2lxHhvBAudhaY0aUyJrUrj47VjEH8imN3MHP3dVT5fisOXHtSbZ+IiIiIEmNAn4Jp06bBy8sLVatWTWkVIqKsyy4H0OVfIGdJ43h63xNPrdIgfwP1XMq1FFoXbZ3oMzcbN7St3Qe5K9RErtGj1LKGZ/RY7TEejSP7w8xghoP3duKXE7/Ef6dNpXxqvvtjtwMxfuMl+IfFqGnuWOqFiIiIKHkcQ58KptwTUba2qh9wZinQaCxQb0Sij+L0cVhzfQ3q5aoGtw0jAXNroO0sqZr31GZ8h32GkA0b4t9HlSyAvi3uINbaHGtar4Gno7F3vsfcI9h5+aF6LcG93gD81bUKmnoZK+QTERERZQchTLknIqKX5l7S+Pzg0pNl/teA8//BHBq0KdYGbvt+NRbRO7cS8D1uXCfgJvDfQMD/qnEzI4bDwtMTGisrQKuF9aXb+HaTEwy6OMw6OwuBUYGYdHQS6pcPha2lGT5uUAT96hdR3/11+xXoJLInIiIiokRYRpiIiFKW83GF+wcXnyyTMfX3zwIFagNerYEjfz757NwqwKMKsGWsMch/cB7ovQMWefKg6JbNapXIs2dxu0tXFLwQgK62GiwyW4vTD0/jRvANOFiuwoEvNsDZ2hkB4TGYf+AWzvmGoNnPu/H1u6VRt1hO3i0iIiKixziGnoiIUmaasu7RVUAXC0QGGoN5cXs/sPFxGr7H43ojF/4zFtC7vNH43u8kcGZZok3alC2LvD/9T71+67gBzkGxKpgXoTGh+POssYHA1c4SE9qWg7OtBa4/DEffBccRHh3Hu0VERET0GAN6IiJKmVN+wMIO0MUAATeepNQ75nsSxMtzl/8AK0cgxBdYOxgw6Ixj6sW2r4H7F4Bz/wCBt41fb9oUtjVqQGswBvVu1jnwrUtX2EUasOTSEviE+qj13i2fF3s/b4i8TtaIjNWpgnl+QZGq5/5RWDR2XLqPt37ZgzWnE1fhJyIiIsoOGNATEdEz/pXQAjlLPEm79zlqfC1T2vXYBHRdbXxY2QMlWhg/u7rF+Pz2RMClIBB2D/ijJrCyJ7C8S/ymXbt2Vc/vXrDB/NvNUfLzOfjxX1vodLH439H/xVe3d7C2QK2ibur1gev++Gz5aYxbcx6NJu9G7/nHcOleKMZvuIjA8Bh8+d85FeQTERERZQcM6ImI6NncvYzPDy8Bdx4H9DJO3swcKNwAsLQzLivT5sl3rJ2Bch2AtybIhCqA1sK4/O5pINw4t7x9g/qqUB5CwxE+Z4Falud2KBqc12KXzy5surUpfnO1iuRQzxvO3sWhm4/U6+DIWFUFX6rh3w2OQruZB7Hw0G18uvgk7gZH8q4SERFRlseAnoiI0lbp/v55wPeY8XX+ak+vV7ihMZAX5TsCFjZAibeBYReBUd7GOe2F90EgNgqaVb3hWiN3/Nety5ZVzz33WcI5zICp27/H2qurcTvkNmo+Duh9AiIhHfeVC7jg2/dK48f3y+KTRsXUZ9cehKnn8BgdvvzvPG75h3MOeyIiIsrSWOWeiIjSVhjv+g4gJgwwtwHcSyfzL4olUH8kcHoxUHPAk+WOeYzPBWoZe/lvHzCOyT/3D5z1GkTWbQHr6o3g0qULbrRoCfj64s+p8oUAxPwyCidya1By1gIUzGGLW48i1Kber5gPnWsUUK8fhEZhxq7riNHp0bBETuy56o9tF++rxweVPTCpXXneYSIiIsqS2ENPRERpm7pOgnmRr5Ix3T45Esj33wc4ez79mUxzZ6qOf/5f4z9C5gbkK3UKOaraQ7usA3J1qgeYmcV/xTIOKHnHgJNLf4/vpTfXatCibJ4n7Q0O1vjyHS+8VyEvfutYEePfLwtPV1v12drTfoiIicOeKw8REhXLO01ERERZCgN6IiJ6Nse8xgr2Jqbq9s/Ls6bx+d5Z4Mrjwnm2bsbK+P/2A27shEPQMpQ4fgwlz5xGyXNnEdG5pVrNfP9JNC5pLIzXvHRuNaVdQl1qFMCvH1ZUBfTaV82P3SMaIJ+zDaLj9Bi05BS6zjmCdn8cxE3/cBXc62TwPREREdEbjgE9ERE9m0bzJO3+ZQJ6p3yAcwHAoAd00UCOYkC7edJND1jaG59D7kAb7guNpSU05uYo2a63+mqJGzHQBW7DXP+F6H1+PkL37FHLY3Qx2HxrM4KigpIcsgaNS7mr15J6Ly7fD0XDSbtUcD9zz3XedSIiInrjMaAnIqLUmQravUxAnzDtXpR+HyhUF/j0BDD0HJC/unH5TWOwLmyLl0C4uwMsdYDtkPHIve80rFdtwZ2+/XDw968waOcgDN89HF8f/PqpXTUqaQzohYO1OeytngwTWHHsDvR6A+J0+hc/FyIiIqJMxoCeiIjSPnWdjI13yPXiV0wK45mYprlzKQDYuACF6hnf39oLHJsL/OgBzc09sG1YXy12CwXizICjJY2BufmsFfA5uQ/v79fj7Jnt8AnxSbSrGoVzwMbCOB6/W82C2DSkLlb2rwlrC61KvS89bjOq/rANvkGc4o6IiIjeTAzoiYgodSVbAi4Fgap9Xu5qFW1sHI/vWStxGr8oWNf4fH0nsPUrICYUOPAbCrRoF79KzgED0WbJPoTkd4V9FDBptg4d9+gx7N84LL74d6LNWVuYYVDjYqhZOAd61ikEDxdbVCnoiqZexqnyImN1CIyIxV97bvAXQERERG8kjcEgM/pSSkJCQuDk5ITg4GA4OiYoCkVERC8mMhAwtzbOU59QXDQwoQAQl6DHXKOFYdA5+Iz8BhoLS3j88jM0FhYIP3AA3j17Jfr67+1s0b7vFNT1qAutJuX26u0X76PX/GPx76UX/8CoRnBJUmiPiIiI6HWPQxnQp9OFJCKidLCgtap2r9i4ApEBQPMfgZoDn1o1cPlyGKKiEPvwIQL+moU7OYARvcxQzaMWfmn4C2wtjFPXJSXt2PMP3ELx3A74Yf1FnPcLQTF3e/zUrjwq5HfmbSQiIqI3Jg5lyj0REb0+ijQyPuevATT8wvj69NJkV3Vp3x6uXbvCrW9faJwc4fEI+GoZcObGAfTZ0gdRcVHJfk8q4HevXQi1irhh3Dul4WRjgasPwjBs2alXdlpERERErwIDeiIien1U7we0mAR0WAiUaQtoLYB7Z4BV/QD/a8l+xczBAR5TpkBrZ4dSt3UYuwI4f/80tntvT3V31Qq5Yvtn9aHVADf8w1XPfbc5R7Dz0gMMWnISy48lLrRHRERE9Dphyn0qmHJPRJSJtn0N7Pv5SQr+8KuA2ZPp5xKKunwFt7t0gT4kBCtra6Dt0wljaoxJ025aTd2Lc74hTy2X8fVL+9bA/ZAoNCttLKZHRERE9Kox5Z6IiN58Tb4G+uwAzG2M4+kDb6W4qnWJ4sjzjXE++jYHDLh7+lCad1O1oGuyy6US/nvT9qPvwuPYdO7eC5wAERER0avDlHsiInq95asM5CxufP3wIqCLBYKST4V3fPttWNSvDa0BKLj/BsJjw9O0i+qFngT075bPi+X9amJgwyKJ1pmy9TKi43QvcyZERERE6YoBPRERvf7cvYzPDy4BW8YCv5QBjs0FHlwElnUGbu2PXzVXuw/Vc80Lelwb+gmu1q2H6BvPnmte5qc3aVMpnxpb/1H1AjCTwfUA7CzNcOV+GMp/swXbLtx/NedIRERE9JySH4hIRET0OslZ0vj84ALgc8T4euNIwMYFCLsHRIUABdeoxXb16iHaxhyuYXHAtkOIA3Bn5OcovGQpNObJ/7PnZm+leuTvh0SjTlE3tSyfsw3m9agKvQGIiI7Dx3+fQFSsHkuPeqNyARfYW5vDwozt4kRERJR5WBQvFSyKR0T0GriyGVjcHrDNAUQ8evpzGWM/yhu4cwRwK4F9Iwchx3bjNHT6x+lodl0/Qv7RY9W0dS/itE+QGk8vpOe+akEXLOpVHeYM6omIiCidsSgeERFlvR56UzDvVgJwKw7kKApYOwNxkcDuicC8lsDyLijSuQ/0GuCupx0WvmuvvhK+YDGu9eqO8wunQR8djc23NmPAtgHoubknLj66mOohlMnnBAdrYw+/Tm/AoRsBKDpmI9rPOAi9dOMTERERZTCm3BMR0evPKT9gYQeYitwVawo0+QbQaFUAj0vrnkxv530Qed7xgOu6dSiVJw8KxtzDbHyID9eFAgeOQHvgCNZfP4bvip1HpDQEAPj1xK/oX74/ImIjUCtfrWQPQXrli+S0xymfoETLj9wKwFnfYJTP7/yKLwIRERFRYhz8R0RErz+tFnB/3EsvCtQyzkcvywvUNi4zJKhAf3oJrIoUgdbWFoWdC6Pb6EX4sY8T9pY2pttrdx5SwXwu21zq/X6//aqnvt+2fjhy9/EY/WS0LJtHPeews8TcHlXhYGVsF39/+n6M+fcs4nSS4E9ERESUMRjQExHRmyFnqSev89dIHNybSC++OLMM8L8GGIyp8MVdiuP3/uvRePLfKhW/8H2g+TE9Rl8shno5a6p1YvWx6vm7Q98hRheT7CF0q1UQX7XywupPaqNhCXeMe7e0Wi4Z938f9sb2Sw/S/bSJiIiIUsKAnoiI3gymHnoZP2+X48ny3GUBSwfj6ybjAGsnIMQX+L0ysHZQ/Go5bHKgSMGKiPAqoN732qpH3sW70Gt9tFSIhau1K3JY58CtkFuYd35esodgaa5FzzqF4OFiq943LJEz0efLj/qk91kTERERpYgBfQqmTZsGLy8vVK1aNeWrR0REGadMWyBvJaDO0MTLtWZA8x+A8h2Bil2AZj8AroWNn13d+tRmCrZsn+i9zfYjmBHeFnNK/4gxrh+pZX+e+RM+oakH5znsrfBbx4roXquger/z8gNsOHv3JU6SiIiIKO04bV0qOG0dEdEbSOaln5Df+HrkbcDmScG62PsPcLN1a1iXKgWbSpXg//vvsC5bFrF37kAXGIhDzTzwc8W7qO/ZEFMbT03zLvsvPI5N5+9BqwG2DK2Hou6PswaIiIiIXlEcyoA+nS4kERG9ZqZ4GVPve24GooKB/NUAGxf1kSEuDjAzQ5yfH641bvLUVzdVMcN/1YGJrWegRv46adpdrE6PD/88hOO3A9GklDsK5LBTKfrXHoTh5w4VYP+4gB4RERFResWh/N8FERFlTTlLGAP67d8Bt/cBxd8GPlqqPtKYG//5s8iXD1YlSyL60iX13jx3bsTdu4e3junw1jHg6s7hwKpDadqdhZkWNQvnUAH9touJi+N98McBVCrggnHveMHK3CzdT5WIiIiyJ46hJyKirCnn4yJ6EsyLK5uA4DtPrebQqGH8a8+5c+A+amT8+2IXgvHw8F74//EH7k+YCF1w8DN3WdbDKdnll+6FYvFhb6w49vT+iYiIiF4UA3oiIsq6PfSJGICjs4EHF+OnsxOOLVtCY2kJu7p1YVWoEHJ0745iBw/gVgFr9bl/t754+OtvCJg3D74jRiD80GHowsKT3WW5BAF95QIumN2tSqLPx/53DkuPeKfraRIREVH2xYCeiIiy/rz1JvumANNrAKeXxC+yKlIERXdsh8fU3+KXmbu4ILBumae+Hr5nL7y7d4fP2NHJ7jK3o7ERQDQq6Y7GpXKhfvGcqlCeyahVZ+EXFAlDgkYFIiIiohfBgJ6IiLKmnMWfvC7RArBzf/L+5p5Eq5q7uUFr/SQYF24t30Xs4+Huo7uZYUXjJ5+HbdmKsHtPp89rNBpMblce71fMh561C6llf3WtglPjmmH3iAbx69WasAOdZx9GeHQc9HoG9kRERPRiGNATEVHWJBXt7XMbX5dsCfTZ8WQO+4eXU/165ZKN8O1HZviysxliSxTAimpx6DPIDFfzAGZ6wKdBU/z0eQNcC7yW6HttK3uoqvY2lsbWAKl072htoarem+arF/uvPULpcZsxfuPFdD1tIiIiyj4Y0BMRUdbVYCRQshXg9R7gnB8o18G43P9qonH0yclhkwOfdp2GgV2n4vs636tlwXYa7Cj/5J/OVmvu45epnTF893B03dgVYTFhz9ymVLpPaulRH9VTT0RERPS8GNATEVHWVaUn8OHfgJWD8b1rEUBjBsSEAiF+qX69fv76aOjZEBXdK6KEi7HIXmSzGtB2aQuDpaV633FNMLbc2ISTD05i0cVFzz6cBAH9/z4op55Do+JUT/36M3df5kyJiIgoG2JAT0RE2Ye5JeBa2Ph6dlNgahUgMjBNX53aaCo+KvkRxtX7DiXGfI+SR49AZ20B1zAg3yPjOgvOL0BwdMpT2+V1tsHvH1VU4+rbV8mvKuGbTN6a+jAAIiIiooQY0BMRUfaczi7EF3h0FbixO01fy2OfB6Orj0Ze+7zqvdbKCtaljZXwxyzTYcwaS+hCQzD//PxnbqdVubxo6pVLvf6wav745TcehuPag2en7BMRERElxICeiIiyF7cE1e+F7/EX3pRDhYrqOUcoUP58BL5YpsPSswux6eamNH3/g8oeWD+oDqoVdFXvu84+jJCo2Bc+HiIiIspeGNATEVH24mqcTi6e74kX3pRNWeM4eJPifsCPf4Rhx+TPcO7h2VS/L9Pclc7rhAlty8LW0gx+wVEYvOQkvl5zHo0n78Lm8/cQEcOCeURERJQ8BvRERJS9FKhtLIxnYWt873cS0OtSXv/RdeDa9mQ/silrTLkXDk2bqudcQUCPbXpc3rAkzYdUOKc9WpbNo17vvPwQ8w7cwvWH4ei38Dhq/Lgd3o8i0rwtIiIiyj4Y0BMRUfaSowjw8QFg8GnA0gGIDQceXnp6vWBfIDoMWNYZWNQGODobODAVCPcH4mIAvR7muXPBoUkT2FatiryTJ8G5ffv4r0cdP/lch1Uyj2Oyy0Oi4vDpkhOc2o6IiIieYv70IiIioizOvaTxOW8F4NZewOcIkKv0k88fXARm1gPyVQYeXDAuWz/M+HznKHB1G1CoHjSPrsGjgjPQa6vkzyPPt9/gYUFnWPzvT9hf9oVep4PWzAyGx3PeS4p9Skrlfjy1HoDONTyx6JB3/PvTd4LxyeITmNujWjpfCCIiInqTsYeeiIiyr4J1jM+7xieel/78v4AuBvA++PR3Lqw29upf2Wiski8BvjwkNR9Aobpvq+fit2NxsXQZ7HqnDvr/2wVNVjbBo8jH89slo0SCgP6Dyvkxt3tVNCiREyUfL5dUfEm/X37MBw9Co9LrChAREdEbjAE9ERFlX7U+BdxLA2H3gfWfPVl+dcvzbUfmtJ9RBwi+A/uiJaDTauL/kc119REcdp7Eg/D7WH19dYqbyGFvhV51CqF9FQ+Uy+eEhiXdMa9HNWwYVBd2lmZqnXshUfh85Rl0+uvwC54wERERZSUM6ImIKPuytANaTja+9jtlHDN//j9jobyEGowGvpAe/AQp81ZOideJjVDflbR6t2FDYChXEiHVjHPe99qqx/TpOszZPQVDdw5FhKybjC9beeF/H5SH9nGDgJDXxoT9J64+CFOV8KNin1HMj4iIiLI8BvRERJS9OXsan8MfAJtGAiu6Pb1O7nLG4N+l4JNlPTcCLackXu/aNuPqvfvCa/m/qDz5r/iP3EKAEf/ocPHEVozcO/K5DrFJqVzqWaa2s7cylr+RSvjyICIiouyLAT0REWVvdjmNz/o44MbuJ8vrDn/yOs/j+ebdvYzPWgsgRzEgT/nE27qxE5j/LhAbqd6am0fA9b2Gieap/98cHa4f34nAqMD45fro6Gce4jfvlkb/+kWwbVh9tCibO3758qM+L3DCRERElFWwyj0REWVv5paAjSsQGQAEPw6Q++0x9sobdIBGCzh5GJe7lwIurwdyljR+L2FlfGtnICoIuLkb2P8bYG4FbPsauWwMcJ/9Hfxm7UbIwQuw0AGfrNPB+9GX0NZ5G7rAQNz/4Qfk++1XODZrluwhuthZYtTbxsr8AxoUxf5rj+AbFIkb/uEqqG9fNf+rv05ERET02tEYTHPpPIdmzZqhZ8+eeP/992FlZYWsLCQkBE5OTggODoajY/JzBBMR0RtuWvXEc9EPvwbYP+65T+jOcWMBvAajgPqfG5fd3AvERQP5qwK/lDMG9SmIfXsOLvf8Ema6p//pNcvphqLbt0NraZmmQ/5s+Wn8c+KOej2ieQnsvPQA71bIizaVPOLT8omIiChrx6EvlHKfP39+9OnTB3ny5MEnn3yCkyeTFA8iIiJ6k9i7P3kt6fS2OZJfz6MyMOYeUG/Ek2WF6gLFmgDWTkCFj565Gwu/bQguahwPn5TuoT8ulyuPkM1pq7D/fesy8a9/2nwZx24H4qvV51Fn4g7cC+a0dkRERNnBCwX0s2fPxr179zB58mScPn0alSpVQsWKFTF16lQEBASk/1ESERG9SvYJgmyHPFJaPuV1JdVek6DafUIJi+YJx8ep+ianFwNOD+LfBtgD158MiVf8hg9H7IMn66TExtIMeZ2sn1oeFBGL3guOIiImLtVtEBERUTYtimdnZ4cePXpg7969uHLlikrDHz58OPLmzYsPP/wQhw9zjlwiInoDA/r/s3cf0E1VfxzAv0m6N4VSoLS0UKAtq4yy995DEFkCMpQpgiBLEBky9Q9KFQQHIlMBlb1kb2TTll32Hp10Jfmf90KTpplNW7q+n3Ny8t599913m3qkv9x7f9eluOXtpA3ohYR5Qw/rVPFw1nzx/fesFpj4gRWm9ZJiXyUJ4m0AZXIyrjdshLMfvIfkmBijj0tR6F81d+l+NOZsj2BQT0RElM9lOsv9jRs3sGLFCqxevRp2dnbo378/kpKSUL9+fcyYMSNreklERPQ2Mt2njtBnRUAvZMG3L6Rpu/tv4j72ZYvE40WVBCS0ccOkhl+iskdlhJWS4of2Muysrhn5tzt2ATsGt0NSSpLBx/kWcVQfbx5RH+0qafr+27HbCJq6E9/tvQaFgcCfiIiICmBAHx8fj99++w2NGzdG2bJlsW/fPjF4f/jwIZYsWYKNGzfi4MGDmD9/ftb3mIiIKFtH6Etkfk97QWogP/hfYMBOIKgTUPEdcbZ+vcAXqOoaBrctY/Br9YnqW86V1v5n2f/cUyyb3xsKpULv4xZ0q4L6/kXwx5A6qFTSFaG9q2HTsLpadb7efRXHbj63/GciIiKiXMuiNLjFihUTs9v37dsXP/zwAwIDA3Xq1KlTR5x+T0RElKeS4mUmoLe21xynJtYTgvzUQN+9jHb9yxthnRCFKbWnYE3EGjh7FsKauyfw1BVoZBWIKpsuo/baS1jZIBQdavSGu5271u0+hR3w+6BaWmUBxXQz4fZefgKrB9dC3TJFLP/ZiIiIKH+M0C9fvhz3798Xk+LpC+ZTRUSk2QKIiIgoryTFy4xG4wGPQCBkoO4199K6ZTf3oXv57tjUaRNa+bXGpnpSHK4oRYPx/8NLLxc4JAI1B32PX6Z1hzk7zQrJ8sp7OovHnYM1X05M3nRJfJcrlOIe9qH7ruNxtCobfkxCMp68OSYiIqJ8PkLfvXv3rO8JERFRXh+hFzSZpHrpkzagr/cJcGQhIEynv74XKN0EDUs2hAQSeDt7o4RrSUQ2rAms2SNWb/vXfZxrtR5Vm7xnsgsrB9XE05hEsa2/zj0Qy249i8PLuCT8b89VcX196nZ3aXWoUgLzulYWvxSIS0zB7efxKOFmBzcHm0x8IERERJRdJEpzvu5PZ+/evVizZo04Up/WoEGD0KtXLzRt2hT5RXR0NFxdXREVFQUXF91pjERElA8o5MBMT0CRDIy6ABQqlT3PiX8BzPNTHQv72X8TCLx+qTpvuwCoORh3o+/CwdoBhe0L4+GJA3jVb4hWEw6fDIXPRyMhMbR1Xjo7Lz/CRyv/M7uLbSoWQ7+6vujx43F1Wc+aPrCzlqKaTyEx6CciIqLcEYdaFNAL+87//PPPCA4O1io/e/YsPvzwQ5w6dQr5BQN6IqIC4vQvQOwToPH47H3OneOAzBrwqg4sawbcP60ql1oBU7WT1ynlclz+eDCiH99DoUt31eWOK0LhU8v8L88zGtQbU9rDETtGNURsYgquPIqBi70VKpRwzZK2iYiI6C0E9ML2dE+ePNFpWHiop6cnXr9+jfyCAT0REWWb7+sCTy6rju3dgfG39FaLT47H/KEh6H5Yk+1+2mAn3PBUolOZTphSZ4rRxyQkyxEwZYf6vG2lYqjp645pm8PE84peLohNSEHk83izul22qBOuPYlVn9tby1CtlBu6ViuJd6qVNKsNIiIiynwcalFSPF9fX+zevVunfOfOnfD29rakSSIiooLHr6Hm+PULIOax9vW4Z8DTq+IU/KvvBOP7dpp/tif9HAvPhwlYf3U9Tj0yPjPOzlqmdf597+roX88Py/rWQO3S7vjx/RrYP64Jfny/urrO4AZ++KZ7FVhJdaf2pw3mxa4ny3Hk+nOMWX8eTRfsF/e9v/siXhzBF5LwERERUS5KijdixAgMHjwYt2/fRsOGDcWsu8K+88Je9DNnzsz6XhIREeVHTScDDu7A0cVAYhRwaQNQZxhw8wDw5wAg/pmq3kcHMafBHJwstg9Jp5fA5vFL2MiBCevlOF9agtnPh6BZ0wEYHjzc5CPTxuctgjzFV6rA4poRgF61SsGviCNKezhh+aGbmNwuELeexqHX8hNG27/5LA6lJ23TKvv63SooU9QJwd5uGfhwiIiIyBSLptwLZs+ejblz54pTAATCdIDx48dj4sSJyE845Z6IiLLdgXnAvlnCP8vAx2eBb7Vz1IgafgY0ngilRILHy37Ey28WajdRUYIuv/4LT6dieh8xd0cEfth/A7PfqSQmudNH+JNg/IYLkCuABe9W1pt4r87svXgYpdri7r/Pm2PjmfuYtS0c7o42eBGXZPTHrFLSFRuG1oWVzKIJgkRERAVGdHauoU+VkpKCGzduiP/gly5dGlZWFg3452oM6ImIKNsJ/xT/UE+znt4QGyegSk/ExPnj3tRFOpejl01DrQb6t7VLkSvErev8izqZnSFfn+M3n6PvTycxvVMF9KjpI06vP3HrBaqXKiROz08/Op9el6peqObjhpLuDmhcziNTfSEiIsqv3kpAXxAwoCciordi9XvAVU3iOmOSkgrhxkZ78Vjq6grFm9lyAq/NG+HsHwAllOI+9G87YK4351/cf/UaE9sE4L0QbzSctw/RCSl669b0c8fawbUh1bNOn4iIqCCLzs6APjExEUuWLMGRI0fw4sULnet79uxBfsGAnoiI3op/PgbOrNAtLxoEPL8ByBPVRUoFELFetR+8zy8/Y9e/PyFg5RHx/IoXMOV9GSCRoIFXAyxqugjWUuu39ksUkuEduf4M79bwhkwqwb2X8bCRSZEkV6D+3H167/mkeVl80rzcW+sjERFRfolDLU6K9/fff6NLly7iVHsiIiLKJCdNcjoEdgTs3YAzvwHNvgD+/ACQay5LpIBX3RdI8mwJh9q14ejwAJE7j8D3CVD+PlDmIXCjBHDo/iGsCV+DvhX6vrVfj7e7gzgVP1XJQg6aH6u4C8IfRuvcs3DPNSSmKDC+dcBb6ycREVF+YFFAv3HjRvz777+oUqVK1veIiIioIHIqqjl28QKaTwPqjAA8ygMpmtF5+NQF7hyFi08CUDhcHIlvXLEDliy8j6TZa1Hu4kvMXiHHF71lCPeR4NzTc+it6A2ZVHvrupywvF8NbD7/AP3r+uLA1af4aOV/6mtCwj4hC76bvbVqPT4T5xEREZlkUZpZGxsbjswTERFl1wi9SwnA2k4VzAvc08yGSzP1PjXQt5HZ4ONqH6NGm/7qS5Mu+4vvu2/vRuP1jXH84fEc/315udljSKMysLOWoWWQJ5b1rYHP2wWqrwsB/ns/Hof/5O2IT9K/7p6IiIgyGdC3adMGa9asseRWIiIiMhXQu3ppX+u6HChRDXj/L6BEVU151B1g7wz1qUfLdupj20eaHDevEl/h97Dfc9XnLiTraxHkiUENSotb6aUXNHUnZm8Lz5G+ERER5esp90lJSRgyZAg2bNgAf39/nQy6ixcvzqr+ERERFbwp986qhHdqJYKBD98klCtZA5DZAhfWAvHPgVPLgTrDgfDNsPGpgzK7duJGy1bAo6co/VCGm8VV/0Yfvn8Y92Pvw8tJ9WVBkjxJHNnPDWqXLqy3fOnBm3gZn4R53bjEj4iIKMtG6GNjY9GxY0fY29vj/v37uHfvntYrN5HL5Zg7dy4qV66MevXq4Z9//snpLhERERkfoXcsYvgTsnUGWn8FDD+lOk94BczzAzZ/DPzSGjYugF1QkHhpwno5nOKVsE5RYsg/SVg57wPEx77CmrkDMGZ8NXTZ2Albbm7J8d+GXxFH/NSvBjYOq4szU1poXVt/+p6YNf/G01hwp10iIqICtg+9sDTgwoUL6NWrFyIiItC/f3/xS4dChQqZdT+3rSMiordmx0QgIQroFComuzNK+Od7hgegSNYul0iR9O4u3Bk1Gcl37+JWh2D4upeBZMUGnSYeuAMr+hTHb8P+RW5y4uZzHL3xHIv2XtMqL1XYAXvGNII1E+YREVE+F52d+9BntZiYGHGk38fHBw4Omu1t0hL2uxdmBnh7e+tM8TdGoVBAKlVNRBB+VC8vL4SFhcHNzc2s+xnQExFRrvVNBSBaz8y4dl/jRbgNHn8126xmrD7oAf/Ppmbo39e34c//7mHsH+d1yud1q4zuNbxzpE9ERER5fh96QWJiIv777z/cuXMHKSnamWj79OljVhvCiPnChQvx559/4vnz59i3bx8aN26sVUf4AYT2du3aBUdHRzg7O+O3335Do0aNxOunTp1Cly5d9LZ/+/ZtyGSabXomTpyIYcOGmR3MExER5WqGtqLbOwPWwd8bvfWxnys8b0WJxym/rEV0/aZwrdcAuUnt0u56yz/78wLcHWzQPCjNMgUiIqICyKKA/vLly+jQoQMeP36M+Ph49TcHAk9PT7MD+r1794p72Y8aNQpBb9b7pTd8+HDcvHkTDx48gLu7OyZMmIBOnTrhxo0bKFy4sHj/8eP6t+JJDeaFUfqRI0eiWLFi+Pzzzy35kYmIiHKftPvTp5XwClZPD2kV2QYGosRPSzFhemO0PKvA2SF18M74Herr92Z8Caf1GyEzMgrwtpUs5ICl71eHs50VHG2s0Cn0iPraoN9Oi/vZd6hSQty3noiIqCCyKCne6NGjxVFxYaq84NWrV7hy5Qpq1aolXjOXEKwPHTpUHHXXR2h33bp1GD9+vBi8C1MBp06diuTkZKxdu1asY2Njg5IlS+p9CYQvHLp27YqAgABMmTLFrJkHwvSGtC8iIqJcSZ6kOQ7soHXJGo/Vx+79+6P0po2wc/dAnSFTsXR0OXzQbBwc3+2qriONvI8HEyYit2lVoRjqlimCKt5u2DC0rhjEp/r1aCS6/nAUvx65laN9JCIiylMBvTDNXZi+LqxNF4JsIcAuV64cfvnlF/zwww9Z1rlz586J0/lr166tLhOm3QsZ64Xp/ub49ddfsWXLFjHTfWqgLyTJM2T27NnijIPUl7Bmn4iIKFfyra96dygMdFwMvPsr0PE7sUh2faO6msTeTn38XsB7+Lvz3yjhVAIlJ03Gza8GqK/F/vsv5LGxyK2EkfhpHSvolE/bHAbfCVvxOkmeI/0iIiLKUwG9MHJepIhqSx3hXZgOLxCC30ePHmVZ54R19anPSEs4f/bsmVlt9OvXD7du3RKn5ae+AgMDDdYXvqgQlg+kvu7evZvJn4KIiCibtPsaqDMC+GAHYO8GVOgCuPmIl4T8dlJrhXjs/CbvTHpSe3uUadwB+ytpkuE937oZrxJeISoxCtdeameZzy0WvhestzxwqmYJARERUUFgcVK8VHXr1sWXX36Jjz/+WByhF6a2Z5XUNfBJSUk60+KdnJzMakMY0Rde5rK1tRVfREREuZ5TUaDVrHRlxdSHpds8QXKsNewrlDfYRBm3Mni3vQx3iyjw/j4Frs2fjlHRs5Boowry/+jwBwLcs+7f9qzQKbgEqvq4wbuQA0auPYutFx6qr3X47jBmdq4oTtEnIiLK7ywaof/000/Vx8JU9mPHjqFq1apYv349Fi9enGWdS53u/vCh5h9qgTALIHWNPBEREaXhrMn8bu2ggEPRRODOMYMfkbXUGlNqT8H2GhI8dgPcY4HGF5UoHK1E1esKXL5h+N6cIiz3K1XYEVKpRBytPzBOs0POxftRYvK89ac4w46IiPI/iwL6BQsWqI/Lly+P8PBwcRq+MPW+fv036/mygLBWXshsv2OHZgqdsE3epUuXdLa3IyIiIgB2ekamYzQJ8vTpXr47+gYPxOEg1aj8wF0K/BAqx8Q/FAh6fx7CAwJx7+NRUCqVue4jtpZJxeB+UlvtWQSfbbiAuTsicqxfREREuTagTyX8wy4E8cJL2Oxe+MY8I4Q16sJe9MIWdKnBunCeuj7e2tpazEw/a9YsrFy5EgcOHEDPnj1RrVo1ces6IiIiSiftv8U2b5anxZrIb/P6JUb7v4sPWhrOch+zaxfuDhyI5Dd5c3Kbd6qVhJebvVbZD/tv4Pv913OsT0RERNlNorTg63Yhq70QZC9cuFC9/7ybm5u4Zd2kSZNgZWXe0vxNmzaJSejSGzFihPhKtXz5cvz222/iNnn16tUT1+wL29i9DcK2dUK2e+HnFL60ICIiyvUiDwMPzgFxT4Aji1RlH2wHStXVrif8CXB9L7BKtX1dnPdQ3Jn/t+qSkyOUsXE63/yfa1IS8pH90CeoD3KrjosP48I91d8ngmuz2ogj+URERHmFuXGoRQH9yJEjsXHjRnH0PCQkRL2V3fTp0/Huu+9i0aI3fzzkAwzoiYgozzr2PbDzzRfnDkWAz1Qz4nD3JPBTC53qSbEy3NiiWoNf8puvcMr2OQ6u/R/eO6TKlp/q38oSuE6diD4V30du9DQmEd/uvYaVx2+ry96r4Y253SrnaL+IiIhyRUAvjMZv374dderU0So/evQo2rZtK66nz+tCQ0PFl1wux9WrVzlCT0REec/FP4ENA1XHEhnwxQvV8U+tgLvHdaorFcDNHcWhtPdA6QYXIbVSoqafL15DgQq3FfhitXZg73Z4O4oX8UVuNXzVGWy9qEmsO65VeQxv4p+jfSIiIsrKgN6i+WfCtm769nIXyuzs7JAfDB8+HGFhYeLMAyIiojzJOs2acgd31fu/s/QG8wKJFPBr+Qh+9VXBvOB/jx6hmlMp9GsxXqf+tb9XIjcLTrd13fydV/DxmrM51h8iIqKsZlFA36JFC3H9fHrCVHvhGhEREeUCfg0BqzdftMe/AOQpwMF5Rm8RAnmZtWbyXr3XCVhx8RAaFPVFtHbOObiG/gF5dDS+PfMt5p6cm+uy4Per64sPG5bGgHp+6rJ/zj/AvognOdovIiKirGLRlPvevXtj9erVCAoKQo0aNcR/wE+fPi1uXydcE6bkp8rKfelzAtfQExFRnqZQALM8AXkSMOI0sLiGbh1hOj6Uqjn3Rqwq2xHxHoEItvGD/dDpkCmBU4PqYL6Hajbb2nZrUaFIBeRGnUOP4NxdzZLA8OmtYW8j/NxEREQFbA19586dza77119/IS9jQE9ERHle6pr5cq2Bqzt0r3tWAvr9A1zdCfw1xHhbE+4Adq5Y/nEz1Nul2sLu6y5SXPaRINZBgrPvn4WV1Lzdbt4m4c+d/+2+im//1Wxj93FTf4xpWT5H+0VERJSZONSif3HzepBORERUoNQdCaw7rhvMV+sLnPkNaPq5ao29syrDvajeKMDaAdg/W/ue8+uAWh+iSNVawK5NYtGnmzQj+1eX1of/2vWw8fFBbiKRSMTgPW1ALxz7eTiiS9WSOdo3IiIiS3FTViIiovyukJ5M9C1nAu0XAp9cAsq3VpU5pQnogzoDjSfo3rd9HBD7FF4Va+l9lORFFCLf7Q5lcjJyo5q+b5IDvjF63Xn8diwyx/pDRET01gN6Ydra8uXLxW3rPD01//h//vnnePBANf2OiIiIcgm7dFP1Pn+iGrWXygA3b025o4fuscxWt71nV1G6ZBmDj5NHReH5r78iN/qhTzV817Mq2lYqpi6b+vdlJKbIc7RfREREby2gFxLdTZs2De+++y6ePNFkivX19cXMmTMt6ggRERFlE1sX7b3prPQE6alBvHdtwKsG4OKlKhu4C6jaR7ves6sovLQe1rwjx6/tAVuPRJ2mnn79DR7PmQt5TAxyk8JOtuhQpQRmdKoIV3trdXn5z3fgdOSLHO0bERFRRlmUFK98+fL49ddfxRF6YU1aahN3794Vs94/fvwYeV1oaKj4ksvluHr1qslkBERERLmWQg5Md9dktP/CSOCa+meBRKJdfmo5sPVT1bFzCSDmAYSV88K4tiRBisdXnPBhXVfIpcD/lmlGu1M8CqHCnn2Q2hr4EiGHNft6P248jVOf7/ikAQKK8d97IiLKG0nxLBqhj4yMRHBwsHgsBPSpnJ2d8eqVZkuYvGz48OEICwvDqVOqrXiIiIjyLGFqfar0gXp6wnV9dWoMBN5ZrjqOeaD+I0IY47ayU8CrSjTGK57ifmHt26yevsSNHu9BmZSE3Khz8JuZCG+0XngI1x7nrlkFREREWRrQ+/j44Ny5czoBvZD9Xhi9JyIiotxKYuFtEqDyu9rr7NMJSUgU/7A4W1r7GSnhV3CzU2fEHT0KRWKimDDPggmC2aJHTR+ULGSvVdbifwehUOSO/hEREWV5QP/xxx+jX79+6u3rTpw4gS+//FIc1R41apQlTRIREdHbIKyhz6b77ZVKHLpzDws7STHqQxkup9m5LunWLdwZMBBXqgQjolJlPPtuMXIDD2dbHB7fFEcnNNUq3xWW95cPEhFR/mfRv+ojR47E4MGDMWDAACgUCtSuXRv/+9//xKB+4MCBWd9LIiIiyhqmptybotTsOY+pL4AGY7UuuyiU8JEk42FhCX5pa/hZz77/HrlJCTd7bBlZX30+5Pf/cPIWk+QREVE+DOjv3buHcePG4enTp2LCuIiICPF47Fjtf9SJiIgon43Qpw3ohbX5jcYDff8GJj0EPjooFi979ARr7z9EaOxjLGtl+HnC1na5Zeq9oKKXK5b3raE+7770GB5Gvc7RPhEREWXLGnqBTCZD2bJlxXXz1taarV+IiIgot8rCEXqBlQ1QujFg4wB4VhKLCikUqJCUDN+UFIR4RuOlI3CgogTn/CS44CvB3iqqPjyZMxdXa9ZC7OEjyC1KezhqnQ/89XSO9YWIiChbAvoSJUqIo/RERESUR1Trp3pvOjlz7RgbUZfq/llhbSvHkBEyhHaQ4aseMszsKcNvzTT1FDExuDtoEJQpKcgNSns44eOm/urzsIfRWHrgRo72iYiIKEsD+k8//VRcRy9MsyciIqI8oN03wJAjQO1hmWunUjfVe/EqZlVvEJ8AOygR4loWW7pswcdVP8ZrW91ZAlenjs810+/HtCyPPWMaqs9nb49g1nsiIsqVJEoL/vX08vLCgweqPWg9PDxgY2OjdT0/jd5HR0fD1dUVUVFRcHFxyenuEBER5aykOODin0D5NoBTUd3r01xV78L0e996wIkleCmVwlaphMOkR4C1HTr+1RFzJ1zTudVj5pco3PkdPJwyFVDIUXzOHK3tcd8m4c+jpl8fwK1nceL5Lx+EoEl5PT8vERFRDsahFgX0v/76q9Hr/fv3R37BgJ6IiCgDjn0PHFkI9NsMvLoLrOqqudb9NyCoE+acnIPoX1aiwwkFwn0kqBOh/08RGz8/lN66BRI9U/nfBrlCiTKTtonHfkUcseOTBrC1kuVIX4iIqGCJzs6AviAIDQ0VX3K5XMzkzxF6IiIiMwl/Wggj6/f/A5Y11Z72HzIQm65twtSjU9X1Fi5NQQkjO8SV2bMHNiW9cuTj33T2HkavO68+vzW7bY7NGiAiooIj2syA3qKvvIUg99KlSzrlQplwLT8YPnw4wsLCcOrUqZzuChERUd6SGvDauWmXbx0DyFNQ2q20ul5Hu5J42b8G7ngYbu7uRx/l2Pp6Txc7rfMdlx7lSD+IiIiyLKCfO3cu1qxZo1O+evVqLFiwwJImiYiIKL8pXAZoMhmwdtCURR5C6Sc31ae2T8LR6cE/GDvISl2WLAMSNadIunEDKY8fIydU8nqTE+CNoavO5JrkfURERBYF9D/88AOGDdPNkiuULVmyhJ8qERERqTT6DKjQRfNpRD+A8x+aXDtRUimsASx4/BTLWqn+LPmugxTzukkRmSYH3d2hmczObyFnO2ucm9oCgxv4qcvm7IjIkb4QERFlSUD/4sULyGS6SWGEssc59A06ERER5eKR+lT/jBTf3N8s0av3OkF8bxX/GrurSdFvjAzHA6W46CfFZwOtoKxQTj1Kn1Mj424ONmgRVEx9vvTATW5jR0REeTegDwkJwXfffadTvmjRIvEaERERkVqtIZpjpSqQX3f/EeY+eYaOsapt4VKl7lFf1k41PH9kYivVbUlJeDI/55b1hfgWQvNAT/X5nRfxOdYXIiKiTGW5P3LkCJo1a4aaNWuiYcOG4jfmBw8eFBPI7d27F/Xq1UN+wW3riIiIssDlTUCaqfb6fOpRGLucHNEjOgaH7O1x31q1kH797BR1ncCI8Bz7dSgUSpR+s42dkPfv6sw2sJblzJZ6RESUv2VrlnshYD9+/Di8vb2xYcMGbNq0CT4+PmJZfgrmiYiIKIuUVY2069Vqtvg27dkLcS39mBev1MG84Ie2mj9XhEEEeUwM5K9evfVfjVQqQbfqJd/0A6gze+9b7wMREVFaaXLIZkxwcDBWrVpl6e1ERERUkFjba593/gFIiAZKNwJe3RGLnJVKcS29oF78axxxUN1zJEiCoaqBcVwf0A8px1Rbyhbq0weekyep94UXpuVHbdsGu8BA2JUvny0/xqv4ZPXxs9gkPItNRBEn22x5FhERkSkWjdA/ePBAa3u6hQsXolixYuLo/J07qn+UiYiIiHT2phc4FAaCewG1hwBFAwGrNAHxgF3i2/RnL/Dxi1cYbOUJmb0Drr/JSZcazAte/v477g0ZipTnzyGPjRUz4T+cMBG3OnXGzY6dsiWJ3oim/lrnIbP28JdMRER5K6AfM2YMfH19xeN79+5h4sSJ4svLy0u8RkRERGRYmuBeYOemOXYpLr4VlcsxOCoaH187hZO9T+LeB831thR74ACu1auPqzVCEHfkiLo88epVJEVGZvkvIdjbDacma/oifGdw5VFMlj+HiIgo2wL6PXv2oEWLFuLxjh070KpVK4waNUrMfH/gwAFLmiQiIqKCQpLuz4/iVYCGnwEdFwMuXkDxYO3rF9aj1J0NGX7MkzlzkR08nG3x7pu19IKPVp7OlucQERFlS0CvUCgQF6faZmb37t1ixnuBra0t5G/2lc3rQkNDERQUxG34iIiIsnP6fep508lAtfcBqQwYvA8o5Ke5vnEwSkmS1KdL20ixuWa6Nt5wbtkSiioB6tH78IBAxOzZA0WCar/7rDK1Q5D6OPI5t7AjIqI8FNA3btwYAwcOxFdffYXNmzejY8eO6u3s8kuW++HDhyMsLEzcio+IiIiykv5gXE0qBXqu0SoqrFSojz2dE/Hh9/9Cnq6ZZBkwq84j9Gp9Tav83oiRuBJcFUl37yKrONtZ448hddTnm88/yLK2iYiIsjWgF0av3dzcsGXLFixduhSlSpUSy1esWIEvvvjCkiaJiIiooBCS4pkiJMtrNEF9Wlgux+S+MnzbQYoyrq/htn8u/m7mKF7bHSxB73EyDB0uw+GkMCik+r8wuNGiJZIfZF3gHeLrrj4eueYsrj+JzbK2iYiIzCFRZkcK2HwkOjoarq6uiIqKgouLS053h4iIKO8K+wfYNwvo9jPgWcF0/TO/Af+MVJ9W8vMR37979BSNX79GXf9AFL0bi0hPQC7TDuKHbZGjzlUJbBM1I/simQyBly9l0Q8E+E7Yqj4Wvke4ObtdlrVNREQFV7SZcahFI/REREREGRbUERh+wrxgXmDrrHX65dPneC86Bg1fq/aqd3cojBslJDrBvOD7dlIM+FiCGT3S/akjlyPuxMks++VV9dFk6FcogRR5ui8QiIiIspHZAb0wxV54pT029CIiIiLKtLQBfdsFeCc2Dp8/f6n+42VOxEn42xRCaLNQrdt2d9stJtpLtpLgop8U/cbItK4/W/JDlv1yfv2gJua8U0l9XoP70hMR0VtkZW7F5cuX6z0mIiIiyhY2aQL68m2Awv6APBlY/a5YVDEpCZuunAeaeairTa41GcUci2k189pWghFDZOi/R4Ea15WIO34CL9euw7MlSyB1dITPsh9hXaKERV10tbdGl2pemLDxonj+Kj4ZwmpGSfpM/kRERDkZ0Hfr1k3vMREREVG2kKX5M8XOFSjTRHVs7QAkp9kq7r9f0bVsV5x4eALtS7cXi6QSKRRKBQLcAzCx5kT029EP87pJsXaOHFKlEo+mTVPffr1pMxSfMxtunTtb1E1bKxkqebni4v0o8fzMnZeoXkqTMI+IiCjHA/r0rl27hlu3bonfQPv5+cHf3z9re0ZEREQFm42T/uP0TizBtGlRWiPjf3f6G1dfXkVL35aIjIpU1ZNIDK41fDhhIpybNoXMwgS4whZ2AVN2iMddfziGIk42ODCuCRxtLf5Ti4iIyKQMJ8U7ceIEKlWqhHLlyqFVq1Zo2bIlypYtiypVqnDPdiIiIso6HuWB+mOA1nPFYFxNkaK3etpp7r6uvmIwLyhkV8isx71Y8ZvFXbWzlqF/XV/1+bPYJFT4YiceRSVY3CYREVGWBvS3b99G8+bN4enpiT///BMXL14UX3/88QcKFy4sXrt7925GmiQiIiIyrPkXQO0h2mUKuW69078YbMI5zVr8U2U1Qf+DQsDowTLE2qnOX+zanqnfhL2NdvI9Qe3Ze9F64UFx9gAREVGO7kP/ySefIDIyEps2bdJJ9iI007FjR3Hk/uuvv0Z+wX3oiYiIcpm1vYGILYCbD/DqjqZ8mmoNu+jWIeDAXKDVV8DyZpjrXgj3yjXFscjDKP0IiPAGlG/+lql6XYGJf6i2m7Mq6w+v6dPhULVqhrs1a2sYlh26ZfD6mSkt4O5ok+F2iYio4InOjn3o9+7di88++0xv5lahbPz48WIdIiIiomzT8Tug2RfAB6o162oPL6jeU5KAFe2ByEPA0gaAPAnjnz7Gd0fWYEGL/yGlSjmMDfkM8xvOF6vfLKb5uybl2nXc7tkLz37I+NZ2TQKKGr1ebcZunLz1IsPtEhERZckIvbOzMx49egRHR0e912NjY1GiRAnx24T8giP0REREudg0V81xjzVAQFtgWTPg/mkj92hG8ueenIvVl1di7VzdafyODRrA65uvIXNOs32eCULA7lfEES72Vij/ebovHN4o5GCNU5Obw0qW4VRGRERUQERnxwh9XFycwWBe4OTkJAb1+UFoaCiCgoIQEhKS010hIiIicwjT7/+daTyYFzy/AazpKX4ZMPbAMjQrURdh3rrV4g4dwt3BH2bos6/p5w4PZ1txKztDXsYno+XCgxlql4iIKNMj9MK0elNJ77y9vfNV4heO0BMREeViR74Fdk/JdDPKKS9Q66cqKBQLLPxRe7T+f0OLY4p7bxRr0Q7WxYqZ3eaT6ATEJqbA290BZSfrT7h3YVpLuNhZZ7r/RESUv5gbh2Y4oDcHA3oiIiLKkWn3lhp6DJV2vCcerp+tf1s8QcDFC5BYZzwA952w1eC1k5Oboajzm1T7REREMD+gt8rIp7V7925+uERERJT//FAHY1yd8Y17IXFru5Br+sc7IipVRtmjR2Dl7p6h5n/uXwNbLjxELT93jN9wUetazVl7ETmnXaa6T0REBVOGRugLIk65JyIiyuX2fAkc/sZ4nQ+2A7+0MdlUdOdQND85BxUjlUiwAaasVW1nl5Zbj/dQfNo0i7urb7R+VLOyGN2inMVtEhFR/pItSfGIiIiIcp0mk3TLXH2AlrNUxxIZUKqu9j71Brhs/QzTWy7AqfJSXPSTYnx/GZLSzWd8tXYdlCmGp+WbMrFNgE7Zor3XcPTGM4vbJCKigilDU+6JiIiIch2ZNVCqPnD7sKZs2FHA2hFwLgaUrKF7j1MxoOFY4O5J4OJ6TXlyHFq7BqJV3wvq3EHBRSui5wEFOp7QTGp8dfUyHMoHwFZmm+HuftSoDAY3KA2heb+J29TlvZadwK3Zbc3OWURERMQReiIiIsr7SgRrjjssAmydAakUqNQNKOSrW9/OFag5WBXwp/dtVVVQ/foV8HtXNHDxxboGUnzXXgrFm1j7h/k9MWDnAIsTAUulEr2B+/rTxncTIiIiSosBPREREeV91vaaY//mpuunBtNK3TXyUMqBZ9eBuaWA63swJfwokq0lOFRJCumb+L3LMSUuPL2AXy7/gsioSIu7vbhXVa3z9AnziIiIsjygl8vluHTpkk65UCZcIyIiInqrZDaaY6s0wb0pwig9JLr3LK6uPiwql2Ph46fi8ZnSmlH1ZmcV+H3vN+jwVwcxuLdE+8olcHxiM62yuy/iLWqLiIgKHosC+rlz52LNmjU65atXr8aCBQuyol9EREREmR+tT6+Qn+o9sOObc19g0n1g1DmjTTaLf42Lt+5gaVvNn04f7VAg9Ac5pq6SY+u1fyz+bRVztcOGoXXV5w3m7UOyXM/MASIioqwI6H/44QcMGzZMp1woW7JkiSVNEhEREVku7Vp2KzvD9QbsBLosVSXES2XzJnlet59NPsbVNgXXimuXVbyjxLO1a5AQFobwgEBcadkCR28dgELfdH4Dqvm4aZ1vufDA7HuJiKjgsiigf/HiBWQymU65UPb48eOs6BcRERGRZYRkeIY4ewJVegBWerLT+7cw2fQPj5+gWIJuoD5olwK33ukqHivu3EOhNkNQ7ZfKZifNS58gb/S685ArLEu4R0REBYdFAX1ISAi+++47nfJFixaJ14iIiIjyHDsXoMZAo1VKpshROMW8kfd5P8tR55dq2H93v1n1G5f30Drff+WJWfcREVHBZdE+9LNmzUKzZs1w6NAhNGzYUPz2+eDBgzh16hT27t2b9b0kIiIiMiqLRrP9GgKnf1Idl2kG1B0BrOyiVcXGOQVJMao/oZQ+iZDc0b8Xvfcz4Jd5CcC8oUi6cAY2NsaT9f3cLwSDfzuNvRGqQH7kmrMIm946a34uIiLKlywaoa9Xrx6OHz8Ob29vbNiwAZs2bYKPj49YJlwjIiIieqss3A9eh7WD5rjlTMCvEVC2FVBnBNBWlfi3WI1XcPGJR6nmT1Gu+gut20/76+4tL7gx7COz9qb/unsV9Xl8khwRj6It/1mIiCjfkyjNXdxVQEVHR8PV1RVRUVFwcXHJ6e4QERGRPi8jgUVVVAF4P8szzuPWIWBFe9Xxx+cA9zdZ8VPN9QNeawfx8UlS3N5YTDzeEyzBA3cJ+v6rOy3/1x87Ylb9WbCSGp8gufXCQwxffUZ9HjmnneU/DxER5es41KIR+oIgNDQUQUFBzAlARESUFwjbz024A7z/V9ZteZd2tD6VT22dIgcbTfCeWNID3b74DTfbVNSpt/vqVvz1z3zEnzlrtAtFnGy0zp/FJprZeSIiKmjMHqF3c1Ntp/Lq1Sv1sSFCnfyCI/REREQFyP0zwLImquOJ9wBbZ+3rq3sAV7fr3LYp3g2Pnjii26I98HAtgdhXT3Fg2lDcK+eOht8e0qlfbNo0FOrxnt4uHL3+DL2Wn9Aq4yg9EVHBEm3mCL3ZSfGWL1+u95iIiIgo/1AaH6FPe33yY2CWp3jYxeEV4PsKcC0hnju5eaDdwj9xO/o24r/VTWz3yEhAX7Gkq05ZUooCNlacWElERBYG9N26ddN7TERERJRvuJfRHEtlutfTTmy0ttO9nhijNapfyqUUwg086v6YMSj66aew9vLSKnexs8a5qS0QPH23uuzygyhU9SlktOvCvvXhD1VJ9Cp66X4pQERE+Q+/6iUiIiJKZe8GjL4MjLuh/zMpWcP4Z7W+r+r9+l7gwDzg7knM6ab/z63obdtxvVlzhAcEIv7UKa1rbg42WP9RHfX5pE2XDD4yIVmOMevOocykbWj/3WHx5TthK/r9fFLcWpiIiPIvs9fQ29np+RbagISEBOQXXENPREREaskJwLHFQLnWQLGKwPTCgCJF+wP67BYwT5Mdv5KfD5zjlfhpkVzc1q7Gdf1/egWEh0Ei0d72rvH8fYh8Hi8eT+sQhP71NO0qFEr8d+cl3l1yzOAvqHF5D3F/e2FLPHPEJ6Vg+aFbeBGXhOFN/OHhbMtfPhFRLo5DzQ7od+zYoT4+e/YsZs2ahSFDhqizwJ86dQpLlizB5MmTMXHiROQXDOiJiIjIoBc3gWu7ge2fGaxywN4OW50c0b7VEhQt5ANlvS4G63qMHo0iH32oPhdG2vUlx7v9PA6N5u83+xcTMaM17Kz1LCF4I0WuwCfrzmHLhYc6145PbIZiruYP7BARUS4M6NOqX78+JkyYgPbt3+zT+sbmzZsxb948HDqkm801r2JAT0REREYJf0p9aXwHIFH7hUCND7CkWxAaXVLiWIAEdSL0/BkmlSLg3FlIbGxQbcZucbQ81aUvW+HO83i01ZM535Rbs9vqzABITbhX7nPdzP1plS3qhF2jG+q9n4iI8lhA7+zsjPv37+s0LDy0ZMmS4nt+wYCeiIiITPp3FnBwnlkfVOVS3igSDTx1k6D0QyXm/CrXqbOsqzMmf74d3+x4gFUn7mToF+BiZ4XohHTLANJ8IeBkq8mJvO3iQwxbdcbstlcProW6ZYpkqD9ERJR9cahFSfHc3d2xfv16nXKhrHDhwpY0SURERJR3lW1hdlWlVCIG84KbxSX4uosUkUW163TbEYPG6xsjwHU/mvqZzljfs6Y3goq7YPOI+jg7taU4TV6fil/sVB/ffRFvMJi3lukfie+17ASGrPzPZH+IiOjtsGiE/rfffsOAAQPQtm1bcQ290MTp06exbds2rFixAr1790Z+wRF6IiIiMiklCZjpYdYHtd7ZCcvcXFCseA2ce3pOUz5be1T9nJ8EwbdUf6Z90HYYkOyKR476B072jGkI/6Ka7fIEyw/dxP1Xr/HLkUid+n8MqaM3mV41HzesGlQb9jYy/HH6LpYcuIEbT+N06g2s74cp7YPM+nmJiCiXTbkXnDx5EosWLUJYWJh4HhQUhNGjR6NGDRPbueQxDOiJiIjILJuGAOfXmP1hTS9cCH+4aILwOmEKjP5bYfSen4Pa4o9yTdXnv/QPwePoBPSo6WPwnvSJ9QyZ0bki3q9dSmdLvJ+P3MK8HVd06m8ZWZ/73RMR5dWAvqBgQE9ERERm2T0VOLJIdTxgF3DnGPAyEmj3DTC9kE71U3a2GFDcE15OXvijwx/o9k83PHt1H78v0F1Tn1afVlPw3N5VK+u9MSuP38bm8w9w8tYLg3Wmtg9C3zqlYCXTvxpTGOn//fht/LD/hlb59VltDN5DRES5dA19Wk+fPsWTJ08y2wwRERFR3qZMM7ruUwuo/wnQYaGYtV6fkIRErL//EOs7rIezjTMexT9CkrUEJ8oZzyQ/Ifo0Rjb1x/ZRDczqljDqvv6jOgavC/vbD6jvZzQw93Kzx/jWAeJz0/KfbDw7PhERZS+LAnqFQoH58+ejSJEiKFq0KDw9PcVjoUy4RkRERER6BGhv+RuYlAwXKyfxuH1p1bUYB831MG/dJioe245PW5ZHYHHDIzb6LOoRDAcb7b3o53athD7pptkbIzx3SKMyWmUxCckZ6gcREeVwQD9jxgzMnTtX3Ite2HP+8OHD4rFQJlwjIiIiKnDMWcVo46hbdnmj+Dah5gRMrzsdPcYuF88lwRVQYd0mrGsgxX134GYxzS0rz/6MeafmIVlhfjDdKdgLYdNbq8+bB3rivRCfDE+ZH1DPV+u80rRdGbqfiIiyjkVr6IW95oVM902bapKyCP7991/069cPd+/eRX7BNfRERERklp2TgWOLVcfTorSvTXuz9VzlHsCFtdrXSlQFilYAzv0OfHgAKBGM5Pv3YeXhAYmNDZqtb4Ynr5/ANkmJlV9r1tfvrSLBy096onv57ijvXt7sX1LHxYdx4V4UlvSpjtYV03xLkAHRCcmonCaQL+Jkg9Ofm791HxER5eAaemHNvL5s9tWrV+d6eiIiIqL0yr9JXlfrI+DdFdrXHpxVBfOCHxsBv7SFtZeXGMwL/uz4J1a1XQVXN088T7MzXbPzSuw7uQ7dNnfDs9fPzP7M1wyuLe5X36qCp8W/Jxc7a8ikmrX+z2KT8Co+yeL2iIjIMhYF9AEBAVi5cqVOuTBqX768+d8QExEREeUbtkbWtL/3OzDuBuBVDajQGRh2wnDd20eAR5fUp4XsCqGyR2Xs6bYHhWO0qw7aqcpd1GdbH8Ql6+4Xr4+jrRUqlXSFRGI8+Z4pOz/RTsoXPH13ptojIqKMs7LgHnGdfNeuXbF582bUrFlTLDtx4gT27t2LDRs2WNIkERERUd5WeygQeQio0EX3mpDp3rGI5rxogPG2ltTTmbavLwCvelOJ9bNTkGB9G3WjawEyGeY3nC+urW/t2xoyqXYSvKzkX9QZ56a20Arkwx9GZzhZHxERveUR+k6dOuH06dNiZnshqN+yZQs8PDzEMuEaERERUYFj5wL03wKEDDSvfrk2xq8nJ+gUFer7vv5HJwP1wpRQKBX49MCnmHBoAtZdWYfs5mpvrXX+zvdHs/2ZRESUyaR4BQmT4hEREVH2/JHxAPgm0PB1YVp+upF8RUICYvbshVWRIrjTv7/WtWcuwLBhMmEoX132daOv0dK3JbLTi7gkVJuRZpR+emvYp9sej4iIclFSvIIgNDQUQUFBCAkJyemuEBERUX7kUsL49d86AjGPtYqkdnZwbd8OjrVr6VQvEg2EXFPCLlEzViOM1mf32I27ow1srDR/Ui47dDNbn0dERBaM0Lu5uYnvr169Uh8bItTJLzhCT0RERNkmdTs7r+rA/f/01+n+GxCku6Txyf8W4vnSpXpvSbAGBn8sQ6KNBF/U+QLdynVDdtob/hgDV5xWn0fOeZPVn4iIsjUONTsp3vLly/UeExEREVEmOXkCwX0029eltb6v7r72wi2NGxkM6IU19cKe9d0nyPDlsS/hZO2E1n6ts+3X1CzQ8i3wiIjoLa+hv3fvHkqWLImCgCP0RERElO0j9OXbAj3XAMKfZXNKAYnpAviPzwHufjq3v754ETI3N9xs2w7K5GS9j+g+UTV+s7LNSgQXDUZ22XX5ET5cqZplsG9sY/gVccy2ZxER5XfR2bmG3sfHJzN9IyIiIiJ9hIR2XZfplr+6rbe6faVKsPH2hv++fw1+nmXvq8Zu3t/+vrieXpGYmC2fffM0o/RNFuzPlmcQEVEWBPQlSpQQR+mJiIiIKCuk2WPeylb38sp3jN4tZL33XbsG9sHBsClTRutal6MK9NsjF/erjwgMwpUqwbjatUuW/9qk0jQ/AxER5d6A/tNPP8XIkSPx9OnTrO8RERERUUHjVTXNiZ7AWCkHTv+iOY97BpxdBSTFA89viFP37a8tEoP6Mlu3wH3AAHXVGteVaHdKe4Wl/HIE9l/aLO5bn5WKu9qpjw9d49+JRES5cg29l5cXHjx4IB57eHjAxsZG63p+Gr3nGnoiIiLKNo8vA9d2A7WHakbmbx5QbVmnT2pyvNS19+kV9gdGnIYiORlXKlcx+uifW0hRb9QsdPbvjKxy90U8GszbJx4HFXfBtlENsqxtIqKCJDqrs9ynNWvWrMz0jYiIiIgEnhVULy1pxlpsnICkWO3L++cY/uyeXwfOrYbUyRN+rZ/g1o6iBqsO2K0Adk+E8nIHSGSyLPl9eLs7qI/DHkZnSZtERISsDej79+9vyW1EREREZIrUWnMs054FiadXgf2zjd//9zDxzc7NvI86vFsnlF28FFYlSkAiJOXLpGYBRbE34ol4HJ+UAgcbi/7cJCKi7FpDn+rx48c4duxYZpogIiIiorR8agOlmwA1P9QerRccWWjxZyWzk4tb2P3aTPvPP0n4DVxv1hw3WrTMkt/DkxhNFv3fjunPzk9ERDkY0L98+RLt27dHsWLFULduXXV5x44dceTIkSzqGhEREVEBJJUBff8C2s4HPCtqXzu3KkNN+bbQJKbzDFZNgVcaGIRPvncP19f8hMzqWKWE+njO9ohMt0dERFkc0I8bN058v3Hjhla5kPl+xowZljRJREREROl1WQo4FLH4c7EvnIyA9x7Av8NjuPq+xrwnz5BSvzzuuwNXNXG3WvKXC/D466+RJE+y+Jn96/lafC8REb2FLPfFixfHyZMn4e3tLa61Sm3i1atXYgb8uLg45BfMck9EREQ5KnwzsK5PljZ579PLuB19G+7NBhmsc23rXHQsYyDbvgm+E7aqj2/Nbpsla/OJiAqSaDOz3Fs0Qi8E7kLjgrT/gxYeKsuiLKlEREREBMC9jOmPoc18IMj87edKKoB6y9oarXNg0URUWlEJM4/PRKJcsy7eHIfHN1Efv4izfLSfiIiMsyigDwkJwcaNG7UCemGUfvbs2Vpr6omIiIgokzyD9Jc7ptmSrmgA0H2F9vVybYBWBjLi/y/9Vnm6Bu1SoOYVBdZdWYcav9fA2Sdnze6yl5u9+rj6zD1m30dERG8hoBcCd2G9vLB9nRDIT5w4ETVq1MCKFSswc+ZMS5okIiIiInMVqwQMOaRbHpxman6vtUAd1RZ2hjxzVr2fLa1/SvzYjQpYJyuFNZrou72v2d3jFHsiolwc0NerVw9Hjx4Vg/nAwED89ddfCAgIwIkTJ8TAnoiIiIiy0YcHAOdiutPyM7hUfV5PCf6oL8G3HaUYOkyGD0fK8FtT7T8PVy2QY90cOewSlej4V0cM22P8SwJ9LEjZREREWR3QC/8zXrRoEZo1a4ZRo0ahWrVquHTpEsLDw7Fq1SpUqlQpI80RERERkTm6r9Td2k4w4j9g0F7A1ct0GwHtdYq+ef0EVpXjseXpffwZdR+vnCTYXkP/twK/fSPH3ec3cej+IXFt/cPYh0YfN7ltoPp4T/gT0/0jIqLsDejnz5+PsWPHws7ODvb29uKxUEZERERE2cgzzZr3cq01x0X8gZImZkd2+BYIGQy897v2unsAZZOTMenFSxSRK8TX5rsP0CUuFk7D9X9BsHq+HPaJqtH2g/cOGn1ss0DNszadvWe8j0RElP0B/U8//YQ1a9Zg69at4ksYlf/5558tezIRERERmcclzabxZZoZrqdvZnv1fkC7BcLCdqDNXKOP8U1JwRfPX8L7+SmDdVZ8IxffZ56YKY7UC6/JhyfjyosrWvVKezipj7ddfISo18lGn01ERNkc0EdGRqJ9e810rY4dO4plRERERJSNpNZpjo1sEexT23g76be2G33ZYNViIa8MXqsYqRDWYqrP/7nxD7pt7oYuf3fRqmdvrelrlS93ISFZ9WUAERFlDauMVE5KShKn26cSjhMTM7YvKRERERFlkLEgPq3g3oBECnjXMtBOmrGcav0A15IGm3Lzi0dSlBWsnVLw7FoJyGPi1demrlGI72HewPReMiikqnX3119dx/WX1+FfyF88d3e0wf1Xr9X3BUzZgcg57cz7WYiIKGsDesGgQYNMli1fvjyjzRIRERGRIcJ0eX3H+gL2qr3N+xyVb0bLC5cFnl8D+mwAfu+qeYwU8KwWLR6/fvYM0TEOOk0E3QXWztWMuvf8TIYu/3TByd4nYW9ljyLOtloBvWDixouY/U7WJ1KWK5TitP7nsYko4WYPR9sM/5lLRJTnWGV0u7qIiAiTZURERESUTbJqCzh5iur9w/3Ay1uqve07fQ/8rbstXdGq0VAqJIi5Z2+0Sf8HwBVvoOaqmjjT5wzKFHHE+bvaU/fXnLyDjlVKoE6ZwlnyY6TIFfCfvF3vtVuz20Ji7AsQIqKCFNAfPnw4+3pCRERERG+P4k2SOlsnVTAvKF5Zb1VrewVK1n+JOxEVEXfuhsEmZ/wux+9NpOizT4Gj31fGpD1nkShXYOsF7S3uei47jqsz28DGKkPpnPQyFMwL/CZu4xR/IsrXMv9/USIiIiJ6e7JqxFmuJ+u8jaPmuON3Opc9ix8z2awQzAuKRgG/bBqFxT2rokHZIjr1yn1uOBA31+Frz0zWmb09PNPPISLKrRjQExERERWkKfdupVTvgR11r9lotpqDd23AU3utu61rCryXfm/2o4KXHkT3Ld2xtK/+NfPKTP4sfX46YbLO0gM38SgqIVPPISLKrRjQExEREeUlVraZu19YM99vC1Cpm+416zSJ75QKQJZmu7w3nPZ1RpGK0bB11Yzw2xdO0vuoks+BxLBw1FpdC7Pf0x2l/+/2S4t/jLjENzkA3hjXqjyOT2wmrptf+F6w1rXas/da/BwiotyMAT0RERFRXtBwnGrUvKKeQDwjHNwBvwb6p+6nDeilVnoDeoFHxViUbvMUJWq/FAP74rVeolhdPVP4Acz9RZUF/6sLg+AcOAFSu3vqa92WmJ7Cb0iFL3ZqnQ9v4o9irnZiErzOVb3QvnJx7X7sYBJnIsp/GNATERER5QVNPwcG7gSs7bLvGcK2d00mAzU/BIr4AzUGGq3u6vtaDOxtXeRwK/nUYD1Jmqn1jn6LYVtsU5Z2e15X3WR+87ppl/2w33AyPyKivIoBvQGhoaEICgpCSEjI2/2NEBEREeWkRp8Bbeerjit3BwbvA/pvM3mbsG+9f8dH4nR8eVvtrepanFHCOV4T1NsUOgGngEmAJBn7rjzJcBfTT9XvHuKtU8fBxgp7xjTSKkuWqxL2ERHlFwzoDRg+fDjCwsJw6tSpt/sbISIiIsothGn5XtUA33pmVbd2UIjT8YNc4nH4nTfJ9wAM2qXAT4vksE/UBPUSiQIOvt/ju73XMtytrj8cVR8XczE8Y8G/qBNaVfBUnw9fdSbDzyIiys0Y0BMRERGRaU7FMvQH5mAb3fXxK76RY/3sFHy+RrWuXmb3EGfuaI/mZ9TUDkFGr89NMx1/V9jjTD2LiCi3YUBPRERERKbV/yTLPqXKkUqEXFFNf7d2O56he+++iNc6b1PR+BcNbg42FvSQiChvYEBPRERERKaVb6v/2KGw4T80rQ2vWR+3USFsRA+74n/h3kvtID3tPvU1Zu6B74St4uuf8w/Qc5nmC4B3qnmJWe0z4sj1ZxmqT0SUmzGgJyIiIiLTrO01xxXeAXquA9zLqN4NBPUuPq+NNrl+jhzWyUq0+acWBu0ahCS59n72fhO34Vlsovr84zVnce+lps2AYs5m/ea2j2qgPl598o5Z9xAR5QUM6ImIiIjINKs0yeeEUfHyrYGPzwDeIYBLCc21Ypo16/aFNQG6V++KeptdtUC1rv7c7eOo91M1NAitiGR5MhKSVevsjelbx9es31xgcRf18dYLD826h4goL7DK6Q4QERERUR4boTfGSZNV3tYlRX1sH7NHiPYN3rbya00AP8y2E/Y/DwZQx+D4U+WSrrCzlpnXJyKifIoj9ERERERkmsza8DWl/nr2RZJRos4L+LZ8Km5p59/hMXxbPDX5qDELbuHXPzbB2u2kwTrjWpW3+Le249Iji+8lIspNGNATERERUdaN3lfurnXJtVQC7N2TVdUc5bAvnAwnL+Nr6wXuscA/v/4J54DxsHL9T+e6Iu2XCGbYN7ax+vjOi7iM3UxElEsxoCciIiKijPGsoH3eYRHgUhLo8C0Q1BkYsAv48IDB270bvIT394vMetT7/ypgX+IPLOgeiG7VS2raKGTmEoA3/Io4qo+/2haRoXuJiHIrrqEnIiIiIvMMPwlEPwCKBmqXewYBYy5rzn1qmWzK6cJYxK3ZjL+mfQvH5AR0vnlIb70OJ5XocDIF26u3x2cLNqNP7Xp4Ep2A0h5O/K0RUYHHEXoiIiIiMo9HeaBMk6z5tF7dhsS9MH4PbIWllTuZrN7mPyUiOrXHmaiNqFfOvO3qjLn3Mj7TbRAR5TQG9ERERESUPUrVM3q52OXl6TLqqXzTWf+fqB7RwP/++x9qr64NucL0tnbp/fJBiPq4/tx9iHqtWttPRJRXMaAnIiIiohzhdXImIu16i0F9RCFvsWxyg/ewx2oyJvc1viVd8Mpg/HPjnww9r3E5D63zKl/uwuh15yzoORFR7sCAnoiIiIhyVDXJNYyvPwxF/tiIM4VDoExxRZhjDTxy061b6rEShWJUo/qTD0/G3Zi7Zj9HIpHolG06ex+N5u/L3A9ARJRDGNATERERUfZrOsXgpY2205Aks4aylJ+6LOFRJ9xeMh2uk8dhZ1VNID7/ZzmWLpZj6io5yt1Tou3GtniR8CJTXbv9PB4pckWm2iAiygnMck9ERERE2a/Bp0ClbkBSHPBDXb1VJm68qDlR2qB5qVYoEeyArp1aILZmS626Fe8oMXOlHBElgUbKhrjY/5JZ3WgeWBR7wp/olNeb+y9OTGpuVhs1Zu7Gs9gk8dhGJsXFL1vC1sr4EgEiouzAEXoiIiIiyh7eNTXHwnT3Qr5A0SC9VUdb/YG9EdqBtpebaq95bxfV+np9Au4Bzc8p8eDHHxB76LDJLi3uVQ2LegTD2U57XOtxdCLMMWnTRXUwL0iSK1D+8x1QKHST+xERZTcG9ERERESUPRp+BjSbCgw7oSnTs45dMMpqk9Z5WJenkNw6ACiVwDRX2DinGHzMhzsUiPrmW9wdPBhj945GotxwcG5nLUOnYC9cnNYKS/pUy/CPtPrEHb3lpSdty3BbRESZxYCeiIiIiLKHjYNqqn3RALOqO+K1+N5HthsO20cBv3UCvlRlxrN2MG+bOq+fdqLG7zVwP/a+ybqtKxZHUWdb9fkfp40n2HsUlWBWH4iI3hYG9ERERESUM6wdtU6LSKJw1HYEZlr/olO1eM1XZjXZ+owSEqUSrTe0RlRilMn633QPVh+P+/OC1jWlUqk1lb727L1a17tVL6l1vmDnFbP6SESUVRjQExEREVHOsHPVOj1gOwYlJPoz1ls7yhHY4wHKtH+sLitULlZv3XVz5Fg/OwX119Y32QV3Rxu95TsvP4LfxG3iVHrhOL2D45pgwbtVMLGNZvbB4n3XTT6PiCgrMaAnIiIiopwhzfiGS0Jg7+T1Go7FE1C4fJzRukJQP+rfUUbrBJVw0TpPHZH/aOV/6jLh+O9z2lP4fQo7iO8D62u22hM8ieG0fCJ6exjQExEREdHbVb2/6r3h2AzfKuTU827wEj6NXojBvV/rJ1jVWIqpffRvG3f7xF588983SJInIS5Z/xcAjcp5qI8PXX+G2dvCdeqMWntOfdy3Tin1sZVMiuKudurzmrO0p+UTEWUniVJYHEQGRUdHw9XVFVFRUXBx0f4Gl4iIiIgsIPz5mfwaeBoOLGua6Y9QSJd3FTbA2iJ6r/cZK0OStSq7frlC5bCh4wat67O2hmHZoVtmPy9yTjut88QUubh1naHrRETZFYdyhJ6IiIiI3i5hmF3IgF+sClBck5TOUsLYfCCS4OavfwR+yWI5PF+oxrCuPb+Cmj9WFEfsU7WqUCxTz7e1kukE+EREbwMDeiIiIiLKGTIr4MP9Fq2l16d4jSgxcZ5D7yda5U4JwHdL5ah2TYF1c+VY8Y0ci/oHQ6lQiNdr+LobbLNCujX2aZPgpfVN9yrq45O39Cf2IyLKagzoiYiIiChnR+u7Ljddz6s64NfQrCZLyVP0lk/4UxXAC9qfUiIiqILRdsa0KId/RtQ3ulVdqgZlNevwZ23VXYNPRJQdGNATERERUc7ybWD8ukQKDP4XeHeFpkxqbfSW6KYxZj06JklVz9ZK98/iZoFFIZNKcP6LluoyW2v9yfc8nG3Vx9Yy/olNRG8H/29DRERERDnLMV0yu9rDgAaf6taz0mSTR49VwOjLBpusVTQGrqWNb2snmLfwPTy9fx2LelTVuVahhKv47mpvje96VsW3PavCydb08oCL96NM1iEiygpZs2CJiIiIiCirtJ6tej+/Doi+pxnBt9KMgkMiA1z1T39PZeNoOjldr59v4dnPHfDNhzJIZFOglDuJ5b1r+WjV61ClRMZ/DiKibMYReiIiIiLKnT7YqhqpT11jL9U/3d2QQuXiYF8kCUWDtUfME/TM1l/0oxxO5WbCprBqH/nPWulPfmfM5+0C1cePoxMyfD8RUUYxoCciIiKi3KmQL9BsKuBUVPeajaPq3aeu6t3ODZDZaifRt1bCt/kzFA6Ig10hzTZ1+/vF633c+tkp+Pun7XB13YJHCTcz3N33QrzVxzEJ+hPzERFlJQb0RERERJR3tJwFVO8P+NRWnXf/DWg5Exj5HzBFe7u6tErUeQn7wkko2eA5Pol9hetNXxusu3bFfvRb3xW7IndBodRkxjfF2U4z9L/94kOz7yMishTX0BMRERFR3lF3hPa5kwdQd6TJ22xd5PBt8Ux9XsspFs9hb7D+Lwvl6GkzBnKZRDzf130fitinS95nxNe7r2Jks7KwhFKphETYzo+IyASO0BMRERFR/uFZSXNcurHBaoUkphPmrZknh3O8EvaJSjRZ30QMtLPTlUcx8J2wFX4Tt4nvRESmMKAnIiIiovzD3k1z3PdvwLOi3mrW9goUqRitkzAvvZ8WybHiGzlCQ1Ow81r2BdlCEr1WCw9qlY3943y2PY+I8gcG9ERERERUMNiotqRL5VExVkyY514u1uStHtFAqY7jEJdsfG/7eV0rW9S1Wl+psuun9ed/96BQZO+sACLK2xjQExEREVH+1Wi86j24NzDpvt4qHlWixWR5rrs3IsbOeHP95tVC3TV1kaxI1nu9UklX9fGz2ERkVulJ2zLdBhHlXwzoiYiIiCjnBXVWvZdukrl20q9zD+oIjAkHOi42eIuwvb2zVyJKnAlF4LHjgLXhvNEzVsrx05cvMWZcsN7rNlaaP69PR74wq8ubzz8wqx4RUXoM6ImIiIgo53VaDHT5EXj318y103iC6r1aX02ZSwlAasafvWdXwtneFYEXLyIgPAyuXd8xWHXYVgVCfxqKl9HaW+U522m+DLj1TP9+9+mNXHNWfbysbw1EzGitdV3OafdEZAADeiIiIiLKebbOQJX3tJPaWcKvATA+EujwrWX3y1VT6YVt44rPmAGHWrXg/sEHeqs2nb8fj2o2wrW509UZ8Is6a+bsz90RkeHHtwjyhJ21DHXLFFaX3Xpmeo0/ERVMDOiJiIiIKH+xLyRE5MbryGxUWfDTm1EESEkEvq0GyemfUMpvEzzL3cTjX74w2FTKL2sQsrwS1kaszbKt7X7uH6I+XnXiTpa0SUT5DwN6IiIiIip4igYBJarqvzazKPDiBrBtrOr8/Bo0/M/wGnzByq/lqNL5S9T6sZJW+YqjkRZ1TxilT/XLEcvaIKL8jwE9ERERERUcA3YC5dqo1upLNEGzKZKnF82q990SOWyLblaff/HPZaOj9pfuR6mPt35c3+z+EBEVmIBeoVDg9OnTOHPmTJZNgyIiIiKiPMinNtBrLeDuB0gNZ7NPT5jBX6hsLJy8Xhut5/IasCl8BJBqEuLN3XHFYP1P1p1THxdz0d4zr1UFT7P7R0QFU74P6KOiotC0aVOMGDECvXr1QpMmTZCcrH/fUCIiIiIqQCQZ+1O4WPVoeDd4iSedoo3WWz87BXVsp0Fipaq35MANg3WvP9EkvHNKkyFf4GpvrT5+ngV72hNR/pPvA/rY2Fh8/fXXOH78OMLCwvD48WPxnYiIiIgKOJk14FM3w7c1tI+Fda8nKN3zARL/N0lvnalrFHgneiaKvn4snt97ZvxLAIGtlfYSgO41vNXHe8JV7RARpWX+PKNsEhMTg1WrViEiIkIcRff399epc/36dfzxxx9i3Xr16qFdu3bqay9fvsSRI0f0ti3U8/LyQqFChbBlyxbcvHkT9vb2KFu2bLb+TERERESUBwjz6D/YBsQ+Ab4uZ/5tAPwVKeJxcCkX3KpcGQkXLujUG7RLgUGYLx7H/AW8+PxzuPfpbfZzavi6q49vPo0zWd93wlat88g5mr+ZiSh/ytER+uXLl6N8+fLYt28fFi1ahHv37unU2b9/PypVqoSLFy+K69/79++PoUOHqq8/fPgQS5Ys0ftKXS8vTLsXzn/66SdUrlwZMpn5CVCIiIiIKJ8H9c6ewGe3LLt/fV8UHTPGrKqPZ87E62TNGvyM5HZaevCm0esX7r3SKYtPUn3pQET5l0SZg1nizp8/j9KlS4sBt7e3txjYN27cWKtOUFAQ6tatKwb/qQG+sA7+5MmTCAnR7M9piDCq7+zsrD7v3Lkzevbsiffee8+sPkZHR8PV1VXso4uLS4Z/RiIiIiLKI6a5WnbfsBNIjLGGIi4Okd2N/415LECCzmsP41DEa4xaq0mI16WqF/73XrDRUfdtHzdAUAkXs0bnU3GUnihvMjcOzdER+ipVqmgF2+ldu3YN4eHh6NOnj7pMCPh9fHzw119/mfWMnTt3YsCAAVi/fj0WL14sfhEQEBBgsH5iYqL44aV9EREREVEBNvqy8es/t4Ttymqwv7kUgRHhRqvWiVDiw68bYtTas1rlX3QIMtmNtt8eQplJ28zrMxEVCLk6Kd7Vq1fF9zJlymiVC6P6QrBvjm7duqFNmzbYtGkTLl26JK7FF75IMGT27NniNyGpL2HmABEREREVMO2+0RxbpdlOLqizbt2EN3vJn/sduL4HTjMnGG16xko5Wj/egCpPrwnz7sUyNwcbvXU/bqad+0muUEKhMH+C7dMYZscnys9ydUAfH6/avzP9KL4w5SD1mjneffddrFmzRlxHLyTVM2bixInitIbU1927dy3sPRERERHlWcG9Af8WQOs52uVtFxi/7/eu8L70MfwOH8C4AYbzNo06dhxzjixFo/uaaff6+Lg76JRN+fuS1vnpyBdaU/c7VimhPh+04pTx/hJRnparA/rUQP7VK+0kH0Jme2NT9TPD1tZW/MIg7YuIiIiIChhrO6DPn0BtTTJmdRI9M9i9vo2e7ScgzBu47ANsqKv/vgmnVxltx7+ok07ZqhN3xPeEZDkiHkWj25Jj6mszO1fEvG6V1efn772ZPUBE+VKuDuiFhHgCYUu7VAqFQpyKn3qNiIiIiChb2TjqPzbmp+boWq4bpvWxwpe9rbDKv5PBqtv/GovYgwf1Xgv2dsOCd6ughKudztT7gCk70HrhIa1ye2sZ7Ky5oxNRQZGrA3oh+V2tWrWwdOlSddnGjRvx9OlTcW08EREREVG2s7YHBu9TvYTjplOAyj1M3mYns8WGNocQe+NTvI42vuzz7ocfIVmejCsvrojvaXWrXhJHJzbTKjOUHE8qNW8GARHlD1Y5+XAh4/zq1asRGxsrngtZ6IXs9a1btxZfgh9//BHNmjVD/fr1xQR1mzdvxowZMxAYGJiTXSciIiKigsSrmua44VjVe92RwBIjgfovbVG26zIokzzE079L10enm4cNVq++siqUb6b0X+x3MVPdXTGgJvr9fFI9NZ+j9kT5U46O0Ds5OcHX1xcVK1bE//73PzFoF87d3NzUdSpXrixOsR82bJi4Zd2xY8cwadKknOw2EREREeVHRcqp3otWMK++fSHj1+8cheR/FWCNFPH0bPu+GN54NHZV1p4+n2rdHDkq3VKIx7VW1dK5vlDPPvVpTW2vWZLawL+I+vjQtWfG+0lEeZZEqXyzVwbpJexDL2xfJ2S8Z4I8IiIionzs1R3g5I9ArSGAa0nT9YXt6ub4qI5lNoA8SW+1LZJGGPH6I/W5leMllHZciVh74KdFcp36N4oBEd4SdF28FX6ufury57GJqD5zj8HunP+iJVztrdXnvhO2iu+NynmII/ZElP/i0Fy9hj4nhYaGion3QkJCcrorRERERPQ2uPkALWeaF8wLJGmSzzWZbLBae+UBrfOUuIq49nwKprX5BpFFdeuXeQS0O6XEk8Ztcean+epyexvdZHdBxTV/6BtaPn/w2lNTP4m4tz33rCfKe3J0DX1uNnz4cPGV+s0IEREREZEWqeXZ5D9uXBUtfcshYv0fUDZ+V28dl9cA5v+Msw72qNpzBBxsdP90X9QjGAeuqgJ2ZzvN6Hxapubjph/5j5zTLmM/DBHlGI7QExERERFZJM2QuH9zwKkYUE6V2Dk9L2iPkhd/sw2dvYMzjgUYz0xv92UoYhNj9F7zKeyAQQ1Kiy9LpZ/GHxWvnWWfiHIvBvRERERERJaQpRkRd/MGRl8Geq4FiusmrztiNwpOiFefW8lUf4a727njTBnTW819O6QWdt3aKUyO1yq3tcrcnvNPohN0yqpM35WpNono7WFAT0RERERk0V/SMmDoUeDDA4CdKyCzAoRt55S6ie4E/WSaQLn97sbANFc4r30fw9wfQ1YvGqW/fAdldglBu64ux5TYvmgMnAMnQSLTP1qf3gf1fE3WqfnVXrPaIqLciQE9EREREZGlPCsAJYxvJ5dqnPV6SKBAiCQCdglvpuDf3Ieg5GSU846Fbfhi2PxcCTZH/tF7/4c7FJi0Vg6ncrPE85UDjWeubxHoqT7O6MZWSSnaMwGIKHdiQE9ERERE9JZ8a70Yf9hON1qnTPxLg9eCbymxfnYK6j8+Ci83e6PtlCzkoD6+/Vwz3d+QT5qXVR9fuPfKZH0iynkM6ImIiIiI3pIOsuOmK/3UAtaOKeLh4g/0T9+ffGwjbM+cMNqMMPs/1a3ncSYfO7yJv/p40d5rpvtJRDmOAT0RERERUVZq82bv+MYTLW6iTNsnKPfOQyx+/RjJDvqnv0ePGoFPP66AJ/FP9E6r93bXjND/dOiWyWdav0nUJzh07ZnFfSeit4cBvQGhoaEICgpCSEjIW/x1EBEREVGeV6oO8PkToPEEi5uQyACZjVLcGK9C68cG6w3apUCLdU3xW4cgRAQG4easaXrrHb6uG6Dfe6mZhr97dEOL+0pEOYcBvQHDhw9HWFgYTp069XZ/I0RERESU91nZZllTQmDv6mt4DfzauXKEXFONzieuXIekx/q/APjvtvba/AkbLqqPi7rYZVl/iejtYUBPRERERJTLFa/1CuW6PoRsYj2TdW80aozIjb/rlHf94ajWecQjzfZ39taq/ezLeDhmSX+J6O1gQE9ERERElF3azMuSZoQEdzJrJcrd+gNh3qbrv540C+EBgehxZQ+qPNUkuFMoNOvso18nq49trFRhQdOAouqyFLn+tfvCWv0Hr15b+qMQURZiQE9ERERElF1qfQS0+yZLm7QNKmV23X7hOzDnyFJIpNHi+b2XmkA8SU/A/kE9P/XxqzQBf6o7z+PhN3Eb6s75F74TtlrQeyLKSgzoiYiIiIiyU40BpusEdQJsnM1rTxme4S5s2zgdTnb/IfRAhDow16e4q2Yt/frTd3WuN5y/T+t85JqzGe4LEWUdBvRERERERNkp7Ybwhry7AvjsplnNtSj0Cod7l8PdLtEo3fYxPKtGieUvAozf98faNdgW0w+VVlRCw/l7DHRV09d5O66Y7Mvm8w/M6jMRZQ8G9ERERERE2exqwDDNSenGgF9DYOhRoEQ1oNEEVdBvZaN7o727TpGtBBgs34+WtrGwdZGjULk4MbCvVfkBBo9UJbczxCZZtYbetuh2YahfPPYv6mSwPqfVE+VuVjndASIiIiKi/C7e2Vdz0uwLwKua6vhD7SnsWiY9BML/ATZ9ZLRt4bsAIbAX/C/mCVY19kDv/foT2v2+QFVvdaOD+KvuYcSEz0H3GiWNti8kwRNG7uVpEurpu05Ebx9H6ImIiIiIsv2v7jQj50WDzLvHxkGIljP0mJCERLT0ewYs/AJ3A9xxJFB/oN3rgCrgt/HYATcHPTMD0jj9Zv/69PvYp9py4WGG+khEWYcBvQGhoaEICgpCSEhIFn7cRERERFQQBZVw1ZxY2Rqu2Hya6r3Dt6p352IZflbD1wkIvDwNLYc2xsANlwzW87+vhG2R/fD0vK1V3qBsEa3zDf/dw90X8ei+9Ji6bN/YxurjL/65nOE+ElHWkCiFOTJkUHR0NFxdXREVFQUXFxd+UkRERESUcZc2An9+oDqepkpiZ1D8C8Dhzdp54U/1A3MBawdg9xSLPvmUoVdwrVETvdem9ZIirJRqjO+jyh9hRNURSJYrsOnMfXy24YLBNm/NbituX5cqck47i/pGRJmLQzlCT0RERESU3TKyxjw1mE+9r/EEIGSQxY+2+qMDYr5ahFsuuqP901Zr1tovvbAUl59fhrVMiu4h3kbb5Jp5otyBAT0RERERUbbLZNI4YT396DDL7n12FaWaN8SwpmPRruNcncszfkvBxHVylHmgRI8tPbArcheyipAlX3jN2R6RZW0SkQYDeiIiIiKi7JYVWeBdvYBBey261f7eEfFdIZUhWpi+n0b5+0DVm0rMXiFHhUgFPj3wKS4/uwyp3X311nb6NCnvYfSZfX8+qT5ecuCGRf0mIuMY0BMRERERZbss2tbNydOi21zWv4NaknDx+P3Whtfif7FGgRLPlRi56j04lfoWjmVn6tRpVE4VyN98Fmf0mQevPtU6//Egg3qirMaAnoiIiIgou/nUUb3bZjLJssxaczz0KDDhjuY8sIPRW9fZzhDfk2TW+Kt0A4P1Fv4oR+gPcqybK4eDPFYY19e6XqNUIfF9RBP/DHX9q22cdk+U1RjQExERERFlNycPYOx14NMrmWtHmiagl9kCdq7AsOOq13u/A1V6mdXMsortzar32zdy2LnvBmRxeKeaFzoHl8CgBqXFa6UKO6rrpd846/6r1+b9PESUKQzoiYiIiIjeVlAvJLfLDJmVblnRQNVL4GY8O31FyU3x3d3FHgGXLpr1yBl798C53Ax82qY4FvaoCnsbmVhe3tNZXefeS+0A/sj1ZzDXqLVnMWL1GbPrE5EGA3oiIiIiorwi7Qi9Pkrt6fHpbbH9XHyva3cbkovr1OV2FSoYvKfibaW4rr71hmZa5VYyTV6AU5EvtK79fvw2zCFkwP/73ANsufAQ3+29ZtY9RKTBgJ6IiIiIKK9Iu4Zeqhop15J26nvLWXqb8JY8xrexnwJ/D4NPk2dwLJYAr+7+cGrUyOi6+vVz5Bg2tgI6bFKt1be3lhlM4n/hXpT6uJafu1k/2te7r5pVj4g0GNATEREREeWlgD5kMFC5B1DIV/d6mSZvDiRA3RF6mzhkO1p97OiZBJ/GL2AT/gO8PuuPwkOHGH38yC0KREbdwh9X/4BUqonifztmeER+VpeK6uPXSXK9x0RkGT2LcEgQGhoqvuRy/o+GiIiIiHKRdgsMX/OtDwzYCbirEtdlhHRlKxT96CBs/fwgcy+MhGtX8XTuPJ16wkh99wlfYvqx6YBkJqC0QsUSrgbblUk1Y4j3Xsaj7Ju1978ejdSpG5+UAgcbhihE5uIIvQHDhw9HWFgYTp06ZfaHSURERESU43xqA05FLbt3aUO4nnkfTpX9ILW1NVitxVnV1H5Hv4Xi+0oja+aLudipj5PkmjX+606l2XLvje0XH1nWb6ICigE9EREREVF+1fcfwL95xu/7JhCFevQweHnwTgXWz07Bn9+YDsBTs+ILvtoWrj6OfB6vU/enw7cy3leiAowBPRERERFRflW6EdBng0W3SqBEmd27TNZrIvsMDSqbt03dkevPcUdPIJ8q7GG0Tlnj+fvEbPh7wh6b9QyigoQBPRERERER6ZruDhsvL5TeugWlt2yGS9u2ej+lzzYoMGn6HIQHBOJ6s+aQx8Sor815p5JO/Ybz90GhSJONP41KXtpr8eUKpXokf9Bvp/lbIkqHAT0RERERUX431sI93qe7w7ZMGdj6+8Prm6/h1tPwNHxB8v37uBpSU31eMV2Anqr0pG16yy/e12x3Jxjy+39a59efxGag80T5HwN6IiIiIqL8ztIkedAeSS/cr59Zd3nEv0SVp9egSDNab4iPu4PBa7vTTbNv/90hs55PVFAwoCciIiIiKgiafG7ZfSmJ6kMbX1+zbvlt1yzMObIU1p1bIPHmLdTzL2ywbrfqJTGjs2avemMSkjVZ8omIAT0RERERUcHgWlJz3HkJ4FMHkMhMT8efaenovsrNtm0R2rEsRjb113u9cXkPPI1OyNQziAoqjtATERERERU0wT2BD7YDkx6YNx3/4p/AV15A/AvYBQWJRVaLpuNGr3r4rJHpafgPG9ZHp2/H6b3mYGOFJLn+JHlEZBwDeiIiIiKigkgiAaztzKu7YSCQFAvM80OpDinw37MbZVu9i/ZTl+NiId1M9vokX7+GwOeRWmVDGpWBf1En9K1TSl2WIldovRszb0eEuKUd96+ngooBPRERERERqXhVNx1A3N4P6xfH1OebR9THBv/6OF5egqsljN/7zaHFqPI0AlAqxdeENgFieSEHG3WdZ7FJ4nv4Q01Cvf51ddfuC1vffb//hng8Y0sYf4NUIFnldAeIiIiIiOhtMDKtvfMPwIG5QKfvge9rmW5q4yCgYldAKoWfhyOWV+wMK5dScCi+FjWvKhHuLUHDS0r0/Vd3lH3OkeWak7ntxTdbK80449aLDzGwvh9O336hLotLTNFp5+cjt7TOX8Qlwd1R88UAUUHAEXoiIiIiogJBYvhScC9g1HmgaABQvq15zZ38UXxzslWNEaZEByPhWUvsd+mOV7bO2FJTgpVNjYcbiTdviu9SqaZvhd8E5V9u1oy6D25YWn2sFEb3Afz53z2ttnotO25ev4nyEQb0BoSGhiIoKAghISFv9zdCRERERJQdgjoChfyA4D7G63X7GWg6xXR7O8arps6rSZD0vBlSomog7trn4hr9zbWMhxs327ZD7IEDSLqnCc6/3n1Fp14JN3v18YlbqpH7iEfae9ynPycqCBjQGzB8+HCEhYXh1KlTb/c3QkRERESUHWwcgY/PAp1Djdeztgeqf2Bem1+66S2uXdod1YpWE497jJdh+FCZwSbufjQEN5q3gGPSa9X5C9V7n9o+6jr21pr7e/zIkXiiVAzoiYiIiIgKUmZ7czgWBj7YYV7daa46RSlyJaQSVaihkEqwc8RZ/PldJ6PN/LlNMyvgv9sv8PvxO+pzWZop+fqm2xsS+SxOPUWfKD9iUjwiIiIiItJVqk4GPhUhaNYE3U52VijvXh6nH58Wz61l1uhd80OsqfsPWpxRwjlBfyvb/xqL665emFrCxejTxv5x3mSPhO3sUkXOaWf+j0KUh3CEnoiIiIiIMmWc006t80H1S2Nk1ZHoG9QXq9uuFstKu5bGkz4tMHC08TFF/6j7sD6v+iIgq+jLkk+UHzCgJyIiIiKiTBme8pvWeT3/wnC0dsS4kHGo5FFJXb6o6SJc7HcRG+sbXlMvmH3kR/S4sge+UQ/VZeNalddbt2QhTcK8VBGPorXOK3yh/YUDUX7BgJ6IiIiIiIxzKWnyE3KAZh69xMRa/V4LNmPocBneM5Iwr1/4Dvyw72txGv61ho3Q/fJOyBRynXo9a2qS5yXLVfvet154yGR/ifIDBvRERERERGSckODuwwNAy5lAh2/1VgmzG6A5iXsO/NwGOP0LEHlYlTjv1E/qy35ufvixz0YopRI8dTOdqC/lyRM8W7wYP+6dp3OtXaXi6uPo18lm/SYv3Y+CQsFkeZT3MSkeEREREREZJ8TcJYJVr1d3TX9a80ur3u8c1ZRtHQN41wKKVRRP/d384evi+2Y0/5pZv4ESwhcF6XilmXL/PC4JhZ1sjbbBZHmUn3CEnoiIiIiITEgziv5mOzpD1tlMN3xxST3g/hlVIKJUYpNzDWyqNQNugwaKZWHepn8RwhR8u5RE8XhRj2BYyzT92RfxBBnBLe0or2NAT0RERERE+oUMVr03/8KsgN5H8hi1pBHGP81lTYDpRYDp7rA6/A1kSxuiWI/GKHvsKJ5/9S36jTGeME+wactk8b1TsJdWeUUvV5P70qe15MBNk88iys0Y0BMRERERkX5t5wNjrwEVu6aJIAwH3AdtR5v3SSq017pLljeD1Y5hcEv0wrP7o3HBVzUjYF5XKdY0khocqY89dAjKpCR12eNoAxvcv7En/LHW+dwdJr58IMrlGNATEREREZF+wvp2p6LaZVZ2mmOPwKz75K5sQ/c9daBILIZJdT9C94lWOF1Oik11pRg4Sv+XCHcHf4iIylWEufPi+Zj1540+YubW8KzrL1EuwICeiIiIiIjMZ+cCtF8ItP+f6jiLOeI15PH+iAmfI+5ZL4hxkOCfWoaz4W//e5w4Ym/MizjNSD5RfsGAnoiIiIiIMqbGB0CNASYT5Fnisp0qQV56O6qbflb6oH5kU3/1cbUZu43eu+bkHTEDftos+ES5HQN6IiIiIiKyTDYE9ILSkgewQoq4f/3Xj5+KZc9cJeg/Wobv2xl/ZnhAIEadXS8eNwlIt1zAiIkbVbMBiPISBvRERERERGSZhsanuVvqX9uxuG7XVzxuGf8a52/dwUWnWvi5/XLsryxFqImgvvXtk+hw8zAevjKeJM+3sIPe7eu2XHiQ6Z+B6G1gQG9AaGgogoKCEBIS8lZ+EUREREREeU6ZpkD/bW8naLn4Byosa4U6xevgQGUpen5mfHu7YRf+QvlZY4zWiXweL75Hv07RKh+x+mzmO030FjCgN2D48OEICwvDqVOn3sbvgYiIiIgob0qfBV8fF+394jPjx/L9xWR5cplqCv53HaQYO1B/cJ98/hwmltLeIk+flccjs6x/RG8TA3oiIiIiIrKcQ2HTdWoPA0aeAWQ2mf+kf2mjPoy3k+BQRSnuFDWcAb/hoomwlmuPwKelUCix/dKjzPeLKAcwoCciIiIiIss5uOsvbz0XKFxWdRzQDihcBmj2heb65MfAxHuWPVOegp1dd2oVfdHb8BT8fzZPwJQTv6rPZ3SqoD5OTFHA1ophEeVN/C+XiIiIiIgy55NL2ufV+gG1hwBDjwJjrwPufrr3WNsBts6WPW9GYZRYEIgTkXcRWn28WBTuI0H3iVboPkGG8766I/Z1H14St7X7PuUMupW0glSpEMsTU+S4+jhW72PiElO4lR3lagzoiYiIiIgoc9y8tc/bLlC9W9kATh6acomeqfHv/a76AqDSuxl+rINSiYZ/Dsfurru0njGrp+HRer8tqxHZogW2/v2ZeH7y1guE+BbSW7fCF5pZAHdfqBLoEeUmDOiJiIiIiCjLKGsMUgXyeukJ6AM7AB2/Bd5ZZvEzi+2ejvjID7XKttYwvK4+lTBif/a/K9h3RbXXvTEj1zDzPeU+DOiJiIiIiCjLSGTWFt4oAT65aNm951ejW9JtxEUOhVJhJRataCHD111MhzsdvxqC2Yd/QIXnt4zWO3f3lWV9I8pGDOiJiIiIiCjrSIyEGEEdVe/FKum/7uajfd5wnNmPnWu9DIrXPoi9MhNJL2uKZScCpOg91vh+9YLgZzew4FAoBl/8B5I3a+uNUSqVSEiWm903ouzCgJ6IiIiIiLKOvnXyqVxLAuMjgcH7TbcjBPdNPwc8K5r96A9kO8T3xEfviHvVC5KtJRg4SoYNdSW4Z2KHvXduHMS2vz8Tp+JfuGd4RN5v4jYETNmBv8/dN7tvRNmBAT0REREREWUdUyPc9oUAmZUZ7bx5778V6LMBGH/b5C1fWK/UOj/03iHxPcZBgnWNZBjzoRV6jJchtJ3pMMi6eR3YyJN1yp/HJqqPR609J76/TpIzGz7lCAb0RERERESUdZSpkXgWsXcD/Jur3sfdMFm9CKJQCNHAoa/hBgmO9TymdV0hleBAJdMJ8wR/b56oUzZ6/XmdssCpOzTtK7L45ycyggE9ERERERFloSwKaPXF3I5FTN42zOpvnLUbAuydDszxgdNXXjgR+HG6tlV71n/wiQy7g40H98L0+2HnN6q/qDh41XhG/A1n7pnsI1FWYUBPRERERERZx4ykcmYp7G/RbQOsNKPlqRy2jcXpnsd1yuPsJVjWRoZVjY2HRR1uHcX2v8chPCDQ5PPH/XlBfJ+3I0Kchr/+1N0M9Z8oIxjQExERERFR7plyP3APUKk70Ol74/U6fgfUGmJ2s7ZflcCEmhP0Xvu7jlQcse83xnRG/P8d+FYn470+3+9XLQ/4bIMqwCfKDgzoiYiIiIgo8xqMBZw8gQafZq4d7xCg6zLApbjl2+MZ0PvEGqx68Mjg9de2EvQaJ8M5P8PT8ANe3kHVJ1fV56/idRPnGQryibIaA3oiIiIiIsq8ZlOAT6+YDsSzSiFfwKd2xu6JPITKiUnYdvcBTnX9V2+VFCsJvuphfKT+q6M/imvr7ZMTcONprM71rRcfap2/iEvKWD+JzMSAnoiIiIiIsn8P+qzSbzPQdgHgWx8I6gx0+yXDTXinpMBugT+S7r0HpdxOb53pPaW4WgJY804D3HQpprfOxq2fw7F1Pa1lBjX93HHmtvYe9pcfRKHT4sPc2o6yHAN6IiIiIiLKO/waAjUHa75AqPgO0O5ri5q6kjwfsVen6r12yVeKz/tZYVP5Y5gw/BnmVe9hsB0hYZ7kTTJAZ1sruDtaa10v7GiL8yN3/3sAADK5SURBVPeiLOojkTEM6ImIiIiIKG8LGWTxrQ5IQvydD0zWOxjggqRChrfN2/b3Z7BLSURsYgpuPovTuvY6Wa51zr3qKaswoCciIiIioryvcFmLbvvZZj7kceUxq+Zy/Puu/nX1AgefnzGz3UujbW3aMhktd/+GjWfua5ULQX5az2ITkSxXiFPwp/x1yaJ+EwkY0BMRERERUd43/IRFt9WWhovvge5B8HDwMFo3wtt0joC6l/aLCfMckhPUZc9jE7XqTP37MspO3i4erzx+Gyly1XR9ooxiQG9AaGgogoKCEBISkuEPlYiIiIiI3jJpusz0QuI8gV8jk7dWltyA/6a2wN1T2NZlG1Jiy0KRqH96vbBffdfe3dCm03x8W6WrwTY3bP1cDOyFtfXHbz7XulaqiIPW+a6wx+J7UgoDe8oYiZKbJBoVHR0NV1dXREVFwcXFJYMfLxERERERvTXTXFXvdT8GWs4AYh4DTkWB2SWBJN3t5fT6/AnKffEvkuQKvF+7JP6K6qO3miLJHW7n+mP53nkmmxzeeDRuunmpz4c0KoMlB26ozx1tZFAoNWvtI+e0M6+vhIIeh3KEnoiIiIiI8pk328g5e6oy4VfobP6tM4vi0DtyTGwTgAltKhisJrV5geia32B0w6EY1mSM0SZD9/8P5V/cVp8fu/FM67qNlVQncR6RORjQExERERFR/iIxEubUGWHyds/NvfFRozJwtLXC/Ibzjda9X28Z7hYxPVV+4cHvUCghWjy2lmn3r3qpQibvJ9KHAT0REREREeUzEr0D9qLC/uY1kaiaot/arzViwmci5soXBqva+y9GvzEydOs0Tlxbb8jqHdMx5MJfcHOw0SrfE/5E6zw+KUVcT38i3dp7ovQY0BMRERERUf4is9Y+t0oTQFfrC9QebrqN2Zo174AVoLBH8qvqBqu/tpVAGvQ/OAdNxMImZXHPUX9SvU43D+PT2e+jzKt7Btu6+TQO5T7fjvd+PC5ubUdkCAN6IiIiIiLKHxqNV43A1x6WrnwCULQC0HquKht+66+ATqEZbj7h4btIelHXZL2jtW9hcKtP0LHDbIN1Fu9fCK8Y7ZH5VDeempnAjwo8BvRERERERJQ/NJkEjPwPcHDXLheS4w07CtQeoimr3MN0e1d3AkolPJxt1UWJjzsiJnwOTvQyvu+9c/lpkBT71+gUfCFD/rtX/9UpPx350nTfiDhCT0REREREBZLMynSd1d2BL92wZVhNnUv2VvYmb7ctsk+cgt/73e4G6wwI2ybuV//JmfXqspXHNRnxiYzhCD0REREREZERRY9/pXVeoYQLJBIJ7GPbQKmUQBbVwujnl+y/URypX1y5i8E6re6chE/0I73XlEolvtl1BdefcCo+aZMohf86yKDo6Gi4uroiKioKLi4u/KSIiIiIiPKLaa7pMuMbDo18E1arj/8eXg9VvN1w72U8vt17DYMalEapIjao8XsNo4/zkLfEzatNsfWvcZAaeVbP1l/glZ2z3muRc9oZfQYVrDiUI/RERERERFQwlWmmOS7f1mhVN8Sojx1sZOJ7yUIOmNetCsp5OsNWZouVTQ8hJnyWwTaeynYBsji06zwfKwNaGqy3ZseXcEx6rfeasKUdUSoG9EREREREVDClTZ7XYZFqSzsDztl9pD62s5bpb04M9PVfS+VcbgYcy87C6oCWmFJnoMF6f26bgnmHvtcp/+fcAygUStx5Hm/0OVQwMKAnIiIiIqKCyTpNYjsnD6Djd0CdESZvEwP6iG3ArYOqgoWVxOn7ztIk8TTmyjTE3zX85YDUKgbOgRNwzl+ONp0X4IBXsN56lZ7fRJWn12AjT1aXfb//BkpP2oaG8/fhg19OimVcRV1wcQ29CVxDT0RERESUTwnb0gmZ7K0dgckPVGUpScBMD73VP0oajZ2KGrheeRWsrm7TWydMUQptk1T7z9+c3QZVfqtitAvxd/tBHhuIao+vYNaxZUbrCsF/NR83nLnzSudao3IeWDFANxs/5U1cQ09ERERERGRMuVZA/23AJxc0ZVY2BqvPsP4FkXa9DQbzgiDpbXjgFSLtekE61w/flX3faBccvFfAofQ3OONZHoP6LTZaV9jerl2g/i8bDlx9avReyp845Z6IiIiIiAou33qAYxGzqhaV6I6M63PKbpjqIOEVGu+ahYtVp6JlKcNJ8GS2T2Dvsxz3oxIwtMkYo23XHdoJtsIsAj0SU+SoNmM3Dl97ZlY/Ke/jlHsTOOWeiIiIiKggb2eXVW1G4XHcYzT/s7nBKrE3PoUyyQPW8mT8s3miySbfa/Mlom0d9V7j9nZ5G6fcExERERERZYZ7maz7/Ka5wnN+OSQ+aW2wilOZryGxikayzBqDmo832eS67V+I0/ClSkXW9ZPyFE65JyIiIiIi0hstWWX55zI/+hJiwr9C0os6eq87lf1KzIB/38lDTIInvDq3N7y3vWDr35/BWq69P/3LOP3T8il/YUBPRERERESUlmcl1XuV97L8c+kkO4pSkidIfNwBvcoOQ6OSjfTWE4J6iY0q0V2ilS3adJpvtN1/Nk/AlOO/qM/H/XkevhO2Ysy6c1n8E1BuwoCeiIiIiIgorQ+2An02AnVGZsvncsBWSHwnRe/y/bC42WKjU/Ct3Q8JO80DEonJdus+uozgp9fE4z3hT8T3jWfvZ2HPKbdhQE9ERERERJSWnSvg30y1hV3Xn7Lls9lr8yl8visurq1vUzTEYD07z61wCpgkHr/f6nP8UKkTznqUNVh/9pGlKJQQDaek+GzpN+UuzHJvArPcExEREREVcNmR9T6dSn4+Rq+nxPnj9Z1BOuWrtn8J98QYg/cJU/U3DKuLrj8cw8Q2AfioURYm+qNswyz3REREREREWcFP/zp3Lf23AT3XWfyIC7fuYNWDRwavWzleF8J63ce2nGy03e1/j8P8qcvE49nbIyzuH+VOnHJPRERERERkTMfvTH8+vvWA8mm2pKvSC2jwqdmfq7BCvnJiElrGxhms4xz4OWSOqjXyqZJlVmjbaR56tZ5q8L4vTvyKlTtmiJnwE1PkOH7zOZRKpdl9o9yLAT0REREREZExMhvdssH7jH9mhcsAzaYCthmbrv/10+c4GXnX4HUHH2FNv1yrTCmR4qWdi9F2iyREiZnwG45ehR4/HkeHxYcz1C/KnRjQExERERERGeNSXPu81VeAVzXAydPwPalZ6c3ITp+evVKJmPBZeK+8/m3znAP1T7Mf2sT0jIBfd89G2Zd3cel+dIb7RbmPVU53gIiIiIiIKE8JGax6F7a22/4Z0HSKnkqpAb1lY6iRdu9DvgOo3m8tPjv4md596l/f64WUuHKAwk51j2txtOm8AK6JMfjo4j8oGfMEZaN0t6379sAi8T22nSucGtS3qH+UO3CEnoiIiIiIKCOE7ewExSoCH2wDStXRrVPkzdZyFTqr3j0CgE6hGXqMDECzFT0MXrcvuRrO5afBwW8hJDJNpvsoW2fMq9EbHzcZjaUVOxq8/+7gwdgy5RskhIdnqF+Ue3DbOhO4bR0REREREWltXTctyvAHcucE8OAsUOsj1XR7YT/4yxuBsi0Bp6IWbYFnaku7VEkv6iLxsW4AXyT+FdpFHkWPq/8avNexfn34LFdlw6ecx23rMik0NBRBQUEICQnJmt8IERERERHlfz61gNpDNGvnbRyAqn1UwbyF5j95hq5lu0IptzVaz8b9KKycL+qUP3Nww+8BrYzeG3f4MBRJSRb3kXIGp9wbMHz4cISFheHUqVNv9zdCRERERET5V82PNMeTHwGV3jV5S+u4eEyrOw1xN0ebrGtfchWkdnd0yuVSmbi+XnhtL1VL771XKldBeEAgni3jSH1ewYCeiIiIiIjIFCtV4rlMS5v13tpekzzPlGmucEixRUz4bJNVHf2+F5PmQaJ/xP3bqu/igxYTDd7/9Otv8PT7783rF+UoBvRERERERESmiMF3NpBZa46HHTda9bLdQPELgJgr01DMsZjJpp0DpgLSRL3XHjkWxoR6H+Gbqt31Xn/27XfiaL0iIcHkcyjnMKAnIiIiIiJ6WyP06TWZDLj6AM2+AIoGmnePwg67u+3G6T6n0cCrgdGqzuW/gNTunt5r5z3KYnepmkbvvxJcFTdatzGvX/TWMaAnIiIiIiIyxdxg25T0yfFcvYDRF4EGY8y6vZn0P/WxrcwW3zf/Hn+1PSbuSW+Io99iSKyfG7w+uuEIo89MiowUR+vlUUay+1OO4LZ1JnDbOiIiIiIiQswjYPdUIGQQ4G18VNuo5NfAPx8DAe00e9SnZca2dgEJvyDC7gOgxkDg9E9imW/CKlg5XxKT4hkTe30slMlFDF6fcGolGt0/b/C6c4sW8Fq0EBIpx4ZzQxzKgD6LPkgiIiIiIqJMs2Cf+lR+Cb/jp/41MepEc5N1Y658ASh08wLYpiTi43Mb0PTeGZNtBISHQZI2yR9lGe5DT0RERERElJdNMTxNXp9bdn3QdG05/BtsOIN9KufyX4qZ8KW2D7TKE61sMb9GL7TtNM9kGxGBQeJU/PjTpzPUT8o6nCdBRERERESUW9R5s5695oeAzMqiJjw2DcXe2nPMqutY+lvVFndI0SpXSqRo33Eu+reYiHuOhqfoC273eV8M7MV19rFxFvWZLMMp9yZwyj0REREREb01Cjnw6CJQrBIglWVqCv7AkIE4cv0ZrN3OmlU/JtzwlwABL27jfwe/M6ud8hfOQ2pjY3Y/SRen3BMREREREeU1QhBfIlj1nkk/nfoJyoddoJTbmlVfNVKvX4R7KQxqPh4b/BuZbOdK5SpIuHoVSqUyQ/2ljOMIvQkcoSciIiIiohyTiRH6VL6Jv6J9sDt2XDsBe++VJuvHhM8EYHy6f+Wn19HgwXm0v3XMZHsBly5CYmXZ8oGCKtrM5OxcQ09ERERERJRbtV0ABLTPVBNWSil2XYxHA6/GKCvtL5YpktwM1ncO/Nxkmxc8/BFapStGNv7EZN2IipXw5OuvM9hrMgdH6E3gCD0REREREeW4jR8CF9ZZdOsueXXcaLYMQxuXEc99J/4NKGWwL7kCkChg5XQ1w/vV69Pu1lGMOL/RaJ3AiPAM9r5giuY+9G/3gyQiIiIiIso2cc+A+aqAPFOmRcF3wlatIivnS7Av+bve6gkP30Hyq5pmN+8T/QhL/11gtI5Dndrw+fln7mFvBKfcExERERER5RfGto4bsMv8dn7rBFskaRWlxFQ0WN2u+EbYev5tdvN3XIrhH7+6RuvEHzsu7mFPmcc19ERERERERHnNO8s1xz61zL/v5n5csVOto08r9tpEKFIc9N5i434MMqcIYXd6sx7xQ+Uu6Nx+Ftp0XoD3Wxlejy/sWx+1ebP5fScdDOiJiIiIiIjymuKV9Zf3NW80fbvNeK1zZYor4q5NNVjfwftXOAdOhMTqlenGJRIkWqm2yntm74Y2neYbrPpg3GdiYB9RrbpZ/SZtDOiJiIiIiIjyGo/ywDvLgH7pRrjN3Ps9UHpXb/nru+8bvc+p7Bxxv3orp8vm91Uiwbttp0MOicEqyvh4MbBPefnS/HaJAT0REREREVGe4OihfV65O+DXULvszci4OdKvpRekxFZATPgcpMSWN3qvsJ+9ENhDGm/Ws2JtHNC+83xxGv4Zv2oG612rU1cM7BOuXDGr3YKOI/RERERERER5gbHR94bjgKDOgHdts5sT1tKXkxgaqf8AKTEBJttwLj8dUrt7yIjJVXrB4xPj+9ff6tRZDOzvfTI6Q20XNNyH3gRuW0dERERERLnCvDJA/DPV8bQow/UeXgCeXwO2jgVevzDZbGDCz3gNO90LkhRYOV+Evdc6s7oXH/kh5K9Lw1wBxZyxfVQDszPelz10EFYe6WYp5FPcto6IiIiIiChfUZqfMK9iV2DATrOqh9sNMPA4K6REV4VSYW1WOw6+P6qm4ZvZz4hHMWjxv4OQ7zkGt3e7max/rUFDcdReaWaegIKAU+6JiIiIiIjygndXAFZ2QIdF5tX3KJclj4278Sle338PMREzEBMxE8nRlYzWd/SfbXbb15/Eov3iIyg+Ywb8Dx4w6x5hRD/x5k2zn5GfMaAnIiIiIiLKC/waAJMeANV195E3qanh/eBVDI96K1PcxJF6KK3FUfuE+72NtiS1joZT+SkZ7qJ10aIIjAgXX2V27jBa92bbduJofUHHgJ6IiIiIiCivkMosu09iPPTzgPH95Zf3rYEtI+urz2OvjUdyVFUjj0t+M/3ePL4TtmL/lSfqc5tSpRAQdll8eU6aaPC+8IBAyKOjUVAxoCciIiIiIsrvhKn6RpyyGw5vyWP1+cVpLRHg6SQeOyEeNVL+Q0VPe/V1ZUohJDx4DzFXpiI5xvBIuRjUS5LN6mL/X05pnUukUvHl3revOGpfYv48vfddrVlLFdjHxqKgYUBPRERERESU3xUuC1T/AKgzwmCVQ7aaLeKcv6+CHVEdEWnXC5fsBsFtY09gpgdKyjQj+ccmNsWMDiFIuNcPcbdGGmzXOWAKpHb3M/0juHbogFKrfjd4/WqNEDGwV8TFoaBgQE9ERERERJRfNZoAlGsD+DcDOiwEWs2CUmZjsLoUCrxrfxqI1h+AH7YehkJQTXEv7mqPPeGqafKKBC/E3+1rsF1Hv+8gsXppsrtPYxKNXneoXl0crS+1epXBOleq18CTRWYmDszjuA+9CdyHnoiIiIiI8pWNHwIX9O8tf0vhCT+pZuq9Keebr0a3LSlIhpWqQJII54AvDNaPuzUCioSSRtu89GUrSABsOHMPPUJ8YGOlfxxamZKCiIqGM+67f/ABPMd/hvwchzKgz6IPkoiIiIiIKE/Y+BFwYW2WNumbsFpzIk2EbZG9sCl80GD92OtjoUwuYnb7t2a3hUQihPm6lEqluJWdIWWPHIZV4cLIj3Eop9wTERERERFRprwjPYgxLd7se6+wReKTtkh4+I7B+k7+C2DtbjjgT+/LzWEGr0kkEgRcOA+Jjf6lBNfq1Uf0tm3IjzhCbwJH6ImIiIiIKF/ZPxfY/1W2NP1Bqd3Yd+Wp+tze+2dYOV3NspH6yDntTNYJN7I/vbD+Pi/gCD0RERERERHpqvcxUGso0PefLP90fnzSS+v89d0BiIscanSkHtJ4s9sfsvI/xCQY3wYvIDwMDjVrZjjYz4s4Qm8CR+iJiIiIiCjfmuaa5U2mrqd3sbPC8n4h6L70GCRWr+BUdo7Je2PCZwthapaM1ieEheHWO111yp2aN4P34sXIzThCT0RERERERG9dd9k+8X1qhwoo4+EoHitT3BAXMQdxN0cZvdc5cCKcAiZB5mh4mn6qpBSF0et2QUEICLusUx67Zy/yCybFIyIiIiIioiwzz3qZ+G5vLYPiyg6UlKj2qp/frQq6VgxBTMR0o/dLJAo4+PwsZNczWq/c59uxN9z4FnsSqVRcNx9w+ZK6zOMT418q5CWccm8Cp9wTEREREVG+FbEVWKu97j0rvJ80ASttdKfYfxW8Fz8efwyp3X04+n1nsp2Y8FkAZJlOlCdIuncfr/87DZf27SGRGW8zp3Ef+rf8QRIREREREeVJSfHAV8WN17F3B16/yNI19rbFNsGm0AnT3XtVA4kPu2U6oM9LuIaeiIiIiIiITLNxMF3n4zPAwD1Z8mlG2qlmBCQ97oLX93tAqZQgOSrYcPfcTsM5cAIcy84AJLoZ7n0nbEVBxTX0REREREREpKt4FdW7e2nAvhDgHaJ93dUbKNfGok/OFkk4OK4JUqKDERsxGwkPeiDhYWej90it4uAcMAWQpOhc23n5Ecb+cR43n8YWqN8k19CbwCn3RERERERU4Lava/YFUPV9wMYRkNkAMivdel+8Ap5GAN/XtvixvgmrtLapk8hiIbV5BgffJRZvb3fosybwdjdj1kEuxin3REREREREZJbndSapj1Oq9gMajAGcPFTT8VODeYGNs+ZYIgEU8kx9wpF2vVFOcld9rpQ7Qf7aF7HXx5nc3s6QBvP24VlsIgoCTrknIiIiIiIq4OLLaaa7pzT+3HDFfn8DRcoDvf9UnRf2V43gO3la/OxdtuN1ypTJhRETPgexVw33xcZjp8Gt7T7fpNmmLj9jQE9ERERERFTACfu1a06MbOnmVR0YcRIo20J1bm0HTLgDjL6cLf0SRuyFwD7+9oc612yL7INz4CRAGq9zbcflR9h28SHyOwb0REREREREBZwk7Xp0YSp9RljbAzJroJHuSLu5VJnvlQavy+NLI/HJmy8R0nEuP13vvcNWnUF+V2AC+ri4ODRp0gRTpkzJ6a4QERERERHlKp4utupjW6sMBvSpmkwCWszI1Hp6Y5KeN8XrB90Mr6mXJBW4Le0KTEA/duxYBAcH4+HD/D/tgoiIiIiIKCOspGkyzRubcm9Ktfez8YOXICWqhsGrzgFT9a6pz89Bfa4I6JOSkvDo0SPx3VgdIXW/JVatWoXq1aujSpU3+ygSERERERGRhjJNICzJRJgo7Fefqv4YoN9mzXnRCmY0YHjafaq4W8OgSCyi95qwpt45cIJO+dwdEciPcjSgv337NiZMmAAfHx8UL14cR48e1akTHx+P999/H87OzihatCgqVaqEU6dOqa+fOXMGAQEBel9yuRzXr18X2x00aNBb/umIiIiIiIjyCKXS8jX0hnhWAPwaAp9cBIYcAYYdBUIGmzXtfnrHQDjitXjcIkA7eFck+CDu5tg3e9HrJwT1wkti9Uo8/zf8CfKjNBsKvn3r16+Hq6srtm3bJo6g6/PJJ5/g2LFjuHr1KooVK4bRo0ejXbt2uHbtmnhvYGAg/vrrL733ymQyfPnllzh8+LAY4EdFReH169dwd3fHvHnzsvmnIyIiIiIiyiNcvQGvGqoEd9YOWdNm6ki/m4+mzKW4GTcq0XdXVfS1e3MaCQyXfoytitrpH4D424PgUGq5wZacys5BTMR0XHkcg3/OP0DHKiWQn0iUyrRfxeSMe/fuwdvbG/v27UPjxo3V5TExMShSpAi+//57DBw4UCwTpt0LI/XffvstPvxQd+uC9B48eKCeqr9p0yZxRF+4V5gRYA7hXuGLA+HLABcXF4t/RiIiIiIiolwtNTTM7Aj9NFfV+7u/AhW6aF+7/BfwRz+Lmh2dNBSbFA10yq1czsHea63Re1Wj+RJEzmmHvMDcODRXrKE35Ny5c+La+fr166vLhB9GWAt/8uRJs9ooUaKEegq+EMQLH4qxYD4xMVH88NK+iIiIiIiI8j0hkM+q6fYCG2fja/U7fJuh5v5n8wPekR6EDZK1ylOig5EcFWz0XiELvjAFv9KKSshPcnVA//TpU/FdGKVPy8PDA0+eZHwNRJcuXTBz5kyjdWbPni0G/akvYeYAERERERERmanVV0CVnkCZprrXyrVWTe8P6gxUz/hI/Tc2S3DVrh/aSY9rlSc86IGY8K8QE/GlyTaO3tfN3ZZX5eqAXvLm26GUlBStcuFcWB+fUUKALqzDN2bixInitIbU1927dzP8HCIiIiIiogKrznCgyxJAqifctHEARp0Huq/I1CNCbYTR/fSrx6WA0hYx4XOMJsz7aM9HyC9ydUDv5eUlvj9+/FirXDgXptJnB1tbW3Faf9oXERERERERZRFpmsFZYaTeQq2lmt3PdEmMBvX5Ra4O6IW18kJAvXv3bnWZsF/9hQsX0LBhwxztGxEREREREWWSMJLfe4NFty6xWYjusn1GakgQd/MTndL3g95HfpGjAb2whZwQoKeulX/x4oV4Hhsbqx4tHzdunLjuffPmzTh//ry4J72Q4O6dd97Jya4TERERERFRZgnb5JVtbvHt86yXGb2uSPRQH8fdGoHX93tiXI1xyC9ydB/6f/75B6NGjRKPPT09MWzYMPF47Nix4kswefJk2NnZie/CNnb16tXDihUrYG1tnZNdJyIiIiIioqzS5HNgn/EE5paRIf5uX0gkyVAklBRfqbna8oNcsQ99bsZ96ImIiIiIiN6C1P3rMyhFKYV/4u9m188Le9Hni33oiYiIiIiIqGCIKtXSovusJArUlIQj0q4XfCUPUZAwoDcgNDQUQUFBCAkJebu/ESIiIiIiogIovnht7YJilQFrR2C4sWz2KuttZ4jv+20/FQN7a2hvfZ5f5ega+txs+PDh4it1qgMRERERERFln9cl62tOJj9SJcyTpwCyjIet+2zHoH6isFd9/sYReiIiIiIiIspxfh5OmhPJm73qLQjmBSUlz1AYUcjvGNATERERERFRjpMgTfZ5SbpQ1beB6r2q+XvI/2c3FPkdp9wTERERERFRzlMqNMfSNyP0qXqsBm7uA8q2BKLuqY7N0Fh6FvsVVZFfcYSeiIiIiIiIcl7aUfn0e8XbuQBBnVTr6rv9DDSeZFaTv9rMR3XJFa2y/LRzOwN6IiIiIiIiynlFA4FybYBq/YzXc3AH6o82u9kNtl8KYbz6XK5gQE9ERERERESUdYRR+V5rgY5mZKe3sgE+2G5204utNW2mMKAnIiIiIiIiykGl6ppdtb3shPpYwSn3+V9oaCiCgoIQEhKS010hIiIiIiKiTAqQ3BHfOUJfAAwfPhxhYWE4depUTneFiIiIiIiIMmmH7QTxXcEp90RERERERES5SKfvAXt3k9WO3XiO/IL70BMREREREVHeV7W36iWY5mqw2pOYROQX3LaOiIiIiIiICoTusn2oXNJwsJ/XMKAnIiIiIiKivKnBp6r3JpPNqj7PelmaHenzPk65JyIiIiIioryp6f/buxegqMr3geMPiqCm4D0zL3hDJdPEUDNztHSytDRLu1pmGSap/y5m05RWWjOV5Vih2ViZY2ml5liampl5TclrSmle81KZlwA1NGH/87z9dmeBRRZY2H0P38/MzrJnz1nePbyc8z7v9XmR+PtFqjXy+5ALWc4J6QnoAQAAAAB2CgsTqR5TqEMuZGWLU9DlHgAAAADgLP/3k0jiapHoBnnectCqdQT0AAAAAACHqdZQ5LI2Is165HnLdYFZ7h0vOTlZ4uLiJCEhIdhJAQAAAAAURYOOeTZdcmK7Y84lXe7zkZSUJKmpqZKSklK6fxEAAAAAQGC0uVPk1rdzbKrx+2rHnF0CegAAAACAM5Ur998s+F4izhwVpyCgBwAAAACUGWHZWeIUBPQAAAAAAGdrN8jzY+WMveIUBPQAAAAAAGe7eaLnx+hTO8UpCOgBAAAAAM5WoaLnx38rRIlTENADAAAAAMoQlzgFAT0AAAAAoMz4NfYRcQoCegAAAACA422PvNo8Z0bWEqcgoAcAAAAAOJ4rLOy/Z1e2OAUBPQAAAADA8Vzyv4A+mzH0jpecnCxxcXGSkJAQ7KQAAAAAAIor7H8BPZPiOV9SUpKkpqZKSkpKsJMCAAAAACimMAeeQbrcAwAAAAAcr239auY5IaaGOEV4sBMAAAAAAECJ6z9NJOtfkciqjjnZBPQAAAAAAOerVF2chi73AAAAAABYiIAeAAAAAAALEdADAAAAAGAhAnoAAAAAACxEQA8AAAAAgIUI6AEAAAAAsBABPQAAAAAAFiKgBwAAAADAQgT0AAAAAABYiIA+H8nJyRIXFycJCQml+xcBAAAAAMAPYS6Xy+XPjmVVenq6REdHS1pamkRFRQU7OQAAAAAAh0v3Mw6lhR4AAAAAAAsR0AMAAAAAYCECegAAAAAALERADwAAAACAhQjoAQAAAACwEAE9AAAAAAAWIqAHAAAAAMBCBPQAAAAAAFiIgB4AAAAAAAsR0AMAAAAAYCECegAAAAAALERADwAAAACAhQjoAQAAAACwEAE9AAAAAAAWIqAHAAAAAMBCBPT5SE5Olri4OElISCjdvwgAAAAAAH4Ic7lcLn92LKvS09MlOjpa0tLSJCoqKtjJAQAAAAA4XLqfcSgt9AAAAAAAWIiAHgAAAAAACxHQAwAAAABgofBgJyDUuacY0DEMAAAAAACUNHf8WdCUdwT0BcjIyDDPDRo0CNTfBgAAAAAAv+JRnRwvP8xyX4Ds7GyJjY2VTZs2SVhYmPhLl7tLSUkJ2L4F7aM1OFrpcOjQIcfOxl+Yc2prGgL1+cX5nMIeG+i8XtB+5PXSQV4v3jkhr4dOXgt2Gmy8rhfmmEDtx7W9dJDXi3dOKLMHP5+VpXKMy+UywXy9evWkXLn8R8rTQl8APXkREREXrRXxpXz58n4H1v7s6+/n6T5ODegLc05tTUOgPr84n1PYYwOd1/3dj7xessjrxTsn5PXQyWvBToON1/XCHBPo/bi2lyzyevHOCWX24OezslaOifYjBmVSPD8kJSWV6DH+7FuUNDhNKJyDkk5DoD6/OJ9T2GMDndeLkganCYXvT14v3jkhr4dOXgt2Gmy8rhfmGPJ6yf4dAo28XrxzQpk9+PnMX0lloMzuRpd7h9CualqDk5aWFvQaMaAkkddRVpDXUZaQ31FWkNcRaLTQO0RkZKSMGzfOPANORl5HWUFeR1lCfkdZQV5HoNFCDwAAAACAhWihBwAAAADAQgT0AAAAAABYiGXrADjG33//LcePHzc/X3755VKpUqVgJwkAAKDQE+edPHlSYmJiOHMoEC30ZcCOHTskPj5eLrnkErn99tslIyMj2EkCSsSnn34qvXr1krZt28ratWs5y3CsI0eOSP/+/aVy5coSGxsrixYtCnaSgBLhcrnkjTfekEsvvVRq1qwpzz77LGcajvbvv//KgAEDpFOnTsFOCixBQF8GPPzww5KYmCh//vmnKfxNnDgx2EkCSoTm8z179kj37t05w3C0uXPnygMPPCAnTpyQCRMmyJAhQ4KdJKBE/P7776b3VWpqqmzbts1U3G7cuJGzDccaO3asDBs2LNjJgEUI6C2pqfvuu+8uegM7duyYrFy50tzwvGlr/C+//CJDhw6VKlWqyKhRo2Tp0qWlkGqg8DIzM+Xzzz+X9957L999fvvtN5k1a5bZT7ujAbb69ddfZcmSJZKWlubz/ezsbNm0aZOsWrUqT88qvZb37dvXDCvp0KGDabkEQpUOhdKyx4EDB/LdZ/fu3aYcowG8t3r16sn48eMlOjpaLly4IBUqVCC/I2Rp/n3//fcvWtbW8vyMGTPMPlrG96b3hBo1ash1111XCqmFUxDQhzD9J3/uueekSZMmMnDgQHn66ad97vf6669Lo0aNZPTo0dKlSxfp0aOHnD592nMT1YJeuXL//anr1KnjGWMMhBJtZWzatKm89NJLMnz4cJ/7zJw5U1q2bClz5syRyZMnS7NmzWTdunWlnlagOL7//ntzne7WrZvcdNNN8vPPP+fZ5+DBg9K6dWu59dZbZcSIEVK/fn2ZP3++z3GWWmE7bdo0/igIOfv27ZNBgwaZYVCal7VnSW7nzp0zlVMJCQmmnKNlnueffz7HPtoTpWLFitK4cWO5+eabzb0CCCVaMatlda1gfeGFF2Tq1Kk+h48MHjzYXPcXL14sSUlJ0rFjRzl16pR5/48//pB33nlH+vXrJ/v375esrCzzDBSEgD6E/fPPP+YGpjV5erPzZf369eYG+MUXX0hKSoqp4d67d6/nZli7dm0TwGtLj9Ju97oNCDVaUNPulE8++WS+vVAeffRRefXVV+Wrr76SNWvWyC233CIPPviguUkCtjh06JCMGTPGtEbmR7vQ65hhbdHU/wsdN3z//feba7h3S5AGSXq9pzUHoTrXQ8+ePU25pHr16j73eeWVV0z5RXsTanln2bJlZpt3C6c2TGjrvOb/LVu2yLx580rxWwD+NcLdcccdphJLg3RfNN9+8sknsnr1avnss89MXtbeV9rFXmkDhf4faMB/5513mkBf574CCkJAH8KioqJMC/1ll12W7z4fffSRqfnWicBUrVq1TGuNbtcgR7vZX3HFFabGT2u4dWIZ975AKLn33ntN/s3PwoULTcWU91hhbbnUSqzNmzeb12fOnDFj6M+ePStHjx41rZxAqLnvvvtMkBMWFubz/cOHD8uKFStM5ZZ2L1YjR440z+4WTh1e1adPH9NDq3PnzibYAUKNVjRpRZQ2TuRHyyu6j7uso8dce+21pkuy+vrrr80+Oo5er+3nz583DyCUaPlFW+jd12xftHeh9syKi4szr6tWrWrmQtHtSic61TKMPrRySz/TXb4BLoaA3nJbt26Vdu3a5dimr7VWT1uBlI7l0RrB5s2bmwJkfi2gQCjbuXOnGVqiqzW4aWWV+z2ltd5aYaXj7LXrvt4oARuv68r72q5j5Vu0aGFa61VycrL5+ZprrjHBkj606zJgEw3SteLVVznGndc1ANIWfO1m37VrVzO0UFsvAdtoWcUdzLvpa+1Jq70QvZUvX970XAT8wTr0DrgZ6uQZ3tyTI2lQ37BhQ2nVqpX88MMPQUohEBg6VrhatWo5tmmQExERYd5TGsxrzTZg+3Vd+bq2u8daakCvD8DpeV2v89rLUB+A08ox7qEo+p7Oc+W9XYfVAv6ghd5yGszoWHtv2iXN/R7gFFqoyz3Tt7vrpS7HCDiF+9rt69rOdR1OQl5HWS/HuBskKMegOAjoLRcTE+PpWu89/lJntdfWecApdMiI5m3vscI6+YzS2e4BJ13Xla9ru/s9wAl04kcNcsjrKCvlmNyz1utrHUpYt27doKUL9iOgt5x2MdY16r3XMdZZNHWMmfdYY8B2vXv3Nssx6gz3broeva7a0KlTp6CmDQik+Ph4k68XLFjg2abr0etYYyY1hZPoOGFdwtE7r2tPFJ0Ij7wOp9FVSb755hvP8tE60e/s2bPNBKfu5aWBomAMvQXrFWu3S22ZOXnypCxZssRsd9/odMmuKVOmmGBHl/TasGGDWdtSZ0gGbKJ5Vmes1zFjukLDu+++a7breqxac62t8LpEo+b5H3/80VRi6drbOvsx3ZBhE22N1MmRdNk5pXOc6FhinfROJ0EKDw83yzMmJiaaGZN19u+XX37ZFAZZng420UpYXWJU6fCoXbt2mXKMjhXWiis1fvx4s1LDI488It27d5fp06dLdHS0DB8+PMipBwrnww8/NJOTau9BDda1HBMZGWnKLUpX6dEyi+bze+65R1atWmX21YmrgeIIc7GAc0jTWbq91x12cwf2SieOmTRpkpkRVm+Sw4YNk/bt25dySoHi3wi1Qiq30aNHm9mN3bTlZvny5eYmqeuzktdhG610feutt/Js18KeLnvkndc//vhj02Kps3trgEPlFWyivUq0Yio3Xaf7xRdf9LzesWOHmeRRlxvViXyfeOKJHBOEATbQfOuex8pNx8a/+eabnteZmZkyc+ZMU6mrlbVazr/Y8tSAPwjoAQAAAACwEAM2AAAAAACwEAE9AAAAAAAWIqAHAAAAAMBCBPQAAAAAAFiIgB4AAAAAAAsR0AMAAAAAYCECegAAAAAALERADwAAAm7z5s2yZs0ax/weAABCUXiwEwAAAAovOztbtmzZIkeOHJEGDRrIlVdeKeHhoXNb/+CDD+Tw4cPSpUuXEv2egfo9AADYKHTu/AAAwC+7du2S2267Tc6cOSNXXXWVHD16VDIyMmTixInSp0+fMvU927dvL40bNw52UgEACAoCegAALJOYmCgNGzaURYsWSfny5c22gwcPyrZt2zz7pKamyvbt283P0dHRpmW7fv36OT5nw4YN4nK5pHXr1rJ161ZJS0uTrl27StWqVU3gvHbtWomIiJDOnTtLxYoVfR6nree6rx5XpUqVAtP+008/yd69e01ruwbp7vQX9Xu2bdtWzp49a37WwP/LL7/M8zn6fXr37l2kNAAAEMoI6AEAsMyOHTvkqaeeyhGINmrUyDy8W7cXLFhgfj558qQJzseOHStjxozx7DN16lQzBj09PV3i4uJMkKtB/WuvvSbjxo2TVq1aye7du6Vy5comiK9UqZLnOA2qdd+mTZuaLu/68/Lly83n+KJB/8CBA03a27VrZz5XA20NwOvWrVvk7+nd5V4De/d3dlu9erWpaNCAvihpAAAglBHQAwBgmU6dOsmUKVOkWbNm0qNHD6lWrVqefbSruj7ctCVdjxswYIA0adLEs33Pnj2mdT42NlbOnz8vzZs3l8cee8wE7NqVXYNkfZ49e7YMGTLEc5wes2LFCunevbtkZWVJ//79ZcSIEfLtt9/6TLMG5mFhYabSQFv9dWy8pkW3z5o1q8jf01vt2rVlzpw5ntealnnz5smkSZOKnAYAAEIZAT0AAJbRVmkNQgcPHmwC7pYtW0q/fv3kmWeekaioKM9+2vKugfyxY8dM0K3vbdq0KUdArwG5BvNKg9z4+HgpV66cZ1y6ts63adPGtGZ7u/rqq82xSlvQNT3a7f7EiRNSs2bNHPteuHDBBMzDhw+XhQsXmu76+tAhAHPnzi329/Rl3759pjVeeyToc1HTAABAKCOgBwDAMnXq1JGZM2fK9OnTTUu5tkTrRHH6vH79ehOQa8v0Qw89ZLrE6zj0yMhIOXfunAnuvVWvXj3Ha90v91h43ZaZmZljW0xMTI7X7goAHeOeO6DX36kBuab10KFDOd7r1q1bsb6nL6dPn5a+ffuabvjjx48vVhoAAAhlBPQAAFhKW9Q7dOhgHtrqftddd5ku9Nrirt3fNZjVZ7datWqZVulAOHXqlM/X+jty03Hq2tV96NChprU8kN8zN/1+gwYNMt3ptUVef28g0gAAQCjyXbUNAABC1v79+/Nsc7eg6zhz7V5//PhxadGihef9lStXmu7wgbJu3Tr566+/PK/nz59vJqvLPZO+O5jWmfKnTZuWp0JB15cv6vf0RSfzW7VqlelWr7+3uGkAACCU0UIPAIBl7r77btMSrl3KdXZ2ndFeJ4/T1mftpq50nfZRo0bJ448/bgLvyZMnm/HwgaKfdcMNN8iwYcPkwIEDZuI57R6fXzd4TZ/uf/3115uJ6DQw167z2uL+9ttvF/l7etP5ASZMmGDeT0lJMQ/vZeuKkgYAAEIZAT0AAJbR8ePLli0zre4a5Gpwq2PmdSZ4N+1ursvLaUu6jmlfunSp2ebdTb1jx46ma7q33GvOK53sLveybr169TKT1S1evNgsB6fPPXv29Lzfvn17z7h6pRPr7dy5U2bMmCEbN240gfrIkSPlxhtvLNb39P494eHhpju9LqHnvXydpl0D+qKkAQCAUBbmCtRgOgAAUCZoIO+eNR4AAAQPY+gBAAAAALAQXe4BAECh+OqqDwAASh9d7gEAAAAAsBBd7gEAAAAAsBABPQAAAAAAFiKgBwAAAADAQgT0AAAAAABYiIAeAAAAAAALEdADAAAAAGAhAnoAAAAAACxEQA8AAAAAgIUI6AEAAAAAEPv8P5tgz8NydyXcAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "lat1 = Lattice(dimension=dim, order=\"RADICAL_INVERSE\", seed=12, generating_vector=\"kuo.lattice-39102-1024-1048576.3600.txt\", m_max=20)\n", + "lat_discs1 = lat1.expected_squared_periodic_discrepancies(n_max=n, coord_weights=coord_weights)\n", + "\n", + "lat2 = Lattice(dimension=dim, order=\"RADICAL_INVERSE\", seed=12, generating_vector=np.uint64(searched_lattice_vector), m_max=20)\n", + "lat_discs2 = lat2.expected_squared_periodic_discrepancies(n_max=n, coord_weights=coord_weights)\n", + "\n", + "# Note that the new lattice rule beats the Kuo lattice rule for the somewhat low sample sizes here, but they are comparable closer to n = 2^20.\n", + "\n", + "fig, ax = pyplot.subplots(nrows=1, ncols=1, figsize=(12,10))\n", + "ax.plot(np.arange(1, n+1), np.sqrt(lat_discs1), label=\"Kuo Lattice Discrepancy\")\n", + "ax.plot(np.arange(1, n+1), np.sqrt(lat_discs2), label=\"New Lattice Discrepancy\")\n", + "ax.plot(np.arange(1, n+1), np.sqrt(new_kron_discs), label=\"CBC_MT Kronecker (for N=2^15)\")\n", + "ax.plot(np.arange(1, n+1), np.sqrt(kron_discs), label=\"CBC_MT Kronecker (for N=2^20)\")\n", + "ax.set_xscale(\"log\")\n", + "ax.set_yscale(\"log\")\n", + "ax.legend()\n", + "ax.set_xlabel(\"Sample Size\")\n", + "ax.set_ylabel(\"Periodic Discrepancy\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qmcpy", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/talk_paper_demos/SorokinThesis2025/sorokin_thesis_2025.ipynb b/demos/talk_paper_demos/SorokinThesis2025/sorokin_thesis_2025.ipynb index 125d952c6..ffe5337e5 100644 --- a/demos/talk_paper_demos/SorokinThesis2025/sorokin_thesis_2025.ipynb +++ b/demos/talk_paper_demos/SorokinThesis2025/sorokin_thesis_2025.ipynb @@ -516,20 +516,13 @@ "metadata": {}, "outputs": [], "source": [ - "import io\n", - "import zipfile\n", - "from urllib.request import urlopen\n", - "\n", "import pandas as pd\n", "from sklearn.model_selection import train_test_split\n", "\n", - "with urlopen(\n", - " 'https://cdn.uci-ics-mlr-prod.aws.uci.edu/43/haberman%2Bs%2Bsurvival.zip',\n", - " timeout=30,\n", - ") as resp:\n", - " with zipfile.ZipFile(io.BytesIO(resp.read())) as zf:\n", - " with zf.open('haberman.data') as f:\n", - " df = pd.read_csv(f, header=None)\n", + "df = pd.read_csv(\n", + " 'https://archive.ics.uci.edu/ml/machine-learning-databases/haberman/haberman.data',\n", + " header=None,\n", + ")\n", "df.columns = ['Age','1900 Year','Axillary Nodes','Survival Status']\n", "df.loc[df['Survival Status']==2,'Survival Status'] = 0\n", "x,y = df[['Age','1900 Year','Axillary Nodes']],df['Survival Status']\n", diff --git a/demos/vectorized_qmc.ipynb b/demos/vectorized_qmc.ipynb index 0ab7d522d..d00e046aa 100644 --- a/demos/vectorized_qmc.ipynb +++ b/demos/vectorized_qmc.ipynb @@ -451,20 +451,13 @@ }, "outputs": [], "source": [ - "import io\n", - "import zipfile\n", - "from urllib.request import urlopen\n", - "\n", "import pandas as pd\n", "from sklearn.model_selection import train_test_split\n", "\n", - "with urlopen(\n", - " 'https://cdn.uci-ics-mlr-prod.aws.uci.edu/43/haberman%2Bs%2Bsurvival.zip',\n", - " timeout=30,\n", - ") as resp:\n", - " with zipfile.ZipFile(io.BytesIO(resp.read())) as zf:\n", - " with zf.open('haberman.data') as f:\n", - " df = pd.read_csv(f, header=None)\n", + "df = pd.read_csv(\n", + " 'https://archive.ics.uci.edu/ml/machine-learning-databases/haberman/haberman.data',\n", + " header=None,\n", + ")\n", "df.columns = ['Age','1900 Year','Axillary Nodes','Survival Status']\n", "df.loc[df['Survival Status']==2,'Survival Status'] = 0\n", "x,y = df[['Age','1900 Year','Axillary Nodes']],df['Survival Status']\n", diff --git a/demos/vectorized_qmc_bayes.ipynb b/demos/vectorized_qmc_bayes.ipynb index 2b4ac95be..a015a1da7 100644 --- a/demos/vectorized_qmc_bayes.ipynb +++ b/demos/vectorized_qmc_bayes.ipynb @@ -450,20 +450,13 @@ }, "outputs": [], "source": [ - "import io\n", - "import zipfile\n", - "from urllib.request import urlopen\n", - "\n", "import pandas as pd\n", "from sklearn.model_selection import train_test_split\n", "\n", - "with urlopen(\n", - " 'https://cdn.uci-ics-mlr-prod.aws.uci.edu/43/haberman%2Bs%2Bsurvival.zip',\n", - " timeout=30,\n", - ") as resp:\n", - " with zipfile.ZipFile(io.BytesIO(resp.read())) as zf:\n", - " with zf.open('haberman.data') as f:\n", - " df = pd.read_csv(f, header=None)\n", + "df = pd.read_csv(\n", + " 'https://archive.ics.uci.edu/ml/machine-learning-databases/haberman/haberman.data',\n", + " header=None,\n", + ")\n", "df.columns = ['Age','1900 Year','Axillary Nodes','Survival Status']\n", "df.loc[df['Survival Status']==2,'Survival Status'] = 0\n", "x,y = df[['Age','1900 Year','Axillary Nodes']],df['Survival Status']\n", diff --git a/docs/api/discrete_distributions.md b/docs/api/discrete_distributions.md index 49b60fbb4..82b8b7550 100644 --- a/docs/api/discrete_distributions.md +++ b/docs/api/discrete_distributions.md @@ -28,6 +28,10 @@ jupyter: ::: qmcpy.discrete_distribution.korobov.KorobovLattice +## `lattice_vector_wssd_search` + +::: qmcpy.discrete_distribution.lattice.lattice_vector_wssd_search.lattice_vector_wssd_search + ## `Halton` ::: qmcpy.discrete_distribution.digital_net_any_bases.halton.Halton @@ -52,6 +56,10 @@ jupyter: ::: qmcpy.discrete_distribution.latin_hypercube.LatinHypercube +## `kronecker_vector_search_mobius_transform` + +::: qmcpy.discrete_distribution.kronecker.kronecker_search_methods.kronecker_vector_search_mobius_transform + ## `DummySampler` ::: qmcpy.discrete_distribution.dummy_sampler.DummySampler diff --git a/docs/mpmc-compatibility.md b/docs/mpmc-compatibility.md index f68bec6f8..1f2091734 100644 --- a/docs/mpmc-compatibility.md +++ b/docs/mpmc-compatibility.md @@ -5,7 +5,7 @@ ## Recommended Baseline - Treat MPMC as an optional feature, not part of the minimum QMCPy dependency set. -- Prefer `pyg_lib` plus `torch-geometric`; do not require `torch-cluster` as a separate dependency. +- Require PyTorch and `torch-geometric`; `pyg_lib` is an optional accelerator. When the compiled radius-graph backend is unavailable or fails to load, MPMC uses a native `torch.cdist` fallback. Neither `pyg_lib` nor `torch-cluster` is required to run MPMC. - For reproducible local work and future CI pinning, prefer a modern PyTorch line with matching `data.pyg.org` wheels installed by `qmcpy-install-mpmc`. - `unittests.yml` runs the full suite on `3.10`-`3.14` plus a slim `core-tests` tier on `3.9` (see [Minimum Python Version by Role](CONTRIBUTING.md#minimum-python-version-by-role)); neither installs MPMC. @@ -13,9 +13,9 @@ | Python | Linux / macOS / Windows | MPMC status | Dependency guidance | CI expectation | |---|---|---|---|---| -| `3.14` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`, `pyg_lib >= 0.6.0` from the matching `data.pyg.org` wheel index | Run MPMC doctests and unit tests | -| `3.13` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`, `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | -| `3.12` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`, `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | +| `3.14` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`; optional `pyg_lib >= 0.6.0` from the matching `data.pyg.org` wheel index | Run MPMC doctests and unit tests | +| `3.13` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`; optional `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | +| `3.12` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`; optional `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | | `3.10` to `3.11` | Best effort | Not a release blocker for MPMC | May work with matching PyTorch / PyG wheels, but not required by current CI policy | Optional manual testing only | Python `3.9` is covered only by the slim `core-tests` tier, which never installs MPMC's PyTorch Geometric stack (see [Minimum Python Version by Role](CONTRIBUTING.md#minimum-python-version-by-role)). @@ -29,23 +29,23 @@ The distinction is intentional: The current CI split is: -- `alltests.yml`: the only workflow that installs the MPMC stack (`qmcpy-install-mpmc`) and runs `make doctests_mpmc` plus the MPMC unit tests, on Python `3.13`. The steps are not OS-gated: Ubuntu alone on feature-branch pushes, all three OSes on full sweeps. -- `unittests.yml`: `3.10`-`3.14` on all three OSes, plus a `core-tests` tier on Ubuntu for `3.9`. Neither calls `qmcpy-install-mpmc`, so `test/test_dd_mpmc.py` skips throughout via `pytest.importorskip("pyg_lib")`. This workflow gives **no** MPMC coverage. +- `alltests.yml`: the only workflow that installs the MPMC stack (`qmcpy-install-mpmc`) and runs `make doctests_mpmc` plus the MPMC unit tests, on Python `3.13`. The steps are not OS-gated: Ubuntu alone on feature-branch pushes, all three OSes on full sweeps. Dependency validation requires PyTorch and `torch-geometric`; a missing `pyg_lib` accelerator does not block the tests. +- `unittests.yml`: `3.10`-`3.14`, each version on one operating system, plus a `core-tests` tier on all three OSes for `3.9`. Neither installs `torch-geometric` or calls `qmcpy-install-mpmc`, so MPMC tests normally skip. `test/test_dd_mpmc.py` checks only for PyTorch and `torch-geometric`; when both are available, the tests run with or without `pyg_lib`. See [MPMC Coverage by OS](ci-testing.md#mpmc-coverage-by-os) for the per-operating-system breakdown. -This keeps MPMC enforcement in one place. The trade-off: MPMC regressions are invisible to `unittests.yml`, so raising MPMC coverage means adding a job to `alltests.yml`, not widening the `unittests.yml` matrix. +This keeps required MPMC coverage in `alltests.yml`. Widening the `unittests.yml` matrix alone does not add MPMC coverage; a job must install the required PyTorch and `torch-geometric` dependencies. ## Local Developer Commands -Install the usual test and MPMC extras first, then add the platform-specific PyG runtime with QMCPy's installed helper command: +Install the usual test and MPMC extras first, then optionally add the platform-specific accelerator with QMCPy's installed helper command: ```bash python -m pip install -e ".[test,test_torch,test_gpytorch,test_botorch,mpmc]" qmcpy-install-mpmc ``` -The `mpmc` extra contains dependencies available from PyPI. The helper handles `pyg_lib` separately because its wheel page depends on the installed PyTorch version and accelerator build, which standard project metadata cannot select. +The `mpmc` extra contains dependencies available from PyPI. The helper handles `pyg_lib` separately because its wheel page depends on the installed PyTorch version and accelerator build, which standard project metadata cannot select. If no matching wheel or source build is available, the helper warns and continues; MPMC can use its native PyTorch fallback. Then run the MPMC-specific checks: diff --git a/makefile b/makefile index 82532d79c..a20126040 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,7 @@ PYTEST_XDIST ?= $(shell $(PYTHON) scripts/pytest_xdist.py 2>/dev/null) PYTEST ?= SMOKE_CODE_CELLS ?= 2 WITH_MPMC ?= 0 -HAS_MPMC ?= $(shell $(PYTHON) -c "import importlib.util; mods=('torch','pyg_lib','torch_geometric'); print(int(all(importlib.util.find_spec(m) is not None for m in mods)))" 2>/dev/null || echo 0) +HAS_MPMC ?= $(shell $(PYTHON) -c "import importlib.util; mods=('torch','torch_geometric'); print(int(all(importlib.util.find_spec(m) is not None for m in mods)))" 2>/dev/null || echo 0) # set environment variable for documentation export JUPYTER_PLATFORM_DIRS=1 diff --git a/mkdocs.yml b/mkdocs.yml index f448b8b29..5ee6154c7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - 2020 MCQMC Software Tutorial: demos/talk_paper_demos/MCQMC_Tutorial_2020/MCQMC_2020_QMC_Software_Tutorial.ipynb - Technical Examples: - 2023 Random Lattice Generating Vectors: demos/lattice_random_generator.ipynb + - Lattice and Kronecker Generating-Vector Search: demos/lattice_kronecker_methods.ipynb - 2022 Bayesian Cubature Stopping Criterion: demos/gaussian_diagnostics/gaussian_diagnostics_demo.ipynb - 2020 Why Add Q to MC?: demos/talk_paper_demos/why_add_q_to_mc_blog/why_add_q_to_mc_blog.ipynb - 2020 Bayesian Optimization Expected Improvement: diff --git a/qmcpy/discrete_distribution/__init__.py b/qmcpy/discrete_distribution/__init__.py index 3ee36327d..f95fe5931 100644 --- a/qmcpy/discrete_distribution/__init__.py +++ b/qmcpy/discrete_distribution/__init__.py @@ -1,10 +1,10 @@ from .abstract_discrete_distribution import AbstractDiscreteDistribution from .iid_std_uniform import IIDStdUniform -from .lattice import Lattice +from .lattice import Lattice, lattice_vector_wssd_search from .digital_net_b2 import DigitalNetB2 from .digital_net_any_bases import DigitalNetAnyBases,Halton,Faure,Hammersley from .mpmc import MPMC -from .kronecker import Kronecker +from .kronecker import Kronecker, kronecker_vector_search_mobius_transform from .korobov import KorobovLattice from .dummy_sampler import DummySampler from .latin_hypercube import LatinHypercube @@ -15,4 +15,3 @@ DigitalNet = DigitalNetB2 Net = DigitalNetB2 NetB2 = DigitalNetB2 - diff --git a/qmcpy/discrete_distribution/kronecker/__init__.py b/qmcpy/discrete_distribution/kronecker/__init__.py new file mode 100644 index 000000000..69aed4fdf --- /dev/null +++ b/qmcpy/discrete_distribution/kronecker/__init__.py @@ -0,0 +1,2 @@ +from .kronecker import Kronecker +from .kronecker_search_methods import kronecker_vector_search_mobius_transform \ No newline at end of file diff --git a/qmcpy/discrete_distribution/kronecker.py b/qmcpy/discrete_distribution/kronecker/kronecker.py similarity index 84% rename from qmcpy/discrete_distribution/kronecker.py rename to qmcpy/discrete_distribution/kronecker/kronecker.py index e29596d49..3cb421a0d 100644 --- a/qmcpy/discrete_distribution/kronecker.py +++ b/qmcpy/discrete_distribution/kronecker/kronecker.py @@ -1,6 +1,6 @@ from typing import Union, Tuple, Callable -from .abstract_discrete_distribution import AbstractLDDiscreteDistribution -from ..util import ParameterError +from ..abstract_discrete_distribution import AbstractLDDiscreteDistribution +from ...util import ParameterError import numpy as np import warnings @@ -249,6 +249,7 @@ def __init__(self, - `"CBC"`: uses the first $d$ components of a known good Component-by-Component (CBC) generating vector. - `"RICHTMYER"`: uses $\boldsymbol{\alpha}_j = \sqrt{p_j} \bmod 1$, where $p_j$ are primes. This is the classical Richtmyer construction. - `"SUZUKI"`: uses a deterministic construction $\boldsymbol{\alpha}_j = 2^{j/(d+1)}$. + - `"CBC_MT"`: uses the first $d$ components of a known good CBC generating vector obtained using the Mobius transformation method, which can be found in kronecker_search_methods.py. - np.array: user-specified generating vector. shift (Union[None, np.ndarray]): Shift vector $\boldsymbol{\delta}$. If @@ -293,6 +294,117 @@ def __init__(self, elif isinstance(generating_vector, str) and generating_vector.lower() == "suzuki": self.gen_vec_source = "SUZUKI" gen_vec = _suzuki_generating_vector(self.dvec.max()+1) + elif isinstance(generating_vector, str) and generating_vector.lower() == "cbc_mt": + self.gen_vec_source = "CBC_MT" + CBC_MT = np.array([0.618033988749895, + 0.3173225474723, + 0.59332263014446, + 0.20776441643926, + 0.27373719258623, + 0.649734278361753, + 0.478954018631769, + 0.86866022435182, + 0.22845082022244, + 0.581365429377986, + 0.282365231829842, + 0.0822850909119904, + 0.223849641007295, + 0.5770772201756, + 0.51769659336634, + 0.568025390904592, + 0.156782234569368, + 0.82246227056154, + 0.805675312097409, + 0.63877102813393, + 0.358300563495856, + 0.241741343018598, + 0.705003192174204, + 0.1931911954956, + 0.261022001488623, + 0.897938992038015, + 0.46839743115877, + 0.884022067965329, + 0.752352896871505, + 0.1601583600427, + 0.10727599509739, + 0.151478435512877, + 0.163863657127101, + 0.948303450359399, + 0.80350943597439, + 0.426371623468333, + 0.435930910910882, + 0.21329852459791, + 0.661698149534002, + 0.900679822160453, + 0.122436710671457, + 0.483663584095611, + 0.928181067731583, + 0.443143014606576, + 0.74491332336194, + 0.87948409225588, + 0.0428242449803, + 0.534576896789579, + 0.24340042100879, + 0.30424418245585, + 0.574003104342617, + 0.897289023268963, + 0.541424476559586, + 0.356895660350464, + 0.507567280910795, + 0.513983550428507, + 0.0610821922457415, + 0.183871471606587, + 0.446015178033969, + 0.455684287415085, + 0.280817534817491, + 0.115220095666085, + 0.433740673279323, + 0.515605957977756, + 0.113076735656464, + 0.733928297688305, + 0.0597515651584137, + 0.422268695684775, + 0.0979181139173599, + 0.213699261322352, + 0.866811679881922, + 0.0878569329036737, + 0.678412735893121, + 0.181093969536107, + 0.128913741473518, + 0.109341703717108, + 0.289067270578427, + 0.352218331663839, + 0.303605902333137, + 0.0613899204730832, + 0.959535877660851, + 0.475508309069064, + 0.688698902674194, + 0.657037932118495, + 0.645555897563869, + 0.720658665263604, + 0.914423387894897, + 0.425763295044487, + 0.328825255006553, + 0.892452975558004, + 0.16973367306396, + 0.912292406867098, + 0.0923260018966512, + 0.216301713289429, + 0.147861410064151, + 0.8600781655845, + 0.752129792595509, + 0.337431120990153, + 0.542476014178907, + 0.307279789725491], dtype=np.float64) + gen_vec = CBC_MT + if not (self.dvec.max() < len(gen_vec)): + if warn: + warnings.warn( + f"CBC_MT generating vector only supports dimension <= {len(CBC_MT)}; falling back to Richtmyer.", + RuntimeWarning, + ) + self.gen_vec_source = "RICHTMYER" + gen_vec = _richtmyer_generating_vector(self.dvec.max()+1) else: self.gen_vec_source = "CUSTOM" gen_vec = np.asarray(generating_vector, dtype=float) @@ -353,7 +465,9 @@ def periodic_discrepancy(self, n: int, k_tilde: Union[None, Tuple[Callable, floa gamma (Union[None, np.ndarray]): Coordinate weights, shape `(d,)`. Returns: - np.ndarray: The discrepancy. + np.ndarray: Discrepancies for prefixes 1 through `n`, shape `(n,)` + when replications are omitted, or `(g, n)` otherwise, where + `g` is the number of generating vectors. Note: - If `k_tilde` is not specified, the second Bernoulli polynomial is used. @@ -367,19 +481,19 @@ def periodic_discrepancy(self, n: int, k_tilde: Union[None, Tuple[Callable, floa return np.sqrt(self._square_periodic_discrepancies(n, k_tilde, gamma)) - - def wssd_discrepancy(self, n: int, weights: np.ndarray, k_tilde: Union[None, Tuple[Callable, float]] = None, gamma: Union[None, np.ndarray] = None) -> np.ndarray: + def wssd_discrepancy(self, n: int, sample_weights: np.ndarray, k_tilde: Union[None, Tuple[Callable, float]] = None, gamma: Union[None, np.ndarray] = None) -> np.ndarray: """Calculate the weighted sum of squared discrepancies. Args: n (int): The number of sample points. - weights (np.ndarray): Weights applied to each squared discrepancy + sample_weights (np.ndarray): Weights applied to each squared discrepancy before summing. k_tilde (Union[None, Tuple[Callable, float]]): Same as in `periodic_discrepancy`. gamma (Union[None, np.ndarray]): Coordinate weights, shape `(d,)`. Returns: - np.ndarray: The weighted sum of squared discrepancies. + np.ndarray: A scalar when replications are omitted, or one value + per generating vector otherwise. """ if gamma is None: gamma = np.ones(self.d) @@ -388,12 +502,15 @@ def wssd_discrepancy(self, n: int, weights: np.ndarray, k_tilde: Union[None, Tup k_tilde = (lambda x, gamma: np.prod(1 + (x * (x - 1) + 1/6) * gamma, axis=-1), 1) discrepancies = self._square_periodic_discrepancies(n, k_tilde, gamma) - return np.sum(weights * discrepancies, axis=-1) + return np.sum(sample_weights * discrepancies, axis=-1) def _square_periodic_discrepancies(self, n, k_tilde, gamma): n_array = np.arange(1, n + 1) - k_tilde_terms = k_tilde[0](self.gen_samples(n=n), gamma) + # we need the points without a random shift for the calculation, so we can't use self._gen_samples + i = np.arange(0, n) + points = (i[:,None] * self.gen_vec[:,None,:]) % 1 + k_tilde_terms = k_tilde[0](points, gamma) left_sum = np.cumsum(k_tilde_terms[...,1:], axis=-1) * n_array[1:] right_sum = np.cumsum(n_array[:-1] * k_tilde_terms[...,1:], axis=-1) @@ -401,7 +518,8 @@ def _square_periodic_discrepancies(self, n, k_tilde, gamma): k_tilde_zero_terms = k_tilde_terms[...,0] * n_array summation = np.zeros_like(k_tilde_terms) summation[...,1:] = left_sum - right_sum - return (k_tilde_zero_terms + 2 * summation) / (n_array ** 2) - k_tilde[1] + squared = (k_tilde_zero_terms + 2 * summation) / (n_array ** 2) - k_tilde[1] + return squared[0] if self.no_replications else squared def _spawn(self, child_seed, dimension): diff --git a/qmcpy/discrete_distribution/kronecker/kronecker_search_methods.py b/qmcpy/discrete_distribution/kronecker/kronecker_search_methods.py new file mode 100644 index 000000000..664618627 --- /dev/null +++ b/qmcpy/discrete_distribution/kronecker/kronecker_search_methods.py @@ -0,0 +1,242 @@ +import warnings +from typing import Callable, Tuple, Union + +import numpy as np + +def kronecker_vector_search_mobius_transform( + n_max: int, + d_max: int, + searchsize: int, + kernel: Union[None, Callable] = None, + coord_weights: Union[None, list, np.ndarray] = None, + gen_vec_init: Union[None, float] = None, +) -> Tuple[np.ndarray, float, np.ndarray, np.ndarray]: + """ + Note that the sympy package is highly recommended for this search method, though not required. + + A deterministic CBC search method for finding a generating vector for a Kronecker sequence, minimizing the weighted sum of squared discrepancies (WSSD). + - The first component is gen_vec_init, defaults to the golden ratio. + - We use a modified mobius transformation f(x) = (a*x + b)/(c*x + d) where a, c are distinct primes and b, d are the two pairs of the smallest positive integers such that |a*d - b*c| = 1. + - Each subsequent component is found by performing the mobius transformation on the previous component, searching over all pairs of distinct primes from the first searchsize many primes. + + Args: + n_max (int): The maximum sample size to be searched over. + d_max (int): The maximum dimension for which to find the generating vector. + searchsize (int): The number of primes to search over for each component of the generating vector. + kernel (Union[None, Callable]): The kernel function to use in the search. + coord_weights (Union[None, list, np.ndarray]): An array of coordinate weights to use in the search. If None, weights are set to j^(-2). + gen_vec_init (Union[None, float]): The initial value for the generating vector. If None, the golden ratio is used for the first component. Note that gen_vec_init is taken mod 1. + + Returns: + generating_vector, wssd, discrepancies, coeff (tuple): + - generating_vector (numpy array): The generating vector found by the search. + - wssd (float): The weighted sum of squared discrepancies for n = 1,...,n_max, for the generating vector found. + - discrepancies (numpy array): The discrepancies for n = 1,...,n_max. + - coeff (numpy array): The coefficients of the linear transformation used in the search. A description of the coeff array is found below. + + Time cost: + The time cost of the search is O(searchsize^2 * d_max * n_max). + + Approach: + Conducts a deterministic CBC search for a generating vector, minimizing the weighted sum of squared discrepancies (wssd) with sample weights w_n = n. + + Details on coeff array: + The coeff array is a (d_max-1) x 4 array where each row corresponds to a dimension from 2 to d_max. The columns correspond to the coefficients of the linear transformation used to compute the gen_vec component for that dimension. Specifically, + - gen_vec[dim+1] = (coeff[dim, 0] * gen_vec[dim] + coeff[dim, 1]) / (coeff[dim, 2] * gen_vec[dim] + coeff[dim, 3]) + """ + + if searchsize < 2: + raise ValueError("searchsize must be at least 2.") + if n_max < 2: + raise ValueError("n_max must be at least 2.") + if d_max < 1: + raise ValueError("d_max must be at least 1.") + if coord_weights is not None and len(coord_weights) < d_max: + raise ValueError("Length of coord_weights must be greater than or equal to d_max.") + + + # the quadratic Bernoulli polynomial + if kernel is None: + kernel = lambda t: t * (t - 1) + 1/6 + + # define coordinate weights if not provided, default to j^(-2) + if coord_weights is None: + coord_weights = np.array([j**(-2) for j in range(1, d_max + 1)], dtype=np.float64) + else: + coord_weights = np.asarray(coord_weights, dtype=np.float64) + + # use sympy if it's already installed, otherwise uses slower and recursive direct implementation + try: + import sympy + except ImportError: + warnings.warn( + "sympy is recommended for this search method: it computes the Bezout coefficients " + "for the linear transformation. Without it a slower recursive Euclidean algorithm is used.", + UserWarning, + ) + has_sympy = False + else: + has_sympy = True + + if has_sympy: + # search over the first n primes, n = searchsize + searchspace = np.array(list(sympy.primerange(1, sympy.prime(searchsize)+1)), dtype=np.float64) + else: + def get_primes(n): + primes = [] + num = 2 + while len(primes) < n: + is_prime = True + for p in primes: + if p * p > num: + break + if num % p == 0: + is_prime = False + break + if is_prime: + primes.append(num) + num += 1 + return primes + + # search over the first n primes, n = searchsize + searchspace = np.array(get_primes(searchsize), dtype=np.float64) + + # we define this method here for convenience, to use in computing Bezout coefficients if necessary + def recursive_euclidean_algorithm(a, b): + if b == 0: + return 1, 0, a + x1, y1, gcd = recursive_euclidean_algorithm(b, a % b) + x = y1 + y = x1 - (a // b) * y1 + return x, y, gcd + + # gen_vec is our generating vector, will be found cbc + gen_vec = np.zeros(d_max, dtype=np.float64) + + # we pick the golden ratio as the first component of gen_vec, or let the user specify + if gen_vec_init is None: + gen_vec[0] = np.float64((np.sqrt(5) - 1) / 2) + else: + gen_vec[0] = np.mod(gen_vec_init, 1,dtype=np.float64) + + # precompute several constants for the wssd calculation + diff = np.cumsum(1.0 / np.arange(n_max, 1, -1,dtype=np.float64)) + freq = np.cumsum(diff) + freq = np.flip(freq) + + num = n_max * (n_max + 1) / 2 + + nK0 = (1 + coord_weights * kernel(0)) + nK0 = n_max * np.cumprod(nK0) + + # precompute Bezout coefficients for all pairs of primes in the search space + bezoutCoeffs = np.zeros((searchsize, searchsize)) + if has_sympy: + from sympy.core.intfunc import igcdex + for i in range(searchsize - 1): + a = searchspace[i] + for j in range(i + 1, searchsize): + c = searchspace[j] + # Use sympy.igcdex to get Bezout coefficients + d_coeff, b_coeff, _ = igcdex(int(a), int(c)) + bezoutCoeffs[i, j] = np.float64(b_coeff) + bezoutCoeffs[j, i] = np.float64(d_coeff) + else: + for i in range(searchsize - 1): + a = searchspace[i] + for j in range(i + 1, searchsize): + c = searchspace[j] + # Use the recursive Euclidean algorithm to get Bezout coefficients + d_coeff, b_coeff, _ = recursive_euclidean_algorithm(int(a), int(c)) + bezoutCoeffs[i, j] = np.float64(b_coeff) + bezoutCoeffs[j, i] = np.float64(d_coeff) + + + # setting up some useful variables for the search + coeff = np.zeros((d_max - 1, 4)) # stores the coefficients of the linear transformation at each dimension + t = gen_vec[0] * np.arange(1, n_max) % 1 # t vector is the vector of coordinates generated for the first dimension + kPrev = 1 + coord_weights[0] * kernel(t) # gets the k vector for the first dimension, which is used in the wssd calculation and updated each dimension of the search. + # The k vector is Ktilde(x_i) for i = 1,...,n_max-1, where Ktilde is the kernel and x_i are the points generated by the gen_vec vector, up to the current dimension. + + # the main search loop + for dim in range(1, d_max): + best_wssd = np.inf # stores the current wssd found for each dimension, initialized to infinity + best_gen_vec = 0 # stores the current best gen_vec component found for this dimension, initialized to 0 + best_k = None # stores the k vector for the current best gen_vec, used to update the k vector for the next dimension after the search is done for this dimension + for i in range(searchsize): + p1 = searchspace[i] + for j in range(searchsize): + if j == i: # the two primes have to be distinct, so we skip this case + continue + + p2 = searchspace[j] + + b = bezoutCoeffs[i, j] + d = bezoutCoeffs[j, i] + + if b < 0: # we search over both minimal Bezout coefficients + b1 = -b + d1 = d + b2 = np.abs(b + p1) + d2 = np.abs(d -p2) + else: + d1 = -d + b1 = b + d2 = np.abs(d + p2) + b2 = np.abs(b - p1) + + gen_vec_dim1 = (p1 * gen_vec[dim - 1] + b1) / (p2 * gen_vec[dim - 1] + d1) # the linear transformation to get the next gen_vec_dim candidate to test + gen_vec_dim2 = (p1 * gen_vec[dim - 1] + b2) / (p2 * gen_vec[dim - 1] + d2) # the other candidate from the linear transformation + t1 = (gen_vec_dim1 * np.arange(1, n_max)) - np.floor(gen_vec_dim1 * np.arange(1, n_max)) # vector of coordinates generated by this candidate component + t2 = (gen_vec_dim2 * np.arange(1, n_max)) - np.floor(gen_vec_dim2 * np.arange(1, n_max)) + k_vector1 = kPrev * (1 + kernel(t1) * coord_weights[dim]) # get the k vector for this candidate component, used in the wssd calculation + k_vector2 = kPrev * (1 + kernel(t2) * coord_weights[dim]) + + wssd1 = np.dot(freq, k_vector1) + wssd2 = np.dot(freq, k_vector2) + + if wssd1 < wssd2: + b = b1 + d = d1 + wssd = wssd1 + k_vector = k_vector1 + gen_vec_dim = gen_vec_dim1 + else: + b = b2 + d = d2 + wssd = wssd2 + k_vector = k_vector2 + gen_vec_dim = gen_vec_dim2 + + if wssd < best_wssd: # if this candidate has a better wssd than the best found so far, we update the best coefficients and wssd + coeff[dim-1, 0] = p1 + coeff[dim-1, 1] = b + coeff[dim-1, 2] = p2 + coeff[dim-1, 3] = d + best_wssd = wssd + best_gen_vec = gen_vec_dim % 1 + best_k = k_vector + gen_vec[dim] = best_gen_vec # update the gen_vec vector with the best candidate found for this dimension + + kPrev = best_k # update the k vector for the next dimension with the k vector of the best candidate found for this dimension + best_wssd = nK0[dim] - num + 2 * best_wssd # calculate the best wssd for this dimension using the formula from the paper, which involves the nK0 constants precomputed at the beginning of the function. This is used for debugging and to check the wssd at each dimension of the search. + + # print(coeff[dim - 1, :], (nK0[dim] - num + 2 * best_wssd)) # debugging line to check the coefficients and wssd at each dimension + + # Adapted from Jimmy's code for calculating the discrepancies for n = 1,...,n_max from SURE 2025 + n_array = np.arange(1, n_max + 1) + k_tilde = lambda x, coord_weight: np.prod(1 + kernel(x) * coord_weight, axis=1) + k_tilde_terms = k_tilde(gen_vec * np.arange(n_max).reshape((n_max, 1)) - np.floor(gen_vec * np.arange(n_max).reshape((n_max, 1))), coord_weights) + + left_sum = np.cumsum(k_tilde_terms[1:]) * n_array[1:] + right_sum = np.cumsum(n_array[:-1] * k_tilde_terms[1:]) + + k_tilde_zero_terms = k_tilde_terms[0] * n_array + summation = np.zeros(n_max) + summation[1:] = left_sum - right_sum + discrepancies = (k_tilde_zero_terms + 2 * summation) / (n_array ** 2) - 1 + + if d_max == 1: # the CBC loop never runs, so best_wssd is unset; report the 1-D WSSD directly + best_wssd = float(n_array @ discrepancies) + + return gen_vec, best_wssd, discrepancies, coeff diff --git a/qmcpy/discrete_distribution/lattice/__init__.py b/qmcpy/discrete_distribution/lattice/__init__.py index b57762ece..3eb626fd7 100644 --- a/qmcpy/discrete_distribution/lattice/__init__.py +++ b/qmcpy/discrete_distribution/lattice/__init__.py @@ -1 +1,2 @@ from .lattice import Lattice +from .lattice_vector_wssd_search import lattice_vector_wssd_search diff --git a/qmcpy/discrete_distribution/lattice/generating_vectors/kuo.lattice-39102-1024-1048576.3600.npy b/qmcpy/discrete_distribution/lattice/generating_vectors/kuo.lattice-39102-1024-1048576.3600.npy new file mode 100644 index 000000000..40fe8cb4e Binary files /dev/null and b/qmcpy/discrete_distribution/lattice/generating_vectors/kuo.lattice-39102-1024-1048576.3600.npy differ diff --git a/qmcpy/discrete_distribution/lattice/lattice.py b/qmcpy/discrete_distribution/lattice/lattice.py index b960063e2..0a04315ea 100644 --- a/qmcpy/discrete_distribution/lattice/lattice.py +++ b/qmcpy/discrete_distribution/lattice/lattice.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Callable, Union from ..abstract_discrete_distribution import AbstractLDDiscreteDistribution from ...util import ParameterError, ParameterWarning import qmctoolscl @@ -183,6 +183,17 @@ def __init__( )[None, :] d_limit = 9125 n_limit = 1048576 + elif ( + isinstance(generating_vector, str) + and generating_vector == "kuo.lattice-39102-1024-1048576.3600.txt" + ): + self.gen_vec_source = generating_vector + gen_vec = np.load( + dirname(abspath(__file__)) + + "/generating_vectors/kuo.lattice-39102-1024-1048576.3600.npy" + )[None, :] + d_limit = 3600 + n_limit = 1048576 elif isinstance(generating_vector, str): self.gen_vec_source = generating_vector if not (generating_vector[-4:] == ".txt"): @@ -407,3 +418,98 @@ def _spawn(self, child_seed, dimension): order=self.order, m_max=self.input_m_max, ) + + def expected_squared_periodic_discrepancies( + self, + n_max: int, + coord_weights: Union[None, np.ndarray] = None, + kernel: Union[None, Callable] = None, + ) -> np.ndarray: + """Returns the expected squared periodic discrepancies for each of the first n_max points of the lattice sequence. + + Args: + n_max (int): Maximum number of points to calculate the squared periodic discrepancies for. + coord_weights (Union[None, np.ndarray]): Coordinate weights for the discrepancy calculation. If None, uses weights gamma_j = j^(-2). + kernel (Union[None, Callable]): Kernel function for the discrepancy calculation. If None, uses the second bernoulli polynomial. + + Returns: + discs (np.ndarray): The expected squared periodic discrepancies for the first n_max points. + """ + + if coord_weights is not None and len(coord_weights) < self.d: + raise ValueError("Length of coord_weights must be greater than or equal to the dimension of the lattice") + if coord_weights is None: + coord_weights = np.array([j**(-2) for j in range(1, self.d + 1)], dtype=np.float64) + if self.order == "LINEAR": + raise NotImplementedError("expected_squared_periodic_discrepancies not implemented for linear order") + + if kernel is None: + kernel = lambda x: x * (x - 1) + 1/6 + + coord_weights = coord_weights[:self.d] + + k_tilde = lambda x: np.prod(1 + coord_weights * kernel(x), axis=-1) + + # generate the vdc points without any random shift + r_x = np.uint64(self.gen_vec.shape[0]) + n = np.uint64(2**(np.ceil(np.log2(n_max)))) + d = np.uint64(self.d) + n_start = np.uint64(0) + x = np.empty((r_x, n, d), dtype=np.float64) + _ = qmctoolscl.lat_gen_natural(r_x, n, d, n_start, self.gen_vec, x, backend="c") + s = x + + # evaluate the kernel on the sample points + k_vector = k_tilde(s) + k_vector = k_vector.reshape(-1) + + # get the constant vector term of the summation + k_const = -1 + k_vector[0] / np.arange(1, n_max + 1, dtype=np.float64) + + # group the kernel evaluations by powers of 2 + k_sum = np.zeros(np.ceil(np.log2(n_max)).astype(int), dtype=np.float64) + for i in range(k_sum.size): + k_sum[i] = np.sum(k_vector[2**i:(2**(i+1))]) + + # k_sum @ freq_mtx in O(n_max) memory: the shared 1/(j+1)**2 factor collapses the + # (log2(n_max), n_max) frequency matrix to cumsum(2*w) / (j+1)**2, with w[j] the sum + # of k_sum over the set bits of j. + idx = np.arange(n_max) + w = np.zeros(n_max, dtype=np.float64) + for l in range(k_sum.size): + w += k_sum[l] * ((idx >> l) & 1) + discs = k_const + np.cumsum(2.0 * w) / (idx + 1.0) ** 2 + return discs + + + def wssd( + self, + n_max: int, + coord_weights: Union[None, np.ndarray] = None, + sample_weights: Union[None, np.ndarray] = None, + kernel: Union[None, Callable] = None, + ) -> float: + """Returns the weighted sum of the expected squared periodic discrepancies for the first n_max points of the lattice sequence. + + Args: + n_max (int): Number of points to calculate the weighted squared periodic discrepancy for. + coord_weights (Union[None, np.ndarray]): Coordinate weights for the discrepancy calculation. If None, uses weights gamma_j = j^(-2). + sample_weights (Union[None, np.ndarray]): Sample weights for the weighted squared periodic discrepancy calculation. If None, uses weights w_n = n. Note that the time cost may be higher for other sample weights. + kernel (Union[None, Callable]): Kernel function for the discrepancy calculation. If None, uses the second bernoulli polynomial. + + Returns: + wssd (float): The weighted squared periodic discrepancy. + """ + if coord_weights is not None and len(coord_weights) < self.d: + raise ValueError("Length of coord_weights must be greater than or equal to the dimension of the lattice") + if coord_weights is not None: + coord_weights = coord_weights[:self.d] + if sample_weights is not None and len(sample_weights) != n_max: + raise ValueError("Length of sample_weights must equal n_max") + if sample_weights is None: + sample_weights = np.arange(1, n_max + 1, dtype=np.float64) + + discs = self.expected_squared_periodic_discrepancies(n_max, coord_weights=coord_weights, kernel=kernel) + wssd = np.dot(sample_weights, discs) + + return wssd diff --git a/qmcpy/discrete_distribution/lattice/lattice_vector_wssd_search.py b/qmcpy/discrete_distribution/lattice/lattice_vector_wssd_search.py new file mode 100644 index 000000000..760c4d3f4 --- /dev/null +++ b/qmcpy/discrete_distribution/lattice/lattice_vector_wssd_search.py @@ -0,0 +1,199 @@ +from typing import Callable, Union + +import numpy as np + +def lattice_vector_wssd_search( + n_max: int, + d_max: int, + coord_weights: Union[None, list, np.ndarray] = None, + kernel: Union[None, Callable] = None, +) -> np.ndarray: + """ + CBC search method for finding a lattice rule minimizing the WSSD. + + Args: + n_max (int): The maximum number of points the lattice rule is optimized for. + d_max (int): The dimension of the lattice rule. + coord_weights (Union[None, list, np.ndarray]): The coordinate weights used to compute the discrepancy. Defaults to j^(-2) for j=1,...,d_max. + kernel (Union[None, Callable]): The kernel used to compute the discrepancy. Should accept a single argument and return a scalar. Defaults to the second Bernoulli polynomial. + + Returns: + gen_vec (array-like): The generating vector of the lattice that minimizes the WSSD. + + Time cost: + The time cost of the search is O(d_max * n_max * log(n_max)), though the contribution of d_max is smaller until around d_max = 100. + + Note: + Uses sample weights of w_n = n for n = 1,...,n_max when calculating the WSSD. + + Examples: + >>> lattice_vector_wssd_search(n_max=2**10, d_max=5) + array([ 1, 403, 361, 281, 421]) + >>> lattice_vector_wssd_search(n_max=2**15, d_max=10) + array([ 1, 4825, 13541, 15249, 15405, 9909, 7493, 11407, 14819, + 10089]) + + Custom coordinate weights + + >>> lattice_vector_wssd_search(n_max=2**15, d_max=10, coord_weights=[j**(-1) for j in range(1, 11)]) + array([ 1, 4825, 13541, 15249, 7311, 10339, 5933, 6307, 14729, + 13037]) + + Custom kernels + + >>> bernoulli6 = lambda x: x * (x * (-1/2 + x * (x * (5/2 + x * (-3 + x))))) + 1/42 + >>> lattice_vector_wssd_search(n_max=2**15, d_max=10, coord_weights=None, kernel=bernoulli6) + array([ 1, 12589, 12955, 12021, 25, 14249, 1949, 2487, 8893, + 14279]) + + Ties in the WSSD score are broken by smallest candidate index (tolerance rtol = 1e-13), + so the vector is reproducible across platforms; a strongly degenerate kernel can still + depend on that tolerance, but the default Bernoulli polynomial is well conditioned. + + """ + np.seterr(all='warn') + if kernel is None: + kernel = lambda x: x * (x - 1) + 1 / 6 # default kernel is the second Bernoulli polynomial + if coord_weights is None: + coord_weights = np.array([j**(-2) for j in range(1, d_max + 1)], dtype=np.float64) # default coordinate weights are j^(-2) + + if not callable(kernel): + raise ValueError("kernel must be a callable function") + if not isinstance(coord_weights, (list, np.ndarray)): + raise ValueError("coord_weights must be array-like") + if not isinstance(n_max, int) or not isinstance(d_max, int): + raise ValueError("n_max and d_max must be integers") + + if len(coord_weights) < d_max: + raise ValueError("coord_weights must have length at least d_max") + if n_max < 8: + raise ValueError("n_max must be at least 8") + if d_max < 1: + raise ValueError("d_max must be at least 1") + + m = np.ceil(np.log2(n_max)).astype(int) + if d_max > 2**(m - 2): + raise ValueError("d_max exceeds the CBC candidate pool for this n_max; increase n_max") + + # ---------------------------------------------------------------------- + # Set up rhovector - how often each value appears + # ---------------------------------------------------------------------- + bits = np.zeros((n_max, m), dtype=np.uint64) + for i in range(n_max): + bits[i, :] = 2 * np.array([((i >> j) & 1) for j in range(m)], dtype=np.uint64) + + cumsumbits = np.cumsum(bits, axis=0) # n_max x m + rhovector = np.dot((1.0 / np.arange(1, n_max + 1)), cumsumbits) # 1 x m + + rhovectorNx1 = np.zeros((2**m - 1, 1)) + rIdx1 = 0 + for r in range(m, 0, -1): + rIdx2 = rIdx1 + 2**(r - 1) - 1 + rhovectorNx1[rIdx1:rIdx2 + 1, 0] = rhovector[r - 1] + rIdx1 = rIdx2 + 1 + + # ---------------------------------------------------------------------- + # Get ordering of the search space - needed for circulant matrix + # ---------------------------------------------------------------------- + gR = np.ones(2**(m - 2), dtype=int) # gR determines ordering of rows to have circulant matrix + intMod = 2**m + for idx in range(1, 2**(m - 2)): + temp = (gR[idx - 1] * 5) % intMod + gR[idx] = min(intMod - temp, temp) + + gRows = np.ones(2**(m - 1), dtype=int) # gRows determines* ordering of cols to have circulant matrix + gRows[-1] = 0 + rowVects = np.ones(2**m - 1, dtype=int) # rowVects + gStrtIdx = 0 + vStrtIdx = 0 + + for l in range(m, 1, -1): + gEndIdx = gStrtIdx + 2**(l - 2) - 1 + vEndIdx = vStrtIdx + 2**(l - 1) - 1 + + gRow = np.ones(2**(l - 2), dtype=int) + intMod = 2**l + for idx in range(1, 2**(l - 2)): + temp = (gRow[idx - 1] * 5) % intMod + gRow[idx] = min(intMod - temp, temp) + + gRows[gStrtIdx:gEndIdx + 1] = gRow + rowV = np.concatenate(([1], np.flip(gRow[1:]))) + doubled = np.concatenate((rowV, rowV)) + rowVects[vStrtIdx:vEndIdx + 1] = 2**(m - l) * doubled + + gStrtIdx = gEndIdx + 1 + vStrtIdx = vEndIdx + 1 + + rowVects[-1] = 2**(m - 1) + + # ---------------------------------------------------------------------- + # Set up prodV - where we store information about previous components + # ---------------------------------------------------------------------- + prodV = np.ones((2**m - 1, 1)) + prodV = prodV * rhovectorNx1 + + # Initial 1D case + rowV = rowVects / 2**m + rowV = 1 + coord_weights[0] * kernel(rowV) + prodV = prodV * rowV[:, None] + + # Set up k0 + k0 = 1 + coord_weights[0] * kernel(0) + + # ---------------------------------------------------------------------- + # Begin search + # ---------------------------------------------------------------------- + def best_index(scores): + # smallest index among (near-)tied minima -> reproducible across platforms, since + # bit-level FFT/FMA differences can otherwise flip which near-equal score is the min + lo = scores.min() + return int(np.flatnonzero(scores <= lo + 1e-13 * abs(lo))[0]) + + gen_vec = np.ones(d_max, dtype=int) + + for hComp in range(2, d_max + 1): + wssd = np.zeros(2**(m - 2), dtype=np.float64) + + gamma = coord_weights[hComp - 1] + omega = lambda x: 1 + gamma * kernel(x) + k0 = k0 * (1 + gamma * kernel(0)) + + curIdx2 = 0 + prodIdx1 = 0 + for l in range(m, 1, -1): # we iterate over decreasing size blocks of powers of two= + nextIdx2 = curIdx2 + 2**(l - 2) + prodIdx2 = prodIdx1 + 2**(l - 2) + + curRow = gRows[curIdx2:nextIdx2] + col = curRow / 2**l + fftCol = omega(col).astype(np.complex128) # first column of this circulant matrix block + + pCol = prodV[prodIdx1:prodIdx2, 0].astype(np.complex128) # corresponding section of prodV + + wVector = 2 * np.fft.ifft(np.fft.fft(fftCol) * np.fft.fft(pCol)).real # matrix vector product as fft + numrep = 2**(m - l) + wssd = wssd + np.tile(wVector, numrep) + + curIdx2 = nextIdx2 + prodIdx1 = prodIdx2 + 2**(l - 2) + + wssd = wssd + omega(1 / 2) * prodV[-1, 0] # not actually wssd; we avoid subtracting a constant to save precision + + bestIdx = best_index(wssd) + newH = int(gR[bestIdx]) + + while newH in gen_vec: # avoid duplicates + wssd[bestIdx] = np.inf + bestIdx = best_index(wssd) + newH = int(gR[bestIdx]) + + gen_vec[hComp - 1] = newH + + # set up prodV for next iteration + rowV = (newH * rowVects) % 2**m + rowV = rowV / 2**m + rowV = omega(rowV) + prodV = prodV * rowV[:, None] + + return gen_vec diff --git a/qmcpy/discrete_distribution/mpmc/__init__.py b/qmcpy/discrete_distribution/mpmc/__init__.py index 6504b5e33..066df0d47 100644 --- a/qmcpy/discrete_distribution/mpmc/__init__.py +++ b/qmcpy/discrete_distribution/mpmc/__init__.py @@ -18,12 +18,13 @@ ImportError with installation instructions. You can check availability by running: - python -c "import torch; import pyg_lib; import torch_geometric; print('MPMC dependencies ready')" + python -c "import torch; import torch_geometric; print('MPMC dependencies ready')" + +(``pyg_lib`` is an optional accelerator; MPMC runs without it.) """ try: import torch - import pyg_lib import torch_geometric from .mpmc import MPMC except ImportError as e: @@ -35,11 +36,12 @@ class MPMC(object): """ def __init__(self, *args, **kwargs): raise ImportError( - f"MPMC requires PyTorch, pyg_lib, and PyTorch Geometric, but they are not installed.\n" + f"MPMC requires PyTorch and PyTorch Geometric, but they are not installed.\n" f"Original error: {_missing_dep}\n\n" f"To use MPMC, install dependencies with:\n" f" python -m pip install 'qmcpy[mpmc]'\n" f" qmcpy-install-mpmc\n\n" + f"(pyg_lib is an optional accelerator; MPMC runs without it.)\n" f"For GPU support, see: https://pytorch.org/get-started/locally/\n" f"For torch-geometric installation details, see: " f"https://pytorch-geometric.readthedocs.io/en/latest/install/installation.html" diff --git a/qmcpy/discrete_distribution/mpmc/models.py b/qmcpy/discrete_distribution/mpmc/models.py index c88ff9111..f476d882d 100644 --- a/qmcpy/discrete_distribution/mpmc/models.py +++ b/qmcpy/discrete_distribution/mpmc/models.py @@ -1,6 +1,37 @@ import torch from torch import nn -from torch_geometric.nn import MessagePassing, InstanceNorm, radius_graph +from torch_geometric.nn import MessagePassing, InstanceNorm + +try: + from torch_geometric.nn import radius_graph as _tg_radius_graph +except Exception: # torch_geometric built without the pooling ops + _tg_radius_graph = None + +_tg_radius_graph_ok = _tg_radius_graph is not None + + +def radius_graph(x, r, batch=None, loop=False): + """Edges between points within distance ``r`` (per batch), shaped ``[2, E]``. + + Uses torch_geometric's compiled ``radius_graph`` when its backend is present + and working, otherwise a native ``torch.cdist`` fallback so MPMC runs + without ``pyg_lib`` / ``torch_cluster`` (and on platforms where their + compiled ops -- e.g. ``torch.ops.pyg.radius`` on Windows -- fail to load). + """ + global _tg_radius_graph_ok + if _tg_radius_graph_ok: + try: + return _tg_radius_graph(x, r=r, batch=batch, loop=loop) + except (ImportError, AttributeError, RuntimeError, OSError): + _tg_radius_graph_ok = False # backend missing/broken -- use native from now on + dist = torch.cdist(x, x) + mask = dist <= r + if batch is not None: + mask = mask & (batch.view(-1, 1) == batch.view(1, -1)) + if not loop: + mask = mask & ~torch.eye(mask.size(0), dtype=torch.bool, device=mask.device) + row, col = mask.nonzero(as_tuple=True) + return torch.stack([row, col], dim=0) from .utils import ( L2star, L2ctr, L2ext, L2per, L2sym, L2mix, diff --git a/qmcpy/util/install_mpmc_pyg.py b/qmcpy/util/install_mpmc_pyg.py index b559c3f5b..fe0540cec 100644 --- a/qmcpy/util/install_mpmc_pyg.py +++ b/qmcpy/util/install_mpmc_pyg.py @@ -7,6 +7,7 @@ PYG_LIB_REQUIREMENT = "pyg_lib>=0.6.0" +PYG_LIB_SOURCE = "git+https://github.com/pyg-team/pyg-lib.git@0.8.0" TORCH_GEOMETRIC_REQUIREMENT = "torch-geometric>=2.6.1" @@ -59,7 +60,13 @@ def wheel_urls(torch_version, accelerator): def main(torch_module=None): - """Install PyG dependencies that cannot be resolved from PyPI alone.""" + """Install the PyG dependencies MPMC needs beyond what PyPI resolves. + + ``torch-geometric`` is required and comes from PyPI. ``pyg_lib`` is only an + optional accelerator -- MPMC and ``torch-geometric`` run without it -- so + when no wheel exists for this torch build and the source build fails, we + warn and carry on instead of aborting the install. + """ if torch_module is None: try: torch_module = importlib.import_module("torch") @@ -79,7 +86,18 @@ def main(torch_module=None): TORCH_GEOMETRIC_REQUIREMENT, ) - last_error = None + if not _install_pyg_lib(torch_module): + print( + "WARNING: could not install the optional pyg_lib accelerator for " + f"torch {torch_module.__version__} " + f"({accelerator_tag(torch_module)}); MPMC falls back to " + "torch-geometric's native (slower) scatter path.", + flush=True, + ) + + +def _install_pyg_lib(torch_module): + """Best-effort pyg_lib install; return True on success, False otherwise.""" accelerator = accelerator_tag(torch_module) for wheel_url in wheel_urls(torch_module.__version__, accelerator): print(f"Trying pyg_lib wheels from {wheel_url}", flush=True) @@ -96,15 +114,27 @@ def main(torch_module=None): "--find-links", wheel_url, ) - return - except subprocess.CalledProcessError as error: - last_error = error - - raise RuntimeError( - f"Unable to install pyg_lib for torch {torch_module.__version__} " - f"({accelerator}). " - "PyG wheels at https://data.pyg.org/whl/ may not support this build." - ) from last_error + return True + except subprocess.CalledProcessError: + pass + + print( + "Pre-built pyg_lib wheels were unavailable; trying the official " + "source release", + flush=True, + ) + try: + run( + sys.executable, + "-m", + "pip", + "install", + "--no-build-isolation", + PYG_LIB_SOURCE, + ) + return True + except subprocess.CalledProcessError: + return False if __name__ == "__main__": diff --git a/scripts/colab_notebooks_manifest.json b/scripts/colab_notebooks_manifest.json index e683f8291..0f41e892a 100644 --- a/scripts/colab_notebooks_manifest.json +++ b/scripts/colab_notebooks_manifest.json @@ -18,6 +18,7 @@ "demos/gaussian_diagnostics/gaussian_diagnostics_demo.ipynb", "demos/iris.ipynb", "demos/korobov_hammersley_latinhypercube_demos.ipynb", + "demos/lattice_kronecker_methods.ipynb", "demos/lattice_random_generator.ipynb", "demos/lebesgue_integration.ipynb", "demos/linear-scrambled-halton.ipynb", diff --git a/test/booktests/tb_lattice_kronecker_methods.py b/test/booktests/tb_lattice_kronecker_methods.py new file mode 100644 index 000000000..5164e63a2 --- /dev/null +++ b/test/booktests/tb_lattice_kronecker_methods.py @@ -0,0 +1,22 @@ +import unittest +from __init__ import BaseNotebookTest + + +class NotebookTests(BaseNotebookTest): + + def test_lattice_kronecker_methods_notebook(self): + # Keep enough lattice candidates for the reduced dimension: dim <= n / 4. + replacements = { + "dim = 100": "dim = 8", + "dim = 20": "dim = 8", + "n = 2**15": "n = 2**5", + "searchsize = 20": "searchsize = 4", + } + self.run_notebook( + "../../demos/lattice_kronecker_methods.ipynb", + replacements=replacements, + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_dd_lattice_kronecker.py b/test/test_dd_lattice_kronecker.py new file mode 100644 index 000000000..49dcb50ab --- /dev/null +++ b/test/test_dd_lattice_kronecker.py @@ -0,0 +1,279 @@ +import inspect +import re +import sys +import unittest +from unittest.mock import patch + +import numpy as np +import numpy.testing as npt + +from qmcpy import ( + Kronecker, + kronecker_vector_search_mobius_transform, + Lattice, + lattice_vector_wssd_search, +) + +###################################################### +# Helper functions +###################################################### +def _bern2(x): + return x * (x - 1) + 1 / 6 + + +def _pkern(x, coord_weights): + return np.prod(1 + _bern2(x) * coord_weights, axis=-1) + + +def _direct_disc(points, coord_weights): + """Evaluate the periodic-kernel definition directly for small prefixes.""" + return np.array( + [ + _pkern( + (points[:n, None] - points[None, :n]) % 1, coord_weights + ).mean() + - 1 + for n in range(1, len(points) + 1) + ] + ) + + +###################################################### +# Test class for Lattice and Kronecker methods +###################################################### +class TestLatKron(unittest.TestCase): + + def test_lat_disc_wssd(self): + n, coord_weights = 8, np.array([1.0, 0.25]) + lattice = Lattice(2, randomize=False, order="RADICAL_INVERSE") + expected = _direct_disc( + lattice.gen_samples(n=n, warn=False), coord_weights + ) + + for actual in ( + lattice.expected_squared_periodic_discrepancies(n), + lattice.expected_squared_periodic_discrepancies( + n, coord_weights=coord_weights, kernel=_bern2 + ), + ): + self.assertEqual(actual.shape, (n,)) + self.assertTrue(np.isfinite(actual).all()) + npt.assert_allclose(actual, expected, rtol=0, atol=5e-15) + + npt.assert_allclose( + lattice.wssd(n), np.arange(1, n + 1) @ expected, rtol=0, atol=5e-14 + ) + sample_weights = np.linspace(0.5, 1.5, n) + npt.assert_allclose( + lattice.wssd( + n, coord_weights=coord_weights, sample_weights=sample_weights + ), + sample_weights @ expected, + rtol=0, + atol=5e-14, + ) + + def test_lat_valid(self): + lattice = Lattice(2, randomize=False) + with self.assertRaisesRegex(ValueError, "coord_weights"): + lattice.expected_squared_periodic_discrepancies(8, coord_weights=[1.0]) + with self.assertRaisesRegex(ValueError, "coord_weights"): + lattice.wssd(8, coord_weights=[1.0]) + # sample_weights length must match n_max exactly, both too short and too long + for bad in (np.ones(7), np.ones(9)): + with self.subTest(sample_weights_length=len(bad)), self.assertRaisesRegex(ValueError, "sample_weights"): + lattice.wssd(8, sample_weights=bad) + with self.assertRaisesRegex(NotImplementedError, "linear order"): + Lattice(2, randomize=False, order="LINEAR").expected_squared_periodic_discrepancies(8) + with self.assertRaisesRegex(ValueError, "n_max must be at least 8"): + lattice_vector_wssd_search(3, 3) + with self.assertRaisesRegex(ValueError, "candidate pool"): # d_max > CBC pool, once an infinite loop + lattice_vector_wssd_search(8, 3) + + def test_lat_wssd_kernel(self): + # wssd must forward a custom kernel through to expected_squared_periodic_discrepancies + lattice = Lattice(3, randomize=False, order="RADICAL_INVERSE") + n = 16 + bern4 = lambda x: x**4 - 2 * x**3 + x**2 - 1 / 30 + npt.assert_allclose( + lattice.wssd(n, kernel=bern4), + np.arange(1, n + 1) @ lattice.expected_squared_periodic_discrepancies(n, kernel=bern4), + rtol=0, atol=5e-14, + ) + self.assertFalse(np.isclose(lattice.wssd(n), lattice.wssd(n, kernel=bern4))) + + def test_lat_search(self): + default = lattice_vector_wssd_search(16, 4, None, None) + npt.assert_array_equal(default, [1, 5, 3, 7]) + # passing the built-in weights and kernel explicitly must reproduce the default + npt.assert_array_equal( + lattice_vector_wssd_search(16, 4, np.array([1.0, 0.25, 1 / 9, 1 / 16]), _bern2), + default, + ) + # coord_weights[0] and kernel= must each reach the search (both were once ignored) + bern6 = lambda x: x**6 - 3 * x**5 + 2.5 * x**4 - 0.5 * x**2 + 1 / 42 + w = np.array([1.0, 0.25, 1 / 9, 1 / 16]) + base = lattice_vector_wssd_search(64, 4, w) + self.assertFalse(np.array_equal(base, lattice_vector_wssd_search(64, 4, np.array([1e-6, 0.25, 1 / 9, 1 / 16])))) + self.assertFalse(np.array_equal(base, lattice_vector_wssd_search(64, 4, w, bern6))) + # selection is deterministic and yields a valid generating vector + v = lattice_vector_wssd_search(2**6, 5, kernel=bern6) + npt.assert_array_equal(v, lattice_vector_wssd_search(2**6, 5, kernel=bern6)) + self.assertEqual(v[0], 1) + self.assertEqual(len(np.unique(v)), 5) + self.assertTrue(np.all((v % 2 == 1) & (0 < v) & (v < 2**6))) + + def test_import_conventions(self): + mod = lambda name: inspect.getsource(sys.modules["qmcpy.discrete_distribution." + name]) + # importing qmcpy must not need optional sympy: no top-level `import sympy` + self.assertIsNone(re.search(r"(?m)^(import sympy|from sympy)\b", mod("kronecker.kronecker_search_methods"))) + # the discrepancy code must not use np.vecmat (a NumPy >= 2.2 only API) + self.assertNotIn("np.vecmat", mod("lattice.lattice")) + + def test_kron_disc_wssd(self): + n = 8 + kronecker = Kronecker( + 2, generating_vector="SUZUKI", randomize="SHIFT", shift=[0.1, 0.2] + ) + points = (np.arange(n)[:, None] * kronecker.gen_vec[0]) % 1 + sample_weights = np.arange(1, n + 1) + expected = _direct_disc(points, np.ones(2)) + default_expected = expected + actual = kronecker.periodic_discrepancy(n) ** 2 + self.assertEqual(actual.shape, (n,)) + npt.assert_allclose(actual, expected, rtol=0, atol=5e-15) + npt.assert_allclose( + kronecker.wssd_discrepancy(n, sample_weights), + sample_weights @ expected, + rtol=0, + atol=5e-14, + ) + + coord_weights, kernel = np.array([1.0, 0.25]), (_pkern, 1) + expected = _direct_disc(points, coord_weights) + for actual in ( + kronecker._square_periodic_discrepancies(n, kernel, coord_weights), + kronecker.periodic_discrepancy( + n, k_tilde=kernel, gamma=coord_weights + ) + ** 2, + ): + self.assertEqual(actual.shape, (n,)) + npt.assert_allclose(actual, expected, rtol=0, atol=5e-15) + npt.assert_allclose( + kronecker.wssd_discrepancy( + n, sample_weights=sample_weights, k_tilde=kernel, gamma=coord_weights + ), + sample_weights @ expected, + rtol=0, + atol=5e-14, + ) + + explicit = Kronecker( + 2, replications=1, generating_vector="SUZUKI", + randomize="SHIFT", shift=[0.1, 0.2] + ) + self.assertEqual(explicit.periodic_discrepancy(n).shape, (1, n)) + self.assertEqual(explicit.wssd_discrepancy(n, sample_weights).shape, (1,)) + npt.assert_allclose(explicit.periodic_discrepancy(n) ** 2, default_expected[None], rtol=0, atol=5e-15) + + def test_cbc_mt_fallback(self): + kronecker = Kronecker(3, generating_vector="CBC_MT", randomize=False) + self.assertEqual(kronecker.gen_vec_source, "CBC_MT") + self.assertEqual(kronecker.gen_vec.shape, (1, 3)) + self.assertTrue(np.isfinite(kronecker.gen_vec).all()) + + with self.assertWarnsRegex(RuntimeWarning, "CBC_MT.*dimension <= 100"): + fallback = Kronecker(101, generating_vector="CBC_MT", randomize=False) + self.assertEqual(fallback.gen_vec_source, "RICHTMYER") + self.assertEqual(fallback.gen_vec.shape, (1, 101)) + + def test_kron_search(self): + n = 8 + coord_weights = np.array([1.0, 0.25, 1 / 9]) # == the j^-2 default for d=3 + # coord_weights also accepts a plain list, not only an ndarray + vector, wssd, discrepancies, coefficients = ( + kronecker_vector_search_mobius_transform(n, 3, 3, coord_weights=list(coord_weights)) + ) + self.assertEqual(vector.shape, (3,)) + self.assertEqual(discrepancies.shape, (n,)) + self.assertEqual(coefficients.shape, (2, 4)) + self.assertTrue(np.isfinite(vector).all()) + self.assertTrue(np.isfinite(discrepancies).all()) + self.assertTrue(np.all((0 <= vector) & (vector < 1))) + self.assertGreater(wssd, 0) + npt.assert_allclose( + wssd, np.arange(1, n + 1) @ discrepancies, rtol=0, atol=5e-14 + ) + + points = (np.arange(n)[:, None] * vector) % 1 + npt.assert_allclose( + discrepancies, + _direct_disc(points, coord_weights), + rtol=0, + atol=5e-15, + ) + + vector, wssd, discrepancies, coefficients = ( + kronecker_vector_search_mobius_transform( + n_max=n, + d_max=3, + searchsize=3, + kernel=_bern2, + coord_weights=coord_weights, + gen_vec_init=1.25, + ) + ) + self.assertAlmostEqual(vector[0], 0.25, delta=0.25 * 1e-6) + self.assertEqual(coefficients.shape, (2, 4)) + npt.assert_allclose( + wssd, np.arange(1, n + 1) @ discrepancies, rtol=0, atol=5e-14 + ) + + vector, wssd, discrepancies, coefficients = ( + kronecker_vector_search_mobius_transform( + n_max=n, + d_max=3, + searchsize=3, + kernel=lambda x: 3 * _bern2(x), + coord_weights=coord_weights, + gen_vec_init=1.25, + ) + ) + self.assertGreater(wssd, 0) + + def test_kron_search_1d(self): + # d_max == 1 skips the CBC loop; best_wssd was once left unbound (UnboundLocalError) + n = 16 + vector, wssd, discrepancies, coeff = kronecker_vector_search_mobius_transform(n, 1, 3) + self.assertEqual(vector.shape, (1,)) + self.assertEqual(discrepancies.shape, (n,)) + self.assertEqual(coeff.shape, (0, 4)) + npt.assert_allclose(wssd, np.arange(1, n + 1) @ discrepancies, rtol=0, atol=5e-14) + + def test_kron_search_no_sympy(self): + # the missing-sympy branch must warn (filterable), not print or call input() + with patch.dict(sys.modules, {"sympy": None}), self.assertWarnsRegex(UserWarning, "sympy"): + vector, *_ = kronecker_vector_search_mobius_transform(8, 2, 2) + self.assertEqual(vector.shape, (2,)) + self.assertTrue(np.isfinite(vector).all()) + + def test_kron_search_valid(self): + cases = [ + ({"n_max": 8, "d_max": 2, "searchsize": 1}, "searchsize"), + ({"n_max": 1, "d_max": 2, "searchsize": 2}, "n_max must"), + ({"n_max": 8, "d_max": 0, "searchsize": 2}, "d_max"), + ( + { + "n_max": 8, + "d_max": 3, + "searchsize": 2, + "coord_weights": np.ones(2), + }, + "coord_weights", + ), + ] + for kwargs, message in cases: + # xdist serializes subtest metadata; keep NumPy inputs in kwargs only. + with self.subTest(case=message), self.assertRaisesRegex(ValueError, message): + kronecker_vector_search_mobius_transform(**kwargs) diff --git a/test/test_dd_mpmc.py b/test/test_dd_mpmc.py index d0aec1846..7b5931b69 100644 --- a/test/test_dd_mpmc.py +++ b/test/test_dd_mpmc.py @@ -7,7 +7,6 @@ import pytest torch = pytest.importorskip("torch") -pytest.importorskip("pyg_lib") pytest.importorskip("torch_geometric") from qmcpy import MPMC, MPMC_net, mpmc_utils diff --git a/test/test_ut_install_mpmc_pyg.py b/test/test_ut_install_mpmc_pyg.py index 25e810ec1..96d959bec 100644 --- a/test/test_ut_install_mpmc_pyg.py +++ b/test/test_ut_install_mpmc_pyg.py @@ -1,7 +1,9 @@ """Tests for the platform-specific MPMC dependency installer.""" +import io import subprocess import unittest +from contextlib import redirect_stdout from types import SimpleNamespace from unittest.mock import patch @@ -77,16 +79,38 @@ def missing_torch(_name): with self.assertRaisesRegex(RuntimeError, r"install 'qmcpy\[mpmc\]'"): install_mpmc_pyg.main() - def test_main_reports_missing_wheel(self): - """Exhausting candidate wheel pages reports the build that failed.""" + def test_main_falls_back_to_official_source_release(self): + """A wheel-index outage falls back to PyG's pinned source release.""" + calls = [] + + def fake_run(*args): + calls.append(args) + if install_mpmc_pyg.PYG_LIB_REQUIREMENT in args: + raise subprocess.CalledProcessError(1, args) + + with patch.object(install_mpmc_pyg, "run", fake_run): + install_mpmc_pyg.main(_torch()) + + self.assertEqual(len(calls), 4) + self.assertIn("--no-build-isolation", calls[-1]) + self.assertEqual(calls[-1][-1], install_mpmc_pyg.PYG_LIB_SOURCE) + + def test_main_warns_when_pyg_lib_unavailable(self): + """When every optional accelerator install fails, warn and return.""" def fail_pyg_lib(*args): - if "pyg_lib>=0.6.0" in args: + if ( + install_mpmc_pyg.PYG_LIB_REQUIREMENT in args + or install_mpmc_pyg.PYG_LIB_SOURCE in args + ): raise subprocess.CalledProcessError(1, args) + output = io.StringIO() with patch.object(install_mpmc_pyg, "run", fail_pyg_lib): - with self.assertRaisesRegex(RuntimeError, r"torch 2\.12\.1\+cpu \(cpu\)"): + with redirect_stdout(output): install_mpmc_pyg.main(_torch()) + self.assertIn("could not install the optional pyg_lib", output.getvalue()) + if __name__ == "__main__": unittest.main()