{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Adaptive PDE discretizations on Cartesian grids\n", "## Volume : Non-divergence form PDEs\n", "## Part : Eikonal equation and variants\n", "## Chapter : Eulerian scheme for Riemannian distances" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook presents a single pass Eulerian scheme for the Riemannian eikonal equation. It is provided solely for pedagogical purposes.\n", "Indeed, a state of the art C++ implementation, much more optimized and versatile, is provided in the HFM and AGD libraries, for CPU and GPU processors respectively. These optimized implementations are illustrated in the following [notebook on riemannian distance](../Notebooks_FMM/Riemannian.ipynb) and more generally in the [volume of this repository related to general eikonal equations](../Notebooks_FMM/Summary.ipynb). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The purpose of this notebook is to illustrate the theory and implementation principles underlying the discretization of the (anisotropic) *Riemannian eikonal equation*, and its numerical solution using iterative solvers such as fast sweeping, adaptive Gauss-Siedel iteration, and the fast marching method method. \n", "The PDE to be solved involves a Riemannian metric $M : \\Omega \\to S_d^{++}$ on a domain $\\Omega\\subset R^d$. Denoting by $u : \\Omega \\to R^d$ the unkown, the problem is written\n", "$$\n", " \\| \\nabla u(x)\\|_{D(x)} = 1\n", "$$\n", "for all $x \\in \\Omega$, where $D(x) := M(x)^{-1}$. Dirichlet null boundary conditions are applied on $\\partial \\Omega$. (Or, in some cases, outflow boundary conditions.)\n", "\n", "The solution $u$ should be regarded as the first arrival time of a front originating from the boundary. Minimal paths toward the boundary, also known as minimal geodesics, can be extracted by solving the ODE *backwards in time*:\n", "$$\n", " \\gamma'(t) := V(\\gamma(t))\n", "$$\n", "where $V(x) := D(x) \\nabla u(x)$. This notebook is devoted to solution of the eikonal PDE, rather than the geodesic backtracking ODE, which can be addressed using standard techniques." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Important ! Note on CPU computation speed.** Because of intrinsic limitations of the Python programming language, and in particular due to the slow execution of sequential programs, the fast marching eikonal solver implemented in this notebook is *extremely slow*. In particular and in contrast to reasonable expectations, it is slower than the global iteration method, and than the fast sweeping method, which are also implemented for illustration. The fast marching method is here only implemented for pedagogical purposes. Please use the C++ implementation, or some re-implementation of the Python programs presented below in a compiled language, for any application.\n", "\n", "The GPU eikonal solver presented in the end of this notebook is reasonably fast, in contrast to the CPU solver, although it could be further accelerated by optimizing the memory layout of data. (Note also that GPU eikonal solvers shine best on three dimensional problems, where more parallelims can be extracted.) Above all, it is much less versatile and general than the GPU eikonal solver provided in the AGD library.\n", "\n", "**Note on accuracy.** The eikonal equation solver presented below is extremely basic, and is presented solely for pedagogical purposes. Two optional techniques, implemented in the c++ version allow to increase accuracy: *source factorization*, and the use *second order finite differences* when conditions allow. See the notebook on [high accuracy](../Notebooks_FMM/HighAccuracy.ipynb)\n", "\n", "**Reference.** The numerical scheme implemented in this notebook is described in the following publication:\n", "* Jean-Marie Mirebeau, Jorg Portegies, \"Hamiltonian Fast Marching: A numerical solver for anisotropic and non-holonomic eikonal PDEs\", 2019, IPOL [(link)](https://hal.archives-ouvertes.fr/hal-01778322)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[**Summary**](Summary.ipynb) of volume Non-Divergence form PDEs, this series of notebooks.\n", "\n", "[**Main summary**](../Summary.ipynb) of the Adaptive Grid Discretizations \n", "\tbook of notebooks, including the other volumes.\n", "\n", "# Table of contents\n", " * [1. The update operator](#1.-The-update-operator)\n", " * [2. Iteration policies](#2.-Iteration-policies)\n", " * [2.1 Global iteration](#2.1-Global-iteration)\n", " * [2.2 Fast sweeping](#2.2-Fast-sweeping)\n", " * [2.3 Fast marching](#2.3-Fast-marching)\n", " * [3. Anisotropic metric](#3.-Anisotropic-metric)\n", " * [4. GPU acceleration](#4.-GPU-acceleration)\n", " * [4.1 Update operator](#4.1-Update-operator)\n", " * [4.2 Cuda kernel](#4.2-Cuda-kernel)\n", " * [4.3 Global iteration](#4.3-Global-iteration)\n", " * [4.4 Adaptive Gauss-Siedel iteration](#4.4-Adaptive-Gauss-Siedel-iteration)\n", "\n", "\n", "\n", "**Acknowledgement.** Some of the experiments presented in these notebooks are part of \n", "ongoing research with Ludovic Métivier and Da Chen.\n", "\n", "Copyright Jean-Marie Mirebeau, Centre Borelli, ENS Paris-Saclay, CNRS, University Paris-Saclay" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. Importing the required libraries" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:51.872243Z", "iopub.status.busy": "2024-04-30T08:50:51.870882Z", "iopub.status.idle": "2024-04-30T08:50:51.887011Z", "shell.execute_reply": "2024-04-30T08:50:51.886204Z" } }, "outputs": [], "source": [ "import sys; sys.path.insert(0,\"..\") # Allow import of agd from parent directory (useless if conda package installed)\n", "#from Miscellaneous import TocTools; TocTools.displayTOC('EikonalEulerian','NonDiv')" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:51.890461Z", "iopub.status.busy": "2024-04-30T08:50:51.890197Z", "iopub.status.idle": "2024-04-30T08:50:52.121456Z", "shell.execute_reply": "2024-04-30T08:50:52.121164Z" } }, "outputs": [], "source": [ "from agd import Selling\n", "from agd import LinearParallel as lp\n", "from agd import FiniteDifferences as fd\n", "from agd import AutomaticDifferentiation as ad\n", "from agd.Plotting import imread\n", "norm_infinity = ad.Optimization.norm_infinity" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.123180Z", "iopub.status.busy": "2024-04-30T08:50:52.123055Z", "iopub.status.idle": "2024-04-30T08:50:52.161382Z", "shell.execute_reply": "2024-04-30T08:50:52.161096Z" } }, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "import itertools\n", "import heapq\n", "import scipy.linalg" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. The update operator\n", "\n", "Our discretization of the eikonal equation is based on a decomposition of the inverse tensors to the Riemannian metric, with the following form. For all $x \\in \\Omega$,\n", "$$\n", " D(x) = \\sum_{1 \\leq i \\leq I} \\rho_i(x) e_i e_i^T\n", "$$\n", "where $\\rho_i(x) \\geq 0$ is a non-negative weight. The offset $e_i=e_i(x) \\in Z^d$ may also depend on the current position $x$.\n", "The numerical scheme reads\n", "$$\n", " F u (x) := \\sum_{1 \\leq i \\leq I} \\rho_i(x) \\max \\{0,\\frac{u(x)-u(x-h e_i)} h, \\frac{u(x)-u(x+h e_i)} h\\}^2 - 1.\n", "$$\n", "for all discretization points $x$ in the domain interior. Dirichlet or outflow conditions are applied on the boundary. One has to solve $F u \\equiv 0$.\n", "\n", "**Historical note.**\n", "This numerical scheme was introduced by Rouy in 1992, in the case of an isotropic metric: when $D(x)$ is proportional to the identity matrix. In that special case, the natural tensor decomposition only uses offsets from the canonical basis. It was observed by Sethian in 1996 that it can be solved in a single pass using the fast marching method. \n", "\n", "The extension to arbitrary anisotropic Riemannian metrics, using adequate methods for tensor decomposition, was introduced in :\n", "\n", "- Mirebeau, J.-M. (2019). Riemannian Fast-Marching on Cartesian Grids, Using Voronoi's First Reduction of Quadratic Forms. SIAM Journal on Numerical Analysis, 57(6), 2608–2655.\n", "\n", "\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.163067Z", "iopub.status.busy": "2024-04-30T08:50:52.162973Z", "iopub.status.idle": "2024-04-30T08:50:52.165507Z", "shell.execute_reply": "2024-04-30T08:50:52.165263Z" } }, "outputs": [], "source": [ "def Scheme(u,coefs,offsets,h):\n", " v = np.maximum(0,np.maximum(-fd.DiffUpwind(u, offsets,gridScale=h,padding=np.inf),\n", " -fd.DiffUpwind(u,-offsets,gridScale=h,padding=np.inf)))\n", " residue = (coefs*np.maximum(0,v)**2).sum(axis=0) - 1.\n", " boundary = np.logical_or.reduce(np.isnan(coefs),axis=0)\n", " return np.where(boundary,0.,residue)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In contrast with the other notebooks of this series, we will not use a Newton method to solve this numerical scheme. Instead, we rely on Gauss-Siedel or Jacobi iterations. In other words we solve, pointwise, the equation $Fu(x) = 0$ with respect to the unknown $u(x)$, assuming that $u(y)$ is fixed for all $y \\neq x$. This defines an updated value of $u(x)$, and the process is repeated until convergence, following an order in the domain that is discussed below.\n", "\n", "**Gauss-Siedel update of $u(x)$.** Consider a fixed point $x \\in \\Omega$, and denote, for all $1 \\leq i \\leq I$,\n", "$$\n", " v_i := \\min \\{u(x-h e_i), u(x+h e_i)\\}.\n", "$$\n", "The value of the Gauss-Siedel/Jacobi update is the unique solution $\\lambda = u(x)$ to the equation\n", "$$\n", " \\sum_{1 \\leq i \\leq I} \\rho_i (\\lambda-v_i)_+^2 - h^2 = 0,\n", "$$\n", "where $a_+ := \\max \\{0,a\\}$.\n", "\n", "**Exact solution, obtained by sorting, and solving quadratic polynomials.**\n", "By a monotony argument, there exists a unique solution $\\lambda \\in R$ to the above equation. \n", "Assume that $v_1 \\leq \\cdots \\leq v_I$, up to sorting these quantities, and denote $v_{I+1}=\\infty$ by convention.\n", "Let $1 \\leq J \\leq I$ denote the unique integer such that \n", "$$\n", " \\lambda \\in [v_J,v_{J+1}].\n", "$$\n", "Then $\\lambda$ is the solution to a quadratic equation\n", "$$\n", " \\alpha \\lambda^2 - 2 \\beta \\lambda + \\gamma = 0\n", "$$\n", "where \n", "$$\n", " \\alpha := \\sum_{1 \\leq i \\leq J} \\rho_i, \\quad \\beta := \\sum_{1 \\leq i \\leq J} \\rho_i v_i, \\quad \\gamma := -h^2+\\sum_{i \\leq J} \\rho_i v_i^2.\n", "$$\n", "\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.166930Z", "iopub.status.busy": "2024-04-30T08:50:52.166853Z", "iopub.status.idle": "2024-04-30T08:50:52.172097Z", "shell.execute_reply": "2024-04-30T08:50:52.171837Z" } }, "outputs": [], "source": [ "def gtr_silentNaN(a,b):\n", " \"\"\"Compare two values without emitting a warning for NaNs\"\"\"\n", " import warnings\n", " with warnings.catch_warnings():\n", " warnings.simplefilter(\"ignore\")\n", " return a>b\n", " \n", "def Update(u,coefs,offsets,h,where=(Ellipsis,)):\n", " \"\"\"Local Gauss-Siedel update for the fast marching algorithm\"\"\"\n", " #Get the values at the neighbors, and sort them\n", " v = np.minimum(fd.TakeAtOffset(u, offsets,padding=np.inf,where=where),\n", " fd.TakeAtOffset(u,-offsets,padding=np.inf,where=where))\n", " \n", " u,coefs=u[where],coefs[(slice(None),)+where] # Akin to coefs[:,where]\n", " \n", " v[coefs==0.] = np.inf; \n", " ai=v.argsort(axis=0)\n", " v,coefs = (np.take_along_axis(a,ai,axis=0) for a in (v,coefs))\n", " \n", " # Initialize the variables\n", " result= np.zeros_like(u)\n", " result[v[0]==np.inf]=np.inf # Far points\n", " boundary = np.logical_or.reduce(np.isnan(coefs),axis=0)\n", " result[boundary]=u[boundary] # Apply bc\n", " considered = np.logical_and(v[0]0.\n", "\n", " solution = (β+sδ)/α\n", " considered = np.logical_and(considered,gtr_silentNaN(solution,vi)) #solution>vi\n", " result[considered]= v[0,considered]+solution[considered]\n", " \n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We next define the some problem parameters, in order to test the scheme.\n", "As a start, we use an isotropic metric on the square, with a single seed point in the center." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.173552Z", "iopub.status.busy": "2024-04-30T08:50:52.173453Z", "iopub.status.idle": "2024-04-30T08:50:52.232522Z", "shell.execute_reply": "2024-04-30T08:50:52.232275Z" } }, "outputs": [], "source": [ "#Define the square [-1,1]^2, sampled on a cartesian grid\n", "aX0 = np.linspace(-1,1,51); aX1 = aX0\n", "gridScale=aX0[1]-aX0[0]\n", "X0,X1 = np.meshgrid(aX0,aX1,indexing='ij')\n", "\n", "# Define the domain, and the problem parameters\n", "metric = fd.as_field(np.eye(2),X0.shape)\n", "bc=np.full(X0.shape,np.nan)\n", "bc[X0.shape[0]//2,X0.shape[1]//2] = 0\n", "\n", "# Decompose the tensors dual to the Riemannian metric\n", "coefs, offsets = Selling.Decomposition(lp.inverse(metric))\n", "coefs[:,np.logical_not(np.isnan(bc))] = np.nan" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.234089Z", "iopub.status.busy": "2024-04-30T08:50:52.234007Z", "iopub.status.idle": "2024-04-30T08:50:52.235616Z", "shell.execute_reply": "2024-04-30T08:50:52.235353Z" } }, "outputs": [], "source": [ "#u0=np.where(np.isnan(bc),np.inf,bc)\n", "#Update(u0,coefs,offsets,gridScale)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Iteration policies\n", "\n", "We present three classical iteration policies, applied to the Gauss-Siedel update implemented in the previous section. The justification of their convergence is based on mathematical properties, referred to as *Monotony* and *Causality*.\n", "\n", "**Monotony.** Denote by $\\Lambda$ the Gauss-Siedel update operator implemented above. This operator obeys a property known as *monotony*: for any discrete maps $u,v$\n", "$$\n", " u\\leq v \\Rightarrow \\Lambda u \\leq \\Lambda v,\n", "$$\n", "where $u\\leq v$ means that $u(x) \\leq v(x)$ for all points $x\\in X$ of the discretization domain $X$.\n", "Monotony is inherited from another property of the numerical scheme $F$, w.r.t. which the the Gauss-Siedel update is defined, known as *degenerate ellipticity*: $Fu(x)$ is a non-decreasing function of the finite differences $u(x)-u(y)$, $y \\neq x$.\n", "\n", "**Iterative methods**\n", "Thanks to monotony, and under mild additional technical assumptions, the repeated iteration of the updates over the domain defines a sequence converging to the numerical scheme solution. The updates may be applied simultaneously to all points of the domain, or one by one, or in groups... as in the following variants.\n", "* *Global iteration, also known as Jacobi iteration.* Set $u(x) \\gets \\Lambda u(x)$ simultaneously for all $x\\in X$.\n", "* *Fast sweeping.* Set $u(x) \\gets \\Lambda u(x)$ simultaneously for all $x$ in a slice of the domain. Iterate slice by slice: left to right, right to left, top to bottom, bottom to top, and then repeat.\n", "* *Adaptive Gauss-Siedel Iteration.* Define an arbitrary ordering sequence, possibly using to a queue with adequate insertion rules. (See the GPU eikonal solver in the end of this notebook.)\n", "\n", "Iterations are stopped when a stopping criterion is met. If the initialization $u_0$ equals $+\\infty$ in the domain interior, or is a sufficiently large value, then one easily shows using monotonicity that the successive iterates obey $u_{n+1} \\leq u_n$. A typical stopping criterion is then \n", "$$\n", " u_{n+1} \\geq u_n+ \\epsilon,\n", "$$\n", "where $\\epsilon>0$ is a given tolerance.\n", "\n", "**Causality.** The Gauss-Siedel update operator corresponding to our numerical scheme benefits from an additional property, known as causality: informally, for any discrete map $u$ \n", "$$\n", " \\Lambda u(x) \\text{ may depend on } u(y) \\text{ only if } u(y) < \\Lambda u(x).\n", "$$\n", "Causality is inherited from another property of the numerical scheme $F$, also referred to as causality: $Fu(x)$ only depends on the non-negative part of the finite differences $u(x)-u(y)$, $y\\neq x$.\n", "Note that, in addition, $u(x)$ may depend on $u(y)$ only if $y$ appears in the stencil of $x$, in other words $y = x\\pm h e_i$ for some $1 \\leq i \\leq I$.\n", "\n", "**Single pass solution.**\n", "Thanks to monotony and causality, and under mild additional technical assumptions, the system of equations discretizing the PDE can be solved in a *single pass* over the domain, by visiting each point a finite number of times that is prescribed in advance. For that purpose, a variant of Dijkstra's shortest path algorithm is used, known as the fast marching method.\n", "\n", "In contrast with the iterative methods, the fast marching does not require a stopping criterion since it naturally stops after a finite number of steps." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.1 Global iteration\n", "\n", "This procedure, also known as Jacobi iteration, simultaneously updates the unknown function $u$ over all the domain. " ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.236977Z", "iopub.status.busy": "2024-04-30T08:50:52.236905Z", "iopub.status.idle": "2024-04-30T08:50:52.239036Z", "shell.execute_reply": "2024-04-30T08:50:52.238797Z" } }, "outputs": [], "source": [ "def GlobalIteration(update,u0,args,eps=1e-6,niter_max=200):\n", " u=u0.copy()\n", " for niter in range(niter_max):\n", " u,u_old = update(u,*args),u\n", " if np.all(u+eps>=u_old):\n", " return u,niter\n", " print(\"Iterative method did not reach stopping criterion within iteration budget\")\n", " return u,niter_max" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.240314Z", "iopub.status.busy": "2024-04-30T08:50:52.240235Z", "iopub.status.idle": "2024-04-30T08:50:52.270346Z", "shell.execute_reply": "2024-04-30T08:50:52.270086Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 27.9 ms, sys: 195 µs, total: 28 ms\n", "Wall time: 28.2 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution,nupdate = GlobalIteration(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The number of updates per point `niter` is quite large with this approach, since the front progresses only by one pixel at a time. \n", "Therefore `niter` is not far from the domain diameter, measured in pixels.\n", "More precisely, `niter` is the length of the longest minimal path in the domain, measured in pixels. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.271911Z", "iopub.status.busy": "2024-04-30T08:50:52.271829Z", "iopub.status.idle": "2024-04-30T08:50:52.273576Z", "shell.execute_reply": "2024-04-30T08:50:52.273343Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Average number of updates per point : 50\n" ] } ], "source": [ "print(\"Average number of updates per point :\", nupdate)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.274895Z", "iopub.status.busy": "2024-04-30T08:50:52.274814Z", "iopub.status.idle": "2024-04-30T08:50:52.339694Z", "shell.execute_reply": "2024-04-30T08:50:52.339444Z" } }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGiCAYAAADqYLxOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABDAklEQVR4nO3de3RU1b0H8O8IZCKWjEggCZcQ0KvhqRdCIYkXiwUDWHy2EoqNcBempasWA3KvRrQCa9kUr1ofiK+CVKVAr4Fqr0gJystFQB4JvgCpAgmakYcwE2xNEM79g5uRYSbJzGTOnP3b+/tZa9aCkzOTfc6c/dvfs8+ZicuyLAtEREREGrnA6QYQERERxRsDDhEREWmHAYeIiIi0w4BDRERE2mHAISIiIu0w4BAREZF2GHCIiIhIOww4REREpB0GHCIiItIOAw4RERFpx9aAs3HjRtxwww3o3r07XC4X/vKXv7T6nA0bNiAnJwfJycm49NJL8dxzz4WsU15ejn79+sHtdqNfv35YuXKlDa0nIiIiqWwNOF9//TWuuuoqzJ8/P6L19+/fj+uvvx7Dhw9HVVUV7r//fkybNg3l5eWBdSorK1FYWIiioiLs2rULRUVFGD9+PLZu3WrXZhAREZEwrkT9sU2Xy4WVK1fi5ptvbnade++9F2+88QZ2794dWDZ16lTs2rULlZWVAIDCwkL4/X689dZbgXXGjBmDzp07Y+nSpba1n4iIiORo73QDzlVZWYmCgoKgZaNHj8bChQtx6tQpdOjQAZWVlZg+fXrIOk888USzr9vQ0ICGhobA/8+cOYOvvvoKXbp0gcvlius2EBERkT0sy0J9fT26d++OCy5o+SKUUgHH6/UiLS0taFlaWhq+/fZbHD16FBkZGc2u4/V6m33dsrIyzJkzx5Y2ExERUWLV1taiR48eLa6jVMABEDKj0nQF7dzl4dZpaSamtLQUM2bMCPzf5/OhZ8+eqK2tRUpKSjyaLcq+Q1fE/Ny/1F8Vx5aobd2Xse8niq+aL1Jt/x09ux+1/XdQZK5N+8TpJiTMzZ12xfzcy3uYs5+a+P1+ZGZmolOnTq2uq1TASU9PD5mJOXz4MNq3b48uXbq0uM75szrncrvdcLvdIctTUlKMDDjf6xTbveWv+Qcj+XtxboyCKrx9AADtL3K4IQY6cKhr2OUXXGj/7z50PPzZYK8eR+z/5RRk08mBuC59j9PNSIjV1hD8JGVnTM81cfxqEsntJUoFnLy8PPz1r38NWrZmzRoMGTIEHTp0CKxTUVERdB/OmjVrkJ+fn9C2muY1/2Cnm5AQTeGG7NVckFFRuLYy9NivqS+aEHRe8w+OOeRQ82wNOCdPnsTf//73wP/379+P6upqXHLJJejZsydKS0vx+eef4+WXXwZw9hNT8+fPx4wZM1BcXIzKykosXLgw6NNRd999N6655hrMmzcPN910E15//XWsXbsW7777rp2bYjQTwg2Djb0kBZpIMPQkToW3D0MOxcTWj4mvX78e1157bcjySZMmYfHixZg8eTIOHDiA9evXB362YcMGTJ8+HR999BG6d++Oe++9F1OnTg16/muvvYYHHngAn332GS677DI8/PDDuPXWWyNul9/vh8fjgc/nM3KKb29t94jXZbihWOgWaGLBwBN/JgSdaEJOduYXNrZETdGM3wn7HhyVMOBEFnB0DzcMNvHDQNMyhp34Ycj5DgNOy+O3UvfgkDoYbqg1DDWRO39fMfDEzoR7c3i5Kj74xzYpBMMNNefAoa6BB8WO+7HtdO/HutfhROAMDgXRuVPpXhDtwkHYXufuX87sREf3G5A5k9M2DDgUwHBDTRhqnMGwEz3dL1kx5MSOl6gIAMMN8bKJavh+REfnfq5zfbYTAw5p23kqvH20LnrxwkFUfXyPIqNzf9e1TtuJl6gMp2un0bnQxQMHS5l4Cat1Ol+y4uWq6HAGx2AMN+bhTIA++F62TNc6oGvdtgNncAykcwfRtai1BQdBvTW9v5zRCaXrp6yaavgsh9uhOgYc0gKDTSgGG7Pw8lV4uoYcah0DDonHcBOMwYY4qxNM5/tyqHm8B4dEY7j5Du/JoPPxmAjGemEWBhwSi8XqLA5i1BoeI99h3TAHAw6JxCLFQYuix2PmLNYPM/AeHBKFhYn32FDb8R4d3nxsAs7gkBimhxuefVO8mX5MmV5TdMcZHBLB5EJk8gBEiWHyjA4/YaUvzuCQ8kwNN6afXVPimXy8mVpndMaAQ0ozsegw2JCTTD7+TKw3OmPAIWWZWGxMHVhIPaYGHRPrjq4YcEhJphUZUwcTUp+Jx6Zp9UdXDDikHJOKi4mDB8lk2nFqUh3SFQMOKcWkomLagEHymRbITapHOmLAIWWYUkxMGyRIPyYdv6bUJR0x4JASTCkiJg0MpDeTgrop9Uk3DDjkOBOKh0mDAZnFlOPahDqlGwYccpQJRcOUAYDMZUqAN6Fe6YQBhxyje7EwpegTNTHheNe9bumEAYccoXuRMKHQE4VjQrDXvX7pggGHEk7n4mBCcSeKhO79QOc6pgv+NXFKKJ2Lgu4FXXXumqRmf9bQszGBLaEmuv+V8gpvH/4VcoUx4FDCMNxQW7UUYmJ9HsOP/Q4c6sqQQwnHgEMJoWu4YbCxR6xBJp6/i8EnvhhyKNEYcMh2DDfUkkSGmWic3y4GnrbT+ZIVQ456eJMx2YrhhsJx1yQFHlKc22ZJ7VaRrv1H13onVUICzoIFC9C7d28kJycjJycHmzZtanbdyZMnw+VyhTz69+8fWGfx4sVh1/nmm28SsTkUIR07Oz8lFRsdw4GO25RIuvYjHeueVLYHnOXLl6OkpASzZs1CVVUVhg8fjrFjx6Kmpibs+k8++STq6uoCj9raWlxyySW47bbbgtZLSUkJWq+urg7Jycl2bw5FSMdOrmtBtotpg79p2xsPup4w6Fj/JLI94Dz++OOYMmUK7rzzTvTt2xdPPPEEMjMz8eyzz4Zd3+PxID09PfDYvn07jh8/jv/4j/8IWs/lcgWtl56ebvemUIR07Nw6FmE7cJA/i/shOjr2Lx3roDS2BpzGxkbs2LEDBQUFQcsLCgqwefPmiF5j4cKFGDVqFLKysoKWnzx5EllZWejRowfGjRuHqqqqZl+joaEBfr8/6EH20LFT61h844mDecu4fyLDfkbxZmvAOXr0KE6fPo20tLSg5WlpafB6va0+v66uDm+99RbuvPPOoOV9+vTB4sWL8cYbb2Dp0qVITk7G1VdfjX379oV9nbKyMng8nsAjMzMz9o2iZukWbnSdPo8HDtqx4T5rmW79TbeaKE1CbjJ2uVxB/7csK2RZOIsXL8bFF1+Mm2++OWh5bm4ufvazn+Gqq67C8OHD8ec//xlXXHEFnn766bCvU1paCp/PF3jU1tbGvC1kBt0KbbxwgI4PBsTm6db3GHKcY+v34KSmpqJdu3YhszWHDx8OmdU5n2VZWLRoEYqKipCU1HIRuOCCC/D973+/2Rkct9sNt9sdXeMpKjp1Yt0KbFtxELZX0/7l9+x8R7fvy+F35DjD1hmcpKQk5OTkoKKiImh5RUUF8vPzW3zuhg0b8Pe//x1Tpkxp9fdYloXq6mpkZGS0qb0UG4YbPXGGIbE4qxNKp/6oU52UwvZLVDNmzMAf/vAHLFq0CLt378b06dNRU1ODqVOnAjh7+eiOO+4Ied7ChQsxbNgwDBgwIORnc+bMwd/+9jd89tlnqK6uxpQpU1BdXR14TUocnTqtTsW0LTjIOo/vwXd06pc61UsJbP9TDYWFhTh27Bjmzp2Luro6DBgwAKtWrQp8Kqquri7kO3F8Ph/Ky8vx5JNPhn3NEydO4Oc//zm8Xi88Hg8GDRqEjRs3YujQoXZvDp1Dp86qUxGNBQdTNfHy1Vk6/R0rXq5KHJdlWZbTjUg0v98Pj8cDn8+HlJQUp5uTcA9/NK7Nr8Fwow+GGzlMDzq6hJx4BZxZ/f83Lq8jSTTjN/8WFRnL9I+B8zKIPKa/Z7r0V51OEFXGgENR06Fz6lIoY2H6IKkDk99DXfquDnVUdQw4FBUdOqUuBTJaJg+KujL1PdWlD+tQT1XGgEMR06Ez6lIYo2HqIGgSE99jXfqyDnVVVQw4FBEdOqEuBTFSJg56pjPtPTetT1N0GHDICCYVQtMGOQpl0vuvQ9/W4QRSRQw41CrpnU+HAhgpkwY2aplJQVeHT0RKr7MqYsChFknvdNKLXqRMGswoOjw25JBeb1XDgEPNkt7ZTAo3RK0x4Tgxpc9TZBhwSEsmFDqemVO0TDhmpPd96SeWKmHAobAkdzLpBa41JgxSZC/djx/pNUBy/VUJAw6FkNy5pBe21ug+MFHi6B6UpdcCyXVYFQw4FERyp5Je0Fqj82BEztH5uNK9JlDLGHCIFKf7mTY5T+djTHLIkXzCqQIGHAqQ3JkkF7GW6DrokJp0Pd4k1wfJddlpDDgEQHYnkly8mqPzGTWpTdfjTsc6QS1jwCHRdCxaug4wJIeuAVtqvZB8AuokBhwS23mkFquW6DiokFw6Ho9S64bUOu0kBhzDSe00UotUS3QcTEg+HY9LHesHhWLAIXF0K066Xg4gffD4VIPUE1KnMOAYTGJn0THcEEmgWxCXWksk1m2ntHe6AeQMdhLn6TRYqKLTQStkWX2Wy4GW6Mtdk4SGno1ONyMuDhzqil49jjjdDLIJA46BpIYbqWdc4TDcxC5ciIllfQaf2DHkOKuphs/q73BDFMeAQyLoEm4YbKIXbaBpy+sy9ESOIYdUx4BDymO4MYtdgSaW383A0zKGHFIZAw4pjeFGf04Gmtac2zaGnfB0CjmkFwYcIpsx3IRSOdQ0h2GneU3HuPSgw1kcvfBj4qQsHWZvGG6CdTpoiQw352vaDh22JZ50ON51qDt0FmdwSEk6FBkdin086B4COLMTTIdLVpzJ0QMDDimH4UYPugebcJq22fSgo0PIIfl4iYoozkwON7x0cxb3gfx+oMOJlukYcEgp0ouK9KIeKw7o4Zke+KT3B+n1yHQMOKQM6cVEejGPhcmDd7RM3VfS+4X0umQy3oNDSpBeRKQX8WiZOFDHi4n36fCeHHICZ3CI2sikcGPqLIQdTNuXkvuJ9BMwUyUk4CxYsAC9e/dGcnIycnJysGnTpmbXXb9+PVwuV8hjz549QeuVl5ejX79+cLvd6NevH1auXGn3ZpBNJBcPyUU7GqYNxolk0n6V3F8k1ylT2R5wli9fjpKSEsyaNQtVVVUYPnw4xo4di5qamhaft3fvXtTV1QUel19+eeBnlZWVKCwsRFFREXbt2oWioiKMHz8eW7dutXtzKM4kFw3JxToaJg3ATjEpQJrSb8h5LsuybO1Vw4YNw+DBg/Hss88GlvXt2xc333wzysrKQtZfv349rr32Whw/fhwXX3xx2NcsLCyE3+/HW2+9FVg2ZswYdO7cGUuXLm21TX6/Hx6PBz6fDykpKdFvlHAj3p7pdBMCpAYcE4q0KQOuiky4P0fqPTkqfQHg+pGPOt2EhItm/LZ1BqexsRE7duxAQUFB0PKCggJs3ry5xecOGjQIGRkZGDlyJNatWxf0s8rKypDXHD16dLOv2dDQAL/fH/Qg5zHcqMmk2QRVmbD/pfYjqXXLRLYGnKNHj+L06dNIS0sLWp6Wlgav1xv2ORkZGXjhhRdQXl6OFStWIDs7GyNHjsTGjRsD63i93qhes6ysDB6PJ/DIzMxs45ZRW0ktElKLcqRMGFilYNAkapuEfEzc5QqebrUsK2RZk+zsbGRnZwf+n5eXh9raWjz66KO45pprYnrN0tJSzJgxI/B/v9/PkENR0znccCBVl84fK5f68XH+rSoZbJ3BSU1NRbt27UJmVg4fPhwyA9OS3Nxc7Nu3L/D/9PT0qF7T7XYjJSUl6EHOkTh7w3BDTtP1fZLatyTWMdPYGnCSkpKQk5ODioqKoOUVFRXIz8+P+HWqqqqQkZER+H9eXl7Ia65Zsyaq1yRnsCiog5dA5NH1PZMackhttl+imjFjBoqKijBkyBDk5eXhhRdeQE1NDaZOnQrg7OWjzz//HC+//DIA4IknnkCvXr3Qv39/NDY24tVXX0V5eTnKy8sDr3n33Xfjmmuuwbx583DTTTfh9ddfx9q1a/Huu+/avTlkIB2Lr46DpEk6HbS0u2Ql8XIVL1WpzfaAU1hYiGPHjmHu3Lmoq6vDgAEDsGrVKmRlZQEA6urqgr4Tp7GxETNnzsTnn3+OCy+8EP3798ebb76J66+/PrBOfn4+li1bhgceeAAPPvggLrvsMixfvhzDhg2ze3OoDSTO3ugWbhhs9MGQowaGHHXZ/j04KuL34CT+e3AYbpzHcKMv3YKOtJDjVMDh9+A4+D04RFIx3JAkfH+dJfEEzgQMOGQ7aZ1fp3Cj602pFEqn91mnPkjOYcAh0pROAx5FRqf3XFrIkXYiZwIGHLKVtE4vrag2R6eBjqKj06ydtP4ord7pjgGHbCOts0srps3RZXCjttHlONClX1LiMeAQQZ8iqsugRvHB4yHxpJ3Y6YwBh2zBTp54HMwoHB2OC11OQCixGHDIeNKLp073XJA9dDg+JPVTnuCpgQGH4k5S55ZUNMPRYeCixOCxQqZhwCESigMWRUv6MSPphETSiZ6uGHAoriR1aknF8nzSBypyjvRjR1K/lVQPdcSAQ0aSVCTPJ32AIudJv29Lcv+lxGHAobiRcrYiuThKHpQSwfNpQ9CDWsbjyX5S6qKO2jvdACKKDAejYJEEmHDr+C5z29EcSjB3TZK4vzpOicWAQ3Eh5SxF6uwNw81Z8ZiVOf81TA88nQ5aqM9yOd2MmEgJOQcOdUWvHkecboZxGHCIFGd6uLH7UtO5r29q2JEccoiaw4BDbcbZG/uYGm6cun/G5LAjNeRwFoeaw4BDRpAYbkyk0o3BTW0xKehIDTlE4fBTVNQmUmZvJDJl9kb1Tz2p3DY7SDzupJzAsF4mFmdwSHtSit+5JA4y0ZIWGky6fCVxJkfKpSpKHM7gUMwknI0w3KhHhxkR6e2PhO7HoVMk1E1dMOAQKUTnQUWHYHMu3bYnHGnHo8QTGrIPAw7FRMJZiLRiJ20wiYbOQcCEoEMkEQMOkQJ0DTcmDf66bqu0Y1PCiY2EE0QdMOBQ1CR0TglFTme6DvaR0HG7GXJIIgYcIodJGzxao+MAHy0dA55ux6nTJJwoSseAQ1GR0Cklnb3pNGjoOKi3lW77Q9LxKqkOkD0YcEgrkoqapMGiNboN5PHE4EfNkXDCKBkDDhG1CQfvyOiynyQFc0knPBR/DDgUMdXPNiQVM0mDRHM4MxE9XfaXDscv6Y8BhyjBdBgcdBmoncBgmFiqn/iofuIoGQMORUT1Tqh6EdMJB+f4kL4fdQjqpDcGHKIEkj4oSB+UVSN9f0o5nlU/AVL9BFIqBhwST/Xi1UTKYBAOL6vYR/p+lXxck94YcKhVPLswm/QBWALuY/tJORGi+GHAIUoAqWe5HHgTR/K+lnp8q4QnkvGXkICzYMEC9O7dG8nJycjJycGmTZuaXXfFihW47rrr0LVrV6SkpCAvLw9/+9vfgtZZvHgxXC5XyOObb76xe1NIMRLOyqQWf8kDrlTc5/aSUC8ofmwPOMuXL0dJSQlmzZqFqqoqDB8+HGPHjkVNTU3Y9Tdu3IjrrrsOq1atwo4dO3DttdfihhtuQFVVVdB6KSkpqKurC3okJyfbvTnG4VmFmTjQOkfqvpca5ElfLsuybD0qhw0bhsGDB+PZZ58NLOvbty9uvvlmlJWVRfQa/fv3R2FhIX7zm98AODuDU1JSghMnTkT0/IaGBjQ0fFc0/H4/MjMz4fP5kJKSEvnGaGLE2zMjXlflgCPhbExi0Zc6wOrGd5nb6SbEpD7L5XQTWtXQs9HpJjSrV48jEa+7fuSjNrZETX6/Hx6PJ6Lx29YZnMbGRuzYsQMFBQVBywsKCrB58+aIXuPMmTOor6/HJZdcErT85MmTyMrKQo8ePTBu3LiQGZ5zlZWVwePxBB6ZmZnRb4yBVA43EjDcUFvwvSBqG1sDztGjR3H69GmkpaUFLU9LS4PX643oNR577DF8/fXXGD9+fGBZnz59sHjxYrzxxhtYunQpkpOTcfXVV2Pfvn1hX6O0tBQ+ny/wqK2tjX2jSAkSZm+k4YCqHonvicRgrxKeWMZP+0T8EpcreMrSsqyQZeEsXboUs2fPxuuvv45u3boFlufm5iI3Nzfw/6uvvhqDBw/G008/jaeeeirkddxuN9xumdO9JJO0Ii9xIDWF59MGsZerVOWuSVL6MhXFh60zOKmpqWjXrl3IbM3hw4dDZnXOt3z5ckyZMgV//vOfMWrUqBbXveCCC/D973+/2Rkcip7KZxGcvSHTSAug0gI+6cnWgJOUlIScnBxUVFQELa+oqEB+fn6zz1u6dCkmT56MP/3pT/jRj37U6u+xLAvV1dXIyMhoc5uJ2kpacZc2eJqK71N8qXyipPIJpiS2X6KaMWMGioqKMGTIEOTl5eGFF15ATU0Npk6dCuDs/TGff/45Xn75ZQBnw80dd9yBJ598Erm5uYHZnwsvvBAejwcAMGfOHOTm5uLyyy+H3+/HU089herqajzzzDN2bw6RVjhoyiLpclWng5aIT1SRvmwPOIWFhTh27Bjmzp2Luro6DBgwAKtWrUJWVhYAoK6uLug7cZ5//nl8++23+NWvfoVf/epXgeWTJk3C4sWLAQAnTpzAz3/+c3i9Xng8HgwaNAgbN27E0KFD7d4ccpjKZ12ArNkbhhuZJIUcIifZ/j04Kormc/Q6au17cFSeHmXAiQ+GG9kkBRzVZ3FUvdk4ku/D4ffgOPg9OEQmkRJuSD4GVP2pfKIpBQMOiaH67I0UHBz1IOV9VD34s67oiwGHgvCsITaqF/EmUgbFtkjacwhJew453YyEMOH9JIpVQr7oj6iteJbVdjoPhuECzbnLGvv0SGRz6Dz8RFVsDhzqGtXfpqJgDDhEbSRl9kY30czS6Bx2+KmqtuM3G+uJl6iIDKDb7E1bLkHpeAlLwvvLEwFKNM7gUICq99+ofHlKQtGWMPhFKp7BpOm1dJvRIaKzOINDRMqzc9ZFlxkdCUFW5RMCVU+kVD3xlIAzOEQakzDotSSRwUOHGR3ej0P0Hc7gEACeJcRC5bNRgOFG2u81her9hvTBgENKU3XamOzldMhw+ve3hfRgSxQvDDhEMVD9LFTqIKfS/TCqtIMSR9UTKs6wx4YBh4iUoGKgUClwRUP1gKv6CQLpgQGHlKXq2ZTqVB/cwlE9RKjePiIKxYBDnP6MEs8+40tKeJDSziYSg64KeGKlDwYcIo1IG9SkhQZp7VUZTxSiwxPR6DHgEJEjpIYFSe2WFniJ4okBh5Sk6jSxymedkgYzSSEhHOntJzIBAw4RJZQu4UDKdqgcfFU9YVD1BIuiw4BjOF7X1YPKg9i5pISCSOm2PUQ6YcAhooRgGHCOlABMFE8MOEQRUnU6XQKdw43O20Zq4Yx7dBhwSDm8/h0dnp07jyEndjxxILsw4BCRrUwZ/FXfTgbh6PBESz4GHKIIqHqWqfqgpfqgT0T6YsAxGK/nEsWX6oFO9UBMFE8MOERkC9UHe7uYut1toeoMKcnGgENK4XVvIqLmceY9cgw4REKpfLnB9FkM07efSAUMOESt4PQ56UTlYKwazijLxoBDRHHF2YuzuB+iwxMJijcGHCKBVD0L56BORKpgwCEisomqgU/VgEwUTww4huKd+EREpDMGHFIGb+iTTdXZCiIyU0ICzoIFC9C7d28kJycjJycHmzZtanH9DRs2ICcnB8nJybj00kvx3HPPhaxTXl6Ofv36we12o1+/fli5cqVdzSeD8cZHaisGPyJn2B5wli9fjpKSEsyaNQtVVVUYPnw4xo4di5qamrDr79+/H9dffz2GDx+Oqqoq3H///Zg2bRrKy8sD61RWVqKwsBBFRUXYtWsXioqKMH78eGzdutXuzSFyHO+fICJqncuyLFtPUYcNG4bBgwfj2WefDSzr27cvbr75ZpSVlYWsf++99+KNN97A7t27A8umTp2KXbt2obKyEgBQWFgIv9+Pt956K7DOmDFj0LlzZyxdujTkNRsaGtDQ8N2g4Pf7kZmZCZ/Ph5SUlLhspyQj3p6p5D04Kl6iUnEGR8WAw1mKljX26eF0E0L4LnM73YSw6rNcTjchREPPRqebEKJXjyNYP/JRp5uRcH6/Hx6PJ6Lx29YZnMbGRuzYsQMFBQVBywsKCrB58+awz6msrAxZf/To0di+fTtOnTrV4jrNvWZZWRk8Hk/gkZmZGesmERFFTcUAqGJQJoonWwPO0aNHcfr0aaSlpQUtT0tLg9frDfscr9cbdv1vv/0WR48ebXGd5l6ztLQUPp8v8KitrY11k4iIosYZHKLEa5+IX+JyBU85WpYVsqy19c9fHs1rut1uuN3szER2aezTQ8lZCpKFl6ci06vHEaebIIKtMzipqalo165dyMzK4cOHQ2ZgmqSnp4ddv3379ujSpUuL6zT3mkQ64Zk3EVHrbA04SUlJyMnJQUVFRdDyiooK5Ofnh31OXl5eyPpr1qzBkCFD0KFDhxbXae41iWKl4hklyaLi5SkiE9j+MfEZM2bgD3/4AxYtWoTdu3dj+vTpqKmpwdSpUwGcvT/mjjvuCKw/depUHDx4EDNmzMDu3buxaNEiLFy4EDNnzgysc/fdd2PNmjWYN28e9uzZg3nz5mHt2rUoKSmxe3PIRipOBVPkOJATkUpsvwensLAQx44dw9y5c1FXV4cBAwZg1apVyMrKAgDU1dUFfSdO7969sWrVKkyfPh3PPPMMunfvjqeeego//vGPA+vk5+dj2bJleOCBB/Dggw/isssuw/LlyzFs2DC7N0cbvXocUfKj4kRERPFg+/fgqCiaz9HraMTbZ2fDVAw4/C6cyKj6EV/eaBxM1VktVe/jUvGSsIozy003GfN7cBz8HhwisoeqA5SqAzoRmYcBh4jiiiHnLO6H6Kg4e0OyMeAQtYKFl3Si6uyfilS8PEWRY8AhEkrlgcr02QvTt59IBQw4pBSeMRERNY/fYhw5BhwisoWpsximbndb8DIw2YEBx2A8EyCKL9XDjcqXNYnijQGHKAKqnmGqPmCpPuATkb4YcIjIVqaEHNW3U/UwrBreDygfAw4ph4UlOhIGLtUH/7bSffvspOrsKMnHgEMUIRZiCofhhhKF901GhwGHSAOcxaGWSDg+iOKNAcdwPCOgRNIt5Oi2PUQ6YcAhooTSJRRI2Q6VZ29UvezL+wD1wIBDSlK1wKhakAG1B7LzSQkHzZHefiITMOAQkSOkhgRJ7ZYUeonijQGHSCPSBjRJYQGQ116VqTwbqiLeLxk9Bhxix4kSC3N8SQkNUtrZRFrYVYWql8cpegw4pCwWmthIHNhUDw+qt4+IQjHgEJESVAwRjX16KNmu1qgecjkLSonAgEMUA9ULtOoDXHNUChSqtIMSR9VZY95GEBsGHFKaqgVHAqkhB3A+XDj9+9tC9fdd9ZMD0gcDDgHgGUIsWKjt5UTIUGkGiYjahgGHSGOqn823JpGBQ4dgI/39Joqn9k43gEiy+iwXOh20nG6G9prCR9KeQ7a9tnQSwo3Ks56qXg7n7HrsOINDAap2JFULjxQSBr5IxXNGh5ejiPTGGRwiA/guc8PzaYPTzYibtszo6BhqJIRYlWdvSE8MOERtxMtUzjk3rLQUdnQMNU0khBvVcZZYTww4JEJDz0a4a5KcboZous3inO/8sKNzqJGGszexUfW2ASl4Dw4FYYeKjZQCbsrZvinhxpT3kygWDDgkBqeR44ODoh6kvI+qh3/WFX0x4BDFieqFnPQhJdxQ7Dib3nYMOBSCHUt/HCDlkvTeqR76OXujNwYcEkX1gqR6QT+XpIGSiChaDDhEBmPIkUXS+yUp7JOebA04x48fR1FRETweDzweD4qKinDixIlm1z916hTuvfdeDBw4EBdddBG6d++OO+64A1988UXQeiNGjIDL5Qp6TJgwwc5NIYqYtMIuadA0Gd+n+FJ5Npi3CcSHrQFn4sSJqK6uxurVq7F69WpUV1ejqKio2fX/8Y9/YOfOnXjwwQexc+dOrFixAp988gluvPHGkHWLi4tRV1cXeDz//PN2bopxVO5gKhcmIjtICzfSQj7pybYv+tu9ezdWr16NLVu2YNiwYQCAF198EXl5edi7dy+ys7NDnuPxeFBRURG07Omnn8bQoUNRU1ODnj17BpZ37NgR6enpdjWfqE2kfbux7l8CKJm0cCMBT5LMYNsMTmVlJTweTyDcAEBubi48Hg82b94c8ev4fD64XC5cfPHFQcuXLFmC1NRU9O/fHzNnzkR9fX2zr9HQ0AC/3x/0oNZxFqdtpJ3FciBVj8T3RNpxrxqV6640ts3geL1edOvWLWR5t27d4PV6I3qNb775Bvfddx8mTpyIlJSUwPLbb78dvXv3Rnp6Oj788EOUlpZi165dIbM/TcrKyjBnzpzYNoTIIJzJUYfEcCOBhJMjio+oZ3Bmz54dcoPv+Y/t27cDAFyu0CRvWVbY5ec7deoUJkyYgDNnzmDBggVBPysuLsaoUaMwYMAATJgwAa+99hrWrl2LnTt3hn2t0tJS+Hy+wKO2tjbazSYFSShUEs9mObA6T+p7IPF4Vwlnb+Ir6hmcu+66q9VPLPXq1Qvvv/8+vvzyy5CfHTlyBGlpaS0+/9SpUxg/fjz279+Pd955J2j2JpzBgwejQ4cO2LdvHwYPHhzyc7fbDbdbZsFwWq8eR3DgUFenm0EJxpkc5zDcEMVH1AEnNTUVqampra6Xl5cHn8+H9957D0OHDgUAbN26FT6fD/n5+c0+rync7Nu3D+vWrUOXLl1a/V0fffQRTp06hYyMjMg3hLQg4a+MS7vhuAlDTuJJDTdSSJj1pfix7Sbjvn37YsyYMSguLsaWLVuwZcsWFBcXY9y4cUGfoOrTpw9WrlwJAPj222/xk5/8BNu3b8eSJUtw+vRpeL1eeL1eNDaePTA//fRTzJ07F9u3b8eBAwewatUq3HbbbRg0aBCuvvpquzaHqE2knt1ywE0cyfta6vGtEl6eij9bvwdnyZIlGDhwIAoKClBQUIArr7wSr7zyStA6e/fuhc/nAwAcOnQIb7zxBg4dOoR/+7d/Q0ZGRuDR9MmrpKQkvP322xg9ejSys7Mxbdo0FBQUYO3atWjXrp2dm2MsdjyzSR54pZC8j6WEG87emMe2T1EBwCWXXIJXX321xXUs67up+169egX9P5zMzExs2LAhLu0jPUi4TAXIvVQFfDcA85JV/EkON0Qq49+iIkogKWe7zeFgHF/S96eU41n12RvOktuDAYcionoHVL2A6UT6oKwC32Vu8ftRSrghczHgECWYDgOD9MHZSdx3iaX6yY/qJ4+SMeBQxFTviKoXsnPpEnI4WEdHl/2lw/FL+mPAIaI20WXQtpsu+0lSuJF00kPxx4BDWpFU0CQNFK3RZfC2A2e6qDmqz4pLx4BDUZHQIRlynMGBPJRu+0PS8SqpDpA9bP0eHCJqneTvxwmH35mjX7ABZIUbCSScLErHGRyKmoSOKe3sTcfBQ8dBPhI6bre041Na/yd7cAaHiGxj0myOjsEGYLixg4STRB1wBodiIqGDSih055I2kERD5/tzdN42IskYcIgUonPIAfSa5TAh2Eg7HqWd1JC9GHAoZpzFsYe0QSVa0oOB9PZHSvfj0CkS6qYuGHBIeww5apIWFKS1ty0kHn8S+znZiwGH2oRnI/aROMjEoik4qBoeVG6bHSQed1LCDetlYvFTVGSEhp6NcNckOd0MaoUqn7oyKdCcS2K4IWoOZ3CozaSclUg5yzuXqQOOU7M6ps3WnEvqsSalX0upkzrhDA6R4nT7puNonRs47JjZMTXQnEtquCFqCQMOxUWvHkdw4FBXp5vRKqmXqkwPOU3iEXYYaIJJDjecvaGWMOAQCcGQE6y5oHJu8GGY0ZeUcEPO4T04FDdSzlIkF0bJZ9uJovqnslTC48l+UuqijhhwyEgMOWSy+iyX6ONIcv+lxGHAobiSdLYiuUhKHpzIWdKPHUn9VlI91BEDDpFQ0gcqSjzpxwzDDUWDAYfiTlLHllQww5E+YFHi8Fgh0zDgkPF0CDkcvKglOhwfkvqppJM8nTHgkC3YwRNPh0GM4k+H40JSuCF1MOAQQZ8CqsNgRvHD4yHxeHKnDgYcso20js6QQzrR5TjQpV9S4jHgkK0Ycpyhy+BG0dPpnixp/VFavdMdAw7ReaQV1eboNNBRZHR6v6X1Q4Yb9TDgkO3Y8Z2l06BHzdPpfZYWbkhNDDhEYehWYHUa/CgU319n8SROTQw4lBASCwBDDqlOx8uQuvU7cg4DDiUMQ47zdBwQTaXj+yixv0msa6awNeAcP34cRUVF8Hg88Hg8KCoqwokTJ1p8zuTJk+FyuYIeubm5Qes0NDTg17/+NVJTU3HRRRfhxhtvxKFDh2zcEjKZxKLbGh0HR1PoGlIl9jOGG7XZGnAmTpyI6upqrF69GqtXr0Z1dTWKiopafd6YMWNQV1cXeKxatSro5yUlJVi5ciWWLVuGd999FydPnsS4ceNw+vRpuzaF4oQFQR26DpQ60/X9khhuSH3t7Xrh3bt3Y/Xq1diyZQuGDRsGAHjxxReRl5eHvXv3Ijs7u9nnut1upKenh/2Zz+fDwoUL8corr2DUqFEAgFdffRWZmZlYu3YtRo8eHf+Nobjq1eMIDhzq6nQzotLQsxHumiSnm2GL+iwXOh20nG4GtYLhRi08WVOfbTM4lZWV8Hg8gXADALm5ufB4PNi8eXOLz12/fj26deuGK664AsXFxTh8+HDgZzt27MCpU6dQUFAQWNa9e3cMGDCg2ddtaGiA3+8PehBFS2ohjgRnc9Sl83sjtU8x3MhgW8Dxer3o1q1byPJu3brB6/U2+7yxY8diyZIleOedd/DYY49h27Zt+OEPf4iGhobA6yYlJaFz585Bz0tLS2v2dcvKygL3AXk8HmRmZrZhyygepBYIqQU5UroOpBLpHGyIEiHqgDN79uyQm4DPf2zfvh0A4HKFdk7LssIub1JYWIgf/ehHGDBgAG644Qa89dZb+OSTT/Dmm2+22K6WXre0tBQ+ny/wqK2tjWKLyS4MOWriwOo8E/a/1H4ktW6ZKOp7cO666y5MmDChxXV69eqF999/H19++WXIz44cOYK0tLSIf19GRgaysrKwb98+AEB6ejoaGxtx/PjxoFmcw4cPIz8/P+xruN1uuN3uiH8nUWt0vienSdMgy/tzEseEYAMw3FBiRB1wUlNTkZqa2up6eXl58Pl8eO+99zB06FAAwNatW+Hz+ZoNIuEcO3YMtbW1yMjIAADk5OSgQ4cOqKiowPjx4wEAdXV1+PDDD/HII49EuznkMIk3HDcxIeQAvAk5EUwJNoDccEPy2HYPTt++fTFmzBgUFxdjy5Yt2LJlC4qLizFu3LigT1D16dMHK1euBACcPHkSM2fORGVlJQ4cOID169fjhhtuQGpqKm655RYAgMfjwZQpU3DPPffg7bffRlVVFX72s59h4MCBgU9VkSySz4pMKda8bGUP0/ar5P4iuU6ZyraPiQPAkiVLMG3atMAnnm688UbMnz8/aJ29e/fC5/MBANq1a4cPPvgAL7/8Mk6cOIGMjAxce+21WL58OTp16hR4zu9//3u0b98e48ePxz//+U+MHDkSixcvRrt27ezcHKKwTJnJAXjZKl5MCjVNGG4o0VyWZRlXqfx+PzweD3w+H1JSUpxuTsKNeHum000IS+qlqiamhJxzMehEj+FGHlUDzvqRjzrdhISLZvzm36IiZahaRCIlvYjHwrRLLG1h6r6S3i+k1yWTMeCQUqQXE+nFPFamDt6tadovpu4b6f1Bej0yna334BCZyKR7cs537kBu8uUrUwPNuRhuyGmcwSHl6FBYpBf3eDBx5sLEbQ6Hxz+pgDM4pCTJ34/TxOSZnHPpPqvDQBNMh3Cjw0kWMeCQwhhy9KNL2GGoCY/hhlTCgENkM4ac8KSFHYaa5ukQbACGG90w4JDSdJjFARhyWqNq2GGoaR3DDamKAYeUp1PIAcz8QsBonB8qEhl4GGiio0u4IT0x4JAIuoQcgLM50QoXOuIRehhm2kancMPZGz0x4BjouvQ9qPD2cboZUWPIoSbNhZNwwYdBJv4Ybpx1Xfoep5sgAgOOoaSGHJ0w5MQfw4z9GG5ICn7Rn8EkngXoVpB0GixIbw09G7U6XqXWEol12ykMOCSO1MLUHN0GDtIPj081MNxEhwHHcFI7jG4hB+AgQmrS8bjUsX5QKAYcYshRiI6DCcml4/EotW5IrdNOYsAh0aQWq5boOKiQLLpeNpVaLxhuYsOAQwBkdyCpRaslug4wpD5djzsd6wS1jAGHAhhy1KPrYEPq0TlUS64Pkuuy0xhwiBSn88BDatD5+GK4MRcDDgWR3KEkF7JI6DwIkXN0Pq50rwnUMgYcCsGQoy7O5lC86H4sSa8FkuuwKhhwKCzJnUt6YYuEzgMT2U/340d6DZBcf1XCgENakl7gIqH7GTjFnwnHjPS+z3ATPww41CzpHU16oYuU7gMWtZ0JwQYwp89TZBhwqEUMOTKYMoBR9Ew5LnTo69LrrWoYcKhV0judDoUvUgw61MSkY0GHPi69zqqIAYeMoEMBjIYpAxuFMinYAHr0bYYbezDgUER06IA6FMJomDbQkXnBVoc+rUNtVRUDDkVMh46oQ0GMFoOO/kx8j03syxQdBhyKCkOOXCYOgroz9T3VpQ/rUE9VxoBDUdOhU/bqcUSbIhktUwdFnZj8HurSb3Woo6pjwCGj6VIsY2HyICmV6e+ZLv2V4SYxGHAoJjp1UF2KZqxMHzQl4HukTz/VqXaqjgGHYqZTR9WleLYFB1H18D05i/2TYmFrwDl+/DiKiorg8Xjg8XhQVFSEEydOtPgcl8sV9vHf//3fgXVGjBgR8vMJEybYuSnUDIYc/XBQdR7fg+/o1C91qpcS2BpwJk6ciOrqaqxevRqrV69GdXU1ioqKWnxOXV1d0GPRokVwuVz48Y9/HLRecXFx0HrPP/+8nZtCLdCp0+pUTNuKg2xiNe1v7vPv6NQfdaqTUrS364V3796N1atXY8uWLRg2bBgA4MUXX0ReXh727t2L7OzssM9LT08P+v/rr7+Oa6+9FpdeemnQ8o4dO4asS865Ln0PKrx9nG5GXPTqcQQHDnV1uhnKOHfAddckOdgSPTHQhNIp2AAMN06xbQansrISHo8nEG4AIDc3Fx6PB5s3b47oNb788ku8+eabmDJlSsjPlixZgtTUVPTv3x8zZ85EfX19s6/T0NAAv98f9CBqiW4FNl44wxAfnK1pnm59j+HGObbN4Hi9XnTr1i1kebdu3eD1eiN6jT/+8Y/o1KkTbr311qDlt99+O3r37o309HR8+OGHKC0txa5du1BRURH2dcrKyjBnzpzoN4KiotMsDvBdoeVsTijO6sSGgaZlDDcUT1HP4MyePbvZG4GbHtu3bwdw9obh81mWFXZ5OIsWLcLtt9+O5OTkoOXFxcUYNWoUBgwYgAkTJuC1117D2rVrsXPnzrCvU1paCp/PF3jU1tZGudUUKR07tG5FN944G9Ey7p/IsJ9RvEU9g3PXXXe1+omlXr164f3338eXX34Z8rMjR44gLS2t1d+zadMm7N27F8uXL2913cGDB6NDhw7Yt28fBg8eHPJzt9sNt9vd6utQfOg2kwPwvpxIcWbnLIaZ6OgYbnQ82ZMm6oCTmpqK1NTUVtfLy8uDz+fDe++9h6FDhwIAtm7dCp/Ph/z8/Fafv3DhQuTk5OCqq65qdd2PPvoIp06dQkZGRusbQAnBkEOmhR2GmujpGGwAhhtV2HaTcd++fTFmzBgUFxdjy5Yt2LJlC4qLizFu3LigT1D16dMHK1euDHqu3+/H//zP/+DOO+8Med1PP/0Uc+fOxfbt23HgwAGsWrUKt912GwYNGoSrr77ars2hGOjYyU3+G1Ztce5lGl2CgI7blEi69iMd655Utn4PzpIlSzBw4EAUFBSgoKAAV155JV555ZWgdfbu3Qufzxe0bNmyZbAsCz/96U9DXjMpKQlvv/02Ro8ejezsbEybNg0FBQVYu3Yt2rVrZ+fmUAx07ey6FudEkRgOJLZZVbr2H13rnVQuy7IspxuRaH6/Hx6PBz6fDykpKU43J+Ee/mhcwn+nbpermvCSlT2cvqTFAGMPXYMN4Ey4mdX/fxP+O50Wzfht28fEic6l4z05AD9KbpfmAoYdwYdhJjEYbijRGHAoYXQNOQBvQE6U1sJIuADEAOM8hhtyAgMOJRRDDtmJYUYtOgcbgOFGdbbeZEwUjs5FgZ+yIjpL936gcx3TBQMOOUL34qB7cSdqjgkhX/f6pQsGHHKM7kXChEJPdC4Tjnfd65ZOGHDIUSYUCxOKPpnNlDBvQr3SCQMOOc6EomHKAEDmMeW4NqFO6YYBh5RgSvEwZTAg/ZkU2k2pT7phwCFlmFJETBoYSE8mHb+m1CUdMeCQUkwqJgw6JI1px6xJ9UhHDDikHNOKikkDBslkWrABzKtDOmLAISWZVlxMHEBIBhOPS9Pqj64YcEhZJhYZBh1ShanHool1R1cMOKQ0U4uNiQMLqcHUYAOYW290xYBDyjO16Jg80JAzTD7eTK0zOuNfEycRdP4r5K1pGnT4l8rJLgw2pCPO4JAYphcizuhQvJl+TJleU3THGRwSpakgmTqbA3BGh9rO5FDThOFGf5zBIZFYnHj2TdHjMXMW64cZGHBILBapszhoUWt4jHyHdcMcDDgkGovVdziI0fl4TARjvTAL78Eh8Uz+hFU4vEeHGGqCMdiYiQHHQD9J2YnX/IOdbkZc8ebjUAw6ZmGoCU/XcPOTlJ1ON0F5vERlKF07h67FrC2aLlNwANQT39vm6VoPdK3f8caAYzBdO4muRS0eOBjqg+9ly3StA7rWbTvwEpXhdLxcBfC+nNacOzDyEpYcDDSt0zXYAAw30eIMDmnbaa5L36N1sYsXzgSoj+9RZHTu77rWaTsx4BAAvTuPzkUvnnivjlr4fkRH536uc322Ey9RUYCul6sAXrKKFi9hOYNhJno6BxuA4aYtGHAoiO4hB+BHyaPFsGMvhprYMdxQS3iJikLo3ql0L4p24mWT+OB+bDvd+7HudTgROINDYek8kwPwklU8cGYncgwy8aN7sAEYbuKFAYeaZULIAXjJKh7OH8BNDzwMNPZguKFoMOBQi3QPOQBnc+xgYuBhqLGPCcEGYLiJNwYcapUpIQfgbI5dwg3+kkMPw0ziMNxQrGy9yfjhhx9Gfn4+OnbsiIsvvjii51iWhdmzZ6N79+648MILMWLECHz00UdB6zQ0NODXv/41UlNTcdFFF+HGG2/EoUOHbNgCamJK5zOlmKrg3BttVb7hVko7dWPSF3WaUl8TzdaA09jYiNtuuw2//OUvI37OI488gscffxzz58/Htm3bkJ6ejuuuuw719fWBdUpKSrBy5UosW7YM7777Lk6ePIlx48bh9OnTdmwG/T9TOqFJhVU14cJEogKFk7+bgpnU/0ypq05wWZZl2f1LFi9ejJKSEpw4caLF9SzLQvfu3VFSUoJ7770XwNnZmrS0NMybNw+/+MUv4PP50LVrV7zyyisoLCwEAHzxxRfIzMzEqlWrMHr06JDXbWhoQENDQ+D/Pp8PPXv2RG1tLVJSUuK3oULsO3RFm57/l/qr4tQS9a37sm37ihKn5ovUsMt7dj+a4JZQrK5N+8TpJiTUzZ12ten5l/cwa38BgN/vR2ZmJk6cOAGPx9PyylYCvPTSS5bH42l1vU8//dQCYO3cuTNo+Y033mjdcccdlmVZ1ttvv20BsL766qugda688krrN7/5TdjXfeihhywAfPDBBx988MGHBo/a2tpWM4VSNxl7vV4AQFpaWtDytLQ0HDx4MLBOUlISOnfuHLJO0/PPV1paihkzZgT+f+bMGXz11Vfo0qULXC5XPDdBhKYEbOoMVqJxfycW93ficZ8nlsn727Is1NfXo3v37q2uG3XAmT17NubMmdPiOtu2bcOQIUOifemA80OHZVmtBpGW1nG73XC73UHLIr3pWWcpKSnGdQ4ncX8nFvd34nGfJ5ap+7vVS1P/L+qAc9ddd2HChAktrtOrV69oXxYAkJ6eDuDsLE1GRkZg+eHDhwOzOunp6WhsbMTx48eDZnEOHz6M/Pz8mH4vERER6SXqgJOamorU1PA387VV7969kZ6ejoqKCgwaNAjA2U9ibdiwAfPmzQMA5OTkoEOHDqioqMD48eMBAHV1dfjwww/xyCOP2NIuIiIiksXWe3Bqamrw1VdfoaamBqdPn0Z1dTUA4F//9V/xve99DwDQp08flJWV4ZZbboHL5UJJSQl++9vf4vLLL8fll1+O3/72t+jYsSMmTpwI4OzU1JQpU3DPPfegS5cuuOSSSzBz5kwMHDgQo0aNsnNztOF2u/HQQw+FXLYje3B/Jxb3d+JxnycW93dkbP2Y+OTJk/HHP/4xZPm6deswYsSIsw1wufDSSy9h8uTJAM7eSzNnzhw8//zzOH78OIYNG4ZnnnkGAwYMCDz/m2++wX/+53/iT3/6E/75z39i5MiRWLBgATIzM+3aFCIiIhIkId+DQ0RERJRItn6TMREREZETGHCIiIhIOww4REREpB0GHCIiItIOA44BHn74YeTn56Njx44Rf4OzZVmYPXs2unfvjgsvvBAjRozARx99ZG9DNXL8+HEUFRXB4/HA4/GgqKio1T82O3nyZLhcrqBHbm5uYhoszIIFC9C7d28kJycjJycHmzZtanH9DRs2ICcnB8nJybj00kvx3HPPJail+ohmn69fvz7kWHa5XNizx5y/Et4WGzduxA033IDu3bvD5XLhL3/5S6vP4TEeigHHAI2Njbjtttvwy1/+MuLnPPLII3j88ccxf/58bNu2Denp6bjuuutQX19vY0v1MXHiRFRXV2P16tVYvXo1qqurUVRU1OrzxowZg7q6usBj1apVCWitLMuXL0dJSQlmzZqFqqoqDB8+HGPHjkVNTU3Y9ffv34/rr78ew4cPR1VVFe6//35MmzYN5eXlCW65XNHu8yZ79+4NOp4vv/zyBLVYtq+//hpXXXUV5s+fH9H6PMab0eqf4yRtRPpX3c+cOWOlp6dbv/vd7wLLvvnmG8vj8VjPPfecjS3Uw8cff2wBsLZs2RJYVllZaQGw9uzZ0+zzJk2aZN10000JaKFsQ4cOtaZOnRq0rE+fPtZ9990Xdv3/+q//svr06RO07Be/+IWVm5trWxt1E+0+X7dunQXAOn78eAJapzcA1sqVK1tch8d4eJzBoRD79++H1+tFQUFBYJnb7cYPfvADbN682cGWyVBZWQmPx4Nhw4YFluXm5sLj8bS6/9avX49u3brhiiuuQHFxMQ4fPmx3c0VpbGzEjh07go5NACgoKGh231ZWVoasP3r0aGzfvh2nTp2yra26iGWfNxk0aBAyMjIwcuRIrFu3zs5mGo3HeHgMOBTC6/UCQOAPnDZJS0sL/Iya5/V60a1bt5Dl3bp1a3H/jR07FkuWLME777yDxx57DNu2bcMPf/hDNDQ02NlcUY4ePYrTp09HdWx6vd6w63/77bc4evSobW3VRSz7PCMjAy+88ALKy8uxYsUKZGdnY+TIkdi4cWMimmwcHuPh2fq3qMg+s2fPxpw5c1pcZ9u2bRgyZEjMv8PlcgX937KskGUmiXSfA6H7Dmh9/xUWFgb+PWDAAAwZMgRZWVl48803ceutt8bYaj1Fe2yGWz/ccmpeNPs8Ozsb2dnZgf/n5eWhtrYWjz76KK655hpb22kqHuOhGHCEuuuuuzBhwoQW1+nVq1dMr52eng7g7FlBRkZGYPnhw4dDzhJMEuk+f//99/Hll1+G/OzIkSNR7b+MjAxkZWVh3759UbdVV6mpqWjXrl3IzEFLx2Z6enrY9du3b48uXbrY1lZdxLLPw8nNzcWrr74a7+YReIw3hwFHqNTUVKSmptry2r1790Z6ejoqKiowaNAgAGevw2/YsAHz5s2z5XdKEOk+z8vLg8/nw3vvvYehQ4cCALZu3Qqfz4f8/PyIf9+xY8dQW1sbFDJNl5SUhJycHFRUVOCWW24JLK+oqMBNN90U9jl5eXn461//GrRszZo1GDJkCDp06GBre3UQyz4Pp6qqiseyTXiMN8PJO5wpMQ4ePGhVVVVZc+bMsb73ve9ZVVVVVlVVlVVfXx9YJzs721qxYkXg/7/73e8sj8djrVixwvrggw+sn/70p1ZGRobl9/ud2ARxxowZY1155ZVWZWWlVVlZaQ0cONAaN25c0Drn7vP6+nrrnnvusTZv3mzt37/fWrdunZWXl2f9y7/8C/f5eZYtW2Z16NDBWrhwofXxxx9bJSUl1kUXXWQdOHDAsizLuu+++6yioqLA+p999pnVsWNHa/r06dbHH39sLVy40OrQoYP12muvObUJ4kS7z3//+99bK1eutD755BPrww8/tO677z4LgFVeXu7UJohSX18fqNMArMcff9yqqqqyDh48aFkWj/FIMeAYYNKkSRaAkMe6desC6wCwXnrppcD/z5w5Yz300ENWenq65Xa7rWuuucb64IMPEt94oY4dO2bdfvvtVqdOnaxOnTpZt99+e8hHZs/d5//4xz+sgoICq2vXrlaHDh2snj17WpMmTbJqamoS33gBnnnmGSsrK8tKSkqyBg8ebG3YsCHws0mTJlk/+MEPgtZfv369NWjQICspKcnq1auX9eyzzya4xfJFs8/nzZtnXXbZZVZycrLVuXNn69///d+tN99804FWy9T0MfvzH5MmTbIsi8d4pFyW9f93IhERERFpgh8TJyIiIu0w4BAREZF2GHCIiIhIOww4REREpB0GHCIiItIOAw4RERFphwGHiIiItMOAQ0RERNphwCEiIiLtMOAQERGRdhhwiIiISDv/By7T/tLYP+L9AAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.axis('equal')\n", "plt.contourf(X0,X1,solution);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let us check that the system of equations is indeed solved." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.341262Z", "iopub.status.busy": "2024-04-30T08:50:52.341155Z", "iopub.status.idle": "2024-04-30T08:50:52.343986Z", "shell.execute_reply": "2024-04-30T08:50:52.343720Z" } }, "outputs": [], "source": [ "residue = Scheme(solution,coefs,offsets,gridScale)\n", "assert norm_infinity(residue)<1e-13" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.2 Fast sweeping\n", "\n", "In this approach, solution is updated slice after slice, left to right, right to left, top to bottom, bottom to top, etc. On large domains, the number of updates per pixel is usually smaller than with the global iteration approach.\n", "\n", "On a domaine of shape $N_x \\times N_y$, the Fast sweeping method repeats the following steps until convergence is achieved\n", "* For $x$ from $1$ to $N_x$\n", " - For $y$ from $1$ to $N_y$ in parallel: update $u(x,y)$\n", "* For $x$ from $N_x$ to $1$\n", " - For $y$ from $1$ to $N_y$ in parallel: update $u(x,y)$\n", "* For $y$ from $1$ to $N_y$\n", " - For $x$ from $1$ to $N_x$ in parallel: update $u(x,y)$\n", "* For y from $N_y$ to $1$\n", " - For $x$ from $1$ to $N_x$ in parallel: update $u(x,y)$\n", " \n", "Some variants of the fast sweeping method use diagonal sweeps, rather than axis aligned ones as above.\n", "\n", "\n", "\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.345455Z", "iopub.status.busy": "2024-04-30T08:50:52.345355Z", "iopub.status.idle": "2024-04-30T08:50:52.347327Z", "shell.execute_reply": "2024-04-30T08:50:52.347109Z" } }, "outputs": [], "source": [ "def SweepSlices(shape):\n", " \"\"\"Enumerates the slices used in the fast sweeping method.\"\"\"\n", " dim = len(shape)\n", " s=(slice(None),)\n", " for d,n in enumerate(shape):\n", " for x in itertools.chain(range(n),reversed(range(n))):\n", " # [:,...(d times),:, x, :,...(dim-d-1 times),:]\n", " yield s*d +(x,)+s*(dim-d-1) " ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.348680Z", "iopub.status.busy": "2024-04-30T08:50:52.348584Z", "iopub.status.idle": "2024-04-30T08:50:52.350773Z", "shell.execute_reply": "2024-04-30T08:50:52.350540Z" } }, "outputs": [], "source": [ "def SweepIteration(update,u0,args,eps=1e-6,niter_max=100):\n", " u=u0.copy()\n", " for niter in range(niter_max):\n", " u_old=u.copy()\n", " for sl in SweepSlices(u0.shape):\n", " u[sl]=update(u,*args,where=sl)\n", " if np.all(u+eps>=u_old):\n", " return u,niter*2*u0.ndim\n", " print(\"Iterative method did not reach stopping criterion within iteration budget\")\n", " return u,niter_max *2*u0.ndim" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note on execution time.** \n", "For some reason that needs to be clarified, the update of a slice of the domain costs almost as much as the update of the full domain with this Python implementation. (This is possibly an issue with memory management.) As a result, execution time is here much larger than with the global iteration approach. \n", "\n", "This is not the expected behavior : for reasonably optimized implementations of the fast sweeping method, usually written in compiled languages to reduced overhead, the fast-sweeping method is less numerically intensive than global iteration. " ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.352162Z", "iopub.status.busy": "2024-04-30T08:50:52.352067Z", "iopub.status.idle": "2024-04-30T08:50:52.653018Z", "shell.execute_reply": "2024-04-30T08:50:52.652738Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 297 ms, sys: 441 µs, total: 298 ms\n", "Wall time: 299 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution,nupdate = SweepIteration(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In principle, fast sweeping allows to substantially reduce the number of updates per point in comparison with global iteration. This effect is pronounced over large domains, but is not much observed in very small domains as here." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.654527Z", "iopub.status.busy": "2024-04-30T08:50:52.654443Z", "iopub.status.idle": "2024-04-30T08:50:52.656328Z", "shell.execute_reply": "2024-04-30T08:50:52.656086Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Average number of updates per point : 48\n" ] } ], "source": [ "print(\"Average number of updates per point :\", nupdate)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.657757Z", "iopub.status.busy": "2024-04-30T08:50:52.657665Z", "iopub.status.idle": "2024-04-30T08:50:52.711871Z", "shell.execute_reply": "2024-04-30T08:50:52.711614Z" } }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.axis('equal')\n", "plt.contourf(X0,X1,solution);" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.713408Z", "iopub.status.busy": "2024-04-30T08:50:52.713298Z", "iopub.status.idle": "2024-04-30T08:50:52.715926Z", "shell.execute_reply": "2024-04-30T08:50:52.715665Z" } }, "outputs": [], "source": [ "residue = Scheme(solution,coefs,offsets,gridScale)\n", "assert norm_infinity(residue)<1e-13" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.3 Fast marching\n", "\n", "The fast marching method updates the solution point after point in a very specific order. It works in a Dijkstra-like fashion, taking advantage of the causality of the numerical scheme.\n", "\n", "A preliminary step, achieved in the `ReverseNeighbors` routine, is to identify all the *reverse neighbors* of a given point $y \\in X$. In other words all $x \\in X$ such that $y = x \\pm h e_i(x)$ for some $1 \\leq i \\leq I$." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.717334Z", "iopub.status.busy": "2024-04-30T08:50:52.717234Z", "iopub.status.idle": "2024-04-30T08:50:52.721696Z", "shell.execute_reply": "2024-04-30T08:50:52.721447Z" } }, "outputs": [], "source": [ "def ReverseNeighbors(shape,offsets):\n", " \"\"\"Reverses a directed graph, defined by offsets on a cartesian grid. \n", " The reverse neighbors for index i are orig[changes[i]:changes[i+1]], \n", " where (orig,changes) is the output.\"\"\"\n", " # Get original and neighbor offset\n", " neigh,inside = fd.OffsetToIndex(shape,offsets)\n", " size = np.prod(shape)\n", " orig = np.broadcast_to(np.arange(size),(neigh.size//size,size)).flatten()\n", " orig,neigh=orig[inside.flatten()],neigh[inside].flatten()\n", "\n", " # Sort according to neighbor offset\n", " ind = np.lexsort((orig,neigh))\n", " orig,neigh=orig[ind],neigh[ind]\n", " \n", " # Count\n", " changes = np.arange(1,neigh.size)[neigh[1:]!=neigh[:-1]]\n", " changes = np.append(np.insert(changes,0,0),neigh.size)\n", " return orig,changes\n", "\n", "def FastMarching(update,u0,args):\n", " \n", " # Compute the reverse neighbors\n", " _,offsets,_ = args\n", " rev,chg = ReverseNeighbors(u0.shape,np.stack((offsets,-offsets),axis=1))\n", "\n", " # Find the seeds\n", " seeds = u0 u[index]: continue\n", " accepted[index]=True\n", " # Update the neighbors\n", " for index2 in rev[chg[index]:chg[index+1]]:\n", " if accepted[index2]: continue\n", " value2 = update(u.reshape(u0.shape),*args,where=np.unravel_index(index2,u0.shape))\n", " niter+=1\n", " if value2 < u[index2]:\n", " u[index2]=value2\n", " heapq.heappush(heap,(value2,index2))\n", " return u.reshape(u0.shape),niter/u0.size" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note on execution time.** Again, the computation time observed here is not representative of what is obtained in a compiled language, compatible with sequential execution on a mutable state.\n", "\n", "For reasonably optimized implementations, the FMM is expected to be faster than fast-sweeping." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:52.723133Z", "iopub.status.busy": "2024-04-30T08:50:52.723052Z", "iopub.status.idle": "2024-04-30T08:50:53.575441Z", "shell.execute_reply": "2024-04-30T08:50:53.575114Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 846 ms, sys: 672 µs, total: 847 ms\n", "Wall time: 850 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution,nupdate = FastMarching(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The number of updates per point is greatly reduced w.r.t. the fast sweeping method (and global iteration as well). In some implementations, this can be counterbalanced by the cost of maintaining a priority queue." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.577204Z", "iopub.status.busy": "2024-04-30T08:50:53.577079Z", "iopub.status.idle": "2024-04-30T08:50:53.578948Z", "shell.execute_reply": "2024-04-30T08:50:53.578678Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Average number of updates per point : 2.9219530949634756\n" ] } ], "source": [ "print(\"Average number of updates per point :\", nupdate)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.580366Z", "iopub.status.busy": "2024-04-30T08:50:53.580261Z", "iopub.status.idle": "2024-04-30T08:50:53.654374Z", "shell.execute_reply": "2024-04-30T08:50:53.654108Z" } }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjgAAAGiCAYAAADqYLxOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABDAklEQVR4nO3de3RU1b0H8O8IZCKWjEggCZcQ0KvhqRdCIYkXiwUDWHy2EoqNcBempasWA3KvRrQCa9kUr1ofiK+CVKVAr4Fqr0gJystFQB4JvgCpAgmakYcwE2xNEM79g5uRYSbJzGTOnP3b+/tZa9aCkzOTfc6c/dvfs8+ZicuyLAtEREREGrnA6QYQERERxRsDDhEREWmHAYeIiIi0w4BDRERE2mHAISIiIu0w4BAREZF2GHCIiIhIOww4REREpB0GHCIiItIOAw4RERFpx9aAs3HjRtxwww3o3r07XC4X/vKXv7T6nA0bNiAnJwfJycm49NJL8dxzz4WsU15ejn79+sHtdqNfv35YuXKlDa0nIiIiqWwNOF9//TWuuuoqzJ8/P6L19+/fj+uvvx7Dhw9HVVUV7r//fkybNg3l5eWBdSorK1FYWIiioiLs2rULRUVFGD9+PLZu3WrXZhAREZEwrkT9sU2Xy4WVK1fi5ptvbnade++9F2+88QZ2794dWDZ16lTs2rULlZWVAIDCwkL4/X689dZbgXXGjBmDzp07Y+nSpba1n4iIiORo73QDzlVZWYmCgoKgZaNHj8bChQtx6tQpdOjQAZWVlZg+fXrIOk888USzr9vQ0ICGhobA/8+cOYOvvvoKXbp0gcvlius2EBERkT0sy0J9fT26d++OCy5o+SKUUgHH6/UiLS0taFlaWhq+/fZbHD16FBkZGc2u4/V6m33dsrIyzJkzx5Y2ExERUWLV1taiR48eLa6jVMABEDKj0nQF7dzl4dZpaSamtLQUM2bMCPzf5/OhZ8+eqK2tRUpKSjyaLcq+Q1fE/Ny/1F8Vx5aobd2Xse8niq+aL1Jt/x09ux+1/XdQZK5N+8TpJiTMzZ12xfzcy3uYs5+a+P1+ZGZmolOnTq2uq1TASU9PD5mJOXz4MNq3b48uXbq0uM75szrncrvdcLvdIctTUlKMDDjf6xTbveWv+Qcj+XtxboyCKrx9AADtL3K4IQY6cKhr2OUXXGj/7z50PPzZYK8eR+z/5RRk08mBuC59j9PNSIjV1hD8JGVnTM81cfxqEsntJUoFnLy8PPz1r38NWrZmzRoMGTIEHTp0CKxTUVERdB/OmjVrkJ+fn9C2muY1/2Cnm5AQTeGG7NVckFFRuLYy9NivqS+aEHRe8w+OOeRQ82wNOCdPnsTf//73wP/379+P6upqXHLJJejZsydKS0vx+eef4+WXXwZw9hNT8+fPx4wZM1BcXIzKykosXLgw6NNRd999N6655hrMmzcPN910E15//XWsXbsW7777rp2bYjQTwg2Djb0kBZpIMPQkToW3D0MOxcTWj4mvX78e1157bcjySZMmYfHixZg8eTIOHDiA9evXB362YcMGTJ8+HR999BG6d++Oe++9F1OnTg16/muvvYYHHngAn332GS677DI8/PDDuPXWWyNul9/vh8fjgc/nM3KKb29t94jXZbihWOgWaGLBwBN/JgSdaEJOduYXNrZETdGM3wn7HhyVMOBEFnB0DzcMNvHDQNMyhp34Ycj5DgNOy+O3UvfgkDoYbqg1DDWRO39fMfDEzoR7c3i5Kj74xzYpBMMNNefAoa6BB8WO+7HtdO/HutfhROAMDgXRuVPpXhDtwkHYXufuX87sREf3G5A5k9M2DDgUwHBDTRhqnMGwEz3dL1kx5MSOl6gIAMMN8bKJavh+REfnfq5zfbYTAw5p23kqvH20LnrxwkFUfXyPIqNzf9e1TtuJl6gMp2un0bnQxQMHS5l4Cat1Ol+y4uWq6HAGx2AMN+bhTIA++F62TNc6oGvdtgNncAykcwfRtai1BQdBvTW9v5zRCaXrp6yaavgsh9uhOgYc0gKDTSgGG7Pw8lV4uoYcah0DDonHcBOMwYY4qxNM5/tyqHm8B4dEY7j5Du/JoPPxmAjGemEWBhwSi8XqLA5i1BoeI99h3TAHAw6JxCLFQYuix2PmLNYPM/AeHBKFhYn32FDb8R4d3nxsAs7gkBimhxuefVO8mX5MmV5TdMcZHBLB5EJk8gBEiWHyjA4/YaUvzuCQ8kwNN6afXVPimXy8mVpndMaAQ0ozsegw2JCTTD7+TKw3OmPAIWWZWGxMHVhIPaYGHRPrjq4YcEhJphUZUwcTUp+Jx6Zp9UdXDDikHJOKi4mDB8lk2nFqUh3SFQMOKcWkomLagEHymRbITapHOmLAIWWYUkxMGyRIPyYdv6bUJR0x4JASTCkiJg0MpDeTgrop9Uk3DDjkOBOKh0mDAZnFlOPahDqlGwYccpQJRcOUAYDMZUqAN6Fe6YQBhxyje7EwpegTNTHheNe9bumEAYccoXuRMKHQE4VjQrDXvX7pggGHEk7n4mBCcSeKhO79QOc6pgv+NXFKKJ2Lgu4FXXXumqRmf9bQszGBLaEmuv+V8gpvH/4VcoUx4FDCMNxQW7UUYmJ9HsOP/Q4c6sqQQwnHgEMJoWu4YbCxR6xBJp6/i8EnvhhyKNEYcMh2DDfUkkSGmWic3y4GnrbT+ZIVQ456eJMx2YrhhsJx1yQFHlKc22ZJ7VaRrv1H13onVUICzoIFC9C7d28kJycjJycHmzZtanbdyZMnw+VyhTz69+8fWGfx4sVh1/nmm28SsTkUIR07Oz8lFRsdw4GO25RIuvYjHeueVLYHnOXLl6OkpASzZs1CVVUVhg8fjrFjx6Kmpibs+k8++STq6uoCj9raWlxyySW47bbbgtZLSUkJWq+urg7Jycl2bw5FSMdOrmtBtotpg79p2xsPup4w6Fj/JLI94Dz++OOYMmUK7rzzTvTt2xdPPPEEMjMz8eyzz4Zd3+PxID09PfDYvn07jh8/jv/4j/8IWs/lcgWtl56ebvemUIR07Nw6FmE7cJA/i/shOjr2Lx3roDS2BpzGxkbs2LEDBQUFQcsLCgqwefPmiF5j4cKFGDVqFLKysoKWnzx5EllZWejRowfGjRuHqqqqZl+joaEBfr8/6EH20LFT61h844mDecu4fyLDfkbxZmvAOXr0KE6fPo20tLSg5WlpafB6va0+v66uDm+99RbuvPPOoOV9+vTB4sWL8cYbb2Dp0qVITk7G1VdfjX379oV9nbKyMng8nsAjMzMz9o2iZukWbnSdPo8HDtqx4T5rmW79TbeaKE1CbjJ2uVxB/7csK2RZOIsXL8bFF1+Mm2++OWh5bm4ufvazn+Gqq67C8OHD8ec//xlXXHEFnn766bCvU1paCp/PF3jU1tbGvC1kBt0KbbxwgI4PBsTm6db3GHKcY+v34KSmpqJdu3YhszWHDx8OmdU5n2VZWLRoEYqKipCU1HIRuOCCC/D973+/2Rkct9sNt9sdXeMpKjp1Yt0KbFtxELZX0/7l9+x8R7fvy+F35DjD1hmcpKQk5OTkoKKiImh5RUUF8vPzW3zuhg0b8Pe//x1Tpkxp9fdYloXq6mpkZGS0qb0UG4YbPXGGIbE4qxNKp/6oU52UwvZLVDNmzMAf/vAHLFq0CLt378b06dNRU1ODqVOnAjh7+eiOO+4Ied7ChQsxbNgwDBgwIORnc+bMwd/+9jd89tlnqK6uxpQpU1BdXR14TUocnTqtTsW0LTjIOo/vwXd06pc61UsJbP9TDYWFhTh27Bjmzp2Luro6DBgwAKtWrQp8Kqquri7kO3F8Ph/Ky8vx5JNPhn3NEydO4Oc//zm8Xi88Hg8GDRqEjRs3YujQoXZvDp1Dp86qUxGNBQdTNfHy1Vk6/R0rXq5KHJdlWZbTjUg0v98Pj8cDn8+HlJQUp5uTcA9/NK7Nr8Fwow+GGzlMDzq6hJx4BZxZ/f83Lq8jSTTjN/8WFRnL9I+B8zKIPKa/Z7r0V51OEFXGgENR06Fz6lIoY2H6IKkDk99DXfquDnVUdQw4FBUdOqUuBTJaJg+KujL1PdWlD+tQT1XGgEMR06Ez6lIYo2HqIGgSE99jXfqyDnVVVQw4FBEdOqEuBTFSJg56pjPtPTetT1N0GHDICCYVQtMGOQpl0vuvQ9/W4QRSRQw41CrpnU+HAhgpkwY2aplJQVeHT0RKr7MqYsChFknvdNKLXqRMGswoOjw25JBeb1XDgEPNkt7ZTAo3RK0x4Tgxpc9TZBhwSEsmFDqemVO0TDhmpPd96SeWKmHAobAkdzLpBa41JgxSZC/djx/pNUBy/VUJAw6FkNy5pBe21ug+MFHi6B6UpdcCyXVYFQw4FERyp5Je0Fqj82BEztH5uNK9JlDLGHCIFKf7mTY5T+djTHLIkXzCqQIGHAqQ3JkkF7GW6DrokJp0Pd4k1wfJddlpDDgEQHYnkly8mqPzGTWpTdfjTsc6QS1jwCHRdCxaug4wJIeuAVtqvZB8AuokBhwS23mkFquW6DiokFw6Ho9S64bUOu0kBhzDSe00UotUS3QcTEg+HY9LHesHhWLAIXF0K066Xg4gffD4VIPUE1KnMOAYTGJn0THcEEmgWxCXWksk1m2ntHe6AeQMdhLn6TRYqKLTQStkWX2Wy4GW6Mtdk4SGno1ONyMuDhzqil49jjjdDLIJA46BpIYbqWdc4TDcxC5ciIllfQaf2DHkOKuphs/q73BDFMeAQyLoEm4YbKIXbaBpy+sy9ESOIYdUx4BDymO4MYtdgSaW383A0zKGHFIZAw4pjeFGf04Gmtac2zaGnfB0CjmkFwYcIpsx3IRSOdQ0h2GneU3HuPSgw1kcvfBj4qQsHWZvGG6CdTpoiQw352vaDh22JZ50ON51qDt0FmdwSEk6FBkdin086B4COLMTTIdLVpzJ0QMDDimH4UYPugebcJq22fSgo0PIIfl4iYoozkwON7x0cxb3gfx+oMOJlukYcEgp0ouK9KIeKw7o4Zke+KT3B+n1yHQMOKQM6cVEejGPhcmDd7RM3VfS+4X0umQy3oNDSpBeRKQX8WiZOFDHi4n36fCeHHICZ3CI2sikcGPqLIQdTNuXkvuJ9BMwUyUk4CxYsAC9e/dGcnIycnJysGnTpmbXXb9+PVwuV8hjz549QeuVl5ejX79+cLvd6NevH1auXGn3ZpBNJBcPyUU7GqYNxolk0n6V3F8k1ylT2R5wli9fjpKSEsyaNQtVVVUYPnw4xo4di5qamhaft3fvXtTV1QUel19+eeBnlZWVKCwsRFFREXbt2oWioiKMHz8eW7dutXtzKM4kFw3JxToaJg3ATjEpQJrSb8h5LsuybO1Vw4YNw+DBg/Hss88GlvXt2xc333wzysrKQtZfv349rr32Whw/fhwXX3xx2NcsLCyE3+/HW2+9FVg2ZswYdO7cGUuXLm21TX6/Hx6PBz6fDykpKdFvlHAj3p7pdBMCpAYcE4q0KQOuiky4P0fqPTkqfQHg+pGPOt2EhItm/LZ1BqexsRE7duxAQUFB0PKCggJs3ry5xecOGjQIGRkZGDlyJNatWxf0s8rKypDXHD16dLOv2dDQAL/fH/Qg5zHcqMmk2QRVmbD/pfYjqXXLRLYGnKNHj+L06dNIS0sLWp6Wlgav1xv2ORkZGXjhhRdQXl6OFStWIDs7GyNHjsTGjRsD63i93qhes6ysDB6PJ/DIzMxs45ZRW0ktElKLcqRMGFilYNAkapuEfEzc5QqebrUsK2RZk+zsbGRnZwf+n5eXh9raWjz66KO45pprYnrN0tJSzJgxI/B/v9/PkENR0znccCBVl84fK5f68XH+rSoZbJ3BSU1NRbt27UJmVg4fPhwyA9OS3Nxc7Nu3L/D/9PT0qF7T7XYjJSUl6EHOkTh7w3BDTtP1fZLatyTWMdPYGnCSkpKQk5ODioqKoOUVFRXIz8+P+HWqqqqQkZER+H9eXl7Ia65Zsyaq1yRnsCiog5dA5NH1PZMackhttl+imjFjBoqKijBkyBDk5eXhhRdeQE1NDaZOnQrg7OWjzz//HC+//DIA4IknnkCvXr3Qv39/NDY24tVXX0V5eTnKy8sDr3n33Xfjmmuuwbx583DTTTfh9ddfx9q1a/Huu+/avTlkIB2Lr46DpEk6HbS0u2Ql8XIVL1WpzfaAU1hYiGPHjmHu3Lmoq6vDgAEDsGrVKmRlZQEA6urqgr4Tp7GxETNnzsTnn3+OCy+8EP3798ebb76J66+/PrBOfn4+li1bhgceeAAPPvggLrvsMixfvhzDhg2ze3OoDSTO3ugWbhhs9MGQowaGHHXZ/j04KuL34CT+e3AYbpzHcKMv3YKOtJDjVMDh9+A4+D04RFIx3JAkfH+dJfEEzgQMOGQ7aZ1fp3Cj602pFEqn91mnPkjOYcAh0pROAx5FRqf3XFrIkXYiZwIGHLKVtE4vrag2R6eBjqKj06ydtP4ord7pjgGHbCOts0srps3RZXCjttHlONClX1LiMeAQQZ8iqsugRvHB4yHxpJ3Y6YwBh2zBTp54HMwoHB2OC11OQCixGHDIeNKLp073XJA9dDg+JPVTnuCpgQGH4k5S55ZUNMPRYeCixOCxQqZhwCESigMWRUv6MSPphETSiZ6uGHAoriR1aknF8nzSBypyjvRjR1K/lVQPdcSAQ0aSVCTPJ32AIudJv29Lcv+lxGHAobiRcrYiuThKHpQSwfNpQ9CDWsbjyX5S6qKO2jvdACKKDAejYJEEmHDr+C5z29EcSjB3TZK4vzpOicWAQ3Eh5SxF6uwNw81Z8ZiVOf81TA88nQ5aqM9yOd2MmEgJOQcOdUWvHkecboZxGHCIFGd6uLH7UtO5r29q2JEccoiaw4BDbcbZG/uYGm6cun/G5LAjNeRwFoeaw4BDRpAYbkyk0o3BTW0xKehIDTlE4fBTVNQmUmZvJDJl9kb1Tz2p3DY7SDzupJzAsF4mFmdwSHtSit+5JA4y0ZIWGky6fCVxJkfKpSpKHM7gUMwknI0w3KhHhxkR6e2PhO7HoVMk1E1dMOAQKUTnQUWHYHMu3bYnHGnHo8QTGrIPAw7FRMJZiLRiJ20wiYbOQcCEoEMkEQMOkQJ0DTcmDf66bqu0Y1PCiY2EE0QdMOBQ1CR0TglFTme6DvaR0HG7GXJIIgYcIodJGzxao+MAHy0dA55ux6nTJJwoSseAQ1GR0Cklnb3pNGjoOKi3lW77Q9LxKqkOkD0YcEgrkoqapMGiNboN5PHE4EfNkXDCKBkDDhG1CQfvyOiynyQFc0knPBR/DDgUMdXPNiQVM0mDRHM4MxE9XfaXDscv6Y8BhyjBdBgcdBmoncBgmFiqn/iofuIoGQMORUT1Tqh6EdMJB+f4kL4fdQjqpDcGHKIEkj4oSB+UVSN9f0o5nlU/AVL9BFIqBhwST/Xi1UTKYBAOL6vYR/p+lXxck94YcKhVPLswm/QBWALuY/tJORGi+GHAIUoAqWe5HHgTR/K+lnp8q4QnkvGXkICzYMEC9O7dG8nJycjJycGmTZuaXXfFihW47rrr0LVrV6SkpCAvLw9/+9vfgtZZvHgxXC5XyOObb76xe1NIMRLOyqQWf8kDrlTc5/aSUC8ofmwPOMuXL0dJSQlmzZqFqqoqDB8+HGPHjkVNTU3Y9Tdu3IjrrrsOq1atwo4dO3DttdfihhtuQFVVVdB6KSkpqKurC3okJyfbvTnG4VmFmTjQOkfqvpca5ElfLsuybD0qhw0bhsGDB+PZZ58NLOvbty9uvvlmlJWVRfQa/fv3R2FhIX7zm98AODuDU1JSghMnTkT0/IaGBjQ0fFc0/H4/MjMz4fP5kJKSEvnGaGLE2zMjXlflgCPhbExi0Zc6wOrGd5nb6SbEpD7L5XQTWtXQs9HpJjSrV48jEa+7fuSjNrZETX6/Hx6PJ6Lx29YZnMbGRuzYsQMFBQVBywsKCrB58+aIXuPMmTOor6/HJZdcErT85MmTyMrKQo8ePTBu3LiQGZ5zlZWVwePxBB6ZmZnRb4yBVA43EjDcUFvwvSBqG1sDztGjR3H69GmkpaUFLU9LS4PX643oNR577DF8/fXXGD9+fGBZnz59sHjxYrzxxhtYunQpkpOTcfXVV2Pfvn1hX6O0tBQ+ny/wqK2tjX2jSAkSZm+k4YCqHonvicRgrxKeWMZP+0T8EpcreMrSsqyQZeEsXboUs2fPxuuvv45u3boFlufm5iI3Nzfw/6uvvhqDBw/G008/jaeeeirkddxuN9xumdO9JJO0Ii9xIDWF59MGsZerVOWuSVL6MhXFh60zOKmpqWjXrl3IbM3hw4dDZnXOt3z5ckyZMgV//vOfMWrUqBbXveCCC/D973+/2Rkcip7KZxGcvSHTSAug0gI+6cnWgJOUlIScnBxUVFQELa+oqEB+fn6zz1u6dCkmT56MP/3pT/jRj37U6u+xLAvV1dXIyMhoc5uJ2kpacZc2eJqK71N8qXyipPIJpiS2X6KaMWMGioqKMGTIEOTl5eGFF15ATU0Npk6dCuDs/TGff/45Xn75ZQBnw80dd9yBJ598Erm5uYHZnwsvvBAejwcAMGfOHOTm5uLyyy+H3+/HU089herqajzzzDN2bw6RVjhoyiLpclWng5aIT1SRvmwPOIWFhTh27Bjmzp2Luro6DBgwAKtWrUJWVhYAoK6uLug7cZ5//nl8++23+NWvfoVf/epXgeWTJk3C4sWLAQAnTpzAz3/+c3i9Xng8HgwaNAgbN27E0KFD7d4ccpjKZ12ArNkbhhuZJIUcIifZ/j04Kormc/Q6au17cFSeHmXAiQ+GG9kkBRzVZ3FUvdk4ku/D4ffgOPg9OEQmkRJuSD4GVP2pfKIpBQMOiaH67I0UHBz1IOV9VD34s67oiwGHgvCsITaqF/EmUgbFtkjacwhJew453YyEMOH9JIpVQr7oj6iteJbVdjoPhuECzbnLGvv0SGRz6Dz8RFVsDhzqGtXfpqJgDDhEbSRl9kY30czS6Bx2+KmqtuM3G+uJl6iIDKDb7E1bLkHpeAlLwvvLEwFKNM7gUICq99+ofHlKQtGWMPhFKp7BpOm1dJvRIaKzOINDRMqzc9ZFlxkdCUFW5RMCVU+kVD3xlIAzOEQakzDotSSRwUOHGR3ej0P0Hc7gEACeJcRC5bNRgOFG2u81her9hvTBgENKU3XamOzldMhw+ve3hfRgSxQvDDhEMVD9LFTqIKfS/TCqtIMSR9UTKs6wx4YBh4iUoGKgUClwRUP1gKv6CQLpgQGHlKXq2ZTqVB/cwlE9RKjePiIKxYBDnP6MEs8+40tKeJDSziYSg64KeGKlDwYcIo1IG9SkhQZp7VUZTxSiwxPR6DHgEJEjpIYFSe2WFniJ4okBh5Sk6jSxymedkgYzSSEhHOntJzIBAw4RJZQu4UDKdqgcfFU9YVD1BIuiw4BjOF7X1YPKg9i5pISCSOm2PUQ6YcAhooRgGHCOlABMFE8MOEQRUnU6XQKdw43O20Zq4Yx7dBhwSDm8/h0dnp07jyEndjxxILsw4BCRrUwZ/FXfTgbh6PBESz4GHKIIqHqWqfqgpfqgT0T6YsAxGK/nEsWX6oFO9UBMFE8MOERkC9UHe7uYut1toeoMKcnGgENK4XVvIqLmceY9cgw4REKpfLnB9FkM07efSAUMOESt4PQ56UTlYKwazijLxoBDRHHF2YuzuB+iwxMJijcGHCKBVD0L56BORKpgwCEisomqgU/VgEwUTww4huKd+EREpDMGHFIGb+iTTdXZCiIyU0ICzoIFC9C7d28kJycjJycHmzZtanH9DRs2ICcnB8nJybj00kvx3HPPhaxTXl6Ofv36we12o1+/fli5cqVdzSeD8cZHaisGPyJn2B5wli9fjpKSEsyaNQtVVVUYPnw4xo4di5qamrDr79+/H9dffz2GDx+Oqqoq3H///Zg2bRrKy8sD61RWVqKwsBBFRUXYtWsXioqKMH78eGzdutXuzSFyHO+fICJqncuyLFtPUYcNG4bBgwfj2WefDSzr27cvbr75ZpSVlYWsf++99+KNN97A7t27A8umTp2KXbt2obKyEgBQWFgIv9+Pt956K7DOmDFj0LlzZyxdujTkNRsaGtDQ8N2g4Pf7kZmZCZ/Ph5SUlLhspyQj3p6p5D04Kl6iUnEGR8WAw1mKljX26eF0E0L4LnM73YSw6rNcTjchREPPRqebEKJXjyNYP/JRp5uRcH6/Hx6PJ6Lx29YZnMbGRuzYsQMFBQVBywsKCrB58+awz6msrAxZf/To0di+fTtOnTrV4jrNvWZZWRk8Hk/gkZmZGesmERFFTcUAqGJQJoonWwPO0aNHcfr0aaSlpQUtT0tLg9frDfscr9cbdv1vv/0WR48ebXGd5l6ztLQUPp8v8KitrY11k4iIosYZHKLEa5+IX+JyBU85WpYVsqy19c9fHs1rut1uuN3szER2aezTQ8lZCpKFl6ci06vHEaebIIKtMzipqalo165dyMzK4cOHQ2ZgmqSnp4ddv3379ujSpUuL6zT3mkQ64Zk3EVHrbA04SUlJyMnJQUVFRdDyiooK5Ofnh31OXl5eyPpr1qzBkCFD0KFDhxbXae41iWKl4hklyaLi5SkiE9j+MfEZM2bgD3/4AxYtWoTdu3dj+vTpqKmpwdSpUwGcvT/mjjvuCKw/depUHDx4EDNmzMDu3buxaNEiLFy4EDNnzgysc/fdd2PNmjWYN28e9uzZg3nz5mHt2rUoKSmxe3PIRipOBVPkOJATkUpsvwensLAQx44dw9y5c1FXV4cBAwZg1apVyMrKAgDU1dUFfSdO7969sWrVKkyfPh3PPPMMunfvjqeeego//vGPA+vk5+dj2bJleOCBB/Dggw/isssuw/LlyzFs2DC7N0cbvXocUfKj4kRERPFg+/fgqCiaz9HraMTbZ2fDVAw4/C6cyKj6EV/eaBxM1VktVe/jUvGSsIozy003GfN7cBz8HhwisoeqA5SqAzoRmYcBh4jiiiHnLO6H6Kg4e0OyMeAQtYKFl3Si6uyfilS8PEWRY8AhEkrlgcr02QvTt59IBQw4pBSeMRERNY/fYhw5BhwisoWpsximbndb8DIw2YEBx2A8EyCKL9XDjcqXNYnijQGHKAKqnmGqPmCpPuATkb4YcIjIVqaEHNW3U/UwrBreDygfAw4ph4UlOhIGLtUH/7bSffvspOrsKMnHgEMUIRZiCofhhhKF901GhwGHSAOcxaGWSDg+iOKNAcdwPCOgRNIt5Oi2PUQ6YcAhooTSJRRI2Q6VZ29UvezL+wD1wIBDSlK1wKhakAG1B7LzSQkHzZHefiITMOAQkSOkhgRJ7ZYUeonijQGHSCPSBjRJYQGQ116VqTwbqiLeLxk9Bhxix4kSC3N8SQkNUtrZRFrYVYWql8cpegw4pCwWmthIHNhUDw+qt4+IQjHgEJESVAwRjX16KNmu1qgecjkLSonAgEMUA9ULtOoDXHNUChSqtIMSR9VZY95GEBsGHFKaqgVHAqkhB3A+XDj9+9tC9fdd9ZMD0gcDDgHgGUIsWKjt5UTIUGkGiYjahgGHSGOqn823JpGBQ4dgI/39Joqn9k43gEiy+iwXOh20nG6G9prCR9KeQ7a9tnQSwo3Ks56qXg7n7HrsOINDAap2JFULjxQSBr5IxXNGh5ejiPTGGRwiA/guc8PzaYPTzYibtszo6BhqJIRYlWdvSE8MOERtxMtUzjk3rLQUdnQMNU0khBvVcZZYTww4JEJDz0a4a5KcboZous3inO/8sKNzqJGGszexUfW2ASl4Dw4FYYeKjZQCbsrZvinhxpT3kygWDDgkBqeR44ODoh6kvI+qh3/WFX0x4BDFieqFnPQhJdxQ7Dib3nYMOBSCHUt/HCDlkvTeqR76OXujNwYcEkX1gqR6QT+XpIGSiChaDDhEBmPIkUXS+yUp7JOebA04x48fR1FRETweDzweD4qKinDixIlm1z916hTuvfdeDBw4EBdddBG6d++OO+64A1988UXQeiNGjIDL5Qp6TJgwwc5NIYqYtMIuadA0Gd+n+FJ5Npi3CcSHrQFn4sSJqK6uxurVq7F69WpUV1ejqKio2fX/8Y9/YOfOnXjwwQexc+dOrFixAp988gluvPHGkHWLi4tRV1cXeDz//PN2bopxVO5gKhcmIjtICzfSQj7pybYv+tu9ezdWr16NLVu2YNiwYQCAF198EXl5edi7dy+ys7NDnuPxeFBRURG07Omnn8bQoUNRU1ODnj17BpZ37NgR6enpdjWfqE2kfbux7l8CKJm0cCMBT5LMYNsMTmVlJTweTyDcAEBubi48Hg82b94c8ev4fD64XC5cfPHFQcuXLFmC1NRU9O/fHzNnzkR9fX2zr9HQ0AC/3x/0oNZxFqdtpJ3FciBVj8T3RNpxrxqV6640ts3geL1edOvWLWR5t27d4PV6I3qNb775Bvfddx8mTpyIlJSUwPLbb78dvXv3Rnp6Oj788EOUlpZi165dIbM/TcrKyjBnzpzYNoTIIJzJUYfEcCOBhJMjio+oZ3Bmz54dcoPv+Y/t27cDAFyu0CRvWVbY5ec7deoUJkyYgDNnzmDBggVBPysuLsaoUaMwYMAATJgwAa+99hrWrl2LnTt3hn2t0tJS+Hy+wKO2tjbazSYFSShUEs9mObA6T+p7IPF4Vwlnb+Ir6hmcu+66q9VPLPXq1Qvvv/8+vvzyy5CfHTlyBGlpaS0+/9SpUxg/fjz279+Pd955J2j2JpzBgwejQ4cO2LdvHwYPHhzyc7fbDbdbZsFwWq8eR3DgUFenm0EJxpkc5zDcEMVH1AEnNTUVqampra6Xl5cHn8+H9957D0OHDgUAbN26FT6fD/n5+c0+rync7Nu3D+vWrUOXLl1a/V0fffQRTp06hYyMjMg3hLQg4a+MS7vhuAlDTuJJDTdSSJj1pfix7Sbjvn37YsyYMSguLsaWLVuwZcsWFBcXY9y4cUGfoOrTpw9WrlwJAPj222/xk5/8BNu3b8eSJUtw+vRpeL1eeL1eNDaePTA//fRTzJ07F9u3b8eBAwewatUq3HbbbRg0aBCuvvpquzaHqE2knt1ywE0cyfta6vGtEl6eij9bvwdnyZIlGDhwIAoKClBQUIArr7wSr7zyStA6e/fuhc/nAwAcOnQIb7zxBg4dOoR/+7d/Q0ZGRuDR9MmrpKQkvP322xg9ejSys7Mxbdo0FBQUYO3atWjXrp2dm2MsdjyzSR54pZC8j6WEG87emMe2T1EBwCWXXIJXX321xXUs67up+169egX9P5zMzExs2LAhLu0jPUi4TAXIvVQFfDcA85JV/EkON0Qq49+iIkogKWe7zeFgHF/S96eU41n12RvOktuDAYcionoHVL2A6UT6oKwC32Vu8ftRSrghczHgECWYDgOD9MHZSdx3iaX6yY/qJ4+SMeBQxFTviKoXsnPpEnI4WEdHl/2lw/FL+mPAIaI20WXQtpsu+0lSuJF00kPxx4BDWpFU0CQNFK3RZfC2A2e6qDmqz4pLx4BDUZHQIRlynMGBPJRu+0PS8SqpDpA9bP0eHCJqneTvxwmH35mjX7ABZIUbCSScLErHGRyKmoSOKe3sTcfBQ8dBPhI6bre041Na/yd7cAaHiGxj0myOjsEGYLixg4STRB1wBodiIqGDSih055I2kERD5/tzdN42IskYcIgUonPIAfSa5TAh2Eg7HqWd1JC9GHAoZpzFsYe0QSVa0oOB9PZHSvfj0CkS6qYuGHBIeww5apIWFKS1ty0kHn8S+znZiwGH2oRnI/aROMjEoik4qBoeVG6bHSQed1LCDetlYvFTVGSEhp6NcNckOd0MaoUqn7oyKdCcS2K4IWoOZ3CozaSclUg5yzuXqQOOU7M6ps3WnEvqsSalX0upkzrhDA6R4nT7puNonRs47JjZMTXQnEtquCFqCQMOxUWvHkdw4FBXp5vRKqmXqkwPOU3iEXYYaIJJDjecvaGWMOAQCcGQE6y5oHJu8GGY0ZeUcEPO4T04FDdSzlIkF0bJZ9uJovqnslTC48l+UuqijhhwyEgMOWSy+iyX6ONIcv+lxGHAobiSdLYiuUhKHpzIWdKPHUn9VlI91BEDDpFQ0gcqSjzpxwzDDUWDAYfiTlLHllQww5E+YFHi8Fgh0zDgkPF0CDkcvKglOhwfkvqppJM8nTHgkC3YwRNPh0GM4k+H40JSuCF1MOAQQZ8CqsNgRvHD4yHxeHKnDgYcso20js6QQzrR5TjQpV9S4jHgkK0Ycpyhy+BG0dPpnixp/VFavdMdAw7ReaQV1eboNNBRZHR6v6X1Q4Yb9TDgkO3Y8Z2l06BHzdPpfZYWbkhNDDhEYehWYHUa/CgU319n8SROTQw4lBASCwBDDqlOx8uQuvU7cg4DDiUMQ47zdBwQTaXj+yixv0msa6awNeAcP34cRUVF8Hg88Hg8KCoqwokTJ1p8zuTJk+FyuYIeubm5Qes0NDTg17/+NVJTU3HRRRfhxhtvxKFDh2zcEjKZxKLbGh0HR1PoGlIl9jOGG7XZGnAmTpyI6upqrF69GqtXr0Z1dTWKiopafd6YMWNQV1cXeKxatSro5yUlJVi5ciWWLVuGd999FydPnsS4ceNw+vRpuzaF4oQFQR26DpQ60/X9khhuSH3t7Xrh3bt3Y/Xq1diyZQuGDRsGAHjxxReRl5eHvXv3Ijs7u9nnut1upKenh/2Zz+fDwoUL8corr2DUqFEAgFdffRWZmZlYu3YtRo8eHf+Nobjq1eMIDhzq6nQzotLQsxHumiSnm2GL+iwXOh20nG4GtYLhRi08WVOfbTM4lZWV8Hg8gXADALm5ufB4PNi8eXOLz12/fj26deuGK664AsXFxTh8+HDgZzt27MCpU6dQUFAQWNa9e3cMGDCg2ddtaGiA3+8PehBFS2ohjgRnc9Sl83sjtU8x3MhgW8Dxer3o1q1byPJu3brB6/U2+7yxY8diyZIleOedd/DYY49h27Zt+OEPf4iGhobA6yYlJaFz585Bz0tLS2v2dcvKygL3AXk8HmRmZrZhyygepBYIqQU5UroOpBLpHGyIEiHqgDN79uyQm4DPf2zfvh0A4HKFdk7LssIub1JYWIgf/ehHGDBgAG644Qa89dZb+OSTT/Dmm2+22K6WXre0tBQ+ny/wqK2tjWKLyS4MOWriwOo8E/a/1H4ktW6ZKOp7cO666y5MmDChxXV69eqF999/H19++WXIz44cOYK0tLSIf19GRgaysrKwb98+AEB6ejoaGxtx/PjxoFmcw4cPIz8/P+xruN1uuN3uiH8nUWt0vienSdMgy/tzEseEYAMw3FBiRB1wUlNTkZqa2up6eXl58Pl8eO+99zB06FAAwNatW+Hz+ZoNIuEcO3YMtbW1yMjIAADk5OSgQ4cOqKiowPjx4wEAdXV1+PDDD/HII49EuznkMIk3HDcxIeQAvAk5EUwJNoDccEPy2HYPTt++fTFmzBgUFxdjy5Yt2LJlC4qLizFu3LigT1D16dMHK1euBACcPHkSM2fORGVlJQ4cOID169fjhhtuQGpqKm655RYAgMfjwZQpU3DPPffg7bffRlVVFX72s59h4MCBgU9VkSySz4pMKda8bGUP0/ar5P4iuU6ZyraPiQPAkiVLMG3atMAnnm688UbMnz8/aJ29e/fC5/MBANq1a4cPPvgAL7/8Mk6cOIGMjAxce+21WL58OTp16hR4zu9//3u0b98e48ePxz//+U+MHDkSixcvRrt27ezcHKKwTJnJAXjZKl5MCjVNGG4o0VyWZRlXqfx+PzweD3w+H1JSUpxuTsKNeHum000IS+qlqiamhJxzMehEj+FGHlUDzvqRjzrdhISLZvzm36IiZahaRCIlvYjHwrRLLG1h6r6S3i+k1yWTMeCQUqQXE+nFPFamDt6tadovpu4b6f1Bej0yna334BCZyKR7cs537kBu8uUrUwPNuRhuyGmcwSHl6FBYpBf3eDBx5sLEbQ6Hxz+pgDM4pCTJ34/TxOSZnHPpPqvDQBNMh3Cjw0kWMeCQwhhy9KNL2GGoCY/hhlTCgENkM4ac8KSFHYaa5ukQbACGG90w4JDSdJjFARhyWqNq2GGoaR3DDamKAYeUp1PIAcz8QsBonB8qEhl4GGiio0u4IT0x4JAIuoQcgLM50QoXOuIRehhm2kancMPZGz0x4BjouvQ9qPD2cboZUWPIoSbNhZNwwYdBJv4Ybpx1Xfoep5sgAgOOoaSGHJ0w5MQfw4z9GG5ICn7Rn8EkngXoVpB0GixIbw09G7U6XqXWEol12ykMOCSO1MLUHN0GDtIPj081MNxEhwHHcFI7jG4hB+AgQmrS8bjUsX5QKAYcYshRiI6DCcml4/EotW5IrdNOYsAh0aQWq5boOKiQLLpeNpVaLxhuYsOAQwBkdyCpRaslug4wpD5djzsd6wS1jAGHAhhy1KPrYEPq0TlUS64Pkuuy0xhwiBSn88BDatD5+GK4MRcDDgWR3KEkF7JI6DwIkXN0Pq50rwnUMgYcCsGQoy7O5lC86H4sSa8FkuuwKhhwKCzJnUt6YYuEzgMT2U/340d6DZBcf1XCgENakl7gIqH7GTjFnwnHjPS+z3ATPww41CzpHU16oYuU7gMWtZ0JwQYwp89TZBhwqEUMOTKYMoBR9Ew5LnTo69LrrWoYcKhV0judDoUvUgw61MSkY0GHPi69zqqIAYeMoEMBjIYpAxuFMinYAHr0bYYbezDgUER06IA6FMJomDbQkXnBVoc+rUNtVRUDDkVMh46oQ0GMFoOO/kx8j03syxQdBhyKCkOOXCYOgroz9T3VpQ/rUE9VxoBDUdOhU/bqcUSbIhktUwdFnZj8HurSb3Woo6pjwCGj6VIsY2HyICmV6e+ZLv2V4SYxGHAoJjp1UF2KZqxMHzQl4HukTz/VqXaqjgGHYqZTR9WleLYFB1H18D05i/2TYmFrwDl+/DiKiorg8Xjg8XhQVFSEEydOtPgcl8sV9vHf//3fgXVGjBgR8vMJEybYuSnUDIYc/XBQdR7fg+/o1C91qpcS2BpwJk6ciOrqaqxevRqrV69GdXU1ioqKWnxOXV1d0GPRokVwuVz48Y9/HLRecXFx0HrPP/+8nZtCLdCp0+pUTNuKg2xiNe1v7vPv6NQfdaqTUrS364V3796N1atXY8uWLRg2bBgA4MUXX0ReXh727t2L7OzssM9LT08P+v/rr7+Oa6+9FpdeemnQ8o4dO4asS865Ln0PKrx9nG5GXPTqcQQHDnV1uhnKOHfAddckOdgSPTHQhNIp2AAMN06xbQansrISHo8nEG4AIDc3Fx6PB5s3b47oNb788ku8+eabmDJlSsjPlixZgtTUVPTv3x8zZ85EfX19s6/T0NAAv98f9CBqiW4FNl44wxAfnK1pnm59j+HGObbN4Hi9XnTr1i1kebdu3eD1eiN6jT/+8Y/o1KkTbr311qDlt99+O3r37o309HR8+OGHKC0txa5du1BRURH2dcrKyjBnzpzoN4KiotMsDvBdoeVsTijO6sSGgaZlDDcUT1HP4MyePbvZG4GbHtu3bwdw9obh81mWFXZ5OIsWLcLtt9+O5OTkoOXFxcUYNWoUBgwYgAkTJuC1117D2rVrsXPnzrCvU1paCp/PF3jU1tZGudUUKR07tG5FN944G9Ey7p/IsJ9RvEU9g3PXXXe1+omlXr164f3338eXX34Z8rMjR44gLS2t1d+zadMm7N27F8uXL2913cGDB6NDhw7Yt28fBg8eHPJzt9sNt9vd6utQfOg2kwPwvpxIcWbnLIaZ6OgYbnQ82ZMm6oCTmpqK1NTUVtfLy8uDz+fDe++9h6FDhwIAtm7dCp/Ph/z8/Fafv3DhQuTk5OCqq65qdd2PPvoIp06dQkZGRusbQAnBkEOmhR2GmujpGGwAhhtV2HaTcd++fTFmzBgUFxdjy5Yt2LJlC4qLizFu3LigT1D16dMHK1euDHqu3+/H//zP/+DOO+8Med1PP/0Uc+fOxfbt23HgwAGsWrUKt912GwYNGoSrr77ars2hGOjYyU3+G1Ztce5lGl2CgI7blEi69iMd655Utn4PzpIlSzBw4EAUFBSgoKAAV155JV555ZWgdfbu3Qufzxe0bNmyZbAsCz/96U9DXjMpKQlvv/02Ro8ejezsbEybNg0FBQVYu3Yt2rVrZ+fmUAx07ey6FudEkRgOJLZZVbr2H13rnVQuy7IspxuRaH6/Hx6PBz6fDykpKU43J+Ee/mhcwn+nbpermvCSlT2cvqTFAGMPXYMN4Ey4mdX/fxP+O50Wzfht28fEic6l4z05AD9KbpfmAoYdwYdhJjEYbijRGHAoYXQNOQBvQE6U1sJIuADEAOM8hhtyAgMOJRRDDtmJYUYtOgcbgOFGdbbeZEwUjs5FgZ+yIjpL936gcx3TBQMOOUL34qB7cSdqjgkhX/f6pQsGHHKM7kXChEJPdC4Tjnfd65ZOGHDIUSYUCxOKPpnNlDBvQr3SCQMOOc6EomHKAEDmMeW4NqFO6YYBh5RgSvEwZTAg/ZkU2k2pT7phwCFlmFJETBoYSE8mHb+m1CUdMeCQUkwqJgw6JI1px6xJ9UhHDDikHNOKikkDBslkWrABzKtDOmLAISWZVlxMHEBIBhOPS9Pqj64YcEhZJhYZBh1ShanHool1R1cMOKQ0U4uNiQMLqcHUYAOYW290xYBDyjO16Jg80JAzTD7eTK0zOuNfEycRdP4r5K1pGnT4l8rJLgw2pCPO4JAYphcizuhQvJl+TJleU3THGRwSpakgmTqbA3BGh9rO5FDThOFGf5zBIZFYnHj2TdHjMXMW64cZGHBILBapszhoUWt4jHyHdcMcDDgkGovVdziI0fl4TARjvTAL78Eh8Uz+hFU4vEeHGGqCMdiYiQHHQD9J2YnX/IOdbkZc8ebjUAw6ZmGoCU/XcPOTlJ1ON0F5vERlKF07h67FrC2aLlNwANQT39vm6VoPdK3f8caAYzBdO4muRS0eOBjqg+9ly3StA7rWbTvwEpXhdLxcBfC+nNacOzDyEpYcDDSt0zXYAAw30eIMDmnbaa5L36N1sYsXzgSoj+9RZHTu77rWaTsx4BAAvTuPzkUvnnivjlr4fkRH536uc322Ey9RUYCul6sAXrKKFi9hOYNhJno6BxuA4aYtGHAoiO4hB+BHyaPFsGMvhprYMdxQS3iJikLo3ql0L4p24mWT+OB+bDvd+7HudTgROINDYek8kwPwklU8cGYncgwy8aN7sAEYbuKFAYeaZULIAXjJKh7OH8BNDzwMNPZguKFoMOBQi3QPOQBnc+xgYuBhqLGPCcEGYLiJNwYcapUpIQfgbI5dwg3+kkMPw0ziMNxQrGy9yfjhhx9Gfn4+OnbsiIsvvjii51iWhdmzZ6N79+648MILMWLECHz00UdB6zQ0NODXv/41UlNTcdFFF+HGG2/EoUOHbNgCamJK5zOlmKrg3BttVb7hVko7dWPSF3WaUl8TzdaA09jYiNtuuw2//OUvI37OI488gscffxzz58/Htm3bkJ6ejuuuuw719fWBdUpKSrBy5UosW7YM7777Lk6ePIlx48bh9OnTdmwG/T9TOqFJhVU14cJEogKFk7+bgpnU/0ypq05wWZZl2f1LFi9ejJKSEpw4caLF9SzLQvfu3VFSUoJ7770XwNnZmrS0NMybNw+/+MUv4PP50LVrV7zyyisoLCwEAHzxxRfIzMzEqlWrMHr06JDXbWhoQENDQ+D/Pp8PPXv2RG1tLVJSUuK3oULsO3RFm57/l/qr4tQS9a37sm37ihKn5ovUsMt7dj+a4JZQrK5N+8TpJiTUzZ12ten5l/cwa38BgN/vR2ZmJk6cOAGPx9PyylYCvPTSS5bH42l1vU8//dQCYO3cuTNo+Y033mjdcccdlmVZ1ttvv20BsL766qugda688krrN7/5TdjXfeihhywAfPDBBx988MGHBo/a2tpWM4VSNxl7vV4AQFpaWtDytLQ0HDx4MLBOUlISOnfuHLJO0/PPV1paihkzZgT+f+bMGXz11Vfo0qULXC5XPDdBhKYEbOoMVqJxfycW93ficZ8nlsn727Is1NfXo3v37q2uG3XAmT17NubMmdPiOtu2bcOQIUOifemA80OHZVmtBpGW1nG73XC73UHLIr3pWWcpKSnGdQ4ncX8nFvd34nGfJ5ap+7vVS1P/L+qAc9ddd2HChAktrtOrV69oXxYAkJ6eDuDsLE1GRkZg+eHDhwOzOunp6WhsbMTx48eDZnEOHz6M/Pz8mH4vERER6SXqgJOamorU1PA387VV7969kZ6ejoqKCgwaNAjA2U9ibdiwAfPmzQMA5OTkoEOHDqioqMD48eMBAHV1dfjwww/xyCOP2NIuIiIiksXWe3Bqamrw1VdfoaamBqdPn0Z1dTUA4F//9V/xve99DwDQp08flJWV4ZZbboHL5UJJSQl++9vf4vLLL8fll1+O3/72t+jYsSMmTpwI4OzU1JQpU3DPPfegS5cuuOSSSzBz5kwMHDgQo0aNsnNztOF2u/HQQw+FXLYje3B/Jxb3d+JxnycW93dkbP2Y+OTJk/HHP/4xZPm6deswYsSIsw1wufDSSy9h8uTJAM7eSzNnzhw8//zzOH78OIYNG4ZnnnkGAwYMCDz/m2++wX/+53/iT3/6E/75z39i5MiRWLBgATIzM+3aFCIiIhIkId+DQ0RERJRItn6TMREREZETGHCIiIhIOww4REREpB0GHCIiItIOA44BHn74YeTn56Njx44Rf4OzZVmYPXs2unfvjgsvvBAjRozARx99ZG9DNXL8+HEUFRXB4/HA4/GgqKio1T82O3nyZLhcrqBHbm5uYhoszIIFC9C7d28kJycjJycHmzZtanH9DRs2ICcnB8nJybj00kvx3HPPJail+ohmn69fvz7kWHa5XNizx5y/Et4WGzduxA033IDu3bvD5XLhL3/5S6vP4TEeigHHAI2Njbjtttvwy1/+MuLnPPLII3j88ccxf/58bNu2Denp6bjuuutQX19vY0v1MXHiRFRXV2P16tVYvXo1qqurUVRU1OrzxowZg7q6usBj1apVCWitLMuXL0dJSQlmzZqFqqoqDB8+HGPHjkVNTU3Y9ffv34/rr78ew4cPR1VVFe6//35MmzYN5eXlCW65XNHu8yZ79+4NOp4vv/zyBLVYtq+//hpXXXUV5s+fH9H6PMab0eqf4yRtRPpX3c+cOWOlp6dbv/vd7wLLvvnmG8vj8VjPPfecjS3Uw8cff2wBsLZs2RJYVllZaQGw9uzZ0+zzJk2aZN10000JaKFsQ4cOtaZOnRq0rE+fPtZ9990Xdv3/+q//svr06RO07Be/+IWVm5trWxt1E+0+X7dunQXAOn78eAJapzcA1sqVK1tch8d4eJzBoRD79++H1+tFQUFBYJnb7cYPfvADbN682cGWyVBZWQmPx4Nhw4YFluXm5sLj8bS6/9avX49u3brhiiuuQHFxMQ4fPmx3c0VpbGzEjh07go5NACgoKGh231ZWVoasP3r0aGzfvh2nTp2yra26iGWfNxk0aBAyMjIwcuRIrFu3zs5mGo3HeHgMOBTC6/UCQOAPnDZJS0sL/Iya5/V60a1bt5Dl3bp1a3H/jR07FkuWLME777yDxx57DNu2bcMPf/hDNDQ02NlcUY4ePYrTp09HdWx6vd6w63/77bc4evSobW3VRSz7PCMjAy+88ALKy8uxYsUKZGdnY+TIkdi4cWMimmwcHuPh2fq3qMg+s2fPxpw5c1pcZ9u2bRgyZEjMv8PlcgX937KskGUmiXSfA6H7Dmh9/xUWFgb+PWDAAAwZMgRZWVl48803ceutt8bYaj1Fe2yGWz/ccmpeNPs8Ozsb2dnZgf/n5eWhtrYWjz76KK655hpb22kqHuOhGHCEuuuuuzBhwoQW1+nVq1dMr52eng7g7FlBRkZGYPnhw4dDzhJMEuk+f//99/Hll1+G/OzIkSNR7b+MjAxkZWVh3759UbdVV6mpqWjXrl3IzEFLx2Z6enrY9du3b48uXbrY1lZdxLLPw8nNzcWrr74a7+YReIw3hwFHqNTUVKSmptry2r1790Z6ejoqKiowaNAgAGevw2/YsAHz5s2z5XdKEOk+z8vLg8/nw3vvvYehQ4cCALZu3Qqfz4f8/PyIf9+xY8dQW1sbFDJNl5SUhJycHFRUVOCWW24JLK+oqMBNN90U9jl5eXn461//GrRszZo1GDJkCDp06GBre3UQyz4Pp6qqiseyTXiMN8PJO5wpMQ4ePGhVVVVZc+bMsb73ve9ZVVVVVlVVlVVfXx9YJzs721qxYkXg/7/73e8sj8djrVixwvrggw+sn/70p1ZGRobl9/ud2ARxxowZY1155ZVWZWWlVVlZaQ0cONAaN25c0Drn7vP6+nrrnnvusTZv3mzt37/fWrdunZWXl2f9y7/8C/f5eZYtW2Z16NDBWrhwofXxxx9bJSUl1kUXXWQdOHDAsizLuu+++6yioqLA+p999pnVsWNHa/r06dbHH39sLVy40OrQoYP12muvObUJ4kS7z3//+99bK1eutD755BPrww8/tO677z4LgFVeXu7UJohSX18fqNMArMcff9yqqqqyDh48aFkWj/FIMeAYYNKkSRaAkMe6desC6wCwXnrppcD/z5w5Yz300ENWenq65Xa7rWuuucb64IMPEt94oY4dO2bdfvvtVqdOnaxOnTpZt99+e8hHZs/d5//4xz+sgoICq2vXrlaHDh2snj17WpMmTbJqamoS33gBnnnmGSsrK8tKSkqyBg8ebG3YsCHws0mTJlk/+MEPgtZfv369NWjQICspKcnq1auX9eyzzya4xfJFs8/nzZtnXXbZZVZycrLVuXNn69///d+tN99804FWy9T0MfvzH5MmTbIsi8d4pFyW9f93IhERERFpgh8TJyIiIu0w4BAREZF2GHCIiIhIOww4REREpB0GHCIiItIOAw4RERFphwGHiIiItMOAQ0RERNphwCEiIiLtMOAQERGRdhhwiIiISDv/By7T/tLYP+L9AAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.axis('equal')\n", "plt.contourf(X0,X1,solution);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Despite the smaller number of updates per point, the system is exactly solved." ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.655973Z", "iopub.status.busy": "2024-04-30T08:50:53.655861Z", "iopub.status.idle": "2024-04-30T08:50:53.658528Z", "shell.execute_reply": "2024-04-30T08:50:53.658286Z" } }, "outputs": [], "source": [ "residue = Scheme(solution,coefs,offsets,gridScale)\n", "assert norm_infinity(residue)<1e-13" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Anisotropic metric\n", "\n", "The numerical schemes above presented apply without modification to non-constant, anisotropic Riemannian metrics." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.659956Z", "iopub.status.busy": "2024-04-30T08:50:53.659860Z", "iopub.status.idle": "2024-04-30T08:50:53.720187Z", "shell.execute_reply": "2024-04-30T08:50:53.719912Z" } }, "outputs": [], "source": [ "#Define the square [-1,1]^2, sampled on a cartesian grid\n", "aX0 = np.linspace(-1,1,51); aX1 = aX0\n", "gridScale=aX0[1]-aX0[0]\n", "X0,X1 = np.meshgrid(aX0,aX1,indexing='ij')\n", "\n", "# Generate the metric\n", "eig1 = np.stack((np.full(X0.shape,1.),(np.pi/2)*np.cos(2*np.pi*X0)))\n", "eig1 /= scipy.linalg.norm(eig1,axis=0) \n", "eig2 = np.stack( (eig1[1],-eig1[0]) ) # Rotate eig1 by pi/2\n", "lambda1, lambda2 = 0.8, 0.2\n", "metric = lambda1**-2*lp.outer_self(eig1) + lambda2**-2*lp.outer_self(eig2)\n", "\n", "# Decompose the tensors dual to the Riemannian metric\n", "coefs, offsets = Selling.Decomposition(lp.inverse(metric))\n", "coefs[:,np.logical_not(np.isnan(bc))] = np.nan" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.721650Z", "iopub.status.busy": "2024-04-30T08:50:53.721555Z", "iopub.status.idle": "2024-04-30T08:50:53.761580Z", "shell.execute_reply": "2024-04-30T08:50:53.761326Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 36.9 ms, sys: 1.03 ms, total: 37.9 ms\n", "Wall time: 38.1 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution_Global,nupdate_Global = GlobalIteration(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.763032Z", "iopub.status.busy": "2024-04-30T08:50:53.762951Z", "iopub.status.idle": "2024-04-30T08:50:53.817579Z", "shell.execute_reply": "2024-04-30T08:50:53.817286Z" } }, "outputs": [ { "data": { "image/png": "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2rJli7V//35r/fr1VnFxsfUv//IvHPN2li9fbnXt2tVatGiR9dFHH1kVFRXWueeeax04cMCyLMu65557rPLy8tD6//M//2N169bNuvPOO62PPvrIWrRokdW1a1fr5ZdfFtUF5SQ65n/84x+tVatWWR9//LH1wQcfWPfcc48FwKqqqhLVBaU0NzeHjtMArIcfftiqra21Dh48aFkW9/F4MeAYYOrUqRaAiMf69etD6wCwnn/++dC/v/76a+vXv/61lZ2dbXm9Xuuyyy6z3n//ffcbr6hjx45ZN998s9W9e3ere/fu1s033xxxy2zbMf/nP/9plZaWWhdccIHVtWtXq2/fvtbUqVOt+vp69xuvgCeffNLKy8uz0tLSrOHDh1sbN24M/W3q1KnWd7/73bD1N2zYYBUUFFhpaWlWv379rKeeesrlFqsvkTFfsGCBddFFF1np6elWjx49rH/7t3+zXn/9dQGtVlPwNvv2j6lTp1qWxX08Xh7L+r8rkYiIiIg0wdvEiYiISDsMOERERKQdBhwiIiLSDgMOERERaYcBh4iIiLTDgENERETaYcAhIiIi7TDgEBERkXYYcIiIiEg7DDhERESkHQYcIiIi0s7/B5triVlWfpVNAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.axis('equal')\n", "plt.contourf(X0,X1,solution_Global);" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:53.819197Z", "iopub.status.busy": "2024-04-30T08:50:53.819085Z", "iopub.status.idle": "2024-04-30T08:50:54.119327Z", "shell.execute_reply": "2024-04-30T08:50:54.119015Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 297 ms, sys: 404 µs, total: 297 ms\n", "Wall time: 298 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution_Sweep,nupdate_Sweep = SweepIteration(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:54.120892Z", "iopub.status.busy": "2024-04-30T08:50:54.120786Z", "iopub.status.idle": "2024-04-30T08:50:54.999916Z", "shell.execute_reply": "2024-04-30T08:50:54.999638Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 873 ms, sys: 407 µs, total: 874 ms\n", "Wall time: 877 ms\n" ] } ], "source": [ "%%time\n", "u0=np.where(np.isnan(bc),np.inf,bc)\n", "solution_FM,nupdate_FM = FastMarching(Update,u0,(coefs,offsets,gridScale))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As before, the number of updates per point is largest for global iteration, and smallest for the fast marching method." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:55.001512Z", "iopub.status.busy": "2024-04-30T08:50:55.001421Z", "iopub.status.idle": "2024-04-30T08:50:55.003351Z", "shell.execute_reply": "2024-04-30T08:50:55.003116Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of updates per point.\n", "Global iteration : 65\n", "Fast sweeping : 48\n", "Fast marching : 3.021914648212226\n" ] } ], "source": [ "print(\"Number of updates per point.\")\n", "print(\"Global iteration :\",nupdate_Global)\n", "print(\"Fast sweeping :\",nupdate_Sweep)\n", "print(\"Fast marching :\",nupdate_FM)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The residue of the fast marching method is zero, up to machine precision. In constrast, the residue of the fast sweeping method and of the global iteration method is expected to be of the order of the prescribed tolerance $\\epsilon$, here we defaulted to $\\epsilon = 1e-6$." ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:55.004744Z", "iopub.status.busy": "2024-04-30T08:50:55.004642Z", "iopub.status.idle": "2024-04-30T08:50:55.008813Z", "shell.execute_reply": "2024-04-30T08:50:55.008565Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Numerical scheme residue.\n", "Global iteration : 2.930180381888192e-06\n", "Fast sweeping : 1.261707627264741e-09\n", "Fast marching : 1.887379141862766e-14\n" ] } ], "source": [ "residue_Global,residue_Sweep,residue_FM = (norm_infinity(Scheme(solution,coefs,offsets,gridScale))\n", " for solution in (solution_Global,solution_Sweep,solution_FM))\n", "print(\"Numerical scheme residue.\")\n", "print(\"Global iteration :\",residue_Global)\n", "print(\"Fast sweeping :\",residue_Sweep)\n", "print(\"Fast marching :\",residue_FM)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. GPU acceleration\n", "\n", "Eikonal equation solvers can take advantage of GPU acceleration. We illustrate here the basic architecture and concepts of such implementations. \n", "\n", "**AGD library eikonal solver.** Note that an eikonal solver with many more features (source factorization, arbitrary dimension, anisotropic metrics, etc) is included in the AGD library. The one presented here is only intended as an introduction and for teaching purposes." ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "execution": { "iopub.execute_input": "2024-04-30T08:50:55.010214Z", "iopub.status.busy": "2024-04-30T08:50:55.010130Z", "iopub.status.idle": "2024-04-30T08:50:55.086154Z", "shell.execute_reply": "2024-04-30T08:50:55.085707Z" } }, "outputs": [ { "ename": "DeliberateNotebookError", "evalue": "Cupy needed for the rest of this notebook", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[31], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mcupy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mcp\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m: \u001b[38;5;28;01mraise\u001b[39;00m ad\u001b[38;5;241m.\u001b[39mDeliberateNotebookError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCupy needed for the rest of this notebook\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'cupy'", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mDeliberateNotebookError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[31], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mcupy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mcp\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m: \u001b[38;5;28;01mraise\u001b[39;00m ad\u001b[38;5;241m.\u001b[39mDeliberateNotebookError(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCupy needed for the rest of this notebook\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mDeliberateNotebookError\u001b[0m: Cupy needed for the rest of this notebook" ] } ], "source": [ "try: import cupy as cp\n", "except ModuleNotFoundError: raise ad.DeliberateNotebookError(\"Cupy needed for the rest of this notebook\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "GPU acceleration is mostly visible on large test cases, preferably in three dimensions. In this illustration, we content ourselves with a two dimensional map of centre Pompidou." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'cp' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mdom\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlogical_and\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlogical_and\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mim\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m==\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mim\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m==\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mim\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m==\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mT\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mcost_map\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwhere\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdom\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minf\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mT\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m \u001b[0mcost_map\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0masarray\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcost_map\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfloat32\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'cp' is not defined" ] } ], "source": [ "im = imread(\"Notebooks_FMM/TestImages/centre_pompidou_800x546.png\")\n", "im = im[68:515,65:770]\n", "dom = np.logical_and(np.logical_and(im[:,:,0]==0,im[:,:,1]==0),im[:,:,2]==255).T\n", "cost_map = np.where(dom,1,np.inf).T\n", "cost_map = cp.asarray(cost_map,dtype=np.float32)" ] }, { "cell_type": "code", "execution_count": 315, "metadata": {}, "outputs": [], "source": [ "plt.contourf(cost_map.get());" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.1 Update operator\n", "\n", "**Update of a point.**\n", "For simplicity, we content ourselfves with the isotropic eikonal equation $\\|\\nabla u\\| = c$ in two dimensions, in contrast with the Riemannian eikonal equation addressed above. We use the standard Rouy-Tourin discretization, reading\n", "$$\n", " \\max\\{0,u(x)-u(x-h e_1), u(x)-u(x+h e_1)\\}^2 + \\max\\{0,u(x)-u(x-h e_2), u(x)-u(x+h e_2)\\}^2 = h^2 c(x)^2,\n", "$$\n", "where $(e_1,e_2)$ is the canonical basis of $R^2$.\n", "\n", "The update operator is defined as the solution $u(x)$ of this equation, when the neighbor values $u(x\\pm h e_1)$ and $u(x\\pm h e_2)$ are fixed.\n", "It can be computed as follows : \n", "* define $v_0 = \\min\\{u(x-h e_1), u(x+h e_1)\\}$, $v_1 = \\min\\{u(x-h e_1), u(x+h e_1)\\}$.\n", "* define $w_0 = \\min\\{v_0,v_1\\}$ and $w_1=\\max\\{v_0,v_1\\}$.\n", "* define $C = h c(x)$.\n", "\n", "The equation to be solved, with unknown $\\lambda = u(x)$, now reads:\n", "$$\n", " (\\lambda-w_0)_+^2 + (\\lambda-w_0)_+^2 = C^2.\n", "$$\n", "The solution $\\lambda$ is :\n", "* $C+w_0$ if this value is smaller than $w_1$.\n", "* the largest root of the quadratic equation $(\\lambda-w_0)^2 + (\\lambda-w_0)^2 = C^2$ otherwise.\n", "\n", "\n", "**Update of a block.**\n", "We group points into blocks of shape $8\\times 8$. Accordingly, a block of $64$ GPU threads loads the solution values at these points as well as their neighbors, into shared memory. Each GPU thread is responsible for one point in the block, and updates its value a in parallel. These updates are repeated a fixed number of times, so that the information can propagate through the block. Afterwards, the block data is written back from shared memory to main GPU memory.\n", "\n", "**Opportunity for optimization.** Loading and writing the solution values accounts for a significant portion of computation time, and could be accelerated by reorganizing the memory layout in such way that the unknowns associated to the $64$ elements of an $8\\times 8$ block are contiguous in memory." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.2 Cuda kernel\n", "\n", "**Main domain.**\n", "The eikonal PDE is solved on a domain of size `xmax_ * ymax_`, whose point coordinates are denoted `x_` and `y_`, with `0 <= x_ < xmax_` and `0 <= y_ < ymax_`.\n", "\n", "**Subdomains.**\n", "A block of threads works on a subdomain of size `10 * 10`, whose point coordinates are denoted `x` and `y`, with `0 <= x <10` and `0 <= y < 10`. The subdomain is divided into two regions:\n", "* the *interior*, where `1 <= x < 9` and `1 <= y < 9`, contains `8*8 = 64` points each handled by one thread within the block. (This means that `64` threads are used.) The values corresponding to these points are loaded, processed and updated.\n", "* the *boundary* which is the complement of the interior. The values corresponding to these points are loaded, but not updated.\n", "\n", "The position of a subdomain is characterized by two variables `X` and `Y`. The interiors of distinct subdomains are disjoints, but they boundaries overlap. The mapping from the subdomain to the domain is `x_ = 8*X + x - 1` and `y_ = 8*Y + y - 1`.\n", "\n", "\n", "\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 350, "metadata": {}, "outputs": [], "source": [ "try: kernel=open(\"TestData/IsoEik_GPU.h\").read()\n", "except FileNotFoundError: import urllib.request; kernel=urllib.request.urlopen(\"https://dl.dropbox.com/s/rzprc0cxn97fss2/IsoEik_GPU.h?dl=0\").read().decode(\"utf-8\")" ] }, { "cell_type": "code", "execution_count": 351, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "const float tol = 1e-8; // Stopping criterion tolerance\n", "const int niter = 8; // Iterations within a block\n", "\n", "extern \"C\" { \n", "__global__ void eikonal_update(float * u_, float * cost_, \n", "int * update_list, char * update_next, int xmax_, int ymax_){\n", "\n", "// Expecting threadDim.x=64, threadDim.y=threadDim.z=blockDim.y=blockDim.z=1\n", "const int tid = threadIdx.x;\n", "const int bid = blockIdx.x;\n", "\n", "\n", "// Blocks are organized in a grid of size Xmax x Ymax\n", "// The current block has position (X,Y) within this grid\n", "const int Xmax = 1+(xmax_-1)/8, Ymax = 1+(ymax_-1)/8;\n", "const float inf = 1./0.;\n", "\n", "__shared__ int X,Y;\n", "__shared__ bool updated; // Wether this block was significantly updated\n", "\n", "if(tid==0){\n", "\tconst long n = update_list[bid];\n", "\tX = n/Ymax; Y = n%Ymax;\n", "\tupdated=false;\n", "}\n", "__syncthreads();\n", "\n", "// Load the solution values at the points of the block and a layer of neighbors\n", "__shared__ float u[10][10];\n", "\n", "for(int r=0; r<2; ++r){\n", "\tconst int m = tid + r*blockDim.x;\n", "\tif(m>=100) break;\n", "\tconst int x = m/10, y=m%10;\n", "\n", "\tconst int x_ = 8*X+x-1, y_ = 8*Y+y-1;\n", "\tu[x][y] = (0<=x_ && x_w1){\n", "\t\tconst float delta = 2.*cost*cost - (w0-w1)*(w0-w1); // Non-negative, up to machine precision\n", "\t\tu_new = (w0+w1 + sqrt(max(0.,delta)) )/2.;\n", "\t}\n", "\n", "\t// Set the new value, if smaller. \n", "\t// (Guaranteed to decrease, up to machine precision, \n", "\t// except at seed points which must be preserved)\n", "\tu[x][y] = min(u[x][y],u_new);\n", "\n", "\t__syncthreads();\n", "} \n", "\n", "// Export the updated values of the solution\n", "if(x_=0) update_next[Ymax*(X-1)+Y]=1;\n", "\tif(Y+1=0) update_next[Ymax*X+(Y-1)]=1;\n", "}\n", "\n", "} // void eikonal_gpu_update\n", "} // extern \"C\"\n" ] } ], "source": [ "print(kernel)" ] }, { "cell_type": "code", "execution_count": 318, "metadata": {}, "outputs": [], "source": [ "eikonal_gpu_update = cp.RawKernel(kernel,'eikonal_update')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.3 Global iteration\n", "\n", "We update here all blocks in parallel, an approach also known as Jacobi iteration. As a result a many of the updates of a given block are actually useless : those before the front reaches the block, and those after the solution values have stabilized on the block. " ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "xseed,yseed = 102,15; cost = cost_map\n", "#xseed,yseed = 3,3; cost = cp.ones((18,23),dtype=np.float32); cost[7,:10]=np.inf #Small instance for testing" ] }, { "cell_type": "code", "execution_count": 320, "metadata": {}, "outputs": [], "source": [ "u = np.full_like(cost,np.inf)\n", "Shape = tuple(int(np.ceil(s/8)) for s in u.shape)\n", "update_next = cp.full(Shape,0,dtype=np.uint8)" ] }, { "cell_type": "code", "execution_count": 327, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Wall time: 165 ms\n" ] } ], "source": [ "%%time\n", "# Make sure the arrays are C-contiguous (silent bug otherwise)\n", "u,cost,update_next = [cp.ascontiguousarray(e) for e in (u,cost,update_next)]\n", "\n", "# Initialize data\n", "u.fill(np.inf); u[xseed,yseed]=0\n", "update_list = cp.arange(np.prod(Shape),dtype=np.int32)\n", "\n", "# Global updates (Bounded number of iterations, just to be safe)\n", "for it in range(2000): \n", " update_next.fill(0)\n", " eikonal_gpu_update( (update_list.size,), (64,), (u,cost,update_list,update_next,*u.shape))\n", " if not np.any(update_next): break" ] }, { "cell_type": "code", "execution_count": 328, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number updates : 209\n", "Number of block udpates : 209\n" ] } ], "source": [ "print(f\"Number of iterations : {it}\")\n", "print(f\"Mean number of block udpates per iteration : {update_list.size}\")" ] }, { "cell_type": "code", "execution_count": 329, "metadata": {}, "outputs": [], "source": [ "plt.axis('equal')\n", "plt.contourf(u.get());" ] }, { "cell_type": "code", "execution_count": 330, "metadata": {}, "outputs": [], "source": [ "u_global = u.copy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The solution has indeed stabilized: further updates have no effect." ] }, { "cell_type": "code", "execution_count": 331, "metadata": {}, "outputs": [], "source": [ "eikonal_gpu_update( (update_list.size,), (64,), (u_global,cost,update_list,update_next,*u.shape))\n", "assert np.nanmax(np.abs(u_global-u)) == 0." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.4 Adaptive Gauss-Siedel iteration\n", "\n", "We update a block when the value at one of its neighbors has changed, following a strategy often referred to as the AGSI. Many other strategies exist, going by the names of fast-sweeping, fast-iterative-method, etc" ] }, { "cell_type": "code", "execution_count": 346, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Wall time: 111 ms\n" ] } ], "source": [ "%%time\n", "# Make sure the arrays are C-contiguous (silent bug otherwise)\n", "u,update_next = [cp.ascontiguousarray(e) for e in (u,update_next)]\n", "\n", "# Initialize data\n", "u.fill(np.inf); u[xseed,yseed]=0\n", "update_next.fill(0); update_next[xseed/8,yseed/8]=1\n", "update_counter = np.zeros_like(update_next,dtype=np.int32)\n", "\n", "# AGSI localized updates (Bounded number of iterations, just to be safe)\n", "for it in range(1000): \n", " update_counter+=update_next\n", " update_list = np.flatnonzero(update_next); update_list = cp.ascontiguousarray(update_list,dtype=np.int32)\n", " if update_list.size==0: break\n", " update_next.fill(0)\n", " eikonal_gpu_update( (update_list.size,), (64,), (u,cost,update_list,update_next,*u.shape))" ] }, { "cell_type": "code", "execution_count": 347, "metadata": {}, "outputs": [], "source": [ "plt.axis('equal')\n", "plt.contourf(u.get());" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Computation time is not much faster in this instance, because it is small and therefore not enough parallelism can be exploited by the GPU. (More substantial differences are obtained with 3D problems.) Nevertheless on can check that the number of bloc updates is significantly reduced. A further computation time reduction could be obtained using the FIM or by adjusting parameters such as the number of iterations per block within the cuda kernel." ] }, { "cell_type": "code", "execution_count": 344, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of iterations 212\n", "Mean number of block updates 43.401685393258425\n" ] } ], "source": [ "print(f\"Number of iterations {it}\")\n", "print(f\"Mean number of block updates\",np.mean(update_counter))" ] }, { "cell_type": "code", "execution_count": 345, "metadata": {}, "outputs": [], "source": [ "plt.contourf(update_counter.get()) \n", "plt.axis('equal'); plt.colorbar();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The solution of the AGSI and of the global update are identical up to small numerical errors." ] }, { "cell_type": "code", "execution_count": 343, "metadata": {}, "outputs": [], "source": [ "assert np.nanmax(np.abs(u_global-u))<1e-3" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }