diff --git a/.gitignore b/.gitignore index bcdc980b..6f2f3be9 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ docs/source/sg_execution_times.rst *.vtu *.vtr *.vtk + +*.npz \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index aaddc29f..ca414194 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -302,6 +302,7 @@ def setup(app): "../../examples/10_normalizer/", "../../examples/11_plurigaussian/", "../../examples/12_sum_model/", + "../../examples/13_mps/", ], # path where to save gallery generated examples "gallery_dirs": [ @@ -318,6 +319,7 @@ def setup(app): "examples/10_normalizer/", "examples/11_plurigaussian/", "examples/12_sum_model/", + "examples/13_mps/", ], # Pattern to search for example files "filename_pattern": r"\.py", diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst index e3b20db1..655a2fce 100644 --- a/docs/source/tutorials.rst +++ b/docs/source/tutorials.rst @@ -24,6 +24,7 @@ explore its whole beauty and power. examples/10_normalizer/index examples/11_plurigaussian/index examples/12_sum_model/index + examples/13_mps/index examples/00_misc/index .. only:: html diff --git a/examples/13_mps/00_mps_overview.ipynb b/examples/13_mps/00_mps_overview.ipynb new file mode 100644 index 00000000..78b4de0b --- /dev/null +++ b/examples/13_mps/00_mps_overview.ipynb @@ -0,0 +1,656 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "40757bdc", + "metadata": {}, + "source": [ + "# Multiple Point Statistics with GSTools\n", + "\n", + "Two-point geostatistics (covariance models, kriging, SRFs) describes spatial\n", + "structure through pairs of points and a variogram. This is powerful, but it\n", + "cannot reproduce *curvilinear* or *connected* features such as meandering\n", + "channels, fractures, or other patterns that depend on the joint configuration\n", + "of many points at once.\n", + "\n", + "**Multiple Point Statistics (MPS)** addresses this by learning patterns\n", + "directly from a **training image (TI)** — an example image deemed\n", + "representative of the spatial structure to simulate. Instead of fitting a\n", + "variogram, MPS borrows whole patterns from the TI.\n", + "\n", + "GSTools provides the **Direct Sampling (DS)** algorithm\n", + "([Mariethoz et al., 2010](https://doi.org/10.1029/2008WR007621)), together\n", + "with the **Direct Sampling Best Candidate (DSBC)** parametrization\n", + "([Juda et al., 2022](https://doi.org/10.1016/j.acags.2022.100091)), through\n", + "two classes:\n", + "\n", + "* `TrainingImage` — the MPS model: the training image plus the distance\n", + " used to compare patterns (the analogue of a `CovModel`).\n", + "* `DirectSampling` — the generator that produces realizations on a\n", + " structured grid (the analogue of `SRF`).\n", + "\n", + "The core idea: to fill each grid cell, DS looks at the values already present\n", + "around it (its *data event*), scans the training image for a location whose\n", + "surroundings look similar enough, and copies that cell's value over.\n", + "\"Similar enough\" is decided by a **distance** between the two surroundings,\n", + "controlled by three parameters — the number of neighbours `n`, the scan\n", + "fraction `f`, and the acceptance threshold `t` (with `t = 0` giving the\n", + "recommended DSBC mode).\n", + "\n", + "This notebook gathers the `examples/13_mps/` gallery into one place and\n", + "builds up from a minimal unconditional simulation to conditioning,\n", + "continuous variables, and a real, published training image:\n", + "\n", + "1. [A first Direct Sampling simulation](#1.-A-first-Direct-Sampling-simulation)\n", + "2. [Conditioning to hard data](#2.-Conditioning-to-hard-data)\n", + "3. [Continuous variables and distance metrics](#3.-Continuous-variables-and-distance-metrics)\n", + "4. [A real training image: the Strebelle channels](#4.-A-real-training-image:-the-Strebelle-channels)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f8e45cf7", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "\n", + "plt.ioff()\n", + "# turn off warnings for a cleaner notebook\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a656ab3f", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib.colors import ListedColormap\n", + "\n", + "import gstools as gs" + ] + }, + { + "cell_type": "markdown", + "id": "5fc8896d", + "metadata": {}, + "source": [ + "## 1. A first Direct Sampling simulation\n", + "\n", + "This is the minimal Multiple Point Statistics example: build a training\n", + "image, wrap it in a `TrainingImage`, and generate one unconditional\n", + "realization with `DirectSampling`.\n", + "\n", + "We use a small, synthetic *channelized* training image generated with\n", + "NumPy, so the example is fast and needs no downloads." + ] + }, + { + "cell_type": "markdown", + "id": "13d2c57d", + "metadata": {}, + "source": [ + "Create a synthetic binary training image with curvilinear \"channels\".\n", + "The two facies (0 and 1) form connected, meandering bands — exactly the kind\n", + "of structure two-point statistics struggles to reproduce." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "46451d6d", + "metadata": {}, + "outputs": [], + "source": [ + "gx, gy = np.meshgrid(np.arange(60), np.arange(60), indexing=\"ij\")\n", + "ti_data = ((np.sin(gx / 5.0) + np.sin((gx + gy) / 8.0)) > 0).astype(float)" + ] + }, + { + "cell_type": "markdown", + "id": "0c28bb1a", + "metadata": {}, + "source": [ + "Wrap the array in a `TrainingImage`. For a categorical variable (facies\n", + "codes) the distance is the fraction of mismatching neighbours, so the\n", + "`distance` argument is ignored here." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "208d0ea1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TrainingImage(shape=(60, 60), categorical=True, distance='l1')\n" + ] + } + ], + "source": [ + "ti = gs.TrainingImage(ti_data, categorical=True)\n", + "print(ti)" + ] + }, + { + "cell_type": "markdown", + "id": "b11a879b", + "metadata": {}, + "source": [ + "Create the `DirectSampling` generator and simulate on a 40x40 grid.\n", + "\n", + "* `n_neighbors` — how many already-known cells define each data event.\n", + "* `scan_fraction` — fraction of the training image scanned per cell\n", + " (smaller is faster, slightly noisier).\n", + "* `threshold=0.0` — the recommended DSBC mode: always take the best match." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2651225f", + "metadata": {}, + "outputs": [], + "source": [ + "ds = gs.DirectSampling(ti, n_neighbors=12, scan_fraction=0.3, threshold=0.0)\n", + "field_simple = ds([np.arange(40, dtype=float)] * 2, seed=20250616)" + ] + }, + { + "cell_type": "markdown", + "id": "ce25b847", + "metadata": {}, + "source": [ + "Plot the training image next to the realization. The realization is not a\n", + "copy of the TI, but it reproduces the same channel patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b3b6f2f8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAICCAYAAADS0a7IAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAASrpJREFUeJzt3QeYVOXZOO6XJggCooKAoGDF3o2IFRRRRGML1tg+NYk1aqJEjSVfQvwM1iTG+MWSaOw/0Rh7YsVuYu+aKCAKKgIiImX+13Pyn/12l13qHnbKfV/XuXbnzJmZM2dmznuetzxvi0KhUEgAAABAk2vZ9E8JAAAACLoBAAAgR1q6AQAAICeCbgAAAMiJoBsAAAByIugGAACAnAi6AQAAICeCbgAAAMiJoBtKwJAhQ9If//jHZnv8kmru1weAanP77bdn5e/06dPnu6459wf4j9b//1+gnnfeeSedcMIJC3Vcfv7zn6fNN998sY/h/fffn7bYYotme/ySau7XB4D6Zs2alYYNG1Zzu1WrVqljx46pV69eaauttkp77LFHat++fYMH7oEHHkj33ntv+vDDD9Pyyy+f1l9//XTggQemHj16lMyB/te//pWVv/E+57euKd14443puuuuS6NHj07t2rVb4P4A/yHohkasvPLK6eSTT66z7rjjjktffvllVuDUttpqqy3RcYyCfY011mi2xy+p5n59AKhvzpw5WRA4cODA9KMf/ShbN3Xq1PTGG2+ks88+Ox177LHpoosuSkcccUTNY2bPnp322muv9PDDD6fvfe97afjw4WnGjBnp6quvTqeffnq67bbbsvtL1X777Zc22GCDtNxyy+Xy/O+99152TOM4Le3XhnIm6IZGdOrUKesmVVvUkEchXn/9klrS52vq/Sm31weAxkTrdP1y6swzz8yC7iOPPDJrsY1W7HD99dene+65J1111VXpv/7rv2q2P+yww9KFF16YPvvss5I+0H369MmWanttKHWCblgCMW5p3333zQrtAQMGpF//+tfptddey7ql77LLLumggw5Kn3/+ebZtmzZtUs+ePbPubLW7u4W4GIhtv/vd787zvNtvv3265JJL0ptvvpkVZtH6Xr9QW9LHh1tvvTXrLhbdwmLf42Ijnm+jjTaqaSFozPxev3///unSSy9N77//ftpyyy2z11922WXT2LFj02WX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Kb388svp/fffT2+++Wa6+eab5wnCP/vss9ShQ4d06aWXplNOOSWNHj260THdM2fOzJaiqVOn5v4eAKDcCboBoEK1adMmtW3bNvv/448/Th988EGd+1944YW09tprZ/8XCoW0//77p8MOOyy9/fbbDT7fyJEj03nnnbcU9hwAKkeLQpSyJSZqzjt37pxSnyEptWyTKkE5ZkeUwROgec7tU6fNTl3WfiFLgtZQN/HF8Yc//CF9//vfzwLvHj16NLjNfffdl3bbbbc0fvz41LNnz4Vq6e7du3dFlddAeV/ryl7O0rSw5bWWbprl5CmgB1i6dthhhzRr1qz01ltvNRp0xzRjYcaMGQ3eH63mxZZzAGDhmDIMACrM66+/nqZNmzZPwrTobt6vX7/s9htvvFFnTHYE3FdeeWXq06dPWn311Zf6PgNApdLSDQAVZsKECdn47E022SR17949vfbaa2nMmDHp8ssvz26HTz75JO2zzz5ps802S127dk0PP/xwllQtkq+1aNGiud8CAFQMQTcAVJhBgwZlQfYDDzyQxo0bl7baaqt0/fXXp5VWWqlmmx133DE9/fTT2TjuCNLPP//8tMsuu6T27ds3674DQKURdANABYr5tr/zne/Md5tIWjp8+PCltk9A5SqVfD2lktANajOmGwAAAHIi6AYAAICcCLoBAAAgJ8Z0NyFjSJr/WJXKeCKgcji3AwBLQks3AAAA5ETQDQAAADkRdAMAAEBOBN0AAACQE0E3AAAAlELQfdlll6VevXrVWdZff/15tnvwwQfTwIED0xprrJGGDBmSnnnmmabcZwAAAKi8KcOmTp2aunXrlu66666adS1b1o3bn3rqqTR06NB07rnnpmHDhqVrr702C8D/8Y9/pHXWWSeVM9PGVPZnZLoxAIDytijXc67tKdnu5csss0ydlu6ePXvWuX/kyJFZkP2Tn/wkbbjhhmnUqFGpb9++2V8AAACoJoscdL/xxhtp3XXXTZtuumk69thj00cffVTn/kcffTTtsssuddZFF/PHHntsyfcWAAAAKrV7+XLLLZfOOuusLIj+4osv0nnnnZc233zz9Oqrr6YVV1wxTZs2LeuC3r179zqPW3nlldP48eMbfd6ZM2dmS1E8BwAAAFRV0H3SSSelFi1a1NwePXp01nX8iiuuyILxuXPn/udJW9d92jZt2qQ5c+Y0+rzRJT0CeAAAAKja7uW1A+5iy/cGG2yQdTkPHTt2TG3btk2ffvppne3idteuXRt93hEjRqQpU6bULGPHjl20dwEAAACVNk93tGz/+9//TiussMJ/nqxly7TFFlukMWPG1Nnu8ccfT1tttVWjzxOBeqdOneosAAAAUFVBd3Qvf//997P/Z8yYkU477bQ0bty4dNhhh9Vsc9xxx6U77rgj/f3vf89u33rrremJJ55IP/jBD5p63wEAAKByxnRvu+222dzbkRTt66+/zrqW33///VnrdtGBBx6YtX5/+9vfzlrCoxU7xnzvtNNOeew/NJk852o0BziULvO0AgB5alEoFAqL+qDIUt6uXbssQVpjZs+enSZPnpx1PW/VqtUiPX9kL+/cuXNKfYak1LLx11jaXJixuATdULpK8dw+ddrs1GXtF7I8J6U85KpUy2uAcj3/U14WtrxepJbuokiYtiCRwXx+ydMAAACg0i1W0A0AAJA3rdGkas9eDgAAADRO0A0AAAA5EXQDAABATozprse4EUrteyXzOTTvbxAAYElo6QYAAICcCLoBAAAgJ4JuAAAAyImgGwAAAHIi6AYAAICcCLoBAAAgJ4JuAAAAyIl5uqFK5xc2/zfQVCY/MCl16tj0lxTOUwBUAi3dAAAAkBNBNwAAAORE0A0AAAA5EXQDAABATgTdAAAAkBNBNwAAAOSk6qYMy2v6JSg3ef4WTPPD0ubcDgCUKi3dAAAAkBNBNwAAAORE0A0AAAA5qbox3QBQLT7//PP0yiuvpI4dO6YNNtggLbPMMvNs89VXX6XnnnsutW7dOm255ZYNblPNY/XlqIDy+b1CqRJ0A0CFiUD6hBNOSPfee2/q169fGjt2bJo+fXq67rrr0i677FKz3cMPP5z233//1KNHjzRz5szscX/5y1/Spptu2qz7DwCVRPdyAKgwEWAPHjw4jRs3Lv39739P77zzTtpzzz3TYYcdVrNNBNgHHnhgti5aw99+++203XbbZesKhUKz7j8AVBJBNwBUmK5du6bhw4enli3/r5jfZJNN0hdffJHmzp2b3b7//vvTxIkT049+9KOabX784x+nt956Kz3zzDPNst8AUIkqsnu5MSVQvr9B4yeh6bzwwgtp/Pjx6f33308XXXRRGjVqVE0g/tJLL6WVV145de/evWb7jTfeOLs/7tt6663neb7ogh5L0dSpU31cAFCNQTcAkNKDDz6YHn300az1OsZtb7XVVjWHJVq9V1hhhTqHKQLuzp07Z/c1ZOTIkem8885zaAFgEeheDgAV6owzzsiSqb333ntp++23z8Z5F1unI0t5jOuub8aMGY1mMB8xYkSaMmVKzRIJ2gCA+RN0A0CFa9GiRTrmmGOyKcRefPHFbF3fvn3TJ598kmbNmlWz3aRJk9LXX3+d3deQtm3bpk6dOtVZAID5E3QDQIWJ4Lq+yFAeopt5iFbvCLDvu+++mm1uu+221K5du7Tjjjsuxb0FgMpmTDcAVJh77rknXXvttWmvvfbKEqW99tpr6bLLLktHH310WmuttbJt1lhjjXT88ceno446Kp1zzjlZAB5/zz777LT88ss391sAgIoh6AaACnPIIYektddeO910003p8ccfT6usskrWij1w4MA621166aVps802y1q7W7dunQXq++23X7PtNwBUohaFQqGQSkwkeYnsqanPkJRatlnkx5syDKjPVGTlr5rO7VOnzU5d1n4hS1ZWyuOmi+X15Lc3T506qsdfVM5LVOs5ulS++3ntczWVV9Vu6kKW18Z0AwAAQE4E3QAAAJATQTcAAADkRNANAAAAORF0AwAAQE4E3QAAAJATQTcAAADkpCwn1TT3HbA0zxulMp9opXNuBwAqkZZuAAAAyImgGwAAAHJSlt3LAQDKXakMqTCEBt8jyJeWbgAAAMiJoBsAAAByIugGAACAnBjTDdCM4y6NpQQAqGxaugEAACAngm4AAADIiaAbAAAAciLoBgAAgJwIugEAACAngm4AAADIiaAbAAAAqnGe7skPTEqdOpb0LgKU5BzgpTr/d55zngP5/y5L9dwCi2NRvs/KL5aElm4AAADIiaAbAAAAciLoBgAAgJwIugEAACAngm4AAAAotaD7mmuuScsvv3w67LDD5rnvzjvvTFtttVVaeeWV07bbbpseeeSRJd1PAAAAKDuLNR/Xm2++mX7605+m1VdfPU2fPr3OfY8++mjab7/90qhRo9KwYcOy4HzIkCHp+eefTxtssEFT7TcAOU1tYkogAIBmbOn++uuv0/Dhw9NFF12UevbsOc/9F1xwQdp1113TiSeemPr27ZvOP//8tO6662bbAwAAQDVZ5KD7lFNOSZtuumnaf//9G7z/iSeeSIMGDaqzbpdddsnWAwAAQDVZpO7ld9xxR7r//vvTiy++2OD9U6dOTdOmTcvGctfWrVu39NFHHzX6vDNnzsyW2s8DAAAAVdPSPW7cuHTsscem66+/PnXs2HH+T9qy7tO2bt06FQqFRrcfOXJk6ty5c83Su3fvhd0tAAAAKP+W7kiE9tlnn6XddtutZl0kUWvRokWWxfzll1/OguVll102ffrpp3UeO3HixKy1uzEjRozIuq3XbukWeAMAAFA1QffQoUOzoLu2Aw44ILVt2zZdd911qVOnTlkAvuWWW6bHH388HX/88XUymn/rW99q9LnjOWIBAACAquxe3qZNm6xFu/YS3caL64tdyk866aQ0evTodNddd6XZs2dnU4Y988wz6YQTTsjzfQAAAEBlzNM9P/vss0/61a9+lY466qg0efLk1L1796wlfMCAAU39UgDkwBzfAAAlEnTffPPNWZfy+qJVO5YZM2ZkY7wBAACgGi1R0N2hQ4f53i/gBgAAoJot9JhuAAAAYNEIugEAACAngm4AAADIiaAbAAAAymXKsKbUZXDXlFq2WarT3QAwf60GdGuWxzq3L55vvvkmLbPMMg3eN2XKlFQoFOZJgtq2bdvFfDXK0ZL8LgFYMC3dAFBhvvjii3Tuueemvn37po4dO6auXbumH/3oR+nrr7+us92KK66YevXqlfr06VOzXH311c223wBQiUq6pRsAWHT3339/atGiRXrkkUfSaqutll588cU0dOjQLOi+/PLL62x72223pSFDhjjMAJATQTcAVJjhw4fXub3JJpukY445Jv3hD3+YJ+gOEYy3a9duKe4hAFQP3csBoAp8+OGHaaWVVppn/be//e3UqVOn1KNHj/STn/xkni7oAMCSEXQDQIV78skn0x//+Md0/PHH11kft997770s0L7xxhvTNddck0466aRGn2fmzJlp6tSpdRYAYP4E3QBQwV5//fW01157pSOPPDJbarvkkkvSKqusklq2bJl23HHHdN5556X//d//bbS1e+TIkalz5841S+/evZfSuwCA8iXoBoAK9eabb6ZBgwalYcOGpd/97ncL3L5fv35p7ty56YMPPmjw/hEjRmTTjBWXsWPH5rDXAFBZKjKRWl7zTZojFqD5OLcvmrfeeivttNNOWWbyaL2ObOYL8vzzz6fWrVtn47sbEvN3m8MbABZNRQbdAFDN3n333TRw4MA0YMCANGrUqDpjr5dffvnsb2Qy/9e//pX23XffbB7vhx56KOte/oMf/CBLrAYANA1BNwBUmLvvvjtNnz49C6RXX331Ovd98cUX2d9DDz00/fa3v82mEpswYULq27dvuuiii9IRRxzRTHsNAJWpRaFQKKQSEzXykaAl9RmSUss2qVToXg6QXzfv5lKK5/ap02anLmu/kI2bLuVW50Utr0vxWFeiSvuNAv/HebQ8y2uJ1AAAACAngm4AAADIiaAbAAAAciKR2lIaI2X8BVAuqm08aJ7v17kfANDSDQAAADkRdAMAAEBOBN0AAAAg6AYAAIDyoqUbAAAAciLoBgAAgJwIugEAACAn5uleSswDC1B9FvvcP3dWKieTH5iUOnV0SVGu1xFA+cjrXDBnzMRcnpf/0NINAAAAORF0AwAAQE4E3QAAAJATQTcAAADkRNANAAAAORF0AwAAQE7M71EBTB0AlMp5AwCAurR0AwAAQE4E3QAAAJATQTcAAADkxJhuAGCJdBncNaWWbRa43ZwxEx3pxVTpx06eCSiv32Cln5OampZuAAAAyImgGwAAAHIi6AYAAICcGNNNLuOrjPOA5mV8JABAadDSDQAAADkRdAMAAEBOBN0AAACQE0E3AAAA5ETQDQAAADkRdAMAAEBOTBlG2U1XZDoygNIy+YFJqVNHlxSUf9luukWqVan8BkvFQp8L5s5aqM20dAMAAEBOBN0AAACQE0E3AAAA5ETQDQAAADkRdAMAAEBOBN0AAACQE0E3AAAA5MSkmpSdJZlD0xyEVBLzyQIAlD4t3QAAAJATQTcAAADkRPdyAACogmFohiVV13fU553/cZ46bXbqsvaCt9PSDQAV6L777ksHHnhg2nzzzdOwYcPS7bffPs82M2bMSGeffXbacsstU//+/dMFF1yQZs+e3Sz7CwCVSks3AFSYa665Jt1yyy3p8MMPT2uuuWZ67rnn0iGHHJLGjh2bTj755JrtDj300PTyyy+nyy+/PH399dfp2GOPTePGjctuAwBNY5Fauj/99NN0+umnp379+qWVVlopbbPNNum2226bZ7so6DfaaKPUuXPntMUWW6QHHnigiXYXAFiQgw8+ON17771p+PDhWUv39773vXTcccelK664omabCLaj9fsPf/hD2nXXXdNee+2VLr744mybjz/+2EEGgOZo6b7ooovSqquumv7617+mTp06pZtuuikr0COoHjRoULbNQw89lBX2v/nNb7LubFHbHn+fffbZtPHGGzfVfsNiyWtsS6WPAQPKyzLLLDPPupYt69azP/zww6ljx45pwIABNeuGDh2a5syZkx577LH0ne98Z6nsKwBUukVq6f7FL36R1ZSvscYaqWvXrumEE05Iq6yySnriiSdqtrnwwgvT7rvvno455pjUo0eP9JOf/CRtuOGGWcAOACx90a38qquuSvvvv3+ddd26dasTjEeF+rLLLpt1MW/IzJkz09SpU+ssAEBOidRmzZqVbr755qzL+ZAhQ7J1hUIhjRkzJu200051to1W8FgPACxdX3zxRdbjbP31108//elPa9ZHi3ZDLeJt27ZtNJnayJEjs6FjxaV379657jsAVGUitddffz1tttlm6ZtvvskK5hgL9q1vfSu7b9q0aWn69OlZzXltcXt+48Oi5jyWIjXnALDkpkyZko3XbteuXbrnnnvqBNkrrrhiVnFeWwTb8Zi4ryEjRoxIp5xySp3yWuANAE3c0r3uuutmteYfffRRNrXIEUcckSVrmd+4sbgdreCNUXMOAE0rAuIIuKP8vf/++7Ou47XFNGGTJk1K77//fs26p556Kts+kqA2JCrb43lqLwBAEwfdLVq0yGrMu3fvnk488cSsQP/1r3+d3RcJWdq3b58V4rVNnDgxrbzyyo0+Z9ScR816cYlxZgDA4omeZzH0KwLoBx98MOsKXt/AgQOz6cTOPPPMrIU7pgw799xz09Zbby3xKQCUwpjuoijQi63YEZBHV/NHH310ngyp/fv3b/Q51JwDQNO58sors1brCRMmZOVyTPVZXIratGmTRo8enV599dW0wgorZEsMEYt8LQBAM43pPuSQQ9LJJ5+cZSOPMdg33HBDNkbs+uuvr9nmhz/8Ydp3332zubr32GOPdN1116Xnn38+XXrppU242wBAY4488sisDF6QSK72yiuvZNnKW7dunfViAwCaMeg+/PDD06mnnpoF0a1atUrrrbdeViO+33771WwTGVKju3lsd8ABB6TVV189m8+7mGwNKlFe838Hc4BXrzy/V1S2Ysv1wurVq1eu+wNA03BtUAVB984775wt0Z08upI3JubojmXu3LnzJFUDAACAarFYEfH8Au46Ty7gBgAAoIpphgYAAICcCLoBAAAgJ4JuAAAAyImgGwAAAEohezlQXlNDmG6s9Jn6AwCgsmnpBgAAAEE3AAAAlBct3QAAAJATY7oBAKBMyQ1CqX+P5oyZmCr2eMydtVCbaekGAACAnAi6AQAAICe6l0MFy6urUDl2EwIAgOagpRsAAAByIugGAACAnAi6AQAAICeCbgAAAMiJoBsAAAByIugGAACAnAi6AQAAICfm6QaW6vzf1TbHd15zpQNAuVqUawHlaPlrtQjXQpV6nailGwAAAHIi6AYAAICcCLoBAAAgJ4JuAAAAyImgGwAAAHIi6AYAAICcmDIMWKrynPqjUqeZAACgfGnpBgAAgJwIugEAACAngm4AAADIiTHdAMAS6TK4a0ot2yxwO3kXqGZ55jQpN44FTfHdWNQypTm/d1q6AQAAICeCbgAAAMiJoBsAAAByYkw3UDHyGquzoDFDxqYBANAYLd0AAACQE0E3AAAA5ETQDQAAADkRdAMAAEBOBN0AAACQE0E3AAAA5MSUYQA5TilmOjEovd/DgqYBhHL6jpbK7wpK3Zxm/F0JugGgQj311FPphhtuSB06dEgXXHDBPPcfddRRac6cOXXWHXTQQWnw4MFLcS8BoLIJugGgAm2xxRapdevWWcD9wQcfNBh0X3fddemkk05KG264Yc26Xr16LeU9BYDKJugGgAp07bXXpg022CCde+65WdDdmF122SUNGTJkqe4bAFQTQTcAVKAIuBfGNddck0aPHp369u2bhg8fnvr06ZP7vgFANZG9HACqVNeuXdOqq66a1l9//Wz893rrrZfuvffeRrefOXNmmjp1ap0FAJg/Ld0AUKVeeuml1K3bfzK0nnDCCemYY45J//Vf/5XGjx/f4PYjR45M55133lLeSwAob1q6AaBKFQPuov333z999NFH6cMPP2xw+xEjRqQpU6bULGPHjl1KewoA5UtLN0CZzgdsblaa2ldffZX9LRQKDd7ftm3bbAEAFp6WbgCoQs8991ydluoIuEeNGpWN715ttdWadd8AoJJo6QaACvSLX/wivf322+nFF19MEydOTIcffni2/uKLL05dunRJLVq0SEOHDs3+j4RqkUgt/t58883NvesAUFEE3QBQgTbddNPUs2fPtOOOO9ZZX+wevsUWW6QXXnghPfvss2nChAnZeO14TMuWOsEBQFMSdANABdptt90WuE2bNm3SgAEDUrVZlHwIeeZloLrk9V2SOwSa73c1ddrs1GXtBW+nOhsAAAByIugGAACAnOheDlCmXUiXZJ9NNwYAsHRo6QYAAICcCLoBAAAgJ4JuAAAAyImgGwAAAHIi6AYAAICcCLoBAACgFILuZ599Nu27775ppZVWSp07d04777xzev755+fZ7pprrkmrr756at26derXr18aPXp0U+4zAAAAVN483T//+c/T4Ycfnn7/+9+nli1bphEjRmSB96uvvpp69eqVbXPPPfekY445Jl133XVp2LBhWQC+//77pyeffDJtueWWeb0PAEpgXnLzf1Np8vxO5/U7hKairGBpm1Oh58VFaum+88470957751WXHHF1KVLl3TZZZel6dOnpwcffLBmm1GjRqU999wzHXTQQaljx47pxBNPTJtttlm69NJL89h/AAAAqMwx3Z9++mmaM2dOWn755bPbhUIhPf3002mHHXaos93AgQOzlm4AAACoJovUvby+aMVeddVV06677prdnjZtWvrqq69S165d62zXrVu39MknnzT6PDNnzsyWoqlTpy7JbgEAAEB5t3SffvrpWbfy22+/PbVv336+286dOze1aNGi0ftHjhyZJWYrLr17917c3QIAAIDyDrrPOuusdMUVV6T77rsvbb755jXrYwx3hw4d0sSJdQfAT5o0KXXv3r3R54uEbFOmTKlZxo4duzi7BQAAAOUddP/0pz/NEqhFwN2/f/8690Vr9jbbbJMefvjhOuv/9re/Zesb07Zt29SpU6c6CwAAAFTVmO7zzjsvXXzxxemvf/1r2mqrrdLs2bOz9TF9WCzhtNNOS0OHDk1XX311NmXYtddem1566aVsmrFFNfmBSalTx3l30ZQ0wKKq1Ckoquk4O/cDABXf0n3hhRemGTNmZNnI27VrV7P893//d802gwcPzubojm379OmTbrjhhjR69Oi06aab5rH/AAAAUBkt3V9++eVCbRdzdMcCAAAA1WyJ5ukGAAAAcpqnGwAAoBzzfyzKfsgrsnS0yunza25augEAACAngm4AAADIiaAbAAAAclKWY7pLZRwIUFrKaWwPS/fzdW4HAJqLlm4AAADIiaAbAAAAciLoBgAAgJwIugEAACAngm4AAADIiaAbAAAAclKWU4aV45RDpqsBaD7O7QDVYVHO967Py1urUpjqee6shdpMSzcAAADkRNANAAAAORF0AwAAQE4E3QAAAJATQTcAAADkRNANAAAAORF0AwAAQE7M010Gc8SWxBx0UMFzLcPicm4HABZESzcAAADkRNANAAAAORF0AwAAQE6M6QaACjV16tR01113pVatWqUDDzywwW3Gjx+fHnnkkdS6des0cODA1LVr16W+n9Uqr5wt8l9QzeRCohQJugGgAh133HHpjjvuSB07dkxz5sxpMOi+9dZb0+GHH56233779PXXX6ejjz46jR49Ogu+AYCmoXs5AFSgzTffPL399tuNtnBPmTIlC7LPOuusdO+996aHH344HXrooVkQPnv27KW+vwBQqbR0l4E8u4npggNQeuf2qdNmpy5rL9nzH3nkkfO9PwLtL7/8Mh177LE160444YT029/+No0ZMybtsMMOS7YDAEBGSzcAVKHXXnstde/ePa2wwgo169ZZZ51s/Hfc15CZM2dm48RrLwDA/Am6AaAKRcC8/PLL11nXokWL1Llz50aD6ZEjR2b3F5fevXsvpb0FgPIl6AaAKrTsssumadOmzbM+1rVv377Bx4wYMSIbC15cxo4duxT2FADKmzHdAFCF1lprrfTJJ5+kGTNmZAF4GDduXJo1a1Zac801G3xM27ZtswUAWHhaugGgCg0ZMiTNnTs33X777TXrrr/++tSpUydJ1ACgCWnpBoAKdOedd6YJEyak559/Phuj/bvf/S5bH9OCdejQIa2yyirpnHPOSd///vezxGkxT3dkLv/Nb36T3Q8ANA1BNwBUoHfeeSe9++67qVevXtny4osvZusPOOCAmm3OPPPM9K1vfSvdd999WbfxRx55JPXv378Z9xoAKo+gu8otyRzg5vim3Oalh2py2mmnLdR2O++8c7ZQOfIsnxflHL0o++HcT3PI6/vM0jFnEa8Zm/MzNKYbAAAAciLoBgAAgJwIugEAACAngm4AAADIiaAbAAAAciLoBgAAgJyYMozFltf0HqZkqGymhQEAoJpo6QYAAICcCLoBAAAgJ4JuAAAAyIkx3QAALBR5V6ik3DG+z+Wt1YBuqVxo6QYAAICcCLoBAAAgJ4JuAAAAyIkx3VTVPM7lNPYDAAAof1q6AQAAICeCbgAAAMiJoBsAAAByIugGAACAnAi6AQAAICeCbgAAAMiJKcOoKksyHZnpxpbOcQYAKLXrFdeBLAkt3QAAAJATQTcAAADkRNANAAAAORF0AwAAQE4E3QAAAFBKQXehUEhffvllmj179ny3mzVr1uLuFwAAAFRX0D158uR08cUXp379+qWOHTumm266qcHtrrjiitSzZ8/Url271KdPn3TzzTc31f4CAABAZc7Tfcstt6QPPvgg3XnnnWnddddtcJu476STTsoC8qFDh6brrrsuHXzwwWnVVVdN/fv3b6r9hoqZe9q8jwAAULkWKeg+9thjF7jNJZdckvbaa6+0zz77ZLePOeaYdM0116TLLrtM0A0AAEBVadJEajHW+5lnnknbb799nfU77bRTevrpp5vypQAAAKCyWroXZNq0aWnGjBmpa9euddbH7YkTG++aO3PmzGwpmjp1alPuFgAAAJR/0F00d+7ceW63aNGi0e1HjhyZzjvvvDx2BQAAYKnl9pGvh1y7l0dG8+WWW26eVu243aNHj0YfN2LEiDRlypSaZezYsU25WwAAAFD+QXe0Zg8YMCD97W9/q7P+wQcfTNtuu22jj2vbtm3q1KlTnQUAAACqqnv5nDlzsjHbRTEO+8svv0xt2rTJAufw4x//OA0ePDj99re/TcOGDUvXXntteu2117K/QNNORdZc3Zfymj4NAACquqX78ccfT927d8+WDh06ZPNxx/+nnnpqzTYDBw5MN998c7rqqqvSJptskv76179my0YbbZTH/gMAAEBltHTvuOOOWcv2guy7777ZAgAAANWsScd0AwAAAP9H0A0AAAA5EXQDAABATgTdAAAAkBNBNwAAAJRC9nKgtJgvG1gSr732WioUCnXW9ejRI6244ooOLAA0EUE3AFSpjTfeOPXq1Sstt9xyNevOOOOMdMghhzTrfgFAJRF0A0AV+93vfpeGDBnS3LsBABXLmG4AqGLTpk1L7777bvrmm2+ae1cAoCIJugGgih166KFp0KBBWRfz7373u2ny5MnNvUsAUFEE3QBQpX75y1+mKVOmpA8++CC9+uqr6cknn0zHHHNMo9vPnDkzTZ06tc4CAMyfoBsAqtRpp52W2rZtm/2/9tprp3PPPTfdfvvt6csvv2xw+5EjR6bOnTvXLL17917KewwA5UciNaBBrQZ0y+XImOYMSldkMo8pxMaNG5f69es3z/0jRoxIp5xySs3taOkWeAPA/Am6AaAKReK0ZZZZps66Rx99NLVr1y6tuuqqDT4mWsWLLeMAwMIRdANAFfrTn/6UHn/88bTvvvumrl27poceeijrPn7WWWel9u3bN/fuAUDFEHQDQBU66qijsnHZV199dZowYULq27dvuuuuu9LgwYObe9cAoKIIugGgSu23337ZsqQmPzApderYumxyS7B0yOFBJXE+YknIXg4AAAA5EXQDAABATgTdAAAAkBNjuqFKNdfYpCV5XeMDAQAoN1q6AQAAICeCbgAAAMiJoBsAAAByIugGAACAnAi6AQAAICeCbgAAAMiJKcOAspHnNGemI4PSUwq/y+aaXrFUlcJnAqX+3XfeoD4t3QAAAJATQTcAAADkRNANAAAAORF0AwAAQE4E3QAAAJATQTcAAADkRNANAAAAOTFPN1Qw80QunWNl3loAABqjpRsAAAByIugGAACAnAi6AQAAICfGdAMAlHjOBjk6wG+Q0jv3T502O3VZe8HbaekGAACAnAi6AQAAICe6l0MZ092wsj+HUunWCgDA4tPSDQAAADkRdAMAAEBOBN0AAACQE0E3AAAA5ETQDQAAADkRdAMAAEBOBN0AAACQE/N0A1ThPOzmAIfy4jcL5VXOUt7n0VYL+92YO2uhNtPSDQAAADkRdAMAAEBOBN0AAACQE0E3AAAA5ETQDQAAADkRdAMAAEBOTBkGJc50FpTa98rURQAAC09LNwAAAORE0A0AAAA5EXQDAABATozpBgCoYnnlDpH/gab4zi3q90guHJbm927qtNmpy9oL3k5LNwBUqc8//zwdccQRqUePHql3797phBNOSF999VVz7xYAVJTcWroLhUL68ssvU8eOHfN6CQBgCey7775p+vTp6aGHHkpff/11OuCAA9Knn36abrzxRscVAJpILi3dF198cVpppZXSiiuumNWe//GPf8zjZQCAxfT000+nRx55JF1xxRVp/fXXT5tvvnkaNWpUuummm9K///1vxxUASrWl+7bbbktnnHFGuuOOO9Kuu+6abrjhhnTkkUemvn37pu22226RnqvL4K4ptWwzz3pjhACaj/GfleGJJ55InTt3zoLtop133jn7O2bMmNSnT59m3DsAqBxN3tJ9+eWXp7333jvtvvvuqVWrVum73/1u2nrrrdOvf/3rpn4pAGAxffTRR6lbt7pJZdq3b5+WW265NGHChAYfM3PmzDR16tQ6CwCwFIPuuXPnpmeffTZtu+22ddbvsMMO6ZlnnmnKlwIAllDLli0bXBd5WRoycuTIrHW8uETyNQBgKQbdkTgtErF07dq1zvq4PWnSpEYfp+YcAJauaOWuXzYXy+P6LeBFI0aMSFOmTKlZxo4du5T2FgDKV5MG3S1atMj+zpkzp8762bNnN1ibXqTmHACWrv79+2dThr322ms16yKxWohhYQ1p27Zt6tSpU50FAFiKQXdMDxbLJ598Umd93I4s5o1Rcw4AS1ckN91ss83SD3/4w2yasPHjx6ef/OQnaciQIWmdddbxcQBAqWYvj0I85vuMQrzogQcemG/m8qg5j6WoZizZ3NkNbj91WsProSLNndXcewBLhXN7rWPxZd0eY3mIHmijR49ORx99dFYxHreHDRuWrrzyyoV+jmJ5vTT2l/IrZ/ymaYrv3CJ/j1w30QQW9ntXLP8ay4VS1KKwoC0W0eOPP54GDhyYLrjggqzwvvbaa9NFF12UXnjhhbTeeust1HOMGzdOchYAql6Mm14aXbhjWFgMEZvfULCGKK8BIGU5Tnr16rX0gu5w9913p5///Ofpww8/TGuttVY6//zz0/bbb79IWdBjKpPoqj5t2rQsAI83YuzY/EXyG8dq4ThWC8+xcqzy4Hs1f8WiOcq9Yr6UUlS7vK69n5X++Xp/5c9nWN58fuVtagWVEVFeR7zas2fP+VZcN3n38rDHHntky+KKHS7WFBQLcQlbFp5j5VjlwffKsfK9Yn7ldTWeN7y/8uczLG8+v/LWqULKiJhCc6kmUgMAAAD+j6AbAAAAqjXojqzm55xzTp3s5jhWvld+g6XI+cqxojp+C95f+fMZljefX3lrW+FlxFJLpAYAAACUQUs3AAAAlCtBNwAAAORE0A0AAAA5yWWe7qYSw81feOGFbOL0fv36pXXXXbe5d6lkzJgxIz3wwANp2WWXTYMHD25wm08//TQ99dRTqV27dmnbbbfNtq028R165ZVX0gcffJD69OmTNtxwwwa3++STT9IzzzyTOnTokB2rakrsUNu7776bXn/99dS1a9e0xRZbpDZt2syzzYQJE9Jzzz2XOnbsmAYMGJCWWWaZVM3++te/pmnTpqUDDjhgnvvGjx+fnn/++Wz+xjhWDR3PSjZ58uR0//33z7N+l112SSuuuGKddR9++GH65z//mVZYYYXUv3//1Lp1SRdPzMdHH32UnSPie7/NNttU1DnijjvuSDNnzqyzboMNNsiWcv6d/u1vf8vmW996660b3Kacf5/ffPNNevDBB9PcuXPTsGHD5rn/1ltvTXPmzKmzbtNNN03rrLNOKpff24svvpjNdbzZZpul9u3bN3gMxowZk5VVW265ZerRo0cqF1OmTMligbie23jjjdNKK61U5/6XX345u26pLY7BnnvumcpBvK94D3Gd2rt37+y715C33347e5/x2W211VapRYsWqVy8//776bXXXsvOH/H9q10mxHfz//2//zfPY6LsWHXVVVNFKZSor776qrDLLrsUunXrVhg8eHChY8eOhaOPProwd+7cQjWbM2dO4eSTTy706NGj0KtXr8Lmm2/e4Ha33HJLoUOHDoUBAwYU1l9//ULPnj0LL730UqGaPPbYY4UNN9ywsNFGGxX22GOPwsorr1zYdtttC5999lmd7a677rpC+/btC9tvv32hX79+hdVWW63w1ltvFarJuHHjCjvvvHNh3XXXLey5556FtdZaK/t+Pffcc3W2+/3vf58dqx133DHbZvXVVy+89957hWp1ww03FJZZZplIRjnPfZdffnlh2WWXLey0006FNdZYo7D22msXPvjgg0I1ie9PHJt99tmnMHz48JrlnXfeqbPdBRdckB2rgQMHZr+/DTbYoDBhwoRm228W3xVXXFFzjlhzzTWz5V//+lfFHNIVV1yx0L9//zrf51tvvbVQjqIsPPzww7PriZVWWqlw8MEHN7hdOf8+zzrrrMIqq6yS7XcsDWnbtm1hu+22q/OZ/uUvfymUusmTJxf222+/Qu/evQu77bZbYeONN86ucx544IE620UZHWV1lNnxu4zfZ5Tl5eDUU0/Nrl/ju7fDDjtk+37JJZfU2eb000/P3nftz+8HP/hBoRw8++yzhU022SS7lh82bFihe/fuhU033bTw0Ucf1dkurvuXW265LC6KbeL7OnXq1EKp++STTwq77757dv2z5557ZtfYcb55/PHHa7aZNGlSdp2w66671vkMn3zyyUKlKdmg+5xzzsl+aB9//HF2OwLGuLi96aabCtXsm2++KVx00UVZYXnSSSc1GHRPnDgx+3FeeOGF2e2oqNh7772zH3Y1uf/++wuvvfZaze0pU6ZkhU5U3hSNHTs2K3DjQjHMnj07q+SJ4LyavPnmm4Wnnnqq5nZ8Z4YOHVrYZptt6hTcbdq0KVxzzTXZ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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 5))\n", + "ax0.imshow(ti_data, cmap=\"cividis\", origin=\"lower\")\n", + "ax0.set_title(\"Training image\")\n", + "ax1.imshow(field_simple, cmap=\"cividis\", origin=\"lower\")\n", + "ax1.set_title(\"DS realization\")\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "5e7a8b30", + "metadata": {}, + "source": [ + "## 2. Conditioning to hard data\n", + "\n", + "Real simulations must honour measured data (boreholes, samples). Direct\n", + "Sampling supports this through `DirectSampling.set_condition`: conditioning\n", + "values are pinned into the grid before simulation and preserved exactly,\n", + "while the rest of the field is filled in around them.\n", + "\n", + "We reuse the synthetic channel training image from the first example." + ] + }, + { + "cell_type": "markdown", + "id": "4b4b9dcb", + "metadata": {}, + "source": [ + "Draw 40 random \"hard data\" points and read their facies from the TI. In a\n", + "real study these would be field measurements; here we sample the TI so the\n", + "conditioning data are consistent with the patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "7a754e7c", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng(0)\n", + "cond_x = rng.integers(0, 40, 40).astype(float)\n", + "cond_y = rng.integers(0, 40, 40).astype(float)\n", + "cond_val = ti_data[cond_x.astype(int), cond_y.astype(int)]" + ] + }, + { + "cell_type": "markdown", + "id": "ed690ca0", + "metadata": {}, + "source": [ + "Set the conditioning data and simulate. `set_condition` snaps each point to\n", + "its nearest grid node, so the values are honoured exactly at those cells." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bcb598a1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "conditioning honoured: 40/40\n" + ] + } + ], + "source": [ + "ds_cond = gs.DirectSampling(\n", + " ti, n_neighbors=12, scan_fraction=0.3, threshold=0.0\n", + ")\n", + "ds_cond.set_condition([cond_x, cond_y], cond_val)\n", + "field_cond = ds_cond([np.arange(40, dtype=float)] * 2, seed=7)\n", + "\n", + "honored = int(\n", + " (field_cond[cond_x.astype(int), cond_y.astype(int)] == cond_val).sum()\n", + ")\n", + "print(f\"conditioning honoured: {honored}/{cond_val.size}\")" + ] + }, + { + "cell_type": "markdown", + "id": "abbb1153", + "metadata": {}, + "source": [ + "Plot the realization with the conditioning points overlaid. Every marker sits\n", + "on a cell whose simulated facies matches the datum." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "640ef1df", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdsAAAHqCAYAAABMXOQ8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXd9JREFUeJzt3Qd4VFXaB/D/nUnvBVKA0Kt0ELAgioCIiAUVFPksa0FBxY6suvbFvpZdRdEVu+IiKCgIKhaKIkUUBekQQiCk9zZzvuc9YUImBTIhN5ny/z3PiLlzZ+b2955z3nOuoZRSICIiItNYzPtqIiIiYrAlIiJqAizZEhERmYzBloiIyGQMtkRERCZjsCUiIjIZgy0REZHJGGyJiIhMxmBLRERkMgZbFzzwwAMwDAPl5eWV06655hq0adOm3t/h6vxmc7flaS5+fn647777jjutOZenKRUUFKB169Z49dVXm+T3Vq5cqc+txYsXe9Vvucvvz549W//mnj170Jzuu+8+vRzN9fkT9X//938499xzvSvYrl69GpMmTULbtm0RGBiI+Ph4DB48GI8++igOHDgAd3b55Zejffv28Ca33367Psgdr5CQEH0xPuecc/D8888jKyur1s/Nnz8fZ555JhISEhAWFoY+ffrgrrvuwrZt2+DL5IZNtqPcwLmjp556Cv7+/rjuuuuae1GIGsWll16Kzp07n9B3PPzww/jmm2+wZMkS7wi2skJDhw5FeHg4Fi1ahJycHPz555+488478e677+Jvf/sb3MXcuXOxf/9+0+Z3N8nJyZDhtLOzsytviKT00717d33HXtWDDz6oD/DTTz8da9euxeHDh/Hiiy/ixx9/xHnnnQdPCIhPPvmkz/22lGpfeuklTJkyBQEBAc2yDETuqFOnThg9ejT++c9/en6w/fDDD/HII4/oEuxrr72Gvn37IigoCLGxsbrE+Ouvv+Lss89u7sX0eXIRbteuna6GXr9+PWJiYnDBBRcgIyNDb5uSkhI8++yzGDFihD4wpYYiODgYw4cPx6pVq3SQJvck56Dc4E6ePLm5F4XI7ch5IQWLzZs3e3awlUCbmJhYZ3tVaGgo7r33XqdpCxcuxKmnnqqrNqWqUqotv/76a6d5LrroIl36Sk9Px2WXXaZLzS1atMDUqVN1YKjuzTff1PNLoJeAv3Tp0nq1eZ5yyin4+OOPsXfvXqdq1+Li4lrnN2sdHG0b8rJYLIiOjtZVvhLoGltERIS+OZKq5Ndff11PKyoq0ussVc3VSfWkzH88jvVNTU3VJeTIyEicddZZ+j0pXf/nP/9Bv379dBCX9yTYSw1IVbKtHdtBfleC/s0334zMzEyX203lZq/qPq36uummm+r9mzt27NDTxRNPPFE5r6xjXb/t2KZ///vf0bFjR32zI+eJ1PLI9nHIz8/X3/X444/r46d///76GO7WrZs+LutDapO6dOmCpKQkp+lVv/vLL7/EgAEDdBPPG2+8od8/ePCg3g6y/rJ8st5yrjqOfQep5Rg2bJjebzLvrFmz4Cozfuunn37CGWec4TRvXe2rTbmujclut+Ohhx7Sx45ca6SUtmvXLqd5vvvuuxpNRrKvpfBTWzuwHM/3338/WrVq5XStmzNnjj7u5Pjr168fli1b5tKy1vfzd999d41rnbSrrlmzpnKek08+WTdp7dy502ndHPk39fkOB0dhT84Tlyg3snv3bnncn7rqqqvq/Zk333xTf+bOO+9UKSkpau/eveq6665TFotFffrpp5XzXXjhhapTp05qwoQJ6rvvvlO5ublq3rx5yt/fXz300ENO3/nKK6/o75TpaWlpaufOnerSSy9VY8eO1dPLysoq57366qtV69atnT4/ceJE1a5du1qXt7b5zViHqmR5t2/frrdrWFiYXp9jLU9tpk+frpcxOTm51vezsrL0+6NHj66c1rNnTxUZGalWrVqlGsKxvuPGjVPffvutysjIUO+8845+75prrtHr8sYbb6j09HS9zSZNmqR/b8eOHbV+X35+vlqxYoXq0qWL03IKq9WqZsyYcdxp1d1+++16vV9++WWXflP2iXzu/vvvr/Vz1X/bZrOpkSNHqqioKDV//nyVk5Oj1qxZo7p27arat2+vt43Iy8vT33vxxRfrfbtr1y51+PBhde211+rjaevWrep4YmJi1BVXXFFjuuO7ZX/I+7Kdt23bppYvX65SU1NVUlKSGjBggFq9erWe94cfflAdO3ZU5557buV3bN68WYWEhKgxY8boz8qy/fOf/9TLK9+9aNGi4y6fGb/122+/qeDgYH2Oy7ki8z799NNq/PjxNeZtqnXdsmWLnq8+r8cee+yY3/Xqq6/q+eS8mTNnjsrMzFS//vqrXuYhQ4Yc87NyDXzxxRf1Mfnuu+/W+M7LL79czZ49W5+Hc+fOVcXFxeqll15ShmGoRx99VH9+x44der3PO+88/Znjaejn5bySbX3llVeqiIgItWfPnsr3LrnkEn09OZ5jfYeDXN+r7uv6cKtgKwFENuQDDzxQr/lLSkpUixYt1LBhw5ym2+121bt3b30Rqnrhlu+Wk6Kqyy67TCUmJjp9Z2xsrD7pqiosLFRxcXGNHmzNWIe6lJeXq9DQUPX4448fc/kbEmyFBD9ZZodNmzapbt266c917txZB0M5iaoG+2NxrO9XX33lNP3777+vNcDJtpTtLut0LP/73//056ueRA0JtvL78j0SyI6n+m+6Gmw///xzPf/rr7/uNN/GjRudvscREOUiWvU4leAcFBSk7rnnnmMup9wcOG78qnN8d6tWrVRpaanTezfccIMOLPv373ea/vXXX+vPSEB2HKtywyA3ilXJzWx9g60ZvyUX4ujoaL2OVUkgqT5vU62rGcH273//e603+nKuHs9FF12khg4dWuM777jjDqf5JNjKtpTzt/qxFRsbe9xge6KfF3J8ys3Tk08+6XKwPdZ3OJxyyimqR48eyhVuV43sit9++01XqY4fP95pulQFyDRJc5dqAwepdpVqoqp69eqlq+Gkik5s2rRJtztKlWRVUg0k1bCesA5Cqiwlg1ga9KWqT75PqiYl+UWqfcwgN29V0/Il81iqdaXq+sYbb9TVM4899hi6du2KGTNm1Os7pQqp+nZ3VN9UrXYVUp0n2+b77793qhocN24c4uLiYLVanaprT2Q7SLWibF9pk65evWbGb0oGpKh+nEj1muxjx/sOUj0o+7tqVb9UC1evMqxOEt8cx1ldxowZU1kNXnWfnHbaaTWaDaQ5ROZ17BNZTmm3r/790mRQlRz31avqP/roI1N+S6xYsULPK004VVW/Dpj1+7WRJpQjBaLjvuqb1T527Nga1w5R9biQ73v55ZcxcOBAvT0c21+aumo7fqtvow0bNugmperTQ0NDMWrUqOMuo6ufl+v19OnTna51ci2Q62F9zzdXv0POJ8e5Ul9uFWwl4Ubs27evXvM7knGkW0l1jmkSyByknaK2jSYkIaTqd0pXo+pqm3aizFgHx0klbRSSKXzo0CHdViMnkbTxlpWVNfp6yIEngVzabaqSACsXpnvuuUdnkktbtgSMp59+GgsWLDju91b/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DqeHkGrj6667TpdU6yJ3rO3atdNPgBJSzTd16lR89tlnusrS3cid9h133IGYmJjjzivVYrIe8txmd+45J9WrVWuApApfSgxSKnT4/vvv9fkkNRUO8v9y3sn5526k9F211CM1RFIqT01N1SW/quRBKI5zTGol3Nlll13mdD45ar0c54osv9Q23HTTTbqUKM4//3xdEpaSojuS/STNRw5JSUl6Hauf//L3okWLsGLFCmRkZMBbuXU1sjx0XnZW1bYLeYqQXLjlPXes5qqvxYsX6ypxuWDLBUNOGGnT9Bayf3r37u00Tf6Wqj15xrE8ftFTvf7667rq+Oeff0bPnj111bM7Vo3X5uuvv9bVr1X3k1y8O3bsWDmtbdu2us1W3hs7diw8YZ2kPVpeVX355Zc64P7xxx+6meC9995zqmp2N7KsUjUs7ZtStSpNFueee25lk4DclFfdd45zSvaTu5LgKU1H+fn5+jwZPHgwrrrqKqd5pNniP//5j26H3rJlC5566inccsst8DZuHWylbaK2koZMk3YaT/Xkk09i9OjROshK8Ln22mv1na0cdKGhofAGjnalqhztnJ6676T0K6U9aaMWkoAjbU3StvbFF1/A3b399tu6RC4Jesc7x2RfecJ+kv3x8ssv6xugqh5//PHKc0zyCq6//npMmDBBn2PummApbesLFy7EoUOHdOCdOHFi5RNlZD+J6vtK9tOePXvgruTGQdYpMzMTmzZt0vuhajKb3Mw98MADCAsL03+/9dZbutZMamW8qfAh3LoaWYbykjui6mRa9exDTyJ3q45HB8oFXIYtk0cKrl+/Ht6itn3n+NtT952skyPQOi58UvUsma/u3hQgQfbGG2/Eq6++qhO8vOEc++mnn3QV8r333qtvWOs6x6T6XM4xqRr/5Zdf4K4kkVCSiqQ6VapV77vvPnzyySf6PcewhrWdU+68n+SGW9Zp2bJlujr/tddew7/+9a/K96WmwRFohexHqWWRmj9v49bBVtLEpRqisLCwcprcbcuzEKtWe3k6xyOmDh8+DG8h+07uzqu3Uwtv23dScnLnUuDnn3+uS0kvvPCCDrjV95OUmhwlJ1FQUKDPO3feT1KFLyVXyQN44oknvO4ck2YWyaT+8ccf9d+OfVHbOeXO+6mq9u3b666bjnWqi9Q8eMp+8ppgK2nucncqSQ4OUu8vd3me2l4r1SrVkzU+/fRTXV0kfSC9xXnnnaf7DEq7dNV9J30DPaV9szopGVUn+07aOKWrkDuSEoJUn0q3kptvvrnG+3IeyflUtf+w/L8cj9W7PrkLKZ1KoJX1kSaZ6uTGp7ZzTK4l7pgrIMta/WZNql2leliSihzVxdJtyVHSdQReKd27a7u6JK1VVVJSotvPHetU2zySzyHzeEo3SK9ps5UkFOlDJnevcmBJP9tZs2bh4Ycfdurn6Ekk8/PKK6/E+PHjdbauZLbOmTNH95dz9w7qVclJL/tDqr+lGkuqiiTR5sILL9Tvy79yIZd+ddOmTdOJDxJspW+uu5Jlk4uetOvJTZGsk5B+3lIVuWTJEt2mJFmgMpiCJOBI8odjPncjgx/Iskv/Uqnyrrqcl1xyiW7CkPNIqlinT5+uL3zSz1bOMemnmpCQAHezc+dOfRMgpTm5cau6TlJ1LPtF2j6lJC/nmJxTGzdu1OeYrFP1QRXcgdQkSHWqZO9KFrKU6v773//qm7iqNRHPPvusPqdkWp8+fXSTgAxAIvvYHckN0fDhw3USV0FBgU4ClYAr1f4Ocn2Qde/bt6++mf33v/+t1+nqq6+Gt/GIp/5IA7skoMidqRyQcrHzZHIxeOedd3SGoXSjke4xnlaqlYzC0tJSp2lyoZs9e3bl3/K+XOSkJCJZ5ddcc40+qdzVPffco2+GaksscrSZyc2RlPzkgigXfNkOsg/dNUv3jTfeqPU9uZhX7WoiJWCpbpbLgZSUHF223I0k2cjNQG0ki1VuYIXcOMh+k3NMalLkuuHOpSWpxpcMZMkslipvWVZJmqw+4pr0XpB9J9X8cs2YMmWK2z6mTkrs7777rj7/g4KC9A2CFDQcXZeqzyPVx9JuLTeCjvZ2b+IRwZaIiMiTuXWbLRERkTdgsCUiIjIZgy0REZHJGGyJiIhMxmBLRERkMgZbIiIikzHYEhERmYzBloiIyGQMtkRERCZjsCUiIjIZgy0REZHJGGyJiIhgrv8HK+p7w1QZ5HsAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(6, 5))\n", + "ax.imshow(field_cond, cmap=\"cividis\", origin=\"lower\")\n", + "ax.scatter(\n", + " cond_y, cond_x, c=cond_val, cmap=\"cividis\", edgecolors=\"red\", s=30\n", + ")\n", + "ax.set_title(\"Conditional DS realization (red-edged = hard data)\")\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d60992eb", + "metadata": {}, + "source": [ + "## 3. Continuous variables and distance metrics\n", + "\n", + "Direct Sampling is not limited to categorical facies: with\n", + "`categorical=False` it simulates continuous variables (permeability,\n", + "porosity, elevation, ...). The `distance` argument then selects how two\n", + "patterns are compared:\n", + "\n", + "* `\"l1\"` / `\"l2\"` — Manhattan / Euclidean distance on the raw values\n", + " (Mariethoz et al., 2010, Eq. 6 / Eq. 4).\n", + "* `\"variation\"` — compares only the *relative* variations within a pattern\n", + " (Eq. 9), tolerating a locally varying mean. Useful for non-stationary data.\n", + "\n", + "Here we use a smooth continuous training image and a small acceptance\n", + "`threshold` (standard DS mode) to allow approximate matches." + ] + }, + { + "cell_type": "markdown", + "id": "7aaef7f6", + "metadata": {}, + "source": [ + "A smooth, continuous synthetic training image." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3fdcae36", + "metadata": {}, + "outputs": [], + "source": [ + "gx, gy = np.meshgrid(np.arange(60), np.arange(60), indexing=\"ij\")\n", + "ti_cont_data = np.sin(gx / 6.0) * np.cos(gy / 8.0)" + ] + }, + { + "cell_type": "markdown", + "id": "9ae54017", + "metadata": {}, + "source": [ + "Build a continuous training image with the Euclidean (`\"l2\"`) distance." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c6f47f49", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TrainingImage(shape=(60, 60), categorical=False, distance='l2')\n" + ] + } + ], + "source": [ + "ti_cont = gs.TrainingImage(ti_cont_data, categorical=False, distance=\"l2\")\n", + "print(ti_cont)" + ] + }, + { + "cell_type": "markdown", + "id": "d94fc2be", + "metadata": {}, + "source": [ + "Simulate. `threshold=0.03` accepts the first pattern within that distance\n", + "(standard DS), which is faster than the exhaustive best-candidate search for\n", + "continuous variables." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "f316fb3f", + "metadata": {}, + "outputs": [], + "source": [ + "ds_cont = gs.DirectSampling(\n", + " ti_cont, n_neighbors=12, scan_fraction=0.3, threshold=0.03\n", + ")\n", + "field_cont = ds_cont([np.arange(32, dtype=float)] * 2, seed=3)" + ] + }, + { + "cell_type": "markdown", + "id": "d52a52ae", + "metadata": {}, + "source": [ + "Plot. The realization reproduces the smooth wavy structure of the TI without\n", + "copying it. A shared colour scale makes the comparison fair." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "105a2d5f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAwQAAAFaCAYAAACg4JpMAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAk4lJREFUeJztnQWUXEXWx6tdpscyk4kRg+ASSCC4u8MuEFwXWFgWZyEsfDgsi+wCizuLL+7uLgkswS3uNt7+vnMr9Gy/qtv9qnus0/3/nfNO0jX1qqvr6a26/3tdlmVZAgAAAAAAAFCRuPu7AwAAAAAAAID+AwYBAAAAAAAAFQwMAgAAAAAAACoYGAQAAAAAAABUMDAIAAAAAAAAqGBgEAAAAAAAAFDBwCAAAAAAAACggoFBAAAAAAAAQAUDg6AXuO+++8QWW2wh4vF4v+zfXfr7+/uaF198Uey6664iFouJUqYSjsvNN98sjj766P7uBgAVx5///Gdx4oknOpb1Z38AAL2HV1QQ5557rnjnnXcc662xxhrijjvuKPp7ZsyYId5//32RTqf7Zf/u0t/f35ckEglxyimniP33318EAoH+7o64/fbbxb333ivefvtt4fF4Ku647LHHHuK0004Te++9t9wAKBXeeustcd5553V99vv9ora2Vqy66qpi6623FjvvvLPwevVHamtrq3yefPbZZ2LBggVi+PDhYuONNxYHH3ywqK6uFqXCV199JZLJpGNZT3L88cfLcbzhhhuM+gMA6D1clmVZokL49ttvxeLFi7s+f/DBB+Lss8+WG72IZKCb9NixY4v+npkzZ4rp06eLzTffXLhcrj7fv7v09/f3JXfeeac44YQTxOzZs8XAgQP7uzvyheOyyy6Thor6clEpx+Xwww8XX3zxhfjvf//b310BoIvHHntMThxceOGFYvvttxepVEo+Tz755BNx9913y+fGv//9b7HpppvanjnbbLONNBzI0B0zZoz4+eefxbXXXivmzp0rr+cBAwaUxChTP+kF/L333rO9lBPrrrtur3znJptsIoLBoDS2VHr7uwEAFbxCsOaaa9o+L1u2TP5LN2lyxegpaAaItv7av7v09/f3tYsKuQuVgjHgRKUcl8MOO0y+WNFqCBk/AJQSq6++uu158bvf/U6cddZZYscddxS77LKL+Pzzz+UzhTjnnHNEc3OzNHCHDBkiy6jeEUccIY488siSd//rz5dxGAIA9C3QECjceOONcqYk47O9zz77yBs+8dxzz8kHAW1bbrmlfJGk1YVff/3V0dc7u90HHnhA7LXXXmK33XaTS6WqC0h39yeoTyeddJLYYYcd5IPnyy+/lL7y1C65nuTD6ftpVp1WVH7/+9+L119/XZbRQhOVU7/23HNP8eijj2rtmo5fof3v7OwUN910k+zPdtttJ33Qs2e5ckHfQQ9vGkcOaveWW24RBxxwgHyI/+lPf+qatcqwcOFCcdFFF8k2dtppJ3HqqaeK77//3lbH9NhdcsklcuwJqp8Zq8z4dPe8oJlNmuHMdVxolaTQ31ZIm6TRIJeoAw88ULZ33HHHiVdeeUXbd9tttxWhUIg9hwAoRRoaGqRbUEtLi7j88su7yn/44QcxYsSILmMgA53fDz30kGhsbMzb7sMPPyyvIzIqnnzySbHffvt1Xe8EXV//93//J69RukedeeaZ2v2RrtHMvWSrrbYS++67r7jmmmtEW1tbwX78TzzxRFdb6kYre4V8J7kEfvPNN2LKlClddbPdBHNpCGjlMPNsoOcQjXdmci/DUUcdJceC7uF0XyV3LrpPvfTSS46/GYBKpaJWCEygly96mSTXjY6ODnHMMceIl19+Wf5to402En/729/k/+lla86cOeK2224Tt956q7xJ0Y0/l693pl1yB1m6dKn44x//KF9GaRmZ/ErpppWhu/vTMjXNrNIs1emnny7cbrf4y1/+ItZaay3ZLv2ufOT7fnr40LIy3aiff/55eaN94403xNNPPy1dXKhfdNOdOHGifOiRcZDBdPwK6T8t2dMLJD0QJk2aJEaPHi37Q2X0Mk/HLxfkp09MmDBB+xu9DFMbixYtkrN8tLr0yy+/SOOEZq+pL7T0T4ZNXV2d/O6qqir5W9Zff305HvTSW8ixozH76aef5Is/1c1oCAYNGtQj58V3330nPv30U+23zps3T7ZLD88Mpr+tkDYPPfRQ6aZ3wQUXiJVXXln+nn/+859i1qxZNiExnUcbbrihePPNN3MeOwBKjXHjxomRI0fK+2K2Ho0+04SG6oZK9zXa8kHXBl1H9DJN90t66X7mmWfk3+ha2n333eVMOrk9hsNhcdddd8lrlFxw1ltvPVnvoIMOki/PBE0mkFF/9dVXSxcouneoWqV8fvzkDpW5h2fXocmSbDdGk+/861//Ku93pCHItEn/z/XdGYOEJhTIACKjgO55dI+jiQYap6FDh8p6ZGQ0NTXJ1UYySEgnRhMmtB9NYtG9HQCgYFUwzz77LOknrNtvv72r7IwzzpBl5557bldZIpHI2UY8HreGDx9unXTSSV1ll1xyiWyjs7NTa/fiiy+27X/MMcdYkUjEikajPbb/zjvvbDU1NVltbW1dZfT3lVdeWbbx7bff5h2XfN9/2WWX2equt9561qqrrmr93//9n6183Lhx1pZbbmk5wY1fIf0/9NBDrerqamvGjBm2dv/6179a4XDYWrx4cc7vnjRpkmyPq3PggQdaoVDImjZtmq08mUxaHR0d8v+77babVVNTYy1atMj29/Hjx1tDhw61YrFYwceO+k11uXOuu+fFxIkTrZEjR2rt0vlPbfz4449dZaa/zbTN1tZWy+12W//85z+1uvQ3FTqugUBAKwegv/jPf/4jz+mHHnooZ50ddthB1mlvb5ef6V41aNAgy+/3W3vvvbd19dVXW2+88UbX9ePEVVddJduj6zkD3RvovjlixAhrwoQJViqVsu2z8cYbW5tvvnnedqdOnSrbfeqpp7rKtt56a20/riybOXPmyPv3wIEDrZ9++qng76S+0ndwqN9Nz4MBAwbI8nQ63VU+c+ZMKxgMynt2hrFjx1per9d65513usponOgZsuuuu+btJwCVClyGckA+nhmyxZ00u0CREWh5lmZQaaaBlnNp6dOE7GVVgtqgZVQnNx7T/Wn2/LXXXpNuLjSrm4Ei6FBZd6FZ3mwoWsaPP/5oG69M+ddff63t7zR+hfSfXFBoxoncZFTfeppForYyLk0cGYE5zYJnE41G5UwULc/TjF82NLNFKx/UNrm7kJsSuQtk//3YY4+Vs3kkNuzJY5+Pnmy7mN/mBAkHaQaTjhe5UWQTiUS0+iS0pONLEVoAWFHIzHBnQhjTCgHdH8m1j+5nFEGMBMl0XZGri2lMj+zrm55HFC2Prm1aGVBXGejeR7PlS5YskZ8pQAGJnimqEblUkmsOuesRps8tjvb2drlCQauptGq4yiqrdP2tN76TVnTpN1E72asRK620knQdopUTEnpnGDVqlLwPZqBx2myzzdjnEgAALkM5UV8EiSuuuEIuc5JvIy2R0ksL3WTIfYZujk5QXfXFNfPCRe4dFL6uu/tT5Aq6KXL9z7jkFAt9P918s8lEyFDbpnK6eVNfMkvSJuNXSP/pxZRe3mkJmvxqMw9X+jfjZ6/6xWdDL/aZh3fm/5l2af+MMJCDXGJoOZtclFQyZRQVqKeOfT56uu1Cf5sJ9BJDrlbk0kSiTHp5oGNGRl7G/Ug1Suihn31cACh16OXY5/PZJhko+tAf/vAHuWXuceTCQvdCMhLo/07Qy2025L5IXHfddeKee+6R97zM/Y+u+cx9rL6+XrrJTJ48WbpdkgsNGeD0wk6Giclzi4Pu0eTiSGLpRx55xBZZifrRG99J7lNErvsS3TPomZMJEME9Q+i+mBkfAIAdaAgY6AVWjUlPN0B6oaUZcroJZ0OzmDT76QS94OTyGTWZKTLZP9MPErepcGWFkO/7ufjb2f0yHb9C+p95WaQY4DRzzcE9PDJkhH70EM82NjIrE6pQjfturp+04pH9W3ri2OfDtG3qMxlQKqSTKPa3mbZJkECfNCWkOSAjjmYVSYNCep1srUNmf3qw5zqvACg16D5Gfu+klcoXFpjuO7RS8NRTT4nHH3/cyCBQny+Za5S0NxtssEHOe9+HH34oV1zp+7JXGUgj1B1OPvlkqY34+9//rgUV6K3vNLkvZU8gOD2TAAB28LQ1JOO+QGLIbGj5k2JJZ4Sf/Q09bIYNGyZvyiofffSRKPXxK6T/gwcPlrPfNFtWTNhYioFN0CxXtkFAfaF2yd2IHh7cw536Sasl7777rvY3KqMXdBLGFutyQAZUT78M028k44dm6LLdsdSxLuS3mbaZbWzTuNNGUUBI8EcvDqpBQLOL2bOOAJQ6//jHP6RxTKtgGf7zn/+wUbjoOqD7SrGJtyjoAl2HNCOe796XcYtU77sUsa1YSOBMUd3I9ZPCrXbnO+l+l+3mk49M8Ae6B9FEQga6R9MEAwV64NwPAQBmQENgCM3QUNQGmtHJzEbQjY9CMWb7TpYCZ5xxhpyhoagKGcgnftq0aSvE+BXS/yuvvFL6y9Lye8Zvl6AH5aWXXsrOVGegl9Kamhq5vwr591LkI3pppaXuzIOH+kSRMQiKPvTxxx+L66+/vms/MiIo/CAtk5NhUygZ1wA1vGdPQKH/KEIRPdAz0O8h1wIV099m2iYZfTSbmDn2mRUYck9S3SHIHYn8o7Mf+gCUKnSvoVUuCtFLUciydVZURitjpCPIQK4tFD2N3BIpGk8x0Ow/uVpSdB4KR5oNRf6iqGOZyEf00k1uRZnoZBSF7Nlnny3qe2lVg9yAKGQ06SI4CvlOuvZpQsfEMCJXQxrLf/3rX7YJB5pMoIklioYGACgeGAQFQDc4CqNIvokU2o2Waim8YzEvfr0JvWRTfH+KxUwPDprtfuGFF7pmrvrLDcN0/ArpP72Q0gORxKrkYkJt0j40W0QPXPLfzQW5hdHLLcX6VpeRSVBM5TTDR36n1C7FDKcQtBkfVdJBkEFCLwM0o04PLHpQUpsU8rQYaDaRvovEcGSwZOch6C405hQf/OKLL5ZjSoYYPagzYr9sTH+baZs0hvTiRHVoGz9+vGyXzgU6L7Kh+OxkQJIgEYBSIxNjnwSqdF1QqEsSvJIhTELa7BVFelmlex7dj0hgTNc2rWzSxAiF4SQ9VbFQyF66Pim0MrVJq3a0ukn3royRTfdWCu1MOT3oWqPwyTSzn+tl3sQgoJd80kGQqyaXh6CQ76TJH5rIoXq06pGdh4CDQjKT2yF9N7VLq5k0DtS2GvACAFAYLgo1JCoUimFMEQfoBSXjskKz0CREzZUhlWYyyBeSbmJ0g6eZEPIbpWHMxH2mGU6aEaU2Mg+HXO1m+kD70mx1T+yf/TeazaYXL7px0oz5+eefL2fmM2JgjkK+n+pRfXXZmmZ4aVPLTcav2P5TX+bPny8fjiSyzefHm4Fm7uhhTREq6IVXhfpFdcjdabXVVmMNDDI8KHcC/TY6l9TjUMyxo+8kVxx6+NKMG70g99R5QeNHs3Lk7kPnPY0ZfR+9UFA0oEJ+W6FtknsAGTgUAYmOUXYUI4J+Lx0PEhpnr04A0N/QOU7XQga6d9H1QBMQquZMhe53dN7TChnFx6cXdpP7E13btB9NDuSayKFriiJ3kdsetcslO6NVCapDbn2ZIAPkZkPXa8ZdMpN0MTtDsFpG1zRd2xzUdraeweQ7M/cYWtWgeywJsjOuQVx/slcXqS90b6HnCO2XDbmBUtnaa69tK6exJGOGjDkAgJ2KNggqDXrJopfx7oq7yq3/JJAjfUKhoTRBz3P//ffLWVMyBFVjAQAAAAC9A1yGyhCacaEl1IzvO0FuHq+++qp8+S11+rr/5G977bXXdoUqBf0HrSiQcQZjAAAAAOg7EGWoDKGlUhKlkusMLc3SsjO5aJC/6YpgEPR1/8kNqJgoRaDnoeV/AAAAAPQtcBkqYygEHvlZko8pvWip/uGlzorefwAAAACAFQEYBAAAAAAAAFQw0BAAAAAAAABQwcAgAAAAAAAAoIIpSVExxSKnTKck9jSJ1QwAAIVCEZcp9jkllnK7y29uBPdRAEBvU+730UqiJA0CMgYoaREAAPQ2lPCNkt+VG7iPAgD6inK9j1YSJWkQZLLBXvzk+yJYFclZz82sHvjcelnQa7da/R7dig37PFpZld8+PBG/2X4Br70PYeX7uT7Jfomk7bM71qbVccXatTJ3XC8T8U7bx3RHq1bFYsrS0Q57najethWP6WXJhNJQShSN2z6mLq89C6WEKXMHw/b9lM/L61RpZa6w/RxzhfRsxGm/vp8VYMr89u9MevTISJ3JNFNmzw8YZep0JPSy9rh9nNsS+rh3MGUxpf04830JiylLFZfH0ORa5a7dgFe/vkI++7UTUa5TosrgWm1vaxU7jF+bzT5dDmR+1ztH7SkifuYa+o260UMc2/KE80f4Cg51nsBJtSxxrOPyO0cScyvXazHtuMPOx9xd3+RYx2LuC3pnnGdNU5GBef8e84Yc21gStT8/OH5aYn8ucPy4mHmeKHw7uyXv3z3Mtazy1S/O50OsU3muMKSVeydHkrkHZjPn6ymObfhCfJb2QgkPGOxYx3J4frbN/9WxDZfH71gnybxPqKw0dqO8f0/FOsQPtx5VtvfRSqIkDYKMmxAZA6Gq6sJeMjzOBkHA1CAIqAaBx2i/oGoQKC8wRMjEIIjqv8Xl18vccebmG7O3n3brNxjLpZelPfabq+XWXwot5ffJsqSn/w2CUMjZIAjpZa5wVV4DIbdBEHE0EjiDwMu8fHsTlmMdN2MQiLj9nEkrBoKEeRh6lPbVz7IPaaasJw0C5VrljXlng6CKMQgiAbNrlShXt8TM7yJjoDqPQVAdDDi25Q3lrxOscn5hTSWDPWMQGHyXo0FQpd8HtDrVzi/7lj/SMwaBw8tUzOvc34TP2SCoijs/8oNR5/76w8y9qECDwBvUJ5ZUkmlng8Dlyd8XwmKef7Y2vM7XgNvXM2Gv3cqkUTEGgctrcJ14nQ0CV8r5nPEEnPtbzvfRSgIOXwAAAAAAAFQwJblCkGHsoBpRlWfmxMNYpJymxacUepnZC2bRQKvHzWBybfmVmU/1s+wna0zbO2EZzFoQKWYmwOVTZsuZ5U53XVwr86SVGYOkPoNgJfX9hDqb3IMrBNxBdal15G+2j4Pl0WdFLbd+yqvjbDHjaTHLr9zx4dpX8XMHX+mqh/nNqisaUa24xjQyk2XJtOVYlmBWA5gikbKKWyEwuVa5Oj7mwlSHz/S6VOu5mBU/AAAAoBIpaYMAAAAAAACAQolGoyIeZyYwHfD7/SIY7BkXsRUJGAQAAAAAAKCsjIGGUER0iMK9FQYPHix+/fXXijMKYBAAAAAAAICygVYGyBg4XAwT/gLksnGRFvfNmy33h0EAAACg16AHzR133CHuv/9+MW3aNJlz5cgjjxQnnHCCrd7nn38uzj77bDF16lQ5Y3XaaaeJI444ouDvm/HWL6LKk1svEZw8y7GNupG1ef9eNXi6YxveKufZtrkf/+JYZ5U9xzvWCQ0ZlPfvi99737GNpj33dawj6s0isDhhebo/N5dgtEIqjWHnyDNDIs6RZ750+K65y6KiJ0gz0c9UWgxCqSaZ8NnZ+MP5z28i2rzQsU5KCffNkeY0eAUSa3UO2WqCUzQjYs7UL/O3keiZY91bhFwe4TeI9JXBY7mEKE4qt8JT0isEK9cHRHVNYUs2BtHOhKvIUIlcVC23QfgtRlNsJipmwpxxglfBxIvXwokxdViUei5TEalp+z0Fc4FbJmHPuBuDWsYIei2T/eS+Xsfzgxe128uYqJkyI6SKOurc4UozhSZH1eD9okfhrglXkSFNTa5VlxKat6+45557xPTp08V1110nE/m888474uijjxaJREKcfPLJss6sWbPE9ttvLw4//HBx2223iXfffVf84Q9/EFVVVWK//fbrl34DAMCKBj1XuHewnPWF4QOyDClpgwAAAMqN4447zvZ54sSJ4oEHHhBvvvlml0Fw0003yUQ/ZDTQBMPKK68s3nrrLXH55ZfDIAAAAEMoeh0XwS5nfVG5+RSQhwAAAPqRKVOmiA8//FDsuuuuXWW0IrDddtvZVht33HFH8cUXX4i2Nj2DOQAAAB1aHSh0q1SwQgAAAP1AY2OjaG9vF7FYTJx33nm2lYPZs2eLTTfd1Fa/qalJuo3NmzdPjBkzRmuP2qEtQ0tLSy//AgAAKG2wQmAODAIAAOgHfv75Z2kQkCvQH//4R9HQ0CBOOeWUrr+71YSKXm9OLQlxxRVXiIsuuqiXew0AACsOhc76e0TlUtIGQV1iiaiJJwrbyURNbigQ1USqpvtpESO4/ZiMucrnFLNfmimzGAVMyu0rSliqCklNNcVc+z0FJxrlUKuZilRNxONspl2ufWUYuBuRR5MCy9ATSjtcHZP9mOPAtWUiAi8BobhJGSsmN9gvnurf6Bi1tbVyO/jgg8U333wjrr766i6DgFYDFi1aZKu/YMEC+e/AgQPZ9iZNmiROP/102woBRTACAIBKBSsEZWIQAABAJUCz/xRlKMPGG28sXnrpJVudt99+W6y++uqirq6ObSMQCMgNAADAclwFimVdFTxwEBUDAEAfcs4550jRMPn7p1Ip8dprr4kbbrhBrhRkoJwElKOAVg0obwEZA/fee6/NpQgAAIDZCkEhW6WCFQIAAOhD9t9/fykifv/99+XL/rBhw8Rf/vIXccYZZ3TVWWONNcQTTzwhw5CSARGJRMRZZ52lJS8zIdGREHF3bvevdMrZ3c/jy+9Z27HIOSHT8K3XdKxTs1KNY5222c4JopyoXWWYY5106zLHOh6/QZ6cULVjFZdDcqeUz+4CymHitdkad05EtajD2U034M0/l9gec05uloileiTpmMW5UirEWpfm/Xu8o7lPEooRHYtn90iCMyfcXM6iIhKTObWT7mvX0gKBhqBMDALXzG+EK5InEySTQMrlZh5cXvvN1MWd4D69zPIoZepnmShMv1FbXvtDwvLqy/hc0rG0oivgMk/GU8WVJZm2uPYTyn4J5mar1pF9V55GBu8YOVH97jkNgY9xzvepIkzGyZ8r8ymnkd+jn1d+5vuMyrikcczD35WMKZ+ZFwTmgaTtl2LqMDf9dEx54DDH2eIegAYP36KvVebadTHXl/AHnK9Ltszelrujf8J3jh8/Xrz44osinU7LLSMWVtl9993l1tnZKYLBoJbwEAAAgIlBUEgegsqlpA0CAAAoVyiKkBpJiCMUCvVJfwAAoNzACoE5MAgAAAAAAEDZgShD5kBUDAAAAAAAQAWDFQIAAAAAAFB2uAtMTOYWlUtJGwTRbz8X/lDuuNouRvzpYsTBLiUShDtUpdcJ6uJld5U94oUrrEeLcIf1qBhpv3OEBYsRNlsuZ7FwZ1IXdXYm9HodCbuQtDmq96k1rpd1JuztR5O6IDWWchYax5k6pngU4a8qFiaCTJQLNfJFmImMEvHrZdV++2UQCeh1Qsz3pdUDJpcnVWGzVkW4UnoUD1eiw/bZHW3Vd+zUy9Id9rJUe4tWx4q26/t12susOCdi1vuZLjRRYAHXKicgdgX1a1W9ftlrt7peLwtH7J+VMQAAAFBewGWoTAwCAAAAAAAAigGiYnNgEAAAAAAAgLIDBoE5MAgAAKCM+XZxpwgqrmzZ+JncHCort9jzXaiEG51Do057dapjHQ/j0qfVCTonXOrw5U9EFRzU6NhGmnG1U/EyLqoqlkniJre3237NXI4VlTaDhGFcjheVDocEZ2nGtVUlqbi1cgwbM8Cxzo+ffC26S6LdOTEZm+OoiERfSYPzyuUQjtgk6ZgJ3qDdjZIj1rok798tJRdOqQGXIXNgEAAAAAAAgLKDzLhCRMWebiRVXdEpaYNgwRc/ik7/cqGhmxMlMla0J6Rbzr6wffbKX6OLEP3Vepmnzj474alt0OvU6yJLt6IzTjPWvCulz9SkXX7H7MKcgJgTDC/qsGeZXRrV+7mkUy9b3Gbfr1lpR/aBmR1SZ4zizAxRivk9qoCYKwsxs4Zhpqw2bB+/urAuUm1Q6hD1IXu9xjSTHVeps7yfjNhZ/Ync7CCXTTimiHzbl2l10ksX6k01L7bXabF/JmLLdDFyvMUuYk60d+ptR/V+phPMeWsgIPf49FuNWxFz+6r0WWZflZ7RO1Bnn9Xy1tTqbStia9mH+ibb53SbfQwAAACUF26Xq6BMxe4Kzghf0gYBAAAAAAAAfaIhcFXuOMMgAAAAAAAAZUfBGgJX5VoEMAgAAAAAAEDZgRWCMjEIFn01R3R6vbn1Aowfua9K9/X2R3x5fZCJUIOeYCzUZE/WFGQSNQkm8oBXSYTm8un6BBHQ/a4ty+6AnmB87qOMb/4SRh+woN3u/z23RU88NXeZ7je+QIkmskRph4gx2oOkoiFIKgnOCIv5PS5OQ+C1l3mZBGN+xqe/RilrqtGT2rXW6X7qiTrdT12FTY7GrC2mlWPIaQhcnIYgGc2rDSCSi+dqZfGFC2yfOxfo2oPoYj2CRmxZm/0zc37E2/XjnGL0IxaTQM/kWvWG7LcfP3ft1ujHK6hcq+GBuhYgxF2rCilGNwEAAKB8wApBmRgEAAAAAAAAFAOJhAsRCrvhMgQAAAAAAED54PK4WE+EnPVdhWsIYrGY+PDDD0VnZ6fYeOONxYABufNntLS0iFdeeYX924YbbihGjRol///aa6+JZcvsK/6jR48W48ePF70FVggAAKCMWRRLiYArt1vXwIBzwqXpjPtYNsMZd0AVLnS0VsfnXMc31zmJFBdaOpv4Et21TiXEhJlWSS6Y5VjHM2i46C4GXnlsmGqVSMD5kZ9u1d0HTcI+ZxMw+B6Pwfkwb5rzcXJ7mTDRCvFOu4tkMZgkJksndXdQFW+wqtvt+MJ6qGUVl8fTI7/JKWlbqScmc3tcwu3uvRWCb7/9Vuyyyy4iFAqJ+vp6MXXqVHHXXXeJ/fffn63f3NwsHn74YVvZzJkzxSeffCJeeumlLoPgzDPPFOl0Wqy22mpd9XbYYQcYBAAAAAAAABSEx+2Y+dmGq7DMZEcccYRYf/31xZNPPincbrf4+9//Lo466iixzTbbiIEDB2r1hw8fLh577DFb2XHHHSfmzp0rdtxxR1v54YcfLg2DvqKkVwgW/bhUdPxmwXIWHidU5ISJwXq7aDTcGDdLwqQkXOJOqqAiIJb7VdlFj66qeq0OJzZVS5LMtFAHk+69hUlMtlhJKDZriS68nLVEF1UuU8Slna36uESZZGXxmCIq5sSnaUZgy4nFvfYyHzODGWBExdGI3zGBGjeLpiZCCzAzV7XMjFcirZcZ3UqYpHRW1H580kxirdQyPYW8KiJum60nL+tY0KKVtS+wJ0KLtTDHuVmf+UkzYvGkck6aXqveoH38AjV60rhwoz7Tm2i3n6PpuD6eHGFlJjHd4TwTCgAAYMWF3IXIbci4vjCv+/3334tPP/1UvPnmm9IYIP70pz+JCy+8UBoI9KLvRHt7u1wxOP3007vayF45ePbZZ8WwYcPEOuusI/x+/RlZMQYBAAAAAAAARbsMFWAQuH8zCMjXP5tAICC3bP773//Kf9dbb72usqqqKrHyyit3/c2J//znP9IoOProo7W/PfHEE9Lo+Prrr+V333///WKTTTYRJWEQkBX08ssv28rIb+qCCy6wlbW1tYlHH31UTJ8+Xay66qrigAMO6HXLBgAAAAAAgGwvhEJchly/hQ4n155s6D2XZv5VPQBRV1dnK29oaNAEwbm48847xc477yxGjBhhK7/sssvErrvuKlcN4vG4OPLII6Uu4bvvvpNGR29QgGOVEO+//7548MEH5Y/PbLW1dnHL4sWLpVL6pptuEtFoVFx++eViiy22EB0dussKAAAAAAAAvblCUMiWcdehF/7MNmnSJKGSWTGg6ELqpHgw6JzfiGb/33vvPXHsscdqf9t99927XIhoQp2MkVmzZonJkyeLknEZGjp0qDjnnHNy/p2smlQqJd555x0RDofFX/7yF6mSvvHGG8VZZ53V3f4CAAAAAADQa9TU1MgtH+QaRMyYMUOsueaaXQlm6cX997//vdHqwODBg8Wee+7pWDcz+b5ggT0Zab8aBAsXLpQv/WT9TJgwQWy55Zaaz9PBBx8sjYHM0gn9WCov1CCY0ZkQQVe6K9ucir9TFxPWMOHx1LIks5+lCIgJt88+PL4qPWuqf0C73lbMbi26ucy0akZbeSI5h5rjRMWtjKhycVs8bwZiTkAs21KExh1KO0RU8a0jElF7WLdUTG/bSqeMwp65fXb3Mh8Tpi0eqXUUt1oGAmKiWhG3NlT5jcY9xRxD9StdXKZiZhw0UXF7i2N2YS4LMScgbp2r79c+3/59Lc368WpmBMRRZky5cVAJMULtoCJYr2fO0SQjmFczI3uU65TwVemzM2qG8lRnaYfLAwAA0AN5CHpJVDxhwgT5jktRg84//3xZ9tZbb8mXdnL3yfDqq6/K3ATZOQSSyaS47777pHbA67U/w8jdiN6hs13tn3rqKZkjYdy4caJkDAKymEhsQRYR+VTttdde4oEHHpAdpeQMpBsYM2aMbR/6TErpXNB+tGVQxRwAAAAAAAAUbhAUoCEQ+kRYLnw+n7j66qtlNKFEIiEaGxvFlVdeKf39N9hgg656p512mhQD33HHHV1lzz33nDQcjjnmGK1deo8+7LDDpGZg5MiR0k3olltuEeeee65MTlYSBgHN/P/1r3/tyuR2wgknSAtpt912E4ceemiXTkBdZqGlDlJR5+KKK64QF110UXG/AAAAQE5aU2mRby0kZDB7xq0WZTNUWWXjUMM4s31p0FdhVYK19kgfHNyKbzZGLwgGSZtcymomW8dgBc2V0ENAZ+MN5U+0RhQwCdrtxEzcSms2fiV0NAcXTroYApHcWWHzrU4X8veeJBXPf6yJQK0evz4bK+Xc35TiqcDhDUWc+1Kdf3ytZFQ4f9OKF2XIFHr5J0HwI488IqZNmyYuvfRSmZsgm5122smWYCyjUaAcA6ussorW5tixY8Xzzz8v7rnnHpmxmMKOvv322zILcm9SkEGQ8ZfKQMkYaPmC9AJkEJDymYyFjPI6e/kjEsl94pFYg2KwZq8QqApvAAAAAAAATKF3UspFYFw/Xbhlvd1228ktF9dee61W9uc//zlvm/QOnHFD6iu6nYeABMQUTYggfycyGkg5nQ19zgguOLj4rgAAAAAAABSL2+OWm3F9q6Dgm2VFQQYBhUeiEKIZPvjgAzFlyhRx0kkndZWRz9NDDz0kzjvvPOk6ROmYyVfq4osvLrhz86MpEfgtjTQrKmasvk6f21H0yFmL3pA+FIEau5tTsEFfGEsz4VStuCLQZISlJjD6TZFgCmNJvf1mJZtwc6cuto62O5dxAuJo8yKtLN7R7Lhcabos6wnY3QaSQX11Kc0KlBtsn70+fUm6I6T/5mUd9rK2mC5kjTIuCNzxMfAOYM8HKxHPfw4xGXqJeKv9/OtcqtfpWKQfC1VEvIjJ6sy5ibQx51qxouKI4lbAtdPI9F3NcOxjspMH6vTrMqFkJk5AVAwAAGVNwaJiq4d878rdILj++utlGFESSyxatEg8/fTTUiFN4ocMFJL0pZdekr5OW221lUxkRm5Fxx9/fG/0HwAAAAAAAA0YBL1kEFD2YVoR+Pjjj2VIJIoytNZaa2kCYvo7RRWiSEQUcpTCL3k8PSMgAgAAAAAAwAm4DPWihoBWB7LDKXGQlsAkKQMAAAAAAAC9QoEuQwIuQ6VJSzItMkHduOPJ+SVz9VStQUTxGc+VrCyhlKWiepKuVELfT6TTPaIhsAST+IpxXI8z/u2qriDJ+IgnGF/5uFIW72hx1AvIttqbHUOrmSYmSyf1cVZxe/VwfwlFa5CIeR2TlxGdythwmgxu3FNMWdpERMCdD+rYJPVzNJ1gzlvlnOQSeSUYrYiqD+D0As3MWPEaAuFI3CChGXuNM/0KqYkGmd/MXZdpJYGfxV27AAAAygYKo+suIMqQ2yDsbrnS7ShDAAAAzFm6dKm44YYbxIsvvigWL14s3S4p9HJ2jGmKPU2hnFU+/fTTgkMy13ndIuDKHTmDC86gMjCQ/1GxhJlwUKkxsBxjTLbqYvAE8+cHMIk6klq6wLGOd9AIxzppn3NuBcvjy/93g4mG4qaddLgAHoUSZyYOVELVzpEFAyHnCPdJxdDnCNYMLHiCSSXGBNMoNGZ/T+U88FXVOtbx+PVs7SqBSL1jnda5v+T9e7rEX6Ap50hBicnSiDIEAACgD6D405S9/R//+IdMZ3/bbbeJrbfeWkZty6Slp1DOlL+FEt1kM3Bg/hcbAAAA3UhMli5tA6c3wQoBAAD0Iffdd59wu/83C3X11VfLrJQPPvhgl0GQYfDgwTg2AADQV1GG0pVrEFTu2ggAAPQD2cZAhlgsJoMxZJNIJMTaa68tVl99dRmkgSK8AQAAKNxlqJCtUinpFQLyQkznSQLl4QS2rHgx/2fCYvZTyyxGvMuWmfgIFik05pI3ceJWtYwTxXJlliKItlIpo9+nlvF10kZlqtDYtA+qGDmdDut1uBPJSCwseheDcU+z5596jjJ955KqmZxXzG/mykzO5GKvS65faYPfzF2Xpcott9wiZs6cKSZOnNhV5vV6xZlnnikOPPBAaUDcdNNNYsKECTKks7qKkG1U0JahhUkqCAAAlQS9ThTmMiQqlpI2CAAAoJx59dVXxSmnnCKuvfZaMXbs2K7ynXfeWeyyyy5dn2+99VYxdepUcdlll4nHH3+cbeuKK64QF110UZ/0GwAAVgRcbpfcCqlfqVTu2ggAAPQjb7zxhth7773FxRdfLIXG2biYyB20QvDtt9/mbI8iFTU3N3dttOoAAACVDK2wZpKTGW3uyn0txgoBAAD0MW+++abM4n7++eeLs88+22ifn376STQ2Nub8eyAQkBsAAIAiRcUerBAAAADoA9555x2xxx57iPPOO0/O6nNccMEF4qOPPhKpVEokk0lx8803y0hExx57LI4RAAAYAlFxmawQUKoWnyt3spQQY8lxSXbUMs4A5PzGPD770pHbrw8Xp0jnsu8ylUQxcOPgcTuXcZn6PF4m07PX3ne3T0+Y42GSuKSUMhPhsWmSGLfPb5RIRi3jfh87Dur5wdQxdSssOsuhskzp8niMkimpsxnc7IbHr7flU7L7mlw3uco44a9JVnG1LdPv8/jdeT/nvC6VMlc/LQ2fc845orOzU1x33XVyy7DrrruKu+++W/5/t912k8bCZ599JuLxuBg5cqT497//LQ455JCi7hn5kk3NZLK0qwxgzqFsGh0SlxGBGufVi8gQe7ZxjuCAGsc6/pqqvH/31jgndnJXOX+PyyD5k+V2HhtXMn9CtrSv2rENv8HMZsJAbJ9gAj2orNyU/zi1MtnDVX6d2+pYp3Go8zEIG5xXHQ4J71Jx5wRokUGjHOvE2pY41ulcOt+xTrXDdyUN+muSdMzrcF0TA8esnvfvqViHcP7VYEWgpA0CAAAoN5577jn5kq8SDP7v5ZKyFpPGIBM1CK5AAABQODTxU8jkjwsaAgAAAH0BZSc2BYYAAAAUT0YsXEj9SgUrBAAAAAAAoPwoNNmYBwZBSVLj84jAb772nHsk55dcq/j9y3YUX/JgdcDIv9Ufsfuke4O63zpXJry+HtELuIT+o33MQAQVv38irPgGBhgfX1/A41jmD+u+tulEwlE3wfk4phNxI72FNxCyfw7q/qp+xr/XH7b78/rY36yXhZSxUsdO7seca5zWwAjufFDHQT2H6PuC+jnqq7L/Zn+Vfj76q/S2ahW/8YRh4jXOp19NOsZdq9x+EeW6VK9Toiqs9139jb4qs+vS7bMfe1fSTNMCAABgBXYZKuAl3wWXIQAAAAAAAMoHaAjMgcsQAAAAAAAo07CjngLqp0SlAoMAAAAAAACUbR6CQuoXyieffCIeffRRGU562223Ffvtt1/e+v/3f/8nZsyYYSvbeuutxVFHHdWtdrtL5aonAAAAAABA2eJ2uwveCuGRRx4Rm2++uUwiOXToUHHiiSeKP/3pT3n3eeaZZ0Q0GhXbbLNN17b66qt3u92yXiEYEvKIkGv5Ug+XWCdoIFQkqhTBcLgxpLdVryeY8VWHbZ+9Vfp+7pCeAMelCkKZxDSWQQIrzlD1MSdrkPnNtWG7qHKAIpAmOjt1cXA66awu5ZKCxTualXbiZmJkLgGXMn4+RlQciOjjHlZ+o/qZqGUEqHWKcLWKSUAX4JKccQnuTHTGzPngCtjPP3dQ/33+mrBeppyj4UZdzJ2KOy+B+pZGtbIqRh3cmdLPj4SSmIy7VjMJBvNdq5yAONygX3Pq9RusDxslplIF2Ez8gbJktYhPhPMkS+QE3yq1A/Tj4JQcTsXHiNv1dpyX9lMJ56RXqah+/8nGxVxfKu5InfP3NI52bqd9sWMdy5s/uVa+xHIZggYzm4Mizkm8ljLPBZXaQP5jWb1qo+gJUkrAAo5fYs7ngxMrrTHMsU5nW/5ziojUDXKss2zhCMc6Mcdkgc5Jx4LM/VQlEHKuE6nLn3wvGfWJH0RlrhAkk0lxyimnyGSTl1xyiSzbcMMNxS677CJOOOEEsc466+Tcl+odeeSRPd5u2RoEAAAAAAAAlJpB8Nlnn4n58+eLiRMndpXtuOOOorGxUSagzPfi/vLLL4sff/xRDBs2TOyxxx5i3LhxPdJud6iQOTIAAAAAAFBJuFzLMxUbb67lr8UtLS22LZM1PpuffvpJ/jtq1KiuMnI5Gj58uPj5559z9ikcDouVV15ZrLvuumL69Olik002Eddff3232+0uWCEAAAAAAABlR7ErBMOHD7eVX3DBBeLCCy+0lZHYl6iqsrsgVldXd/2N46mnnhJNTU1dn8eOHSvOOusssf/++4shQ4YU3W53wQoBAAAAAAAoW4OgkI2YOXOmaG5u7tomTZokVGpqlidIXbZsma18yZIlorZWT+qaIdsYIMgQiMfjYsqUKd1qt7uU9ArByPpQXjGcL6R33xv0OgqGQ4yAONykZ74NN9mFO6EGvY6byZjrUoTGFiMi5bLVqroxTkgW9unjEWGEeA2KoLYtqovJ4sm0VrZYERi6vS4jIVI8FnAUJ6cV8WkuPIqYlcsuzPUhqIgW62r04zyEEUg1KfWqmQzO3Lj7GDGmWmQxx5kTlLv8QcfzKlBXrZWFmuxi4LSB4JITb3KZukMt+hJpKq6fM+lUuuDv467fQI0u+A7W62LWqia7yDw0UBd/Brlrtdpez+3RhdQAAADKB7fHLbdC6mdeyjMv5rkglx/i66+/FltssYX8P83g//LLLzIqkCkdHR3yX+u3d6SeardQsEIAAAAAAADKDpfbVZiGwG0SKnA5a621lnT3yfb/v/322+WL/T777NNVdt5554m7775b/p80AF988UXX3yis6OWXXy6Nj8zLv2m7FbVCAAAAAAAAQCkmJrvnnntkONDx48eLhoYG8d5774mbb75ZagGyNQMkHKbEY4FAQBx++OEytOjIkSPFl19+KWf/H3vsMZs7kEm7PQ0MAgAAAAAAUHb0tkGw/vrry6hAb775pnyxp5UACiWazWWXXSYGDhwo/7/SSiuJ999/X+oFKOwo5Rugl/5gMFhwuxVlEAxYdYCI+JZ30c0kSXJzft2MrsBfE8qbzIkINuhCjXCT3efYO6DRKHmNO2z3O0t7mOQfjG+5WsKdl2HGF7uW0U3EUro/toqfSbYVUdpawiTyamcSwSSUshSjT7CYJDPc8pxH6ZeXOc4hRldQqyRiaWL84ofU6T7pTUqynvqgfryqmXH3Mn03WmxkEru5QpG8/u6yrLNdK6tK2ZOOcb6SXiUhFxGoa7N9jrcu92HMJskkx0lG9bK0kqyMu1a5m6yq9/Ez51qgjklKV1/tqBcINi2/+WbjqW2wf/b2XrSGUqKxISyqmASA+bQbJsc9m0Hr2kVyHCYJ8qqHDXCsox5/jsiw/ImxLCZxopMWjK3TttCxjvD4i0pWWChtBuMbZjRhKmsM1K85lZ+X6PeLbJZ0OuvFRjY6j68J1czzT6V1pfxCzOYO52RsrcwzR+sLcw8rJhlYzCE5HKerU5mwiv1+xzF9kf5MMXlPyCbeYaYN7O+wo4XUL5RIJCL23HPPnH/fe++9tbINNthAbt1pt6IMAgAAAAAAAIrB5fEId54JEa5+pQJRMQAAAAAAABUMVggAAAAAAEDZ0dsagnICBgEAAAAAACg7YBCUiUHQtP5wUR3ILdLx/CY4zsbt0wU7PkVUyYksfbW6MNFTXe8sIK7XxYsiEHYUkQpG5OJSElb5GdFqgBFsNoadhUw+5vuqGKGsmqRrGSO26ozrAsNORdAWY0TFpqhi3RDTz5BfP/Z1iqi4hhEHcwLsWkWgxY1nyOc2SkzmUcu4xGSMyNDy2c8Zd40uCPMoAmLZvHK+VzFCyOAA/dxWRcSJdj1JVyrKJCYzTHym9ZM5/zx+X97rlPCG9TJ/TZXzdcmIsj31duGrx+csqAMAALDikskvUEj9SqWkDQIAAAAAAACKASsEfWAQ/PDDD+Kmm26SsVKPPPJI29+am5vFAw88IKZPny5WXXVVccghh4hQSA/3CAAAAAAAQK9lKi5EQ+A2z1RcbhS1NhKLxcTEiRPlS/9zzz1n+9uCBQvEuHHjxP333y8TLdxwww1is802E+3tWJ4HAAAAAAB96zJUyFapFLVCcOaZZ4oJEyawWdMuvfRS4fV6ZXY1StF86qmnitVWW01cf/31YtKkST3RZwAAAIZsdukhoiace4X2u7ufdWwjMiR/YqfIMEZLpeANOWud3IwuTMUTNEi6OHzl/N/D6E60OnX5k5tJLGetVCpc3+12LMs5+VNd0Dl+ekfCuZ20cP5NqwzQk3tm06jouTgGGCToSqTTPZKYLO6gaUsySTOLQdXS8X1xruPE5J+XdDuhGLHGUF1fVuj4Rts94mFRurjcHrkVUr9SKdggePrpp8Wrr74qJk+eLA444AD274cddpg0Boj6+nqZae2pp54q2CBoWG8NUcOICrtgDpzLq99kXD77A8QV1G9mLr/+Pe6qmryfibQv5FhmefW2LS5TsbJS5WMExCHmIle0yL/ta//OiF8XgzYk9LHqSDiLg2MpvSyhZKtNGTzAcuFRfhA3DpxIOqCMTZgRAoeZrMcRRaAcZMY45NP74OeyZ6sFrKhYH3fLH3J8QfB4GRG9ck5a9fpKnIfJcOyL2susRMIsm2vSOaMnC3eTVY6hOxAyui7VMleVnrnWHWSyoCplLpeeyRoAAEAZQc+eQl7y3TAIjJg5c6Y4/vjjpZtQOBxmXYlmzJghVlllFVs5fSaDIBe0H20ZWlpazA8eAAAAAAAA3MRTIW5A7sp1GTL+5alUSoqDTznlFLHhhhuydTo6locyrK62z9jV1NR0/Y3jiiuuELW1tV3b8OHDzX8BAAAAAAAACi6Pp+CtUjE2CF5++WXx8ccfi7lz50pdAG3ffPON+PLLL+X/lyxZIqqqqmQs/WXLltn2Xbp0qWYkZEOuRBSZKLPRSgQAAAAAAADddhkqZKtQjDUEq6++urjyyittZRRK1OfziVGjRkkhsd/vF2PGjBHfffedrR59XmuttXK2TXqDjOYAAAAAAACAnnEZKkRD4K7YQTc2CEgHQCsB2bz22msytGh2OQmN7733XvHXv/5VCopptp80B+QWVCiBNcaLQIQRB+YTFTPLPZaqunXrP9tiyoSSUTbFCJa5rLPqfpyIlOuDKlHlMhULpikvcwKrASeqGIEtJ/xVoy1wwRe4spS2X/GiYrdyvLTsv8xYLa+nfGbU1owOWHiVQi4DsZo9OZeoWO8rMw5M5mpVQuzizg9VeEwE7dFfXCld9OtNM9mFlTJXWo98YTGZkQVTzwiDa1W7Tk2vVaZOmhNuK+OeSjtHqwEAALDigkzF5vS4KXTOOeeIoUOHio022kgcccQRYtNNN5V5CI477rie/ioAAAAAAAB4XAW6C7ngMlQU9JLvUWb5IpGIeP/996XmgCIOUQjS7bffXmoLAACg0mltbRU333yzePHFF8XixYulO+XZZ58tNthgA83V8rzzzhNTp04VgwcPliux++yzT8Hft+izL0UskHs1ZMR263U7P4DXIDeAb5Cet0YluWiucztDRzvW8TQMzvt3LoS0SqqqQfQVlj9/XP+AwfMz4DZZlXVuJ8SEZlaZ0cyEJM6iNuicY6DeIA9Bc5RZ3VRoqnI+9xa05+9vwCCTbXPMuS9q+G0OLhy2ypSZdh2myh+3G+PYhhqGm4NbDS+UjoBzroh+BWFHezcxWYa99tqLb9TrFbvvvnt3mgYAgLKEQjePGDFCXHDBBWLAgAHi9ttvF5tvvrn48MMPxdixY7syvm+55ZbyPvrggw+Kd999V+y3337imWeeEbvttlt//wQAAFghgMtQHxkEvU1y0OoiWZM7OhELkwiqp4QkXDIxo+8z3M9t4LfOlVleZz9/k0yXcj9RHN2QDBSFyYKT6Zmgrl6ZTppw1fR93cXpTrgZQ4OsqC6ujkG2TxaD7+tRTK/dHrpW0ynnGcre4L777pOTJhluuOEG8corr4j777+/yyC48cYbZZ0777xTrsKOGzdOfPLJJ+Liiy+GQQAAAKZghcCYypVTAwBAP5BtDGTnecl2v3zrrbfEDjvsYCvbZZddpFGQL6cLAAAAJsqQ8eau2OGr3F8OAAAlwF133SWmTZsm9t9//66yWbNmSd1ANvSZVvooFwwHZXunLO/ZGwAAVDJITGYODAIAAOgnaCXgT3/6k/jb3/4mxo8f31WeTqe1lQTK+ZJZTeBAxncAAADFAoMAAAD6gffee0/sscceMlTzmWeeaftbY2OjjECUzaJFi7r+xoGM7wAAwLkMFbgVQCKRkNHgKCnvsGHDxKGHHirmz5+fd5/nn39e3vuHDx8udWPnn3++aG9vt9WhoBJ1dXW27eSTTxYVKypekA6KzhSTjKm7YlOmjsdikl+5DPYzSH7F1XExIlxtP0sPc+bikkylko6CUC7xFCsaVcuYOqxwtTcFqIZiU01Iyu3HJaBT6llcVkMPkyDLoH2uToqxw7XEbkLvQyrNJZdT+sQmktMLLYNkc6UgFOeuOZdyqnGJ60yu1Y50/8WbptDMu+66qzjjjDNktCGVCRMmiLffflszIChBJEUm4kDGdwAA6FtR8Zlnnin+85//iIceekhO1px44oky8MOnn34q3Ixx8cEHH8iw01Rv3XXXFb/88os45phjxLfffisee+wxW3hqavukk06y3eN7E6wQAABAH0LhRckYOP3008VFF12UM8cL5SG444475Ocvv/xSZoA/4YQTcKwAAMAQl9tT8GbKsmXL5Mv95ZdfLrbeemux9tprS03Y5MmTxUsvvcTuQ8l6n3vuOWk00AoB7UfR45588knR1tZmqxsMBm0rBKFQYRPkZbVCAAAA5QatCtCNn17wacuw0047idtuu03+n5aRKQwpLRFTfRIMkzFw2mmnFfx9Qw84RNREqrrV59h3n+f9e2C9zURP4FtjI8c6qXnTnBuqG5S/jYDBeBiE63VKKCbreJzD28at/HNzfoOA0EmD+b2Ixzm5VtzgtWBEbf5kYHGDBF0JbllSYbVAwrHOtKTzMdh8eP5EdO0J5/Fd0O48dqsHOx3rfNrsfD6MHZy/v0OrfT3S36Yq52PdHOM1SxnaAs7f06+4CnQDcpnX/fjjj6XLECXfzUCruKNHj5YrulzOGC5JLz0PKKJcRieW4ZprrhFXXXWVdEXae++9ZQJLMhJ6CxgEAADQhzz++OPyBV8lHLa/2Bx44IEy8hAlKauvr+/VBwEAAJQjhc76u36rq0Zp41wyZ8+eLf8dNMg+AdHU1CTmzJlj9H1Lly6VKwx0v89uf8MNNxS///3vpVvRlClTxJ///GfxxRdfyJWE3gIGAQAA9CFDhgwxrkuzRoXUBwAAwOQhMMW9fIWA3HmyIa3XhRdeyO6izvpThDiTZLCdnZ1in332EdXV1TJBZTYZd1FipZVWksbCzjvvLL7++mvpmlRxBsH3iztEVSz3gXSzgl69zKeodX3M8lHAy5UpB5lRKvo9TJlSL8i0ze2nCYETUa2KK6nPLLqSTL1EzLlOSl+OTcfsS55WXN9PJPX9LDUUIidiNkW5eCmOsIZXXzJ1e+3L2C4/M6Pq05e6La+9nuXVhTuWj2nLoF7a7TdaKo8lLccl93hKX9aOK20lmZXvaFI/Fgmlfa5PCeb7OIGyCSbXqnqdml6r/H7O12prvBvnKAAAgNKn0MhB7uV1Z86cKWpqavIKemklgKCIcNl5Y2hVl7QCTsbAXnvtJaPHvfnmm6K2tjZvfVoxIL7//vteMwggKgYAAAAAAGVHsYnJampqbBtnEGy00UYyktC7777bVUauQj/99JPYeOONc/YpGo1KTQC5HL3xxhtdhkU+KMgEYVK3WGAQAAAAAACA8g07WshmCGkHJk6cKN2Jpk+fLnUHFPhh1KhRYs899+yqt9lmm3XlECD92L777iuNAVoZUPUHGbEyRR7KZKWfOnWqDFNKwSacVh66AwwCAAAAAABQfvSiQUDceuutYoMNNhCrrbaaaGhoELNmzRIvvPCCbUWBDIWOjg75/9dee02GJP3111/F6quvbgsrSrkICHrxp4hDm2yyiWxn2223lblpXn31Vakr6y1KWkMwZXaLCFblDgfm53zzmTLVhz/g0etEAvpQVPvtAx/x63VqmP3CPnv7biYLGadHEEoiMs7H3xWzx6mV7cda9aba7Qr5lPKZSLct0/frtGfLS0fb9ToxXVeQTtj7nooXH4rMpRwfj08fYzcTccUVtEdpcYf00ILusB7OzV1dZ/8c0X35rFREK0tzQV8UHQPncc/pA6KKv34HEwavjfF5b44mHeu0McciqogNOhL6fnFGkKAmUMtVpsIlD1Ov1aBXv9Fx+puIcl2Gffp+tUH9nKkN2nUnbfFeTKYHAACg33G53XIrpH4hkCD4gQceEPfdd59IpVLC7/ezuWdIaEyQMJgiC3FkNAsUUY4yz9MWj8fZNivOIAAAAAAAAKAoXAXO+ruKm4Gnmftcs/dkNGQgw4BWA0zpK2OAgEEAAABlTGLYOiKRFS1DxR3TVwG1OsPH5v8Og3644509ksRL1I9wrJJymOWzPD3zkO1MMyu9CiG38wqa5bTK5vw1wmuQvMxyOz/yA0xEOhWPEplNxW/wm9lVcoWEyB95hRgVbXasIzryr1jHAwMcm1ipxvncbBfOddZsFN0em2CUn2HOxl9j/tKZjzEt3+T9e0ur8/2jX6FodgUkGxNMRLxKAQYBAAAAAAAoP8gYKMggcItKBQYBAAAAAAAoOyyXW26F1K9UStog+OinxcIXihqLEomQIjgkqhWBYYQRHDZE9BizDVX2ZeX6kM8oUZPL5XNMkhTwMvupicmYpVt3nBH5turLh6mlC5TPC7U6yWVLtLLYMrtoOd6if1+iXe9XMhrPKzKW/WQSXakCYlmmLPd7g/ryvrdKX7L219hFxIE6XQjsq9VdJzwdDfbPcT3Or5tZUXYx7g2WL6Q0LoxEuJ0Je1lLVBf5LuqIO5Yt6dSdNxa36fs1K/u1KuJkoo0p44TGJnDXr3qtctduXdjneK3Wh/XzY0Dc5yikbm/XxwUAAEAZgRWC8jAIAAAAAAAAKF5DUIAuwAUNAQAAAAAAAOUDeRwUEkrUDZchAAAAAAAAygZoCMypXFMIAAAAAAAAUNoagvlzWoQnsFzY6GJEiR5GkOpVsgQTAUUMXK2IhYmBNbrAcKUBYUcBMS8YtvchxIifmaZ0kozokckcnGperJctnmffbYEuKm6fq+8XXWzPaBxbpn9frCWmlcXb7GLWFJP51mIy9Lo8zHFVMs96Q/ppGqjRReCBGrvQONigC4jDTR16WVIR4jJJTFxBPeuxy2c/PySKMJwLL87pchNKxSVRXRy8gBHBzm2xC7xnLdF/3wLmeC1V2oozYuR4TBcVJ5kMymoMde5adTN+mV6//brwK5mEiQhzrQ6I2H/PkDpdYJ5Qrt3lfbB/7mB+MwAAgDICouLyMAgAAAB0j3kxt2iP5l4MDnhyJy3L0NmZfwbDMpjhCPv0qF8qKX0eQcPPTCKoOOXFisec++uUK4zwuJwr6Sa6Tm3AIZGagdBRjVJXbB2Rdq7jiy3O//ceCt3oirU510k4J7xLTc+fXKtu6GjHNkyOQTpU71hHeLzFTQZm98WXPzEcEWm3TwpyuKKtzl2pGZz/7y7nNvoVGATGwCAAAAAAAADlBwwCY2AQAAAAAACAsoNWdgpLTOYSlUpJGwRL5y4Rbv9yH2kX49ft9ev+xb6Ax1FDEGN8hzsZn3eT5Ephxd+dqFV8oWsD+rIyp0dQ/c9dab1PaUZDkG5dppXFly511At0zNMTk7XNtWsIOhbpy7GdS/XEZIl2+5jG4nrfU8xv9jAXn09xCfAzfuSBGr0s3BjOmywtV3I0t89+GYQYvUA6Uqf3vVrPVsYdM5Nx6FDGq43x31/MJCZTNQOzlujHa+kyvaxDSVYWZfQJiZj+W5JxvV9pZXmbu1bdXl0f4FUSkQUYrUg0oh/nzljQMdGbhwkdF1S0PJ0xaAgAAKCswQpBeRgEAAAAAAAAFAUSkxkDgwAAAAAAAJQfWCEwBgYBAAAAAAAoO5CYzBwYBAAAAAAAoDxXCBhNWd76FUpJGwTtC2YI12/xdjmhoscf0sq8IV0QGqqxiz9TjLCUY6EiGK4O6sM1MKInyOpQBMqciJSNXq3GiE7rAk4rpgt6LUZoHFvWljfhGNG+QI/x3DrHXta+QI+ivYwRZbcrScc6mTGOp81ExSFFVFzFiFvrGREslwxN+z5FQEz4a+znTKCuxWiMRSrueAy5+OycCDaqjFcbI8pe1qGP+2JFHLxMSVQm21qml7UrycpibXos6XiHPg6pmC5QtgyE1G6v3/FaTUR0kXaKy+KmsJBJ/Mddqw2KQDka1a8vAAAAZQRchsrDIAAAANA9flrSKariuW/1Szu7bxjd+OoPjnXO22stxzpcRmunaFEcarZ4vQ19gknFJPhgwOtcq8rn3F9vOt7thGKWRze6Vdzt+ROKye+Kd3Q/uVbK+Zxyx3omoVW6eZFjHSuuT4pkk1q6wLmNRNy5TvIXxzpuZtKy0O9y+j3ye+qbHOukO5mJLgWPQzueNuc2+hUYBL1nEEydOlU899xzYtGiRWK11VYTBx10kKiurrbVWbx4sbj33nvF9OnTxaqrriqOPPJIEYk4Z6kEAAAAAACgR4BBYExBzlJ/+9vfxDHHHCPa29vFoEGDxD333CPWWGMNMXPmzK468+bNExtssIF45plnxODBg2WdTTbZRLS2lnh6awAAAAAAUHaJycw3l6hUCjII9t9/f/Hxxx+LSy65RJx11lnirbfeEvF4XDz44INddehvtBrwyiuviEmTJok33nhDLFiwQFx33XW90X8AAAAAAAByrxAUshXBd999J6ZMmSLfiXtyn2La7ROXoVVWWcX2mVYKYrGYaGr6n48ZrQwcddRRwv9bFuGamhqx1157yfLzzjuvoM5FWxYJlzeQW1Qc0EXFvrguelRxuRv0/QL6ULQp4tlWRoQYTeqCyoQiEGU0pEawfqPJhJEfYLLdPg7xVt0vNLrUOQsxJyBexAhemxNpR1GxojvOiSoqrmWyQXNC7aaldqGslxGWchmO4y3tjhmOvcy4u7hs0wZw3sDqORNlxLRtUb0PzYrQOKpkjM5VpoqIo4wfbryjWStLdrYVJSrmAgCo1yrXjss9SCvzKudDIKT/Pu5aVbNBR+NmwQUAAACsoPRyYrKZM2eKPffcU8yaNUu+77a1tYmHHnpIbL/99t3ap5h2u0vBptCcOXPEH//4R3HYYYeJjTbaSJxyyini8MMPl3+LRqOy86NHj7btQ59/+umnnG2SUdHS0mLbAAAAAAAAKNUVgsMPP1zU19fLd+NffvlFHH300dKbZtmyZd3ap5h2+9wgCIVCYv311xdrr722aGhoEC+++KKYP3++/Ftn5/IZP1VkTNZNR0fuyAVXXHGFqK2t7dqGDx9e+C8BAAAAAADgNwrTD7jlZsqvv/4qXefPOeecLq8Y+j/N5j/11FNF71NMu/1iEJDFQisE1Ln33ntPzu5fcMEF8m+kHXC73ZoFs3TpUmkU5IK0Bs3NzV1btkgZAAAAAACAvlohaFG8VuhdV4V8+4nx48d3ldXV1cnompm/FbNPMe32ex4Cn88n1ltvvS53IPpMHf7mm29s9egzrSjkIhAIyE0l0dkqXJ7lB8HFZJpLJ81EFqr/ciKoh0BNxHxaWVJJdBVn/Lq5soQiGijS1VxPVCbjHOu/2eL89RN2H+oU4y+dYOKPJxXfazXhGNHG+bcnnTUECcvMIk1Zzj58frdepyNm73sw6vz7iLQyVurn5TvqfupWivGdN4kXzoyDes6kmUqdjHYjrpyj6jmbqywRtWsBkspnWadd1xCkGI1OWhmbYq9VLnlZMsjoYxL2azXJjAs7Vso5quo2+hK6J95yyy3itddek2GZ//KXv9j+TrNDNPGi8vrrr4thw4YV9F0fT18mAlW548JzifJUmjvyH79xY3RdlspHM5yXuuvC+n3YJJmhSmM4f0z+wdX680Yl6HGeL3O5nPMZuFnVkJ1Wh7j+3P1OJdTuHI/fxSS7LCo/gNvh1cHge6x2Z9fg1MLZznWanXMrsIkls4jPdM4fwGnLVHzh5YlUu4vL4dwz6Ys3+LNjHe7dQcVTpyeMzCbW4ZwTof+jDJnrAqzf6qqeKjTxfeGFF9rKlixZIv8dMMA+RuQ9k/mbisk+xbTb5wbBCy+8IHbbbbeuz+QqRFGESE+Q4cADDxS33367OPfcc0VjY6Nc+nj++efF1Vdf3bM9BwCAFRB62T/xxBPFcccdJ1KplIzCpkJLw3Tv/PLLL23l2QEcAAAA9A4zZ860ebZwk9Y0CU7Q6gG502cg9/mMq08x+xTTbp+7DD322GNyReCQQw4Rv/vd72RiMlrSyI4eRDNdFI2IyidOnCg23XRTsd1228n8BQAAUOnQPZFWCE499VTbzV7F5XLJPC/ZW+ZBAQAAwBlabC90I8gYyN44g2DkyJHy39mz7StZJAQeMWKEVt90n2La7XOD4K677hJPPvmkDIVERgHlJKCsxdlWVDgcFm+++aasu8suu0gj4umnnxZeb7e8kwAAoCzgHiwciURCbL755mLjjTeWEyo//vhjr/cNAADKCXK/LXQzhe7NFESHwupn+PTTT+WL+4477thVRn7/Gdd6k31M2+1pCn5Lp9l/NR+Bisfj6dVYqQAAUM7Q6sAf/vAH6YJJgRpuvvlmMXbsWPHZZ5+JtdZai92HlpezhW8I3wwAqHTo9b4QGadVQF1a4SVdwfnnny+CwaB0kyePGcq9tdlmm3XVI7f6TTbZRNxxxx1G+5i229OU3bQ9J/Q0SZxkAie+MxHkcRSbrIzDSqcdxUKceMhiBMNppWNcAjAuwZhazzQJGSdpMtmX65fJ7zPBRGjV25j8PlPUY8qdM2nDa8TkWuLOR5O2uLb5MlW034MXU4lAq6u777571+etttpKTJgwQVx00UXikUceyRm+mf4OAABgOfS4KOR9K13g4+T0008XQ4YMEQ8//LD08afEvKeddpqtzrhx42TAnUL2ManT05SdQQAAACs6tMqqrhhsscUW4tVXX80bvpkeItkrBMjpAgCoZGjCqJBJI6uICaaDDjpIbrm47777Ct7HtE5PAoMAAABWAGbMmCETNxYavhkAACqV3l4hKCcKTkwGAACgd7nqqqvEd9991/X5oYcekhkqjzjiCAw9AAAUoSMw2SoZrBAAAEAfQpncSWBGTJs2Tca7pmht66yzjozKlvE5Pfjgg+XfksmknPm/7rrrZO6CQpk8banwhXInMooyietUgr78CbhMtFQhf4djHZN21hha7VhnQaueVTSb5pieaFBl5fqw6AkG1nd/1SYgnPvrShkk/+vUkw2qWD7n3+1qy58MLDl/hmMb0Q1/51jH98NNjnXaZzgnL0t25h+b9nnOyc3cBonq3D7nVyq337lOOp4/sVtk2EDHNuZ/8q1jHX+1wbH2/Jr3760xswSx/QVWCMrEIHB7fcLlWR532+X2GGU29Qb0uN6eQFBrV6vDXOzkt5uN36vX4cpMMmmyiSdNzFNmHLjMsOqNyePX9+PKvMqDP8hkfA159M7H02ofmKzOzMOeGyu1fS5LZ4g5Xmo9b1A/vd0+5/Fjb+rMGLP8lva8UNQh5cbFw4yDSynjzmMPd4767deEV8nmTaRinY5Zv7ksxKbXqke5Vrm22TLl93APam6s1DKT67Q3oHByNNuvkp2TgKK0TZ48WWalpORlAwc6vwAAAADoew1BuVDSBgEAAJSjYJiSjJmgpq4HAABgDk1NFhI3MF3BgwuDAAAAAAAAlB3Z2YdN61cqMAgAAAAAAEDZAQ2BOTAIAAAAAABA2QENQZkYBIGaRuHy5o7QwAkhfVV6nG5/2F4WCOk/2xfQy/yKKDXMiHA5UbFPES8WrV1kBKouRhDtDtoFooQ3aB83X5U+joEarswenaOWERVzCYBVgWZnSv/RpomDVVFxhBnjGqYsWG3/Pf6IPlZcmbfKPn4eJgqEixHFupTkUaZw54NPES0HmN8XYUTSVcp52xnQ+8Sd78l4bV5hcC6S8c6ishd7uAAAwYjts7+qRqvjD+vntj9kP4Z+5trlxkq9ft2J4o4fAACAFQNoCMrEIAAAAAAAAKAYZH6BQjQEonKBQQAAAAAAAMqOtGXJrZD6lQoMAgAAKGN+/nq+cPtzJyBabb3Bjm3MnZU/oVXSILkZlyNDJRTRXctU/pt0Dgw4bIDuTlpoArSAQX/rFfc1jgjjxqcyqCr/o9iVyp+oStZJ5E/GJom2O9dJtzhWScydlr+JZudEX9bDlznWWTxjvugJlnw7K+/fuZw8KrEWg/E1oHNp1LGOxyERoHeqczI2y8BHN51a4lineqjdtVOlI+6cNK8/KTQDsSUql5I2CEIDBgu3L5Qz2RHnl+wL1zj6UFfVBI0eRAOq7GW1Yb1OmLlwVf9vLgGSy0AzYLkZX3Ym8ZorqD/s/TX2skCdflGH2/UbXIrRDKj4mBtaJGF/SHem0obaA+GYYIzTEITq9GMYbrSPTahBHxduHPw1VfY+RfQ6LiWRF2Ex56RJYjLufPApA8GdV5Gg/vIxQDlv22P6i0OKORZan7z6S2Fc8fE31RCYXqtq0rFglX5uh6qZa075zXWKdkSWhfWxiihaA08SGgIAAChnEGWoTAwCAAAAAAAAiqLAPASigpcInKczAQAAAAAAAGULVggAAAAAAEDZkRaW3AqpX6nAIAAAAAAAAGUHuQsVFHbUEhVLSRsEtYOGCk8gd3QMLxMZwMuJfBVRcSiiixAH1uqi0SGKcLWJSeRVyyRFUgWhPsYxy81lp1JPRE5UzIhb3UxCp2CDPfFUMqonnrIYsalLEbd6maRWwXq9D5H2hKM4OW2Ymczjtw+YXxF350qqpvYr3KSPS7ipXisLNdjruRlhuitkFx4TlsfvKCp2MceZC16injMR5tweyJy3nco4c9FT5ikibe46CYZ1MXIsqv++FBPhxTKI2OJhhOFqH/zcucYI+WsVEbF6nRJNTOCAWiVZmT9Z0rc/AAAA3QSiYnPwRAQAAAAAAGUHVgjMgUEAAAAAAADKDmgIzIFBAAAAZUzaYc186kc/ObYRiFTn/Xsi6py0qY5x4VOJRZ0TcLUs63Ruh8nHUWhismrFxYxDzW1RrE8yl5vERto5l4gJ6bZljnWSCwySXjkkOIsvXerYRmxZm2Od6GLnJGkdC5zrzPpojuguvipfjyQDi7Y6Xyuc63M+116ORa26m7DKiFF212KOqEMitXaDpHn9CVYIzIFBAAAAAAAAyo60ZcmtkPqVSkkbBA1Dq4U3qIs5M3gYdSYnNK5V0svXMllMORGiKiIeyIhbVaEiUa3MGnkZa56dEHIrmYq9ej/dYT17rKe2QW8raRf5VqV0ka/Hp/fdW2Ufh0CdPouTbNdnDJLRnhQVK6JsRmzqDQcdszOrwmpOQCzbGtDoOJ7ucLWRqNhymunLMRuoZreuZbISxxhBb9qyZ/f1M+JdbqZzsXIutzEzs51c1mNOVGxwAzW5VrnZVu5abVAyFbPXLiPArlfGNJB0nvEDAACw4kKxU5j4KXnr9zY//fSTeOqpp0RnZ6fYdtttxRZbbJG3fjKZFC+99JL48ssvxYABA8Quu+wiRo8ebatz/fXXizlz7CthEyZMEL/73e+M+4XEZAAAAAAAoGxXCArZepOXX35ZrLPOOuLzzz8XCxYsELvttpu48MILc9b/+eefxdprry1uu+020dHRId555x2x1lpriQceeMBW76677hJfffWVqKur69pCIfuE4Qq9QgAAAAAAAEAx0At+qkRchtLptDj++OPldt1118myrbbaShx44IHi0EMPFWPGjNH2qaqqkkbEqFGjusr+7//+T5x44onioIMOEu4szxJabTjzzDOL7h9WCAAAAAAAQNmxPKZCISsEoteYPHmymD59ujjiiCO6yvbdd19RU1Mjnn76aXafwYMH24wBYty4caKlpUU0Nzfbyj/44ANxwQUXyNWEadOmFdy/kl4hWGtEnfD/5jPvYZIrcWUhv/6TQoqvMudTXcf4bKvJoeoVLQIxgCkLKJoBH9NPzhKzlKRWrI+6T0/U5q5hNAQqjB4hXKVHnAgqPvYJTi/QoZel4nZ/83RC9z+3GB2Dy6NrPlyKlsLNHFOfonWQZYquwMNERnFX1+llEXuZm9NkhBgNgU/3U1eTyTGHnj0fgso5w51XHD7FN59LaNakJPIimjvtmo8ORvPRqRxTIs5oCNSILabXqqp34K5d7lqtUcpqAz7HOsQA5RpvT0FDAAAA5UyxGoKWFnv0qkAgILfu8MMPP8h/s1cCvF6vGDlyZNffTLj//vul21B9/f8SrXo8HpFKpeRGxsWpp54qbr/9dnHIIYeUh0EAAAAAAABAX0YZGj58uK2cZt45X/9rr71WagFyQS4/559/vvx/e/vycL3V1fYJxtra2q6/OXHDDTfIF/7XX3/dVv7II4/YDI2LL75YuibtvPPOorHRHjglFzAIAAAAAABA2ZEqUEOQ+q3uzJkzpStPhlyrA/RyH4/nzvkQDv/PqyMSiXStPpARkGHZsmVizTXXdOzbvffeK8444wwpKCbtQTaq/uDoo4+WRgy5Ke20007CBBgEAABQxrTOmybcPt3FrhBirfkTTbl9zu5XbX7dBVLFxfnYKXgH5Q5FnSHeA4nJuDC/KkEmnK6KQRXnFxaPQQI0d/5kVqakluae7TRNGLb0h5k90pfW2c6J1Jqn2/2oi6HD4XwhFhkkFGtOOJ8zcYNzz+l8GGbgUmryPb/+6jy+jQ4J+jrSurtpKUFHpBBdQPq3f8kYyDYIcnHssccat5156f/+++9lSFCCjAny96cX+Hzcd9998rvo3wMOOMDxu8h1iIhG8yeWywaiYgAAAAAAUHaQ8V/o1luMHTtWrLrqqtK3P8NDDz0kw4nuvffeXWX//Oc/xRNPPGHTDGSMAYpIpEKrGSRWzuZf//qXDDu62WabGfevpFcINh49QIQYYWgGN5PgiRNsqsJLNQkUEWZShYd9iujRq9cJeDmBqH0/P5uYjM1MZvtkefUlKivAzI4pYmTZks8+G+fiEmvVd+hlsU7bZ1+UqZPUl8eshF2kKroza6DMdLHCY3/QscwVYOowie5cSrI3y6vvl2bGnatnKTN53FFmTj8RUs61/81T/A83k6SrWhERdzB1OhL6sYgqs5/cbGiCuTEWG5KNu1bVBG1cAj9uBla9VtlrlxFXa2L/ZEnf/gAAAHQTSpxZyHPL6sWwo/Ted8cdd4jdd99dzJ49W/r2P/744+Jvf/ubLZIQ1dlkk01kUjHKV3DkkUeKddddV3zxxRdyy3DaaaeJQYMGiUQiIaMVURskUCY3oa+//lrcfffdxvoBAk9EAAAAAABQdqSs5Vsh9XsT8v2niELPPfeczFRMeQPWW289Wx160R86dKj8PyUYu/TSS9m2KLIQsfLKK4vPPvtMvPbaa+LHH38UW2+9tdhmm21EQ4NBBMpiDQKynCh98ttvvy0tko022kj6MmUnRiDmz58v7rzzTrmEQcsjtNSRLaAAAAAAAACgFKMM9SZDhgzJqz045phjuv6/yiqriHPOOcexTZ/PJ3bddVe5FUtBGgKyOMgviSwWSpYwadIkseOOO4pk8n+CHFoG2WCDDaTRsMYaa0g/KBJPqAkUAAAAAAAAAP1PQSsENOufHdqI/Jvo84svvij23HPPrtintEzx/PPPy4QLxx13nKxDIgkKgQQAAAAAAEBvU6hQONWbqYrLySBQ45yOGDFCLlMsXLiwq4wMgT/84Q/SGMgkZSBj4dlnny3YIFh3ULWIVOcO+8TpcrmodS5F2smFgVMFjlw9j6GIWRWNcnUY/aS+YOPVw/RxQc1cTEZjoYhgXUHdZcvFCH9d6WTezxIr7VzG1TFFFUkzommuTM30rGYNlnU8PkchMJch2rQttV/c+ehnTkC3y34T8nJZuJkbVSJtb6su7TUKYadmbrQEJyDWikSxq6km16p6nXJ1uGuHy4LsNbnmNCE3AACAcqIUXYZKlW49EUnBnE6npYAhE++UXIay1dIEff75559zthOLxWSihuwNAADKFbof/uUvfxHjxo0T//jHP9g6pMGiyRWKNrHPPvuIN954o8/7CQAA5SAqLmSrVIqOMvTxxx+LU089VboIkeiBIMV0ZlVAzeSW+RvHFVdcIS666KJiuwIAACsM77zzjhSN0dbW1iYnUVSWLFkiNt98c6m/uvzyy8W7774rU9C//PLLYrvttivo+zqXzBEuJoRxIbiZ1cpsBq+zqWMbHcsWO9YJ1zlHxWhb5pxop2FI7nDVhJ+L/WsQulbFxy/12ogl++YNw2Uys8mEa1ax1OVDho6F+RPVpRPOib6W/Oh8PsTb4859WZT73SLD7Lb87XQa/GaTpGMGp4NoM0h458QXBtfAYIeEYkRVyLlOwuENmQtPXUpghaCXDQKKcUpK5uOPP16ce+65trTMFHGI0jCrD7d8UYZInHz66ad3faYVguHDhxfTNQAAKGkoOhuFnaOY1A8//DBb5+abb5Yrp4888oh0yyQj4KuvvhIXXnhhwQYBAABUKum0JbdC6lcqBRsEU6ZMETvssIM4/PDDtaVuenCtvvrqMiFCNlOnThXrrLNOzjYDgYDcVIbX+EVNjXO6eyef456i2KbN+6QmJmNmc7hkWCb++t3x6TehN9vnNASl0H6R+3Gu66rOJGAVd7b15K2sr++LPXntmjTlCjjPAPcGlD3Siddff11GcKN7aoY99thDriqQa2Yw6DzTCwAAlU66QDegdOXaA4VpCL788ssuY4CiBnEcdNBBclaLchEQlCThhRdeEAcffHDP9BgAAMocSkVPsaqzoUQ1pNmaM2cOuw+0WAAAwLsMFbJVKgWtEJCbUDwely5BlEo5AwneaCMo6xrlIKBcBLQ0/v7778uZrez6AAAAckOJH9VV08xn+hsHtFgAAKBH2eMi7eUiBYPAjCuvvFKkUnqoyuyoQrQc/uqrr4oPPvhAzJgxQ5x//vliww03xDkKAACGUC6XxYvtosvM51zp6KHFAgAAO9AQ9NIKwWGHHWZUj8RyFCGDNgAAAIUxfvx48cknn9jKPvzwQ5n7pbGxsSAtFgAAVCo0hV2IhiAlKpeiw472BeGl00Q4GSlIwGlxGZC0hFWGia7UZFRccio3I0xU6qmJr3K2pfSBi3JmkmRqeT21jj4u3EWiNp9mZKrcilracj4+FrMjGY89JTZVE8dxTXNh4dT2ucR1RomuuKRZXN+ZZG+upD0snpurwySS09pixN1scrm0cyI5ozCG3L5Fiq3Za5e7dpT2tYR0Oa4vtcwXbRWlCuUfuOOOO8Rjjz0m9ttvP5m34J577hGnnHJKf3cNAABWGBB2tEwMAgAAKDdIg7XNNtvI/3///fdSJPzaa6+JtddeWzzwwAOynPIP3HLLLeKoo46SIZkpSAMFZjjnnHMK/j5vKJI3D0F06TzHNjz+/JGR5n9jX83gCFQPcKzjC0ac2wnZ89wUQ1vUOU5+3CBefGvceT5xWI2r+5FNDCZFXIkO5++JO8evdxtEsHJxk2pZxFqcvyfWEnOs0zq3TfQEVQ4JAuZFnY913CD8jEkdEx/1docpbaffY5pbIWEw5+PU31L3uYeGwBwYBAAA0IdQokaa7VcJh8O2z8cdd5x005w2bZpoamrKqR0AAACQW0OQQh4CI2AQAABAH+LxeMT6669vVJeCNKy55pq93icAAChHUgUaBKkKTkQAgwAAAAAAAJQdMAjKxCBI/fpfkarK47vKCHpdnMjX67PXCeg+ki6/XuYOhh0zB1uMb67lU+r59N9g+e1tEyklT1ySsVSjjF9rjPE3jCtl6uflZXpbHQm7T2yC2S+hClKZet3xK1TFwW5G0Ktm9iWCXvv4BZTPy+t4HNvyM/6ZQa9extULKIpkj2DEugndv9aV6HSuk9R9bl1JpV7M3g5hMX7DapmVsIuaiXSSiXfPCZtNMLlWletU1vH5Ha9VD+NTbnn0tiyf/Zpzd/aMfzIAAIDShF5zClshEBVLSRsEAAAAAAAAFANWCMyBQQAAAAAAAMoOGATmwCAAAAAAAABlB6IMmVNcBiEAAAAAAABAWVDSKwRtU78UrmDuhDpuv959t08v8wbtwkQ3I0J0V9VoZa5wtb1OdZ1Wx1Ndr5Wl0/b90lwmVa8ulkwrmVQ5IXBnUi/rYFIat8TsiXealc+yDpOcRxUVq5+JNqatmCJ25kQ8psIev4E4OOT3OIqKI8z5EWH2qw3aBai1AX2/FJuJWS/zutXfyIiKU7pY1xW3JxVyx/QsulZ7i96v5sX2b+vQ66Tbmbai7fZ2orpgORnVhcYWo7jiylRcTPpn9VpVr1NZR4nNL8uCVY7XJXc9u2vscfxdHfYxKFdiLYuEy6OPbQZ/xDlhWDKaX4DtDeRPXCbrGCQUSysZu9l2fPqxVYm2M4L4bBoLvw8Vi0l8Bcd7Y09N3THCfRWXQR2LCSyRTbBev25Vwo3OycvaFzgnW1vWmTBI0pV/fA3yfBnVMcEp6RgRZIJmZFPrY4KnKJgE9mhmnu8qNQ7XgcHP6f/EZIWIiq0S/0G9CFYIAAAAAABA2WoICtl6E8uyxL/+9S+xxRZbiPHjx4szzzxTtLToE3nZ7LvvvmKNNdawbeeff363212hVggAAAAAAAAoB1HxRRddJK677jpxyy23iMbGRvni/vnnn4s333wz5z6//vqr2HvvvcVRRx3VVVZXV9ftdlVgEAAAAAAAgLKD8jl5CnjJT/aiQdDW1ib+/ve/i2uuuUZMnDhRlt13331ivfXWky/u2267bc59Bw4cKFcGerrdFcYgWPTfn0XUn9ufkdMLeBg/ZF+VPZGRv1r3ZQ3UR/SyAXYLzBMdqHeCSd7kHmD3/3P5AkY+mJbLfiImmBMzxvhrL4vqfZjfbvfFXdyh++YuatPLFrfZfcmXdehttzHag8643RcxziRQM/XN8yv+5pxeIBLUj31d2H6uDIjo495QpZ8fnYoGI8GMscett+XT9AJChNRuca6gKX3c3UpiMqt1qb7b4nl62dIFts/xpfp+sWW6/3e8xe4/n2jtMNIQpOPJgv2Jc12rapl6nRL+GuZarbNfq4E6u46C8NQ36WVK8rVUu7N/MgAAgBWXUloh+Pjjj0VnZ6fYZZddusrWXXddMXz4cMcX99tvv138+9//FsOGDZOrBccee6xwu93dbneFMQgAAAAAAADoy7CjLYr/fSAQkFt3mDlzpvx3yJAhtvLBgweLWbNm5dxvzJgxYvfdd5cv+VOmTBHnnnuuNALuuuuubrWrAoMAAAAAAACUHTLKUAGRg1K/1aXZ9WwuuOACceGFF2r1d9ttN/HLL7/kbK++vl58+OGHy9tOLfek8Pns3gxkaCST+up7hkceeUR4PMs9JTbccEOpEfjd734nJk2aJFZdddWi21WBQQAAAAAAAMqOYl2GZs6cKWpq/hfiONfqwI033ihiMT1sdwav93+v2Q0Ny0NfL168WDQ1/c+tddGiRfJFPxcZYyDDVlttJf/9+uuvpUFQbLtaX41rAgAAAAAAUOYGQU1Njc0gyMXo0aON2868nH/00Udir7326npp//HHH8V5551n3M6MGTPkv7W1tT3abkkbBAu+XijaPcu76GKygngYsamXEZsGauyWXaheT6ITatKTG6UUUWU4rSfxcDEJxlxK4iSXXxcsi6AuxEwbqN1VASyxhEnMooqIZyyxi1aJucv0snnL7Mli2hRxMhFnRMzxmH1s0oyoOOOb54RHSYTiC+jHORDSxebhsP1YDKzR+95WpwtX1eVELidMmEkEE2QS+GhLkxaTmCzNLOHFOvImHOMExER0wULb5/a5+n6dC5fp+y21f1+sRZ/hSDDHPtHJiIoNMtOYXKuBWn0GJlSvH6/gAPtNOtSkJwesYoThLmWWJd2un//liMvtkVsu/FXLHyr5cPtyJzaTbVQ7JzczEZ8HDR7APiZxoEookr+/JlQxiQ2Luaf5DTJamdRxwsqTfK6rTsw5GZjFBMpQ4QJzZLP0+zmiJwjUOP+mAT7ndEpTF+RPQjjQ4JzqNEjA2MY891RCBsfaKZFa3OC8G8y8CxXT3xWdUhIVr7TSSlILcOmll0qhb1VVlcwnQBGE9tlnn6569FI/duxYcckll0jNwKeffiqOPPJI4ff7xbx588TJJ58sVlllFbH55psX1K4TSEwGAAAAAADKjpSVFql0AZvVu0bS3XffLaqrq+XLOrn6vPrqq+Lpp5+WL/EZSJMwd+5c+X968SfXIHIFGjFihBg5cqTUJdB+ZCAU0u4KvUIAAAAAAABAX0YZ6i3ohf31118XCxculKFCSbzsctlXjZ599lkRCi33ZCG3JUo4du2114rZs2fLSEKqeNi0XSdgEAAAAAAAgLKDjAF3ibgMqS/whegSSFhMKwTdadcJGAQAAAAAAKDsIJmEq6BMxaJiKWmDYP7MVhHOI4bjxDl+JVstJ0xMtOuiqVQiWVS21VBYzwzrrq5zFpEa+KlxlmpHQhc2t8SSjlmIOQHxjMV6ptb2ZrvorJ0Rm8YZYWms0/596aQuSLUYUTaHWxFq+4K62DRYpR/nWMR+XOPMWHFj6vd6HAXEtUyG3notLbEQJvcdFyPas6J20ZvV0arV4bIQqyLi9tmL9DoL9EzFbfPt3xddyhxnRlQcZTNQC0f8jFI7qIwfJyCMNYS1sirl/OPEqi4l2zURCdvbSnU4CywBAACsuJTqCkEpUtIGAQAAAAAAAMUAg8AcGAQAAAAAAKDsgEFgDgwCAAAAAABQdpRalKFSpqQNghmdSRF0LfcP5nJ5hBg/4aq47jc+oMPus51ifMs5vMEWx2QsQcbXW03+4mY0BC41gRXtZzn7ZnM+3JyuYHGb3Sd8IaMF6GjVy9pUDUGzXifWpvuyJ9rtY5WKdxppCLiESaqGwBvSxz0R1RMhpZK6v7nKQiXpGVGn6E6alER2OX3nGRmIegxdnFaEKbPi9nFPd+qJdBKtuuYjtsyuD+hgdCGtc3QNQfsCe71lTHK7FuY3cwlz1CQ53LXKaQjUa3UAo01JxZ21Nh6/fky9VXpCs1CDfRxSnbnTzZcTtSPWFG6fnowxQ/uCmY5thOoH5/27T0nGyBGIOCdAq2U0I1o7jHZHZbBDOw0GicuYx0tRxExENg5YbuffbHl0XZWGQdIxK6Frh1S8wfzjF6wPF5WssBhaZtmfPRxrMPf0bNqZhJ8qJqcD906iMpu51zomuFQYpSTh5DDRxq5mcB0MWKk679/bU0khloiShYyBQkTFqQo2CJCYDAAAAAAAgAqmpFcIAAAAAAAAKAbLsoRVwKy/5bA6U87AIAAAAAAAAGUHaQIK0QWkK9hlCAYBAAAAAAAozxWCAmb9LawQlCaLYkkRcC2XOXhcuigx5GFEj4xoVMXLJGHyVzFJkVrsQs9Ehy6UTUX1trxqUi4uMZkBnLAoxgg9OcFrW9T+ne1M8rKoIraWZUrSNk5AHGvWk1/F25t7TVTsZ5Kccbi9dlGdP6Dbu3FlXIhWpUwdOyLBzBoki51JYM4HVcinioxlH9qjjkLjGCcCb9HHTxURL2LE+M2M0K6NTUxmFSW26/RajiI4zzL9N/vUhGa1umAwxIxVMhrP+xkAAEB5Qe5CBbkMpbFCAAAAAAAAQNkAl6FedBlatGiRuOuuu8TkyZPFSSedJLbYYgutzuzZs8Wtt94qpk+fLlZddVVx4oknigED9DCRAAAAAAAA9AYU5ZuL/p2vfqVSUNjRBx98UKy//vpiwYIF4pFHHhHTpk3T6syYMUOMGzdOTJkyRWyyySbitddeExtuuKFYsqSEA9UCAEAJ8cEHH8jJFnWbN29ef3cNAABWOA1BIVulUtAKAT2Qfv75ZxEIBMQ111zD1rn44ovFkCFDxFNPPSU8Ho848sgjxZgxY8Q//vEPcckll/RUvwEAoGyhCZRPP/1UvP7667by+vr6gttqGjVSeAK5E0XFhgxzbMPrkESqp/xuPV4mq52Cj9EGFcqqg/MnWyLqg86JvobV6AnwVAxkbcLNaOSycZno0H7T2+XDM9D5WLsXz3X+rsX5k4GFBzt7BHCJFlVcXJbDIgjW5z9OEUU7x1Hf5lwn3h7vkVnYnvjZEYMkaRGHcSFW2nxU3r+3xhNCfPm5KFXgMmROQXfWESNGONZ54YUXxB//+EdpDBChUEjsueee4vnnny/YIOhIWyIplj9o3L/9m03K0q8aTnwcVR5WMUZAmWSEpCmlXjqu10kz6Wotg2yQxa5LcQLOOCP0VMXHSeY3p5n9kspvTDHi1iQjGFZFxMmonh3XSuvf53K7HUXFnPDY49czr6p9TSZ0oXiSyeqsjh8nFk4aZhtNFzu7oIwNly00RTderSzleB5zZWrGYS4DMScg7mTO94SyK/cYShmc7yHmKdiZ0lsLKRmN1TFYXsZcq0qZlShO7N9XuFwu1iUTAACAGRAV91PY0c7OTjF37lzNcBg5cqR4+OGHc+4Xi8XklqGlxTkVOQAAlDPJZFLsvffe8t911llHnHbaaWLw4MH93S0AAFhxKDDKkKjgKEMFaQiciEaXz9BGIhFbOX3O/I3jiiuuELW1tV3b8OHDe7JbAACwwkHGwKGHHiqOPvpo8cUXX4g111xT/PLLLznr06QKTaZkbwAAUMnQqn2hW6XSowYBvfi73W5NQLx48WJRV1eXc79JkyaJ5ubmrm3mzJk92S0AAFih2H777cXjjz8u9t9/f/H73/9eulzSRMkFF1yQcx9MrAAAgB0pFE4XsFmVaxD0qMuQz+eTs1hTp061lX/11Vdi3XXXzbkfiZRp6y0M3b+LwmJ8qssJK5UySjDGlfUmff19KzKczqWY5GLL6xl8n1FLxbXNYfXmBd5PkPYqG6/XK7bddlvxxhtv5J1YOf3007s+0woBVlsBAJUMNAT9tEJAHHLIITIk6Zw5c+Tnb7/9Vrz44oty6RsAAEBxzJ8/X4TDuaMF0aRKTU2NbQMAgEqG4nVkIg2ZbaJiKWiFgGb+L7300q7PN954o3juuefEdtttJ4477jhZRjNU7733nhg7dqzMR/Dxxx/LZe/DDjus53sPAABlyG233SY1BIMGDZKfX3nlFfHEE0+Iv/3tb/3dNQAAWGEoNLeA1UcuQ0uXLpXaWgrTn494PC7ze3E0NTV1TfyQq312cB6CNLkDBw7sHYOAGt5nn33k/zP/Equssoptlor8XT///HP5IyhfAUXIAAAAYH6v3WyzzYTf75dRhih627nnnitOPfXUgoewYUi18Iaqcv491ukcJtnjENM8ZeCWZpI/YFCtc1z0kENOBKLJIT8AF6q5P4ONOLrsmcSlZ0I4q1hJgzj5Vc4rS1VjxuT/nvQPjm0k23MHGsngr869IpbBG9TDS6u0zGp2rOP8Pd4eyZswulEPmV2oG6TJ96Tizud43chaxzodC/IHJ+hMGIRZ70dKLVPx4sWLpSfNm2++Kd+Xhw4dKh544AExfvx4tv706dPFrrvuaitrb2+XSSofe+wxqTEjKLz/rFmzbHrdAw44QFx++eW9YxDQbNWBBx5oVJd+XK4fCAAAIDf77ruvnHT58ccfRSqVEiuvvHKv6qwAAKAcITcgVwHWfbqXZwIoatyiRYukCygF4jnhhBPkyzzd66uq9ImbVVddVfz000+2snPOOUfceuutYrfddtPKzzzzzNIQFfc0AZdLbgRnEIeYWSsuuVHQbS8LMDNMbp/elkep52K+z+PXh5BLpMVUEsXAJV7zM6kwA0qZh6njZsq8yu/xBPSZNi+TFCzNJNIyEQJzY6UmJvMG9O/zMGXqfm7ueDG/WR0/r3K+yLYMM0c6ZRzNvaO9Dy6fPuvl8fscz1GP3200s+WPpRyvpTh7Y3Sux12rfmYA1WuVu3a5/Xwh++/xKp9zXpfKb+SS4pVaYrLVVlutv7sBAAArLKUkKp49e7Z49tlnxZNPPtk1k08z+Hfffbd4+umnxcEHH+zYBq0Y33fffVKXqwafoL+R6xBN3tPqcqGU9hMRAAAAAACAIigo5Gi6wCRmBfLZZ59JjcKmm25qcw+lVQD6mwkvvPCCdCE99thjtb+RW+lGG20kVx5o1SGX9mCFXCEAAAAAAACgL2lREjvmCo9PL90k/M2Fx+MRo0ePlv8nVyGioaHBVqexsbHrb07ceeedYsKECWK99dazlR9++OFy1YCExhTlk4L5kOspBfahsNUmwCAAAAAAAABlB2UedhUQOSj9W101hwslhbzwwgu1+jRT//PPP+dsb8CAAeKTTz6R/6fEvUQikZCGQgYyKLI/54KExLRCcMstt2h/y85BQ0Ll66+/Xmy44YZiypQpctXABBgEAAAAAACg7ChWQzBz5kxbLpdcQR1efvll47YzRga92I8aNaqrnD7vtNNOjvvfe++9UjcwceJEx7qZ9ilKUVkYBI0Bjwj+Jr5lxbSM4LCGEY0OUISXgRpdbBGo0Q+2v8pez1fFiFuDzEniVcSf7uKGmfvNPk40zfzmiCIkjYR0QWqUEZvGFYFmMjLAqK+qoDcV79TqpA2zC6uiZW8ootUJROq1sqByvALMb+bKwsr5wYU1VEXasl+mSmMDQbkqInap55A81/Tz1hsOOp7HwVp9ObM+mnQMe8iJgzuZcHgmWY450XJEGVPu2q1ifo96/fqr9LHyVgUdx89dwSnqAQCgYvIQFGIQWMvr9kZyx4033lgEg0FpRBx//PGy7Ouvv5ZuR9tss01XPTJGqJ6aQ+Cuu+4SBx10kNQIqGJi1S2I8oERYxxCBK8wBgEAAAAAAADFQMZAIaFErV4UFVdXV8uwoCT+pZd90g6cdtppYsstt5QJfjNQ3oFNNtlE3HHHHV1l7777rvjhhx9kzgIV0gn8/e9/F3/4wx/EyJEjxeTJk8XZZ58tNQTrr7++cf9gEAAAQBmz/dpNIlhVnfPvyzqcEwstbMmfREpdkSyWlQY4J6Ja0BJzbqc+f/KnemalUCVikACNW51VqQ04t+O02mj1UEBAV9B5fD31TY51ErPscdFVwisNFj1Bx4KlPZKYLFCnrzJnkzBIkhZb1u5YZ2DQ+bxKRksnkVegLnfCwgzhpv8luuLwR51DjvcnpZap+KKLLpK6giuvvFJ0dnaKbbfdVpZRmOkMI0aMkOLgbChbPbkVkS5AZfPNNxcnnXSS1BZQPoNhw4aJSy65RBxzzDEF9Q0GAQAAAAAAKDtKKQ9BRlhMqwK05YKEwyr0gp+PHXfcUW7dAQYBAAAAAAAoO6S7UAllKi5lStogWCnkFSGXp6Dsp+GA/pOC9XZhYrhBXzaNDKpyXGb0V+v7uYL6fu6QvcxiRMUWIxhWi7iktwGvvvwc9ulltWH7MuqAiL6s2h6zC0uJVCpt7wMzxl6/vqScCNf2mqjYF9SX//1MdtpwxH6cQ8xvHlDFlCn7VTGuApxrAKOTZY+ZBnM+uPxBx/OKO/8C9fZzNNyhL32n4vZjyh1X71J9v9p2fWm7Uzk/ZPsG90/uWq1SsoNzguhwo37sQ8r1G2zQ3WH81fr4+artY+XzlPTtDwAAQDex0im5FVK/UsETEQAAAAAAlB0wCMyBQQAAAAAAAMoOK50ucIUgLSoVGAQAAAAAAKDssFIpuRVSv1IpaYNgyJCIqMqTztnLhLrzMb7lqm9yuFH3xQ4O0BNQhIc02Os06HXc1XpILs3/m0tMxiSnUvFxGgnF75qI+PX2Byp+8Z1x/SRPMeKZhYpjvJ/TZIR17UEiZv++VCpStHrfbdAHTkOgJiZrrNZ90lcaoPukNynnR23AZ6TT4EIFqkUWc5wtt96WWzlnuPPKP6BF75cS8s1ifPzdPuY6UZJ5Bev1RF4RRkOQVBKaEWnmO1U83Pgpx5DTEISYfqmagXBTvVGoPHfEXuZ26zoXAAAA5YNlFaghsGAQAAAAAAAAUDZAQ1AmKwQAAAC6x/DakAhHcifqGsyspKmMYiKzZdOoRDXjSBisJPm40F0Ko+vDPZJUzIkhBuPi58LfKXgMQo9xUbhsWM7jkg7kTj5XyMq02+N8LH3u/O2kFs52bKPK6/w9JgnOOmbNc25nsH21XyWd0Fc+VZZ8O92xTt2YYY51UnHn73I5XAcmydiSBgnD1NV4Dh8T3S6bVKdzosD+BAaBOTAIAAAAAABA2QGDwBwYBAAAAAAAoOxAlKEyMQga12gQEd9y8aObWZr1MMvCHmYpzRe2CxP9NYyouKHWscxT3+QoVCRcYbugNu3xGS3dug2Wmjlxa33IZ7A8r/9mP5Nsqy5sb2tZh75s3tzBJKxK2IU4qWTaSFTMJT5Ty4LMb44wgvIGJRGZmnCMExBz7g71jGCZE25zom/tmHGiYm6pXElMxomKPXE9eZjqCOJm+skl6QrUtdo+J9r1tpNskjNdcJVWMpNx16qLKVOXvX1VQaPl6mB9tWNAAO+AgVqZp9buNuDxdmh1AAAAlA8yIWoBouI0EpMBAAAAAABQPsBlqExWCAAAAAAAACgGGATmOEvMAQAAAAAAAGULVggAAAAAAED5QZmK3QUkG0shMVlJMnC9lUX1b8JDLi4vF0OXz8pqFyt6wrpQ0R3WYzi7q2ochZ7uGj2+seWzt2/5dLGkYOI4uxRBKhfjOsiUDWBExSoBRkBcHdDFuq019r62MzGTOxhhqZoJOZVOG2VG5vB7PY7i5zAjKI8oGY0jBnWIWqWMG08uQzR3fAxCjpMa3vGccdcMMGhICJcqRg7rAttAo57hON1hF9QmGAFxmjn2KSZeN5cdWcXDiJ3Va5UNCFAVcswEzl6XjNhfref2tTv0GgAAwIqMzDyMTMVGYIUAAADKmPWHVIvqat1QzNCRcDbojAxdB2JM5DGTSQoVy2BewSBfmCPeHko6xsxnaCiBuvQ20s7JrKyAPbodS6zVuR2DxGTu+vwJw+I//dexDf/K6zjWSbfrkxkqNQOHFTxxopJautCxjfDIkY51hNsgIZ7By6mV1CP5FQo3UaLVUSZXONLR/BMn8fZOUephRwsyCNLO96lyBQYBAAAAAAAoS1FxYQZBqlf7U8rAIAAAAAAAAGXH8hUC81l/CysEpUnteuuIGsaHON/ynMur+3+7lERQrkDQaElR9VVWk0cRlpcp84ecfcbdjE+1y3mpOcT4srtc+nqz1+3L6ydPdCjJxLhl/SjjH64nPRMioegD0oZ6AQ63MhDcsjyXFEzVSQQ57QGT5Ezbj3EV4DQYnIbAKDEZk6guHajKqych3Mx55Kmut7fTqS/vWsySrxWzawb8ybjZsnWxsyfctaqWcdcud10q1y+77B0IGVyr+d0IAAAArNhghcAcrBAAAAAAAICyAwaBOTAIAAAAAABA2ZFOp4QLGgIjYBAAAAAAAICyQ4bFdhUgKk4hyhAAAAAAAABlA/IQlMkKgW/M+sJXnTu2ssUINoVJGSf0ZESPlsfrKAQWTJkmGlXaydUHVUbq5xKvMQJiv0cvS6TsraWY0NIpS//NqnFsMUG/OftZrdYNTbEmruZCfXOhvVUhLjN8rEBZ1QZ7GMEyF5LcZ1KPO9cUkTsLIyBOMXHGXSlFDBxm4pVb+hFzq0uoTB22jMFlUI+9VrVOmV0nlnIM08z1xV6rypim0wbHoR9ZtGiRuPrqq8XUqVPF4MGDxYknnijGjRvX390CAIAVS0NQyApBGmFHAQAAlAhtbW1i0003FSNGjBDHH3+8ePfdd8Vmm20m3n77bbHxxhsX1FZjyCtqwt2b+0mZZAPrgSRe3ASE1g5jiKvMa8uf2KnBYDxM+ssEfSsKJ6M64XGOiOWLtznWSQeqnfviN0hwltQzm2fj33xf5+8xePHyOnyPaSI1p8kNtxKtjaW2ybGKyyCBnOh0Tg4nQvmPkysZEz2Byfngdhg7b6vzedefwCDo5xWCadOmiZtuuklMnz5drLrqquLkk08WTU3OFxMAAAAhbrnlFrlC8OWXX4pwOCwOOOAA8csvv4jzzjtPvPrqqxgiAAAwwEpEC5v1T3U/S/SKSo8bBL/++qvYcMMNxbbbbit233138dBDD8nPkydPFo2NjT39dQAAUHa88sorYscdd5TGQIZ9991XnHDCCSIejwu/v7TdnQAAoD+heyS5Ws775tGC9x08eHBF3mN73CC4+OKLxahRo8Sjjz4q3G63OPjgg8WYMWPEtddeKy6//PKe/joAACg7aJV1jz32sJWttNJKIplMitmzZ4vRo0dr+8RiMbllaGlp6ZO+AgBAqREMBuUENU2gFIrf75f7Vxo9bhC8+OKL4qSTTpLGABEIBMSee+4pyws1COJNq4l4TY2oFFR3VLehkFWXI1PFHusW6AasRzSXudrEDxb0KKmkLqovFeghFgrZsy1nVgtyPeCuuOIKcdFFF/VJ/wAAoNShl/pKfLEvlh6SRC2ns7NTzJ8/XwwfPtxWTp/JUssFzWrRbFb2BgAAlUp9fb1YsmSJrWzx4sVdf+OYNGmSaG5u7tpmzpzZJ30FAACw4tOjBkFmuTrb75WIRCIiGs0dLYBmtmpra7s21aAAAIBKYv3115e6q2zo85AhQ3IGaKDV2JqaGtsGAAAA9LnLEL34ezwesXTpUm1mK9esVmZm6/TTT+/6TLNbFG6vtdUgPBcAABRB5v5iEuqyrznyyCNlYIbXXntN7LDDDmLu3LnirrvukuWmZH5XT9xHV7Swo63t+SOF+JIrVtjRpMHcnS/e7vxFBjlDXCbHOpU/HKgrmeiRsKOuPgo7KtoMxs7d1jNhR6MG35XMf+65koX7xXOk465uj13Lb2NXivdR0I8GgdfrFWuttZYMlZfNF198IdZbb72c+9HMFm0ZMi5DFLIUAAB6E3phppXJUmLrrbcWl112mdRfrbnmmuKnn36SZeeff75xGxlDYK3VcR8FAFTefRQUhsvqYbPuqquuEn//+9/F559/Lmf5v/rqKzFhwgRx++23i0MPPdSojXQ6LebMmSMtTmqDfGFXtOVvMmrI9Ql9x7jjnClN6P5CD7GhQ4d2BUEoNRYuXCi+++47GQav0AmSzH20urpaZvFeke9JKwIYX4xvJZ7DK8J9FPRTlKFTTz1VfPTRR2Ls2LFyVYAMg8MOO0wccsghxm3QSUUh9jIrBSuyPyz6jnHHOVO6lPqM1sCBA+VWDJn7aDndk1YEML4Y30o7h0v9Pgr6ySDw+Xzi8ccfF19//bWYMWOGzEEA1x8AAAAAAAAqxCDIsPbaa8sNAAAAAAAAULqUtMMXCY0vuOACm+B4RQF9x7jjnAGlxIp8T1oRwPhifFd0cA5XNj0uKgYAAAAAAACsOJT0CgEAAAAAAACgd4FBAAAAAAAAQAUDgwAAAAAAAIAKpteiDHWXtrY28d5774lUKiU233xzUVdXJ0qVn3/+WXz22Wdiww03FKussgpbh7I3//LLL2LllVeWORpKAUomMnnyZBGLxWTOCEp+xDFlyhQxbdo0GUJ23XXXFaUA9Zn6tXjxYrHaaqvlDG1LeTAo/C3VKbWoV4sWLRKvvfaaGD16tNh4441tfyNpD51Ts2bNkplq11hjDdHf0Hh///33tjJKerX77rtrff/kk0/E3LlzZeZyGnvQvySTSfHhhx+KJUuWiA022EAmfATdez69+uqror6+XmyzzTZsnXnz5snrIBKJyGcYxNzmUNhyel5Skqz1118/5/2TzulgMCi22GILEQqFijyalcfs2bPFF198Ie/f48aNk+eoyrJly8T7778vPB6PHF+uDigzrBLkvffesxoaGqxx48ZZG2+8sVVbW2u9/PLLVqnx5ZdfWjvvvLO1yiqrWF6v17r55pu1OolEwtpvv/2s+vp6a6eddpL/0mcq708uvPBCa9iwYdbWW29tbb/99lYoFLIuueQSW51YLGbttdde8lhQ3+k4HHLIIVYqlbL6k8cff9waM2aMtcUWW1i77babVV1dbe29995WNBrtqtPZ2Wntsssu1sCBA2Xfa2pqrKOPPtpKp9NWKUD9oH75/X7riCOOsP2tvb3d2m677axBgwbJOvT7TjjhBKu/OeWUU6yhQ4daEydO7NqoLJuWlhZryy23tIYMGWLtuOOOVlVVlXXqqaf2W5+BZc2dO9daZ511rFGjRsnziq71K6+8EkNTBHRfOfHEE+X5TRvdOznuvPNOKxwOy/vraqutJsf+xx9/xJg78PHHH1sbbLCBtfbaa1t77LGHvN9stNFG1rx582z1Hn30UXlv2XzzzWVdqkfPY5AferYceOCB1ujRo60999zTWn/99eU7yZNPPmmrR+9b9Lyn9y86HvQOQO9loLwpOYOAXpRHjhxpHX/88V1lZ5xxhtXU1CRP5lLinXfesV544QX5gkw3J84guOGGG6y6ujrr119/lZ9//vln+XL6r3/9y+pPbr/9dqu1tbXrM/0Osg/ff//9rrKrrrrKamxstGbOnCk/f//99/Ihd8cdd1j9ybPPPmstWrSo6/OMGTOsYDBo3XbbbV1lZNwMHjzYmjNnjvw8depUKxAIWPfff79VCtAL2e677y5fGFSD4K9//au10korWQsWLJCfJ0+eLA3Oxx57zOpP6OWf+pwPulbpYbN48eKuB7zb7baee+65PuolUCHDbfz48fJllqCHv8vlkucVKIxly5bJe3dzc7O8bjmDYNq0adLQp3sskUwmZb1tttkGw+3AG2+8YX3xxRddn+mZv+6661oHHXRQVxndFyORiHw+ZSZX9t13X/lyC/KzZMkS65lnnrGVnX766fKFPzNZRmNOE2lnn312V51jjjlGGrX9PZEJKswgePPNN+WLKb18Zpg1a5Z8gD311FNWqZLLICAL+6ijjrKVHX744dYmm2xilRo0c3jjjTd2fR47dqw2M33AAQeU3IONHrhkuFxzzTVdZauvvrp12mmn2erRagetGvQ39JJMqzPz589nDQIyiCdNmmQro5WCffbZx+pvg4BWZZ5++ml5ndLDRYVWNS666CJb2VZbbSVnpUDfQw93ejmlGetsVl55ZevMM8/EIekGuQwCelGlWdfslyd6CaPnWmZyBZhz1llnWWuuuWbXZzK0aAKora3NNjlH40sTP6Awbr31Vvnsp+do9oQBrSxm+Oabb+T4vvXWWxjeMqbkRMVfffWV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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vmin, vmax = ti_cont_data.min(), ti_cont_data.max()\n", + "fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 5))\n", + "im = ax0.imshow(\n", + " ti_cont_data, cmap=\"RdBu_r\", origin=\"lower\", vmin=vmin, vmax=vmax\n", + ")\n", + "ax0.set_title(\"Training image (continuous)\")\n", + "ax1.imshow(field_cont, cmap=\"RdBu_r\", origin=\"lower\", vmin=vmin, vmax=vmax)\n", + "ax1.set_title(\"DS realization\")\n", + "fig.colorbar(im, ax=(ax0, ax1), shrink=0.7)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "017d0c21", + "metadata": {}, + "source": [ + "## 4. A real training image: the Strebelle channels\n", + "\n", + "The previous examples used tiny synthetic training images. Here we use the\n", + "classic **Strebelle (2002) channelized fluvial training image**, the de-facto\n", + "benchmark for MPS, and condition the simulation on random hard data.\n", + "\n", + "> **Data source / license.** The training image is downloaded from the\n", + "> [GeoDataSets](https://github.com/GeostatsGuy/GeoDataSets) repository by\n", + "> Michael Pyrcz (GeostatsGuy), which is distributed under the **MIT license**\n", + "> (redistribution permitted with attribution). The underlying channel TI is\n", + "> due to Strebelle, S. (2002), *Conditional simulation of complex geological\n", + "> structures using multiple-point statistics*, Mathematical Geology, 34(1),\n", + "> 1-21. If the download is unavailable, the example falls back to a synthetic\n", + "> training image so it still runs offline." + ] + }, + { + "cell_type": "markdown", + "id": "a322ccd4", + "metadata": {}, + "source": [ + "Load the Strebelle training image, with a synthetic fallback for offline use." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d3d46961", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TI (256, 256) (Strebelle (2002) via GeoDataSets (MIT)), sand fraction = 0.291\n" + ] + } + ], + "source": [ + "import os\n", + "import urllib.request\n", + "\n", + "TI_URL = (\n", + " \"https://raw.githubusercontent.com/GeostatsGuy/\"\n", + " \"GeoDataSets/master/MPS_Training_image_and_Realizations_500.npz\"\n", + ")\n", + "CACHE = \"mps_strebelle.npz\"\n", + "try:\n", + " if not os.path.exists(CACHE):\n", + " urllib.request.urlretrieve(TI_URL, CACHE)\n", + " ti_strebelle_arr = np.load(CACHE)[\"array1\"].astype(float)\n", + " source = \"Strebelle (2002) via GeoDataSets (MIT)\"\n", + "except Exception as err: # network fallback\n", + " print(f\"download failed ({err}); using a synthetic channel TI instead\")\n", + " gx, gy = np.meshgrid(np.arange(150), np.arange(150), indexing=\"ij\")\n", + " ti_strebelle_arr = (\n", + " (np.sin(gx / 6.0) + np.sin((gx + gy) / 10.0)) > 0\n", + " ).astype(float)\n", + " source = \"synthetic fallback\"\n", + "\n", + "ti_strebelle = gs.TrainingImage(ti_strebelle_arr, categorical=True)\n", + "print(f\"TI {ti_strebelle.shape} ({source}), sand fraction = {ti_strebelle_arr.mean():.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ea12c5fc", + "metadata": {}, + "source": [ + "Take 80 random conditioning points from the training image patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "53752f84", + "metadata": {}, + "outputs": [], + "source": [ + "sg_size = 80\n", + "rng = np.random.default_rng(0)\n", + "cond_x = rng.integers(0, sg_size, 80).astype(float)\n", + "cond_y = rng.integers(0, sg_size, 80).astype(float)\n", + "cond_val = ti_strebelle_arr[cond_x.astype(int), cond_y.astype(int)]" + ] + }, + { + "cell_type": "markdown", + "id": "3db056f5", + "metadata": {}, + "source": [ + "Simulate with DSBC-style parameters (best-candidate + partial scan)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "513e273e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "conditioning honoured: 80/80\n" + ] + } + ], + "source": [ + "ds_strebelle = gs.DirectSampling(\n", + " ti_strebelle, n_neighbors=30, scan_fraction=0.2, threshold=0.0\n", + ")\n", + "ds_strebelle.set_condition([cond_x, cond_y], cond_val)\n", + "field_strebelle = ds_strebelle([np.arange(sg_size, dtype=float)] * 2, seed=42)\n", + "\n", + "honored = int(\n", + " (\n", + " field_strebelle[cond_x.astype(int), cond_y.astype(int)] == cond_val\n", + " ).sum()\n", + ")\n", + "print(f\"conditioning honoured: {honored}/{cond_val.size}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5d64acde", + "metadata": {}, + "source": [ + "Plot the training image crop next to the conditional realization." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f3ba3037", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cmap = ListedColormap([\"#c9a96e\", \"#2b6cb0\"]) # shale / sand\n", + "fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 5.5))\n", + "ax0.imshow(ti_strebelle_arr[:sg_size, :sg_size], cmap=cmap, origin=\"lower\")\n", + "ax0.set_title(\"Training image (crop)\")\n", + "ax1.imshow(field_strebelle, cmap=cmap, origin=\"lower\")\n", + "ax1.scatter(cond_y, cond_x, c=cond_val, cmap=cmap, edgecolors=\"k\", s=18)\n", + "ax1.set_title(\"Conditional DS realization\")\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "52495d79", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "* The standalone, sphinx-gallery versions of these four examples live in\n", + " this same folder (`00_simple_unconditional.py`, `01_conditional.py`,\n", + " `02_continuous.py`, `03_channel_strebelle.py`) and are built into the\n", + " online documentation gallery.\n", + "* For a deeper dive into the algorithm internals (the distance layer, the\n", + " parallel DAG engine, and an equation map to Mariethoz (2010) / Juda\n", + " (2022)), see `src/gstools/mps/CODE_GUIDE.md`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4895092d-349a-4948-9470-a1ee803e96c3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (gstools)", + "language": "python", + "name": "gstools" + }, + "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.14.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/13_mps/00_simple_unconditional.py b/examples/13_mps/00_simple_unconditional.py new file mode 100644 index 00000000..b83773da --- /dev/null +++ b/examples/13_mps/00_simple_unconditional.py @@ -0,0 +1,54 @@ +r""" +A first Direct Sampling simulation +---------------------------------- + +This is the minimal Multiple Point Statistics example: build a training image, +wrap it in a :any:`TrainingImage`, and generate one unconditional realization +with :any:`DirectSampling`. + +We use a small, synthetic *channelized* training image generated with NumPy, so +the example is fast and needs no downloads. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +############################################################################### +# Create a synthetic binary training image with curvilinear "channels". +# The two facies (0 and 1) form connected, meandering bands — exactly the kind +# of structure two-point statistics struggles to reproduce. + +gx, gy = np.meshgrid(np.arange(60), np.arange(60), indexing="ij") +ti_data = ((np.sin(gx / 5.0) + np.sin((gx + gy) / 8.0)) > 0).astype(float) + +############################################################################### +# Wrap the array in a :any:`TrainingImage`. For a categorical variable (facies +# codes) the distance is the fraction of mismatching neighbours, so the +# ``distance`` argument is ignored here. + +ti = gs.TrainingImage(ti_data, categorical=True) +print(ti) + +############################################################################### +# Create the :any:`DirectSampling` generator and simulate on a 40x40 grid. +# +# * ``n_neighbors`` — how many already-known cells define each data event. +# * ``scan_fraction`` — fraction of the training image scanned per cell +# (smaller is faster, slightly noisier). +# * ``threshold=0.0`` — the recommended DSBC mode: always take the best match. + +ds = gs.DirectSampling(ti, n_neighbors=12, scan_fraction=0.3, threshold=0.0) +field = ds([np.arange(40, dtype=float)] * 2, seed=20250616) + +############################################################################### +# Plot the training image next to the realization. The realization is not a +# copy of the TI, but it reproduces the same channel patterns. + +fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 5)) +ax0.imshow(ti_data, cmap="cividis", origin="lower") +ax0.set_title("Training image") +ax1.imshow(field, cmap="cividis", origin="lower") +ax1.set_title("DS realization") +fig.tight_layout() diff --git a/examples/13_mps/01_conditional.py b/examples/13_mps/01_conditional.py new file mode 100644 index 00000000..bdf55118 --- /dev/null +++ b/examples/13_mps/01_conditional.py @@ -0,0 +1,56 @@ +r""" +Conditioning to hard data +------------------------- + +Real simulations must honour measured data (boreholes, samples). Direct +Sampling supports this through :meth:`~gstools.DirectSampling.set_condition`: +conditioning values are pinned into the grid before simulation and preserved +exactly, while the rest of the field is filled in around them. + +We reuse the synthetic channel training image from the first example. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +############################################################################### +# Same synthetic channelized training image as before. + +gx, gy = np.meshgrid(np.arange(60), np.arange(60), indexing="ij") +ti_data = ((np.sin(gx / 5.0) + np.sin((gx + gy) / 8.0)) > 0).astype(float) +ti = gs.TrainingImage(ti_data, categorical=True) + +############################################################################### +# Draw 40 random "hard data" points and read their facies from the TI. In a +# real study these would be field measurements; here we sample the TI so the +# conditioning data are consistent with the patterns. + +rng = np.random.default_rng(0) +cond_x = rng.integers(0, 40, 40).astype(float) +cond_y = rng.integers(0, 40, 40).astype(float) +cond_val = ti_data[cond_x.astype(int), cond_y.astype(int)] + +############################################################################### +# Set the conditioning data and simulate. ``set_condition`` snaps each point to +# its nearest grid node, so the values are honoured exactly at those cells. + +ds = gs.DirectSampling(ti, n_neighbors=12, scan_fraction=0.3, threshold=0.0) +ds.set_condition([cond_x, cond_y], cond_val) +field = ds([np.arange(40, dtype=float)] * 2, seed=7) + +honored = int( + (field[cond_x.astype(int), cond_y.astype(int)] == cond_val).sum() +) +print(f"conditioning honoured: {honored}/{cond_val.size}") + +############################################################################### +# Plot the realization with the conditioning points overlaid. Every marker sits +# on a cell whose simulated facies matches the datum. + +fig, ax = plt.subplots(figsize=(6, 5)) +ax.imshow(field, cmap="cividis", origin="lower") +ax.scatter(cond_y, cond_x, c=cond_val, cmap="cividis", edgecolors="red", s=30) +ax.set_title("Conditional DS realization (red-edged = hard data)") +fig.tight_layout() diff --git a/examples/13_mps/02_continuous.py b/examples/13_mps/02_continuous.py new file mode 100644 index 00000000..f70aed12 --- /dev/null +++ b/examples/13_mps/02_continuous.py @@ -0,0 +1,60 @@ +r""" +Continuous variables and distance metrics +----------------------------------------- + +Direct Sampling is not limited to categorical facies: with ``categorical=False`` +it simulates continuous variables (permeability, porosity, elevation, ...). The +``distance`` argument then selects how two patterns are compared: + +* ``"l1"`` / ``"l2"`` — Manhattan / Euclidean distance on the raw values + (Mariethoz et al., 2010, Eq. 6 / Eq. 4). +* ``"variation"`` — compares only the *relative* variations within a pattern + (Eq. 9), tolerating a locally varying mean. Useful for non-stationary data. + +Here we use a smooth continuous training image and a small acceptance +``threshold`` (standard DS mode) to allow approximate matches. + +.. note:: + + The acceptance ``threshold`` is **not comparable across metrics**: the + ``"variation"`` distance is normalised by ``2·d_max``, so the same value is + stricter than it would be for ``"l1"``/``"l2"``. Re-tune it when you switch + the distance metric. +""" + +import matplotlib.pyplot as plt +import numpy as np + +import gstools as gs + +############################################################################### +# A smooth, continuous synthetic training image. + +gx, gy = np.meshgrid(np.arange(60), np.arange(60), indexing="ij") +ti_data = np.sin(gx / 6.0) * np.cos(gy / 8.0) + +############################################################################### +# Build a continuous training image with the Euclidean (``"l2"``) distance. + +ti = gs.TrainingImage(ti_data, categorical=False, distance="l2") +print(ti) + +############################################################################### +# Simulate. ``threshold=0.03`` accepts the first pattern within that distance +# (standard DS), which is faster than the exhaustive best-candidate search for +# continuous variables. + +ds = gs.DirectSampling(ti, n_neighbors=12, scan_fraction=0.3, threshold=0.03) +field = ds([np.arange(32, dtype=float)] * 2, seed=3) + +############################################################################### +# Plot. The realization reproduces the smooth wavy structure of the TI without +# copying it. A shared colour scale makes the comparison fair. + +vmin, vmax = ti_data.min(), ti_data.max() +fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(10, 5)) +im = ax0.imshow(ti_data, cmap="RdBu_r", origin="lower", vmin=vmin, vmax=vmax) +ax0.set_title("Training image (continuous)") +ax1.imshow(field, cmap="RdBu_r", origin="lower", vmin=vmin, vmax=vmax) +ax1.set_title("DS realization") +fig.colorbar(im, ax=(ax0, ax1), shrink=0.7) diff --git a/examples/13_mps/03_channel_strebelle.py b/examples/13_mps/03_channel_strebelle.py new file mode 100644 index 00000000..22eb0900 --- /dev/null +++ b/examples/13_mps/03_channel_strebelle.py @@ -0,0 +1,83 @@ +r""" +A real training image: the Strebelle channels +---------------------------------------------- + +The previous examples used tiny synthetic training images. Here we use the +classic **Strebelle (2002) channelized fluvial training image**, the de-facto +benchmark for MPS, and condition the simulation on random hard data. + +.. note:: + + **Data source / license.** The training image is downloaded from the + `GeoDataSets `_ repository by + Michael Pyrcz (GeostatsGuy), which is distributed under the **MIT license** + (redistribution permitted with attribution). The underlying channel TI is + due to Strebelle, S. (2002), *Conditional simulation of complex geological + structures using multiple-point statistics*, Mathematical Geology, 34(1), + 1-21. If the download is unavailable, the example falls back to a synthetic + training image so it still runs offline. +""" + +import os +import urllib.request + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.colors import ListedColormap + +import gstools as gs + +############################################################################### +# Load the Strebelle training image, with a synthetic fallback for offline use. + +TI_URL = ( + "https://raw.githubusercontent.com/GeostatsGuy/" + "GeoDataSets/master/MPS_Training_image_and_Realizations_500.npz" +) +CACHE = "mps_strebelle.npz" +try: + if not os.path.exists(CACHE): + urllib.request.urlretrieve(TI_URL, CACHE) + ti_arr = np.load(CACHE)["array1"].astype(float) + source = "Strebelle (2002) via GeoDataSets (MIT)" +except Exception as err: # pragma: no cover - network fallback + print(f"download failed ({err}); using a synthetic channel TI instead") + gx, gy = np.meshgrid(np.arange(150), np.arange(150), indexing="ij") + ti_arr = ((np.sin(gx / 6.0) + np.sin((gx + gy) / 10.0)) > 0).astype(float) + source = "synthetic fallback" + +ti = gs.TrainingImage(ti_arr, categorical=True) +print(f"TI {ti.shape} ({source}), sand fraction = {ti_arr.mean():.3f}") + +############################################################################### +# Take 80 random conditioning points from the training image patterns. + +sg_size = 80 +rng = np.random.default_rng(0) +cond_x = rng.integers(0, sg_size, 80).astype(float) +cond_y = rng.integers(0, sg_size, 80).astype(float) +cond_val = ti_arr[cond_x.astype(int), cond_y.astype(int)] + +############################################################################### +# Simulate with DSBC-style parameters (best-candidate + partial scan). + +ds = gs.DirectSampling(ti, n_neighbors=30, scan_fraction=0.2, threshold=0.0) +ds.set_condition([cond_x, cond_y], cond_val) +field = ds([np.arange(sg_size, dtype=float)] * 2, seed=42) + +honored = int( + (field[cond_x.astype(int), cond_y.astype(int)] == cond_val).sum() +) +print(f"conditioning honoured: {honored}/{cond_val.size}") + +############################################################################### +# Plot the training image crop next to the conditional realization. + +cmap = ListedColormap(["#c9a96e", "#2b6cb0"]) # shale / sand +fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 5.5)) +ax0.imshow(ti_arr[:sg_size, :sg_size], cmap=cmap, origin="lower") +ax0.set_title("Training image (crop)") +ax1.imshow(field, cmap=cmap, origin="lower") +ax1.scatter(cond_y, cond_x, c=cond_val, cmap=cmap, edgecolors="k", s=18) +ax1.set_title("Conditional DS realization") +fig.tight_layout() diff --git a/examples/13_mps/README.rst b/examples/13_mps/README.rst new file mode 100644 index 00000000..a5ecfca8 --- /dev/null +++ b/examples/13_mps/README.rst @@ -0,0 +1,38 @@ +Multiple Point Statistics +========================= + +Two-point geostatistics (covariance models, kriging, SRFs) describes spatial +structure through pairs of points and a variogram. This is powerful, but it +cannot reproduce *curvilinear* or *connected* features such as meandering +channels, fractures, or other patterns that depend on the joint configuration +of many points at once. + +**Multiple Point Statistics (MPS)** addresses this by learning patterns +directly from a **training image (TI)** — an example image deemed +representative of the spatial structure to simulate. Instead of fitting a +variogram, MPS borrows whole patterns from the TI. + +GSTools provides the **Direct Sampling (DS)** algorithm +(`Mariethoz et al., 2010 `_), together +with the **Direct Sampling Best Candidate (DSBC)** parametrization +(`Juda et al., 2022 `_), through +two classes: + +* :any:`TrainingImage` — the MPS model: the training image plus the distance + used to compare patterns (the analogue of a :any:`CovModel`). +* :any:`DirectSampling` — the generator that produces realizations on a + structured grid (the analogue of :any:`SRF`). + +The core idea: to fill each grid cell, DS looks at the values already present +around it (its *data event*), scans the training image for a location whose +surroundings look similar enough, and copies that cell's value over. +"Similar enough" is decided by a **distance** between the two surroundings, +controlled by three parameters — the number of neighbours ``n``, the scan +fraction ``f``, and the acceptance threshold ``t`` (with ``t = 0`` giving the +recommended DSBC mode). + +The following tutorials build up from a minimal unconditional simulation to +conditioning and continuous variables. + +Examples +-------- diff --git a/src/gstools/__init__.py b/src/gstools/__init__.py index 4d12007c..e61f7597 100644 --- a/src/gstools/__init__.py +++ b/src/gstools/__init__.py @@ -23,10 +23,21 @@ tools transform normalizer + mps Classes ======= +Multiple Point Statistics +^^^^^^^^^^^^^^^^^^^^^^^^^ +Classes for Multiple Point Statistics (MPS) simulations + +.. currentmodule:: gstools.mps + +.. autosummary:: + DirectSampling + TrainingImage + Kriging ^^^^^^^ Swiss-Army-Knife for Kriging. For short cut classes see: :any:`gstools.krige` @@ -139,6 +150,7 @@ covmodel, field, krige, + mps, normalizer, random, tools, @@ -169,6 +181,7 @@ ) from gstools.field import PGS, SRF, CondSRF from gstools.krige import Krige +from gstools.mps import DirectSampling, TrainingImage from gstools.tools import ( DEGREE_SCALE, EARTH_RADIUS, @@ -200,7 +213,7 @@ __all__ = ["__version__"] __all__ += ["covmodel", "field", "variogram", "krige", "random", "tools"] -__all__ += ["transform", "normalizer", "config"] +__all__ += ["transform", "normalizer", "config", "mps"] __all__ += [ "CovModel", "SumModel", @@ -237,6 +250,8 @@ "SRF", "CondSRF", "PGS", + "DirectSampling", + "TrainingImage", "rotated_main_axes", "generate_grid", "generate_st_grid", diff --git a/src/gstools/mps/__init__.py b/src/gstools/mps/__init__.py new file mode 100644 index 00000000..2a3be355 --- /dev/null +++ b/src/gstools/mps/__init__.py @@ -0,0 +1,18 @@ +""" +GStools subpackage for Multiple Point Statistics (MPS). + +.. currentmodule:: gstools.mps + +Multiple Point Statistics +^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autosummary:: + :toctree: + + DirectSampling + TrainingImage +""" + +from gstools.mps.direct_sampling import DirectSampling +from gstools.mps.training_image import TrainingImage + +__all__ = ["DirectSampling", "TrainingImage"] diff --git a/src/gstools/mps/direct_sampling.py b/src/gstools/mps/direct_sampling.py new file mode 100644 index 00000000..7062f42c --- /dev/null +++ b/src/gstools/mps/direct_sampling.py @@ -0,0 +1,836 @@ +""" +GStools subpackage providing the Direct Sampling MPS simulation class. + +.. currentmodule:: gstools.mps + +The following classes and functions are provided + +.. autosummary:: + DirectSampling +""" + +import queue +from concurrent.futures import ThreadPoolExecutor + +import numpy as np + +from gstools import config +from gstools.field.base import Field +from gstools.random.rng import RNG + +__all__ = ["DirectSampling"] + +_VALID_BOUNDARY = ("strict", "partial") + +# DS-mode scan block size. Large enough that per-call NumPy overhead is +# negligible (essentially full vectorization speed), small enough that the +# greedy DS scan does not overcompute far past the first accepted match. +# This is a call-overhead-amortization constant, not a cache-tuned one. +_SCAN_BLOCK = 4096 + + +def _precompute_offsets(shape, max_offset=None): + """Neighbour offsets from the origin, sorted by Euclidean distance. + + Parameters + ---------- + shape : tuple + Simulation grid shape. + max_offset : int, optional + Maximum offset in any dimension. + Default: ``max(shape)``. + + Returns + ------- + numpy.ndarray, shape (N, dim) + """ + dim = len(shape) + if max_offset is None: + max_offset = max(shape) + rng_vals = np.arange(-max_offset, max_offset + 1) + grid = np.array(np.meshgrid(*[rng_vals] * dim, indexing="ij")) + offsets = grid.reshape(dim, -1).T + offsets = offsets[np.any(offsets != 0, axis=1)] + idx = np.argsort(np.sum(offsets**2, axis=1)) + return offsets[idx] + + +def _select_neighbors( + x_i, + offset_arr, + sim_shape_arr, + sim_shape, + path_pos_map, + curr_idx, + informed, + max_radius, + n_neighbors, +): + """Closest valid neighbours of ``x_i``, with their path indices. + + A candidate is valid when it is in bounds, has path index ``< curr_idx`` + (already-simulated in path order) or ``-1`` (conditioning data), and — if + ``informed`` is given — is marked informed. ``offset_arr`` is + distance-sorted, so slicing the first ``n_neighbors`` survivors yields the + closest ones. + + Passing ``informed=None`` treats every in-bounds lower-index/conditioning + cell as available; this is correct when building the dependency DAG, where + all earlier-path nodes are informed by definition. + + Returns + ------- + coords : numpy.ndarray, shape (m, dim) + Neighbour coordinates, ``m <= n_neighbors``. + path_idx : numpy.ndarray, shape (m,) + Path index of each neighbour (``-1`` for conditioning data). + """ + # ``offset_arr`` is distance-sorted, so the closest ``n_neighbors`` valid + # candidates are the first ``n_neighbors`` survivors and we can stop the + # moment we have them. Iterating with an early break avoids masking the + # whole offset array for every node (O(N**2) on large grids without a + # ``max_radius`` cap). Tradeoff: when fewer than ``n_neighbors`` valid + # candidates exist (sparse early-path nodes), the scan still walks the full + # ``offset_arr`` in Python; this affects only the first few nodes and is + # bounded by the ``max_radius`` ball when one is set. + dim = offset_arr.shape[1] + r_sq = max_radius * max_radius if max_radius is not None else None + found_coords = [] + found_vidx = [] + for off in offset_arr: + # Distance-sorted: the first offset beyond the radius ends the scan. + if r_sq is not None and float(off @ off) > r_sq: + break + cand = x_i + off + if np.any(cand < 0) or np.any(cand >= sim_shape_arr): + continue + vi = path_pos_map[int(np.ravel_multi_index(tuple(cand), sim_shape))] + if not (vi < curr_idx or vi == -1): + continue + if informed is not None and not informed[tuple(cand)]: + continue + found_coords.append(cand) + found_vidx.append(vi) + if len(found_coords) >= n_neighbors: + break + if found_coords: + return ( + np.array(found_coords, dtype=offset_arr.dtype), + np.array(found_vidx, dtype=path_pos_map.dtype), + ) + return ( + np.empty((0, dim), dtype=offset_arr.dtype), + np.empty(0, dtype=path_pos_map.dtype), + ) + + +def _build_dag( + path, + n_neighbors, + sim_shape, + offset_arr, + path_pos_map, + max_radius=None, +): + """Build the simulation dependency DAG. + + Edge ``j -> i`` means path node ``j`` (j < i) is among the n-closest + neighbours used when simulating node ``i``. Conditioning data carry no + edge. Uses the same vectorized neighbour selection as the simulation + (:func:`_select_neighbors`), so the resulting dependencies match the set + each node would actually pick at simulation time. + """ + N = len(path) + sim_shape_arr = np.array(sim_shape) + indegree = np.zeros(N, dtype=np.int32) + out_edges = [[] for _ in range(N)] + + for i in range(N): + _, vidx = _select_neighbors( + path[i], + offset_arr, + sim_shape_arr, + sim_shape, + path_pos_map, + i, # build-time: all path nodes with index < i are informed + None, + max_radius, + n_neighbors, + ) + # path-node neighbours (conditioning data have index -1, no edge) + for j in vidx[vidx >= 0]: + indegree[i] += 1 + out_edges[int(j)].append(i) + + return indegree, out_edges + + +def ds_simulate( + training_image, + sim_shape, + n_neighbors, + threshold, + scan_fraction, + rng, + conditions=None, + cond_weight=1.0, + boundary="strict", + max_radius=None, + num_threads=None, +): + """Direct Sampling univariate simulation (Mariethoz2010, Juda2022). + + Parameters + ---------- + training_image : TrainingImage + Training image; provides ``training_image.distance()`` and + ``training_image.adjust_value()``. + sim_shape : tuple + Simulation grid shape. + n_neighbors : int + Maximum number of neighbours in the data event (Juda2022 §2). + threshold : float + Distance threshold for early acceptance (Juda2022 §2). + ``0.0`` → DSBC mode. + scan_fraction : float + Fraction of the per-node search window to scan (Mariethoz2010 §3 ¶24). + Evaluates at most ``floor(f · |window|)`` candidates per node. + ``1.0`` → full window scan. + rng : numpy.random.RandomState + Random number generator. + conditions : dict, optional + ``{tuple_index: value}`` mapping of conditioning data. + cond_weight : float, optional + Weight δ for conditioning nodes (Mariethoz2010 §3 ¶26). + boundary : str, optional + Search-window strategy: ``"strict"`` (default) or ``"partial"``. + max_radius : float, optional + If set, SG neighbours beyond this Euclidean distance are excluded + from the data event (Mariethoz2010 §3 ¶19). + num_threads : int or None, optional + Number of threads for outer DAG parallelism. ``None`` defaults to + ``config.NUM_THREADS``. + + Returns + ------- + numpy.ndarray + """ + ti_data = training_image.data + ti_shape = np.array(ti_data.shape) + sim_shape_arr = np.array(sim_shape) + sg = np.full(sim_shape, np.nan) + is_cond = np.zeros(sim_shape, dtype=bool) + informed = np.zeros(sim_shape, dtype=bool) + + if conditions: + for idx, val in conditions.items(): + sg[idx] = val + is_cond[idx] = True + informed[idx] = True + + n_threads = ( + num_threads if num_threads is not None else (config.NUM_THREADS or 1) + ) + executor = ( + ThreadPoolExecutor(max_workers=n_threads) if n_threads > 1 else None + ) + max_off_int = int(np.ceil(max_radius)) if max_radius is not None else None + offset_arr = _precompute_offsets(sim_shape, max_off_int) + + path = np.argwhere(np.isnan(sg)) + path = path[rng.permutation(len(path))] + node_seeds = rng.randint(0, 2**32, size=len(path), dtype=np.int64) + + path_flat = np.ravel_multi_index(path.T, sim_shape) + path_pos_map = np.full(int(np.prod(sim_shape)), -1, dtype=np.intp) + path_pos_map[path_flat] = np.arange(len(path_flat)) + + def _rand_ti(node_rng): + return ti_data[tuple(node_rng.randint(0, s) for s in ti_shape)] + + def _get_neighbors(x_i, informed_in): + curr_idx = path_pos_map[ + int(np.ravel_multi_index(tuple(x_i), sim_shape)) + ] + coords, _ = _select_neighbors( + x_i, + offset_arr, + sim_shape_arr, + sim_shape, + path_pos_map, + curr_idx, + informed_in, + max_radius, + n_neighbors, + ) + return coords + + def _scan_ti(lo, win_shape, lags, de_sim, cm, ln, node_rng): + # Precondition: win_size >= 1 (callers guarantee win_lo <= win_hi on all axes). + # Also captures scan_fraction, ti_data, threshold, cond_weight from outer scope. + win_size = int(np.prod(win_shape)) + max_scan = max(1, int(scan_fraction * win_size)) + start = int(node_rng.randint(0, win_size)) + + # All scan positions in visit order — shape (max_scan,) + positions = (start + np.arange(max_scan)) % win_size + # Anchor coordinates for each position — shape (max_scan, dim) + y_all = lo + np.column_stack(np.unravel_index(positions, win_shape)) + + # lags are integer-valued float64; cast once, reuse for all candidates + int_lags = lags.astype(int) # (k, dim) + + def _de_ti(y_rows): + # TI data events for the given anchor rows — shape (len(y_rows), k) + coords = y_rows[:, None, :] + int_lags[None, :, :] + return ti_data[tuple(coords.transpose(2, 0, 1))] + + # DSBC (threshold == 0): no early exit is possible — the global minimum + # over the whole scan is required — so evaluate every candidate in a + # single vectorized call. This is the fastest path and stays exact. + if threshold <= 0: + all_de_ti = _de_ti(y_all) + all_dists = training_image.vec_distance( + de_sim, all_de_ti, cm, cond_weight, ln + ) + best_k = int(np.argmin(all_dists)) + return ti_data[tuple(y_all[best_k])], all_de_ti[best_k] + + # DS (threshold > 0): chunked vectorized scan with an early-exit + # checkpoint between blocks. Each block is a full vectorized distance + # call (so the per-element cost matches the single-call version); only + # the threshold test runs per block. Blocks advance in scan order, so + # the first under-threshold candidate found is the first one globally — + # identical to the unchunked argmax(under) result. + best_d = np.inf + best_y = None + best_de = None + for b0 in range(0, max_scan, _SCAN_BLOCK): + y_blk = y_all[b0 : b0 + _SCAN_BLOCK] + de_blk = _de_ti(y_blk) + d_blk = training_image.vec_distance( + de_sim, de_blk, cm, cond_weight, ln + ) + under = d_blk <= threshold + if np.any(under): + k = int(np.argmax(under)) + return ti_data[tuple(y_blk[k])], de_blk[k] + # No acceptable match in this block. Track the running best with a + # strict ``<`` test so that, if no candidate ever falls below the + # threshold, the returned fallback equals the global argmin with the + # same first-occurrence tie-break as the unchunked version. + k = int(np.argmin(d_blk)) + if d_blk[k] < best_d: + best_d = float(d_blk[k]) + best_y = y_blk[k] + best_de = de_blk[k] + return ti_data[tuple(best_y)], best_de + + def _simulate_node(x_i, node_rng, sg_in, informed_in): + nbrs = _get_neighbors(x_i, informed_in) + if len(nbrs) == 0: + return _rand_ti(node_rng) + + lags = (nbrs - x_i).astype(np.float64) # (k, dim) + data_event_sim = sg_in[tuple(nbrs.T)] # (k,) + cond_mask = is_cond[tuple(nbrs.T)] # (k,) + lag_norms = np.linalg.norm(lags, axis=1) # (k,) + + if boundary == "strict": + # Search window Y(L_i) — Juda2022 Eq. 5, Mariethoz2010 §3 ¶19 + win_lo = np.maximum(0, np.ceil(-lags.min(axis=0))).astype(int) + win_hi = np.minimum( + ti_shape - 1, np.floor(ti_shape - 1 - lags.max(axis=0)) + ).astype(int) + if np.any(win_lo > win_hi): + return _rand_ti(node_rng) + best_v, best_de_ti = _scan_ti( + win_lo, + tuple(win_hi - win_lo + 1), + lags, + data_event_sim, + cond_mask, + lag_norms, + node_rng, + ) + return training_image.adjust_value( + best_v, data_event_sim, best_de_ti + ) + + else: # "partial" — Mariethoz2010 §6.2: global template reduction + # Lags are distance-sorted (closest first) because offset_arr is. + # Drop farthest neighbours one at a time until the bounding box of + # the remaining data event fits inside the TI, per the paper's + # "ignore until it becomes possible to scan" directive (§6.2). + valid_count = len(lags) + while valid_count > 0: + lags_p = lags[:valid_count] + sw_lo = np.maximum(0, np.ceil(-lags_p.min(axis=0))).astype(int) + sw_hi = np.minimum( + ti_shape - 1, np.floor(ti_shape - 1 - lags_p.max(axis=0)) + ).astype(int) + if np.all(sw_lo <= sw_hi): + break + valid_count -= 1 + else: + # No subset of the data event fits inside the TI (the closest + # neighbour's lag already exceeds the TI in some dimension). + # Recover like the empty-window case in strict mode rather than + # aborting the whole simulation. + return _rand_ti(node_rng) + best_v, best_de_ti = _scan_ti( + sw_lo, + tuple(sw_hi - sw_lo + 1), + lags_p, + data_event_sim[:valid_count], + cond_mask[:valid_count], + lag_norms[:valid_count], + node_rng, + ) + # For variation distance, adjust_value uses the mean of the + # truncated data event (valid_count neighbours), not the full + # neighbourhood mean. This is intentional — the mean-shift + # must be consistent with the lags actually used in the scan. + return training_image.adjust_value( + best_v, data_event_sim[:valid_count], best_de_ti + ) + + try: + if executor is not None: + indegree, out_edges = _build_dag( + path, + n_neighbors, + sim_shape, + offset_arr, + path_pos_map, + max_radius, + ) + # Running ready-queue: a node is dispatched the instant its last + # dependency completes (no per-wave barrier). Workers read the + # live sg / informed arrays; this is safe because (1) a node is + # only submitted once all its dependencies are written, so the + # values it reads are final, and (2) all shared-state mutation + # (sg, informed, in-degree, submission) happens on this main + # thread — workers only read. Each numpy access holds the GIL for + # its duration, so element reads never tear against the writes. + # The result of every node depends only on its seed and its + # (final) neighbour values, so the output is independent of + # completion order and stays identical to the serial run. + done_q = queue.Queue() + counts = {"submitted": 0, "done": 0} + + def _run(i): + return i, _simulate_node( + path[i], + RNG(int(node_seeds[i])).random, + sg, + informed, + ) + + def _submit(i): + executor.submit(_run, i).add_done_callback(done_q.put) + counts["submitted"] += 1 + + for i in range(len(path)): + if indegree[i] == 0: + _submit(i) + + while counts["done"] < counts["submitted"]: + i, val = done_q.get().result() + counts["done"] += 1 + x_i_t = tuple(path[i]) + if np.isnan(val): + raise ValueError( + f"Simulation produced NaN at {path[i]}. Check TI data." + ) + sg[x_i_t] = val + informed[x_i_t] = True + for j in out_edges[i]: + indegree[j] -= 1 + if indegree[j] == 0: + _submit(j) + # Defensive guard: every node must have been scheduled and run. + # The DAG is provably acyclic, so this never trips in practice — but + # if a future change ever introduced a cycle, no node would reach + # in-degree 0, the loop above would exit immediately, and an all-NaN + # field would be returned silently. Fail loudly instead. + if counts["done"] != len(path): + raise ValueError( + "DirectSampling: parallel scheduler simulated " + f"{counts['done']}/{len(path)} nodes; the dependency graph " + "is not acyclic (this should never happen)." + ) + else: + for i, x_i in enumerate(path): + x_i_t = tuple(x_i) + val = _simulate_node( + x_i, + RNG(int(node_seeds[i])).random, + sg, + informed, + ) + if np.isnan(val): + raise ValueError( + f"Simulation produced NaN at {x_i}. Check TI data." + ) + sg[x_i_t] = val + informed[x_i_t] = True + finally: + if executor is not None: + executor.shutdown(wait=True) + + return sg + + +class DirectSampling(Field): + """Multiple Point Statistics simulation using Direct Sampling. + + Subclasses :class:`gstools.field.base.Field`. Takes a :class:`TrainingImage` + (analogous to :class:`CovModel`) and produces fields on structured grids. + + Parameters + ---------- + ti : TrainingImage + The training image (the MPS model). + n_neighbors : int, optional + Maximum neighbors in data event. Default: 32. + scan_fraction : float, optional + Fraction of the per-node search window to scan. Default: 1. + threshold : float, optional + Distance threshold for early acceptance. 0.0 -> DSBC mode. Default: 0.0. + + .. note:: + The threshold is **not comparable across distance metrics**. The + ``"variation"`` distance is normalised by ``2·d_max`` (and clamped + to ``[0, 1]``), so the same threshold is stricter for + ``"variation"`` than for ``"l1"``/``"l2"``. Re-tune ``threshold`` + when you change the training image's distance metric. + cond_weight : float, optional + Weight for conditioning nodes in distance. Default: 1.0. + boundary : str, optional + Search-window strategy: ``"strict"`` (default) or ``"partial"``. + max_radius : float, optional + Exclude SG neighbours beyond this Euclidean distance from the + data event. Default: ``None`` (no limit). + The minimum effective value is 1.0 (the grid-cell Euclidean + distance to the nearest neighbour). Values in ``(0, 1)`` accept + no neighbours at all, making every node fall back to a random TI + sample. + num_threads : int or None, optional + Number of threads for outer DAG parallelism. ``None`` defaults to + ``config.NUM_THREADS``. + seed : int or nan, optional + Master RNG seed. Default: nan. + """ + + default_field_names = ["field"] + + def __init__( + self, + ti, + n_neighbors=32, + scan_fraction=1, + threshold=0.0, + cond_weight=1.0, + boundary="strict", + max_radius=None, + num_threads=None, + seed=np.nan, + ): + if boundary not in _VALID_BOUNDARY: + raise ValueError( + f"DirectSampling: boundary must be one of {_VALID_BOUNDARY!r}, " + f"got {boundary!r}" + ) + if int(n_neighbors) < 1: + raise ValueError( + f"DirectSampling: n_neighbors must be >= 1, got {n_neighbors!r}" + ) + if not (0 < float(scan_fraction) <= 1): + raise ValueError( + f"DirectSampling: scan_fraction must be in (0, 1], " + f"got {scan_fraction!r}" + ) + if float(threshold) < 0: + raise ValueError( + f"DirectSampling: threshold must be >= 0, got {threshold!r}" + ) + if float(threshold) > 1.0: + import warnings + + warnings.warn( + "threshold > 1.0 guarantees the first candidate is always accepted.", + stacklevel=2, + ) + if max_radius is not None and float(max_radius) <= 0: + raise ValueError( + f"DirectSampling: max_radius must be a positive float, " + f"got {max_radius!r}" + ) + super().__init__(model=None, dim=ti.ndim, value_type="scalar") + self._ti = ti + self._n_neighbors = int(n_neighbors) + self._scan_fraction = float(scan_fraction) + self._threshold = float(threshold) + self._cond_weight = float(cond_weight) + self._boundary = boundary + self._max_radius = ( + float(max_radius) if max_radius is not None else None + ) + self._num_threads = num_threads + self._cond_pos = None + self._cond_val = None + self.rng = RNG(None if np.isnan(seed) else int(seed)) + + def __call__( + self, + pos=None, + seed=np.nan, + mesh_type="structured", + post_process=True, + store=True, + ): + """Generate the spatial random field via Direct Sampling. + + The field is saved as ``self.field`` and is also returned. + + Parameters + ---------- + pos : :class:`list`, optional + The position tuple, containing main direction and transversal + directions. Only structured grids are supported. + seed : :class:`int`, optional + Seed for the RNG. If ``np.nan``, the current seed is kept. + Default: ``np.nan`` + mesh_type : :class:`str`, optional + Grid type. Must be ``"structured"``. + Default: ``"structured"`` + post_process : :class:`bool`, optional + Whether to apply post-processing transformations (mean, + normalizer, trend) to the field. Default: :any:`True` + store : :class:`bool` or :class:`str`, optional + Whether to store the field (``True``), not store it (``False``), + or store it under a custom name (string). + Default: :any:`True` + + Returns + ------- + field : :class:`numpy.ndarray` + The simulated field. + """ + if mesh_type != "structured": + raise ValueError( + "DirectSampling: only structured grids are supported." + ) + name, save = self.get_store_config(store) + pos, shape = self.pre_pos(pos, mesh_type) + conditions = self._conditions_to_grid(self.pos) + if not np.isnan(seed): + self.rng.seed = int(seed) + rng = np.random.RandomState( + int(self.rng.random.randint(0, 2**32, dtype=np.int64)) + ) + field = ds_simulate( + training_image=self._ti, + sim_shape=shape, + n_neighbors=self._n_neighbors, + threshold=self._threshold, + scan_fraction=self._scan_fraction, + rng=rng, + conditions=conditions, + cond_weight=self._cond_weight, + boundary=self._boundary, + max_radius=self._max_radius, + num_threads=self._num_threads, + ) + # Categorical + post-processing (R2): mean/normalizer/trend are meant + # for continuous fields. Applied to categorical output they turn facies + # codes into meaningless real values, silently. Warn the user. + if ( + post_process + and self._ti.categorical + and ( + self.mean is not None + or self.normalizer is not None + or self.trend is not None + ) + ): + import warnings + + warnings.warn( + "DirectSampling: mean/normalizer/trend post-processing is set " + "on a categorical training image. This will alter the facies " + "codes and produce meaningless values. Pass post_process=False " + "or unset mean/normalizer/trend for categorical simulations.", + stacklevel=2, + ) + return self.post_field(field, name, post_process, save) + + def _conditions_to_grid(self, axes): + """Smart snapping: Mariethoz 2010 collision rule.""" + if self._cond_pos is None: + return {} + # Axis bounds for the out-of-grid check (R6): points outside the domain + # snap to the nearest boundary node, which is silently misleading. + bounds = [(axes[d].min(), axes[d].max()) for d in range(self.dim)] + n_outside = 0 + candidates = {} # idx -> (val, dist_sq) + for k in range(self._cond_val.shape[0]): + if any( + self._cond_pos[d][k] < bounds[d][0] + or self._cond_pos[d][k] > bounds[d][1] + for d in range(self.dim) + ): + n_outside += 1 + idx = tuple( + int(np.argmin(np.abs(axes[d] - self._cond_pos[d][k]))) + for d in range(self.dim) + ) + dist_sq = sum( + (axes[d][idx[d]] - self._cond_pos[d][k]) ** 2 + for d in range(self.dim) + ) + if idx not in candidates or dist_sq < candidates[idx][1]: + candidates[idx] = (self._cond_val[k], dist_sq) + if n_outside: + import warnings + + warnings.warn( + f"DirectSampling: {n_outside} conditioning point(s) lie " + "outside the simulation grid and were snapped to the nearest " + "boundary node. Check your conditioning positions.", + stacklevel=2, + ) + return {idx: val for idx, (val, _) in candidates.items()} + + def set_condition(self, cond_pos, cond_val, cond_weight=None): + """Set the conditioning data for the simulation. + + Parameters + ---------- + cond_pos : :class:`list` + The position tuple of the conditioning data ``(x, [y, z])``. + cond_val : :class:`numpy.ndarray` + The values at the conditioning positions. + cond_weight : :class:`float`, optional + Conditioning weight δ. If given, overrides the ``cond_weight`` + set at construction. Default: :any:`None` (keep existing weight) + """ + from gstools.krige.tools import set_condition as _gs_set_condition + + self._cond_pos, self._cond_val = _gs_set_condition( + cond_pos, cond_val, self.dim + ) + if cond_weight is not None: + self._cond_weight = float(cond_weight) + + @property + def ti(self): + """TrainingImage: The training image model.""" + return self._ti + + @property + def n_neighbors(self): + """:class:`int`: Maximum neighbours in the data event.""" + return self._n_neighbors + + @n_neighbors.setter + def n_neighbors(self, value): + if int(value) < 1: + raise ValueError( + f"DirectSampling: n_neighbors must be >= 1, got {value!r}" + ) + self._n_neighbors = int(value) + + @property + def scan_fraction(self): + """:class:`float`: Fraction of the per-node search window to scan.""" + return self._scan_fraction + + @scan_fraction.setter + def scan_fraction(self, value): + if not (0 < float(value) <= 1): + raise ValueError( + f"DirectSampling: scan_fraction must be in (0, 1], got {value!r}" + ) + self._scan_fraction = float(value) + + @property + def threshold(self): + """:class:`float`: Distance threshold (0.0 → DSBC mode).""" + return self._threshold + + @threshold.setter + def threshold(self, value): + if float(value) < 0: + raise ValueError( + f"DirectSampling: threshold must be >= 0, got {value!r}" + ) + if float(value) > 1.0: + import warnings + + warnings.warn( + "threshold > 1.0 guarantees the first candidate is always accepted.", + stacklevel=2, + ) + self._threshold = float(value) + + @property + def cond_weight(self): + """:class:`float`: Weight for conditioning nodes in distance.""" + return self._cond_weight + + @cond_weight.setter + def cond_weight(self, value): + self._cond_weight = float(value) + + @property + def boundary(self): + """:class:`str`: Search-window strategy (``"strict"`` or ``"partial"``).""" + return self._boundary + + @boundary.setter + def boundary(self, value): + if value not in _VALID_BOUNDARY: + raise ValueError( + f"DirectSampling: boundary must be one of {_VALID_BOUNDARY!r}, " + f"got {value!r}" + ) + self._boundary = value + + @property + def max_radius(self): + """:class:`float` or :any:`None`: Euclidean cap on SG neighbour selection. + + Values in ``(0, 1)`` disable all neighbours (nearest grid cell is + at distance 1.0), causing every node to fall back to a random TI + sample. + """ + return self._max_radius + + @max_radius.setter + def max_radius(self, value): + if value is not None and float(value) <= 0: + raise ValueError( + f"DirectSampling: max_radius must be a positive float, " + f"got {value!r}" + ) + self._max_radius = float(value) if value is not None else None + + @property + def num_threads(self): + """:class:`int` or :any:`None`: Number of threads for outer DAG parallelism.""" + return self._num_threads + + @num_threads.setter + def num_threads(self, value): + self._num_threads = None if value is None else int(value) + + def __repr__(self): + return ( + f"DirectSampling(dim={self.dim}, " + f"n_neighbors={self.n_neighbors}, " + f"scan_fraction={self.scan_fraction}, " + f"threshold={self.threshold}, " + f"boundary={self.boundary!r})" + ) diff --git a/src/gstools/mps/distance.py b/src/gstools/mps/distance.py new file mode 100644 index 00000000..32c7241c --- /dev/null +++ b/src/gstools/mps/distance.py @@ -0,0 +1,308 @@ +"""Pure distance functions for MPS pattern comparison. + +No class state — takes arrays and scalars, returns floats. +``TrainingImage.distance()`` uses these internally; other algorithms +can import them directly. +""" + +import numpy as np + +__all__ = [ + "compute_node_weights", + "categorical_dist", + "l1_dist", + "l2_dist", + "lp_dist", + "variation_dist", + "vec_categorical_dist", + "vec_l1_dist", + "vec_l2_dist", + "vec_lp_dist", + "vec_variation_dist", +] + + +def compute_node_weights( + n, lag_norms, distance_power, cond_mask=None, cond_weight=1.0 +): + """Compute normalized spatial-decay weights for a data event. + + Combines spatial decay (Mariethoz2010 Eq. 5) with conditioning data + multipliers (Mariethoz2010 §3 ¶26). + + Parameters + ---------- + n : int + Number of neighbours in the data event. + lag_norms : array-like or None, shape (n,) + Euclidean norms ``‖h_i‖`` of each lag vector. ``None`` or + ``distance_power == 0`` → uniform spatial weights. + distance_power : float + Exponent δ. ``0.0`` → uniform. + cond_mask : array-like of bool, optional + ``True`` where the neighbour is a conditioning datum. + cond_weight : float, optional + Bonus weight multiplier for conditioning nodes. + + Returns + ------- + numpy.ndarray, shape (n,) + Node weights normalized to sum to 1. + """ + if lag_norms is not None and distance_power != 0.0: + norms = np.asarray(lag_norms, dtype=np.float64) + norms = np.where(norms == 0.0, 1e-10, norms) + raw_w = norms ** (-distance_power) + else: + raw_w = np.ones(n, dtype=np.float64) + + if cond_mask is not None: + raw_w = raw_w.copy() + raw_w[np.asarray(cond_mask, dtype=bool)] *= cond_weight + + total = raw_w.sum() + if not np.isfinite(total) or total == 0.0: + # e.g. all neighbours are conditioning data with cond_weight == 0: + # fall back to uniform weights rather than emit NaNs. + return np.full(n, 1.0 / n, dtype=np.float64) + return raw_w / total + + +def categorical_dist(data_event_sim, data_event_ti, node_weights): + """Weighted categorical distance (Mariethoz2010 Eq. 3). + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + data_event_ti : numpy.ndarray, shape (n,) + node_weights : numpy.ndarray, shape (n,) + Normalized spatial and conditioning weights. + + Returns + ------- + float + Distance in [0, 1]. + """ + return float( + np.dot( + node_weights, + (data_event_sim != data_event_ti).astype(np.float64), + ) + ) + + +def l1_dist(data_event_sim, data_event_ti, node_weights, d_max): + """Weighted L1 distance / Manhattan (Mariethoz2010 Eq. 6). + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + data_event_ti : numpy.ndarray, shape (n,) + node_weights : numpy.ndarray, shape (n,) + Normalized spatial and conditioning weights. + d_max : float + Data range for normalization. + + Returns + ------- + float + Distance in [0, 1]. + """ + return float( + np.dot(node_weights, np.abs(data_event_sim - data_event_ti) / d_max) + ) + + +def l2_dist(data_event_sim, data_event_ti, node_weights, d_max): + """Weighted L2 / RMS distance (Mariethoz2010 Eq. 4–5). + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + data_event_ti : numpy.ndarray, shape (n,) + node_weights : numpy.ndarray, shape (n,) + Normalized spatial and conditioning weights. + d_max : float + Data range for normalization. + + Returns + ------- + float + Distance in [0, 1]. + """ + return float( + np.sqrt( + np.dot( + node_weights, + ((data_event_sim - data_event_ti) / d_max) ** 2, + ) + ) + ) + + +def lp_dist(data_event_sim, data_event_ti, node_weights, d_max, p): + """Weighted Lp (Minkowski) distance. + + Warning: Computationally heavier than l1_dist or l2_dist due to + the generic C-level pow() evaluation. Use only when p != 1.0 or 2.0. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + data_event_ti : numpy.ndarray, shape (n,) + node_weights : numpy.ndarray, shape (n,) + Normalized spatial and conditioning weights. + d_max : float + Data range for normalization. + p : float + The Minkowski exponent (e.g., 1.5, 3.0, 5.0). + + Returns + ------- + float + Distance in [0, 1]. + """ + diffs = np.abs(data_event_sim - data_event_ti) / d_max + return float(np.sum(node_weights * (diffs**p)) ** (1.0 / p)) + + +def variation_dist(data_event_sim, data_event_ti, node_weights, d_max, p=2.0): + """Weighted variation distance (Mariethoz2010 Eq. 9, de-meaned). + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + data_event_ti : numpy.ndarray, shape (n,) + node_weights : numpy.ndarray, shape (n,) + Normalized spatial and conditioning weights. + d_max : float + Data range for normalization. + p : float, optional + Lp aggregation exponent. Default ``2.0`` (RMS, Mariethoz2010 Eq. 9). + + Returns + ------- + float + Distance in [0, 1]. + """ + diffs = (data_event_sim - data_event_sim.mean()) - ( + data_event_ti - data_event_ti.mean() + ) + # 2*d_max normalises the common case to [0, 1]; SG values are not bounded + # by the TI range (conditioning data / accumulated mean-shifts), so clamp. + return float( + min( + 1.0, + np.dot(node_weights, np.abs(diffs / (2 * d_max)) ** p) + ** (1.0 / p), + ) + ) + + +# --------------------------------------------------------------------------- +# Vectorized variants — same maths, operate on all TI candidates at once. +# Each accepts all_de_ti of shape (max_scan, n) and returns (max_scan,). +# np.dot(X, w) with X (max_scan, n) and w (n,) is a standard BLAS matvec. +# --------------------------------------------------------------------------- + + +def vec_categorical_dist(data_event_sim, all_de_ti, node_weights): + """Vectorized categorical distance over all TI scan candidates. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + all_de_ti : numpy.ndarray, shape (max_scan, n) + node_weights : numpy.ndarray, shape (n,) + + Returns + ------- + numpy.ndarray, shape (max_scan,) + Distance in [0, 1] for each candidate. + """ + return np.dot( + (data_event_sim != all_de_ti).astype(np.float64), node_weights + ) + + +def vec_l1_dist(data_event_sim, all_de_ti, node_weights, d_max): + """Vectorized L1 distance over all TI scan candidates. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + all_de_ti : numpy.ndarray, shape (max_scan, n) + node_weights : numpy.ndarray, shape (n,) + d_max : float + + Returns + ------- + numpy.ndarray, shape (max_scan,) + """ + return np.dot(np.abs(data_event_sim - all_de_ti) / d_max, node_weights) + + +def vec_l2_dist(data_event_sim, all_de_ti, node_weights, d_max): + """Vectorized L2 distance over all TI scan candidates. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + all_de_ti : numpy.ndarray, shape (max_scan, n) + node_weights : numpy.ndarray, shape (n,) + d_max : float + + Returns + ------- + numpy.ndarray, shape (max_scan,) + """ + return np.sqrt( + np.dot(((data_event_sim - all_de_ti) / d_max) ** 2, node_weights) + ) + + +def vec_lp_dist(data_event_sim, all_de_ti, node_weights, d_max, p): + """Vectorized Lp distance over all TI scan candidates. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + all_de_ti : numpy.ndarray, shape (max_scan, n) + node_weights : numpy.ndarray, shape (n,) + d_max : float + p : float + + Returns + ------- + numpy.ndarray, shape (max_scan,) + """ + diffs = np.abs(data_event_sim - all_de_ti) / d_max + return np.dot(diffs**p, node_weights) ** (1.0 / p) + + +def vec_variation_dist(data_event_sim, all_de_ti, node_weights, d_max, p=2.0): + """Vectorized variation distance over all TI scan candidates. + + Parameters + ---------- + data_event_sim : numpy.ndarray, shape (n,) + all_de_ti : numpy.ndarray, shape (max_scan, n) + node_weights : numpy.ndarray, shape (n,) + d_max : float + p : float, optional + Lp aggregation exponent. Default ``2.0``. + + Returns + ------- + numpy.ndarray, shape (max_scan,) + Distance in [0, 1]. + """ + de_sim_c = data_event_sim - data_event_sim.mean() + all_de_ti_c = all_de_ti - all_de_ti.mean(axis=1, keepdims=True) + diffs = de_sim_c - all_de_ti_c + # 2*d_max normalises the common case to [0, 1]; SG values are not bounded + # by the TI range (conditioning data / accumulated mean-shifts), so clamp. + return np.minimum( + 1.0, + np.dot(np.abs(diffs / (2 * d_max)) ** p, node_weights) ** (1.0 / p), + ) diff --git a/src/gstools/mps/training_image.py b/src/gstools/mps/training_image.py new file mode 100644 index 00000000..6fba49dd --- /dev/null +++ b/src/gstools/mps/training_image.py @@ -0,0 +1,292 @@ +""" +GStools subpackage providing the TrainingImage class for MPS simulations. + +.. currentmodule:: gstools.mps + +The following classes and functions are provided + +.. autosummary:: + TrainingImage +""" + +import numpy as np + +from gstools.mps.distance import ( + categorical_dist, + compute_node_weights, + l1_dist, + l2_dist, + lp_dist, + variation_dist, + vec_categorical_dist, + vec_l1_dist, + vec_l2_dist, + vec_lp_dist, + vec_variation_dist, +) + +__all__ = ["TrainingImage"] + + +class TrainingImage: + """Training image for multiple point statistics simulation. + + The MPS analogue of :class:`gstools.CovModel`: encapsulates training + data and the distance function for comparing data events. + + Parameters + ---------- + data : numpy.ndarray + Training image data (n-d array). + categorical : bool, optional + Whether the variable is categorical. Default: ``True``. + distance : str, optional + Distance metric for continuous variables: ``"l1"`` (Juda2022 + Eq. 7, default), ``"l2"`` (Mariethoz2010 Eq. 4–5), or + ``"variation"`` (Mariethoz2010 Eq. 9). Ignored when categorical. + distance_power : float, optional + Exponent δ for spatial-decay weighting of neighbours + (Mariethoz2010 Eq. 3). Applied to **all** distance types. + ``0.0`` → uniform weights (oracle-compatible default). + ``1.0`` → closer neighbours weighted more heavily. + """ + + def __init__( + self, data, categorical=True, distance="l1", distance_power=0.0 + ): + self._data = np.array(data, copy=True) + self._categorical = bool(categorical) + self._distance_power = float(distance_power) + if self._distance_power < 0: + raise ValueError("distance_power must be >= 0") + self._distance_type = distance + self._p_norm = None + self._variation_p_norm = None + if not self._categorical: + distance_lower = str(distance).lower() + if distance_lower.startswith("l"): + try: + p_val = float(distance_lower[1:]) + except ValueError: + raise ValueError( + f"TrainingImage: distance starting with 'l' must be followed by " + f"a positive number (e.g. 'l1', 'l2', 'l3.5'). Got {distance!r}" + ) + if p_val <= 0: + raise ValueError( + f"TrainingImage: Lp norm exponent must be > 0, got {p_val}." + ) + self._p_norm = p_val + elif distance_lower == "variation": + self._variation_p_norm = 2.0 + elif distance_lower.startswith("variation"): + try: + p_val = float(distance_lower[len("variation") :]) + except ValueError: + raise ValueError( + f"TrainingImage: distance starting with 'variation' must be " + f"followed by a positive number (e.g. 'variation1', 'variation1.5'). " + f"Got {distance!r}" + ) + if p_val <= 0: + raise ValueError( + f"TrainingImage: variation exponent must be > 0, got {p_val}." + ) + self._variation_p_norm = p_val + else: + raise ValueError( + f"TrainingImage: distance must be 'l

' (e.g. 'l1', 'l2'), " + f"'variation', or 'variation

' (e.g. 'variation1'). " + f"Got {distance!r}" + ) + + if not self._categorical: + dmax = float(self._data.max() - self._data.min()) + self._d_max = dmax if dmax > 0 else 1.0 + else: + self._d_max = None + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def data(self): + """numpy.ndarray: Raw training image data.""" + return self._data + + @property + def ndim(self): + """int: Number of spatial dimensions.""" + return self._data.ndim + + @property + def shape(self): + """tuple: Shape of the training image.""" + return self._data.shape + + @property + def categorical(self): + """bool: Whether the variable is categorical.""" + return self._categorical + + @property + def distance_type(self): + """str: Distance metric (e.g. ``"l1"``, ``"l2"``, ``"variation"``, ``"variation1"``).""" + return self._distance_type + + @property + def distance_power(self): + """float: Spatial-decay exponent δ for node weighting.""" + return self._distance_power + + # ------------------------------------------------------------------ + # Distance + # ------------------------------------------------------------------ + + def distance( + self, + data_event_sim, + data_event_ti, + cond_mask=None, + cond_weight=1.0, + lag_norms=None, + ): + """Distance between two data events. + + Applies spatial-decay weights (Mariethoz2010 Eq. 3) to all + distance types when ``distance_power > 0``. + + Parameters + ---------- + data_event_sim : array-like, shape (n,) + Values at SG neighbourhood nodes. + data_event_ti : array-like, shape (n,) + Values at TI neighbourhood nodes. + cond_mask : array-like of bool, optional + True where the neighbour is a conditioning datum. + cond_weight : float, optional + Weight multiplier δ for conditioning nodes + (Mariethoz2010 §3 ¶26). Default: ``1.0``. + lag_norms : array-like, shape (n,), optional + Euclidean norms ``‖h_i‖`` of each lag vector. Required for + spatial-decay weighting (``distance_power > 0``). + + Returns + ------- + float + Distance in [0, 1]. + """ + data_event_sim = np.asarray(data_event_sim, dtype=np.float64) + data_event_ti = np.asarray(data_event_ti, dtype=np.float64) + n = len(data_event_sim) + if n == 0: + return 0.0 + + w = compute_node_weights( + n, lag_norms, self._distance_power, cond_mask, cond_weight + ) + + if self._categorical: + return categorical_dist(data_event_sim, data_event_ti, w) + if self._p_norm == 1.0: + return l1_dist(data_event_sim, data_event_ti, w, self._d_max) + if self._p_norm == 2.0: + return l2_dist(data_event_sim, data_event_ti, w, self._d_max) + if self._p_norm is not None: + return lp_dist( + data_event_sim, data_event_ti, w, self._d_max, self._p_norm + ) + return variation_dist( + data_event_sim, + data_event_ti, + w, + self._d_max, + self._variation_p_norm, + ) + + def vec_distance( + self, + data_event_sim, + all_de_ti, + cond_mask=None, + cond_weight=1.0, + lag_norms=None, + ): + """Vectorized distance between SG data event and all TI candidates. + + Same maths as :meth:`distance` but operates on all TI scan candidates + at once, returning a distance per candidate instead of a scalar. + + Parameters + ---------- + data_event_sim : array-like, shape (n,) + Values at SG neighbourhood nodes. + all_de_ti : array-like, shape (max_scan, n) + TI data events for every scan candidate. + cond_mask : array-like of bool, optional + True where the neighbour is a conditioning datum. + cond_weight : float, optional + Weight multiplier δ for conditioning nodes. Default: ``1.0``. + lag_norms : array-like, shape (n,), optional + Euclidean norms of each lag vector. + + Returns + ------- + numpy.ndarray, shape (max_scan,) + Distance in [0, 1] for each candidate. + """ + data_event_sim = np.asarray(data_event_sim, dtype=np.float64) + all_de_ti = np.asarray(all_de_ti, dtype=np.float64) + n = len(data_event_sim) + if n == 0: + return np.zeros(len(all_de_ti)) + w = compute_node_weights( + n, lag_norms, self._distance_power, cond_mask, cond_weight + ) + if self._categorical: + return vec_categorical_dist(data_event_sim, all_de_ti, w) + if self._p_norm == 1.0: + return vec_l1_dist(data_event_sim, all_de_ti, w, self._d_max) + if self._p_norm == 2.0: + return vec_l2_dist(data_event_sim, all_de_ti, w, self._d_max) + if self._p_norm is not None: + return vec_lp_dist( + data_event_sim, all_de_ti, w, self._d_max, self._p_norm + ) + return vec_variation_dist( + data_event_sim, all_de_ti, w, self._d_max, self._variation_p_norm + ) + + def adjust_value(self, ti_val, data_event_sim, data_event_ti): + """Adjust matched TI value before assignment to SG. + + For ``distance="variation"``, applies the mean-shift correction + (Mariethoz2010 Eq. 9): Z(x_i) = Z(y) − Z̄(y) + Z̄(x_i). + For all other metrics returns *ti_val* unchanged. + + Parameters + ---------- + ti_val : float + Raw value at the matched TI node. + data_event_sim : array-like + SG data event (used to compute Z̄(x_i)). + data_event_ti : array-like + TI data event (used to compute Z̄(y)). + + Returns + ------- + float + """ + if self._variation_p_norm is None or self._categorical: + return ti_val + data_event_sim = np.asarray(data_event_sim, dtype=np.float64) + data_event_ti = np.asarray(data_event_ti, dtype=np.float64) + return float(ti_val - data_event_ti.mean() + data_event_sim.mean()) + + def __repr__(self): + return ( + f"TrainingImage(shape={self.shape}, " + f"categorical={self._categorical}, " + f"distance={self._distance_type!r})" + ) diff --git a/tests/test_mps.py b/tests/test_mps.py new file mode 100644 index 00000000..609e4000 --- /dev/null +++ b/tests/test_mps.py @@ -0,0 +1,777 @@ +#!/usr/bin/env python +"""Unittest for the MPS module (TrainingImage and DirectSampling).""" + +import unittest + +import numpy as np + +import gstools as gs +from gstools import config as gs_config +from gstools.mps.direct_sampling import ( + DirectSampling, + _precompute_offsets, + ds_simulate, +) +from gstools.mps.distance import ( + categorical_dist, + compute_node_weights, + l1_dist, + l2_dist, + lp_dist, + variation_dist, +) +from gstools.mps.training_image import TrainingImage + + +class TestDirectSamplingParallel(unittest.TestCase): + def test_valid_values(self): + rng = np.random.default_rng(0) + data = rng.integers(0, 3, (20, 20)) + ti = TrainingImage(data) + ds = DirectSampling( + ti, n_neighbors=8, scan_fraction=0.2, num_threads=2 + ) + field = ds([np.arange(8, dtype=float)] * 2, seed=0) + self.assertEqual(field.shape, (8, 8)) + self.assertTrue(np.all(np.isin(field, [0, 1, 2]))) + + def test_reproducible(self): + # DAG parallelism is deterministic: same seed → same parallel result + rng = np.random.default_rng(0) + data = rng.integers(0, 3, (20, 20)) + ti = TrainingImage(data) + ds = DirectSampling( + ti, n_neighbors=8, scan_fraction=0.2, num_threads=2 + ) + pos = [np.arange(8, dtype=float)] * 2 + self.assertTrue(np.array_equal(ds(pos, seed=7), ds(pos, seed=7))) + + def test_conditioning_preserved(self): + rng = np.random.default_rng(0) + data = rng.integers(0, 3, (20, 20)) + ti = TrainingImage(data) + ds = DirectSampling( + ti, n_neighbors=4, scan_fraction=0.2, num_threads=2 + ) + ds.set_condition([[5.0], [5.0]], [2]) + field = ds([np.arange(10, dtype=float)] * 2, seed=0) + self.assertEqual(field[5, 5], 2) + + def test_global_config(self): + # num_threads=None reads gs_config.NUM_THREADS + rng = np.random.default_rng(0) + data = rng.integers(0, 3, (20, 20)) + ti = TrainingImage(data) + pos = [np.arange(8, dtype=float)] * 2 + old = gs_config.NUM_THREADS + try: + gs_config.NUM_THREADS = 2 + field = DirectSampling(ti, n_neighbors=8, scan_fraction=0.2)( + pos, seed=7 + ) + finally: + gs_config.NUM_THREADS = old + self.assertEqual(field.shape, (8, 8)) + self.assertTrue(np.all(np.isin(field, [0, 1, 2]))) + + def test_large_batches(self): + # n_neighbors=2 → sparse DAG → large ready batches + rng = np.random.default_rng(1) + data = rng.integers(0, 2, (30, 30)) + ti = TrainingImage(data) + pos = [np.arange(12, dtype=float)] * 2 + ds = DirectSampling( + ti, n_neighbors=2, scan_fraction=0.3, num_threads=4 + ) + field = ds(pos, seed=42) + self.assertEqual(field.shape, (12, 12)) + self.assertTrue(np.all(np.isin(field, [0.0, 1.0]))) + + def test_stress(self): + # large grid, sparse DAG, conditioning, varying thread counts + rng = np.random.default_rng(3) + data = rng.integers(0, 4, (40, 40)) + ti = TrainingImage(data) + pos = [np.arange(25, dtype=float)] * 2 + cond_pos = [ + rng.integers(0, 25, 20).astype(float), + rng.integers(0, 25, 20).astype(float), + ] + cond_val = rng.integers(0, 4, 20).astype(float) + for nt in (2, 4, 8): + ds = DirectSampling( + ti, n_neighbors=3, scan_fraction=0.4, num_threads=nt + ) + ds.set_condition(cond_pos, cond_val) + field = ds(pos, seed=11) + self.assertEqual(field.shape, (25, 25)) + self.assertTrue(np.all(np.isin(field, [0, 1, 2, 3]))) + + def test_serial_equals_parallel(self): + rng = np.random.default_rng(0) + cases = [ + TrainingImage( + rng.integers(0, 3, (25, 25)).astype(float), categorical=True + ), + TrainingImage( + rng.random((25, 25)), categorical=False, distance="l2" + ), + TrainingImage( + rng.random((25, 25)), categorical=False, distance="variation" + ), + ] + pos = [np.arange(16, dtype=float)] * 2 + for ti in cases: + serial = DirectSampling( + ti, n_neighbors=8, scan_fraction=0.5, num_threads=1 + )(pos, seed=7) + for nt in (2, 4, 8): + parallel = DirectSampling( + ti, n_neighbors=8, scan_fraction=0.5, num_threads=nt + )(pos, seed=7) + self.assertTrue( + np.array_equal(serial, parallel), + msg=f"serial != parallel (num_threads={nt})", + ) + + +class TestTrainingImage(unittest.TestCase): + def setUp(self): + arr_cat = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], dtype=float) + self.ti_cat = TrainingImage(arr_cat, categorical=True) + + arr_cont = np.linspace(0.0, 1.0, 20) + self.ti_cont = TrainingImage( + arr_cont, categorical=False, distance="l1" + ) + + def test_properties(self): + np.testing.assert_array_equal( + self.ti_cat.data, + np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], dtype=float), + ) + self.assertEqual(self.ti_cat.ndim, 2) + self.assertEqual(self.ti_cat.shape, (3, 3)) + self.assertTrue(self.ti_cat.categorical) + self.assertEqual( + self.ti_cat.distance_type, "l1" + ) # default ignored for cat + self.assertIsInstance(repr(self.ti_cat), str) + self.assertIn("TrainingImage", repr(self.ti_cat)) + + self.assertEqual(self.ti_cont.ndim, 1) + self.assertEqual(self.ti_cont.shape, (20,)) + self.assertFalse(self.ti_cont.categorical) + self.assertEqual(self.ti_cont.distance_type, "l1") + + def test_raise(self): + with self.assertRaises(ValueError): + TrainingImage(np.ones(10), categorical=False, distance="l0") + with self.assertRaises(ValueError): + TrainingImage(np.ones(10), categorical=False, distance="labc") + with self.assertRaises(ValueError): + TrainingImage(np.ones(10), categorical=False, distance="invalid") + + def test_distance_categorical(self): + # Identical events → 0.0 + a = np.array([0.0, 1.0, 0.0]) + dist = self.ti_cat.distance(a, a) + self.assertAlmostEqual(dist, 0.0) + + # Completely mismatched, uniform weights → 1.0 + b = np.array([1.0, 0.0, 1.0]) + dist = self.ti_cat.distance(a, b) + self.assertAlmostEqual(dist, 1.0) + + # One of three mismatched → 1/3 + c = np.array([1.0, 1.0, 0.0]) + dist = self.ti_cat.distance(a, c) + self.assertAlmostEqual(dist, 1.0 / 3.0) + + # Two of four mismatched → 0.5 (spec-required half-mismatch case) + a4 = np.array([0.0, 1.0, 0.0, 1.0]) + c4 = np.array([1.0, 0.0, 0.0, 1.0]) + dist = self.ti_cat.distance(a4, c4) + self.assertAlmostEqual(dist, 0.5) + + def test_distance_continuous(self): + x = np.array([0.0, 0.5, 1.0]) + y = np.array([0.2, 0.3, 0.8]) + + # l1 + ti_l1 = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="l1" + ) + self.assertAlmostEqual(ti_l1.distance(x, x), 0.0) + self.assertAlmostEqual(ti_l1.distance(x, y), 0.2, places=6) + + # l2 + ti_l2 = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="l2" + ) + self.assertAlmostEqual(ti_l2.distance(x, x), 0.0) + self.assertAlmostEqual(ti_l2.distance(x, y), 0.2, places=6) + + # lp (p=3.5) — non-uniform diffs [0.3, 0.1, 0.3] distinguish lp from l1/l2 + y_lp = np.array([0.3, 0.4, 0.7]) + ti_lp = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="l3.5" + ) + self.assertAlmostEqual(ti_lp.distance(x, x), 0.0) + self.assertAlmostEqual(ti_lp.distance(x, y_lp), 0.2680, places=3) + self.assertGreater(ti_lp.distance(x, y_lp), ti_l1.distance(x, y_lp)) + + # variation (default p=2) + ti_var = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="variation" + ) + self.assertAlmostEqual(ti_var.distance(x, x), 0.0) + self.assertAlmostEqual(ti_var.distance(x, y), 0.094281, places=5) + # constant shift → distance = 0 (key behavioral property of variation distance) + self.assertAlmostEqual(ti_var.distance(x, x + 0.15), 0.0, places=10) + + # variation1 (L^1 aggregation) + ti_var1 = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="variation1" + ) + self.assertAlmostEqual(ti_var1.distance(x, x), 0.0) + self.assertAlmostEqual(ti_var1.distance(x, y), 0.08889, places=4) + self.assertAlmostEqual(ti_var1.distance(x, x + 0.15), 0.0, places=10) + # L^1 < L^2 for non-uniform diffs + self.assertLess(ti_var1.distance(x, y), ti_var.distance(x, y)) + + # variation2 explicit matches variation (regression guard) + ti_var2 = TrainingImage( + np.linspace(0.0, 1.0, 10), categorical=False, distance="variation2" + ) + self.assertAlmostEqual( + ti_var2.distance(x, y), ti_var.distance(x, y), places=10 + ) + + def test_adjust_value(self): + # Categorical and lp: passthrough + self.assertAlmostEqual( + self.ti_cat.adjust_value( + 0.7, np.array([0.1, 0.3]), np.array([0.4, 0.6]) + ), + 0.7, + ) + self.assertAlmostEqual( + self.ti_cont.adjust_value( + 0.7, np.array([0.1, 0.3]), np.array([0.4, 0.6]) + ), + 0.7, + ) + + # variation: Z(y) - Z_bar(y) + Z_bar(x) = 0.7 - 0.6 + 0.3 = 0.4 + ti_var = TrainingImage( + np.linspace(0.0, 1.0, 20), categorical=False, distance="variation" + ) + result = ti_var.adjust_value( + 0.7, np.array([0.1, 0.3, 0.5]), np.array([0.4, 0.6, 0.8]) + ) + self.assertAlmostEqual(result, 0.4, places=6) + self.assertNotAlmostEqual(result, 0.7) # must not be passthrough + + def test_distance_weights(self): + a = np.array([0.0, 1.0, 0.0]) + b = np.array([1.0, 1.0, 0.0]) # first element differs + + # cond_weight=2 on first node → it gets weight 0.5 (double) + d1 = self.ti_cat.distance( + a, b, cond_mask=[True, False, False], cond_weight=1.0 + ) + d2 = self.ti_cat.distance( + a, b, cond_mask=[True, False, False], cond_weight=2.0 + ) + self.assertGreater(d2, d1) + + # distance_power shifts weight toward closer neighbours — use non-uniform + # differences so the weighted sums actually differ: diffs = [0, 0, 0.5] + ti_p = TrainingImage( + np.linspace(0.0, 1.0, 10), + categorical=False, + distance="l1", + distance_power=1.0, + ) + ti_flat = TrainingImage( + np.linspace(0.0, 1.0, 10), + categorical=False, + distance="l1", + distance_power=0.0, + ) + x = np.array([0.0, 0.5, 1.0]) + z = np.array([0.0, 0.5, 0.5]) # only third element differs + lags = np.array([1.0, 2.0, 3.0]) + d_power = ti_p.distance(x, z, lag_norms=lags) + d_flat = ti_flat.distance(x, z, lag_norms=lags) + # power=1 weights far neighbours less → smaller distance for far mismatch + self.assertLess(d_power, d_flat) + + def test_distance_empty_event(self): + dist = self.ti_cat.distance(np.array([]), np.array([])) + self.assertAlmostEqual(dist, 0.0) + + def test_distance_functions_directly(self): + a = np.array([0.0, 1.0, 0.0]) + b = np.array([1.0, 0.0, 1.0]) + w = np.array([1 / 3, 1 / 3, 1 / 3]) + + # weights sum to 1 + w2 = compute_node_weights(3, None, 0.0) + self.assertAlmostEqual(w2.sum(), 1.0) + + # cond_weight=2 on first node → uniform spatial weights → w[0] = 2/(2+1+1) = 0.5 + w3 = compute_node_weights( + 3, + None, + 0.0, + cond_mask=[True, False, False], + cond_weight=2.0, + ) + self.assertAlmostEqual(w3.sum(), 1.0) + self.assertAlmostEqual(w3[0], 0.5, places=6) + + # categorical: identical → 0, opposite → 1 + self.assertAlmostEqual(categorical_dist(a, a, w), 0.0) + self.assertAlmostEqual(categorical_dist(a, b, w), 1.0) + + # continuous: identical → 0 + x = np.array([0.0, 0.5, 1.0]) + d_max = 1.0 + self.assertAlmostEqual(l1_dist(x, x, w, d_max), 0.0) + self.assertAlmostEqual(l2_dist(x, x, w, d_max), 0.0) + self.assertAlmostEqual(lp_dist(x, x, w, d_max, 3.5), 0.0) + self.assertAlmostEqual(variation_dist(x, x, w, d_max), 0.0) + self.assertAlmostEqual(variation_dist(x, x, w, d_max, p=1.0), 0.0) + + # distances in [0, 1] + y = np.array([0.2, 0.3, 0.8]) + self.assertAlmostEqual(l1_dist(x, y, w, d_max), 0.2, places=6) + self.assertAlmostEqual(l2_dist(x, y, w, d_max), 0.2, places=6) + self.assertAlmostEqual( + variation_dist(x, y, w, d_max), 0.094281, places=5 + ) + self.assertAlmostEqual( + variation_dist(x, y, w, d_max, p=1.0), 0.08889, places=4 + ) + # p=2 explicit matches default + self.assertAlmostEqual( + variation_dist(x, y, w, d_max, p=2.0), + variation_dist(x, y, w, d_max), + places=10, + ) + + # lp: non-uniform diffs [0.3, 0.1, 0.3] verify the p-norm exponent is used + y_lp = np.array([0.3, 0.4, 0.7]) + self.assertAlmostEqual( + lp_dist(x, y_lp, w, d_max, 3.5), 0.2680, places=3 + ) + self.assertGreater( + lp_dist(x, y_lp, w, d_max, 3.5), l1_dist(x, y_lp, w, d_max) + ) + + def test_variation_dist_bounded(self): + """variation_dist with distance_power > 0 must stay in [0, 1].""" + # Adversarial: weight concentrated on maximally anti-correlated element + x = np.array([0.0, 1.0, 0.0]) + y = np.array([1.0, 0.0, 1.0]) + lags = np.array([10.0, 0.1, 10.0]) + w = compute_node_weights(3, lags, 1.0) + d = variation_dist(x, y, w, 1.0) + self.assertGreaterEqual(d, 0.0) + self.assertLessEqual(d, 1.0) + self.assertAlmostEqual(d, 0.661747, places=5) + # Also via TrainingImage.distance() + ti = TrainingImage( + np.linspace(0.0, 1.0, 10), + categorical=False, + distance="variation", + distance_power=1.0, + ) + self.assertLessEqual(ti.distance(x, y, lag_norms=lags), 1.0) + + def test_variation_dist_out_of_range_clamped(self): + """Out-of-range SG values (from conditioning / mean-shift) must clamp to [0, 1].""" + ti = TrainingImage( + np.linspace(0.0, 1.0, 10), # d_max == 1.0 + categorical=False, + distance="variation", + ) + de_sim = np.array([5.0, 0.0]) # 5.0 is far outside the TI range + de_ti = np.array([0.0, 1.0]) + self.assertLessEqual(ti.distance(de_sim, de_ti), 1.0) + vec = ti.vec_distance(de_sim, de_ti[np.newaxis, :]) + self.assertEqual(vec.shape, (1,)) + self.assertLessEqual(vec[0], 1.0) + + def test_variation_lp_parsing(self): + """variation

string is parsed correctly and rejects bad inputs.""" + data = np.linspace(0.0, 1.0, 10) + for spec in ("variation", "variation1", "variation1.5", "variation2"): + ti = TrainingImage(data, categorical=False, distance=spec) + self.assertEqual(ti.distance_type, spec) + # invalid suffix + with self.assertRaises(ValueError): + TrainingImage(data, categorical=False, distance="variationX") + # non-positive exponent + with self.assertRaises(ValueError): + TrainingImage(data, categorical=False, distance="variation0") + with self.assertRaises(ValueError): + TrainingImage(data, categorical=False, distance="variation-1") + + def test_variation_lp_adjust_value(self): + """adjust_value mean-shift applies for all variation

variants.""" + de_sim = np.array([0.1, 0.3, 0.5]) # mean = 0.3 + de_ti = np.array([0.4, 0.6, 0.8]) # mean = 0.6 + # expected: 0.7 - 0.6 + 0.3 = 0.4 + for spec in ("variation", "variation1", "variation1.5"): + ti = TrainingImage( + np.linspace(0.0, 1.0, 20), categorical=False, distance=spec + ) + self.assertAlmostEqual( + ti.adjust_value(0.7, de_sim, de_ti), 0.4, places=6 + ) + + def test_node_weights_zero_cond_weight(self): + """All-conditioning event with cond_weight=0 must not yield NaN weights.""" + w = compute_node_weights( + 3, + lag_norms=None, + distance_power=0.0, + cond_mask=np.array([True, True, True]), + cond_weight=0.0, + ) + self.assertTrue(np.all(np.isfinite(w))) + self.assertAlmostEqual(w.sum(), 1.0) + np.testing.assert_allclose(w, np.full(3, 1.0 / 3.0)) + + +class TestDirectSampling(unittest.TestCase): + def setUp(self): + # 1-D categorical TI: alternating 0/1, length 20 + arr1d = np.tile([0, 1], 10).astype(float) + self.ti1d = TrainingImage(arr1d, categorical=True) + + # 2-D categorical TI: 8×8 checkerboard + self.ti2d = TrainingImage( + (np.indices((8, 8)).sum(axis=0) % 2).astype(float), + categorical=True, + ) + + rng = np.random.default_rng(0) + self.ti2d_rand = TrainingImage( + rng.integers(0, 2, size=(20, 20)).astype(float), + categorical=True, + ) + + # 1-D continuous TI + self.ti1d_cont = TrainingImage( + np.linspace(0.0, 1.0, 20), categorical=False, distance="l1" + ) + + self.x1d = np.arange(10, dtype=float) + self.x2d = np.arange(6, dtype=float) + self.y2d = np.arange(6, dtype=float) + + def test_raise(self): + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, boundary="bad") + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, max_radius=0) + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, max_radius=-1.0) + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, n_neighbors=0) + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, scan_fraction=0) + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, scan_fraction=1.5) + with self.assertRaises(ValueError): + DirectSampling(self.ti1d, threshold=-0.1) + ds = DirectSampling(self.ti1d) + with self.assertRaises(ValueError): + ds([self.x1d], seed=42, mesh_type="unstructured") + + def test_repr(self): + ds = DirectSampling(self.ti1d) + r = repr(ds) + self.assertIsInstance(r, str) + self.assertIn("DirectSampling", r) + + def test_properties_and_setters(self): + ds = DirectSampling( + self.ti1d, + n_neighbors=16, + scan_fraction=0.5, + threshold=0.05, + cond_weight=2.0, + boundary="partial", + max_radius=3.0, + ) + self.assertIs(ds.ti, self.ti1d) + self.assertEqual(ds.n_neighbors, 16) + self.assertAlmostEqual(ds.scan_fraction, 0.5) + self.assertAlmostEqual(ds.threshold, 0.05) + self.assertAlmostEqual(ds.cond_weight, 2.0) + self.assertEqual(ds.boundary, "partial") + self.assertAlmostEqual(ds.max_radius, 3.0) + + ds.n_neighbors = 8 + self.assertEqual(ds.n_neighbors, 8) + ds.scan_fraction = 1.0 + self.assertAlmostEqual(ds.scan_fraction, 1.0) + ds.threshold = 0.0 + self.assertAlmostEqual(ds.threshold, 0.0) + ds.cond_weight = 1.0 + self.assertAlmostEqual(ds.cond_weight, 1.0) + + def test_offsets_shape(self): + off = _precompute_offsets((5, 5)) + # shape: (N, 2) for 2-D, no zero row + self.assertEqual(off.ndim, 2) + self.assertEqual(off.shape[1], 2) + self.assertFalse(np.any(np.all(off == 0, axis=1))) + # sorted by Euclidean norm + norms = np.linalg.norm(off, axis=1) + self.assertTrue(np.all(norms[:-1] <= norms[1:])) + + def test_offsets_1d(self): + off = _precompute_offsets((10,)) + self.assertEqual(off.shape[1], 1) + self.assertFalse(np.any(off == 0)) + + def test_offsets_max_offset(self): + off = _precompute_offsets((5, 5), max_offset=1) + self.assertLessEqual(np.abs(off).max(), 1) + # 2-D, max_offset=1: 3^2 - 1 = 8 neighbours + self.assertEqual(off.shape, (8, 2)) + + def test_shape_1d(self): + ds = DirectSampling(self.ti1d, n_neighbors=4, scan_fraction=1.0) + field = ds([self.x1d], seed=42) + self.assertEqual(field.shape, (10,)) + self.assertFalse(np.any(np.isnan(field))) + + def test_shape_2d(self): + ds = DirectSampling(self.ti2d, n_neighbors=4, scan_fraction=1.0) + field = ds([self.x2d, self.y2d], seed=42) + self.assertEqual(field.shape, (6, 6)) + self.assertFalse(np.any(np.isnan(field))) + # All output values must be in the TI value set {0, 1} + unique_vals = set(np.unique(field)) + self.assertTrue(unique_vals.issubset({0.0, 1.0})) + + def test_regression_1d(self): + ds = DirectSampling(self.ti1d, n_neighbors=4, scan_fraction=1.0) + field = ds([self.x1d], seed=42) + self.assertAlmostEqual(field[0], 1.0) + self.assertAlmostEqual(field[5], 0.0) + self.assertAlmostEqual(field[9], 0.0) + + def test_regression_2d(self): + ds = DirectSampling(self.ti2d, n_neighbors=4, scan_fraction=1.0) + field = ds([self.x2d, self.y2d], seed=42) + self.assertAlmostEqual(field[0, 0], 1.0) + self.assertAlmostEqual(field[2, 3], 0.0) + self.assertAlmostEqual(field[5, 5], 1.0) + + def test_seeded_reproducibility(self): + ds = DirectSampling(self.ti2d_rand, n_neighbors=8, scan_fraction=0.5) + pos = [self.x2d, self.y2d] + fa = ds(pos, seed=99) + fb = ds(pos, seed=99) + fc = ds(pos, seed=100) + # Same seed → identical output + self.assertTrue(np.allclose(fa, fb)) + # Different seed → different output + self.assertFalse(np.allclose(fa, fc)) + # Pin two values for seed=99; stable across NumPy versions because DS + # uses RandomState (MT19937) throughout, matching the rest of GSTools. + self.assertAlmostEqual(fa[0, 0], 0.0) + self.assertAlmostEqual(fa[3, 4], 1.0) + + def test_conditioning_honored(self): + ds = DirectSampling(self.ti1d, n_neighbors=4, scan_fraction=1.0) + # Three exact grid node positions — spec requires ≥ 3 to exercise multi-point handling + cond_pos = [np.array([2.0, 4.0, 7.0])] + cond_val = np.array([0.0, 1.0, 1.0]) + ds.set_condition(cond_pos, cond_val) + field = ds([self.x1d], seed=5) + self.assertAlmostEqual(field[2], 0.0) + self.assertAlmostEqual(field[4], 1.0) + self.assertAlmostEqual(field[7], 1.0) + + def test_boundary_partial(self): + ds = DirectSampling( + self.ti2d, n_neighbors=4, scan_fraction=1.0, boundary="partial" + ) + field = ds([self.x2d, self.y2d], seed=42) + self.assertEqual(field.shape, (6, 6)) + self.assertFalse(np.any(np.isnan(field))) + + def test_boundary_partial_collapse_recovers(self): + # TI far smaller than lag span → partial mode must recover, not raise + ti_tiny = TrainingImage( + np.random.default_rng(0).random((3, 3)), + categorical=False, + distance="l1", + ) + ds = DirectSampling( + ti_tiny, n_neighbors=32, scan_fraction=1.0, boundary="partial" + ) + field = ds([np.arange(30, dtype=float)] * 2, seed=1) + self.assertEqual(field.shape, (30, 30)) + self.assertFalse(np.any(np.isnan(field))) + + def test_threshold_above_one_warns_in_constructor(self): + with self.assertWarns(UserWarning): + DirectSampling(self.ti1d, threshold=5.0) + + def test_scan_fraction_window_semantics(self): + """scan_fraction=0.1 applies to the window, not the TI — no crash, valid output.""" + rng = np.random.default_rng(0) + ti = TrainingImage( + rng.integers(0, 2, (20, 20)).astype(float), categorical=True + ) + ds = DirectSampling(ti, n_neighbors=4, scan_fraction=0.1) + field = ds([np.arange(6, dtype=float)] * 2, seed=0) + self.assertEqual(field.shape, (6, 6)) + self.assertFalse(np.any(np.isnan(field))) + self.assertTrue(set(np.unique(field)).issubset({0.0, 1.0})) + + def test_max_radius(self): + ds = DirectSampling( + self.ti2d, n_neighbors=4, scan_fraction=1.0, max_radius=2.0 + ) + field = ds([self.x2d, self.y2d], seed=42) + self.assertEqual(field.shape, (6, 6)) + self.assertFalse(np.any(np.isnan(field))) + + def test_continuous_ti(self): + ds = DirectSampling( + self.ti1d_cont, n_neighbors=4, scan_fraction=1.0, threshold=0.05 + ) + field = ds([np.arange(8, dtype=float)], seed=42) + self.assertEqual(field.shape, (8,)) + self.assertFalse(np.any(np.isnan(field))) + self.assertTrue(np.all(field >= 0.0)) + self.assertTrue(np.all(field <= 1.0)) + + def test_ds_simulate_direct(self): + result = ds_simulate( + self.ti1d, + sim_shape=(8,), + n_neighbors=4, + threshold=0.0, + scan_fraction=1.0, + rng=np.random.RandomState(7), + ) + self.assertEqual(result.shape, (8,)) + self.assertFalse(np.any(np.isnan(result))) + # Check values — seeded values for ds_simulate(seed=7) with ti1d + self.assertTrue(set(np.unique(result)).issubset({0.0, 1.0})) + + def test_empty_search_window_recovery(self): + # n_neighbors >> TI size collapses search windows → must recover silently + ti_tiny = TrainingImage(np.array([0.0, 1.0, 0.0]), categorical=True) + ds = DirectSampling(ti_tiny, n_neighbors=10, scan_fraction=1.0) + field = ds([np.arange(5, dtype=float)], seed=1) + self.assertEqual(field.shape, (5,)) + self.assertFalse(np.any(np.isnan(field))) + self.assertTrue(set(np.unique(field)).issubset({0.0, 1.0})) + + def test_post_process_categorical_noop(self): + # Default construction leaves mean/normalizer/trend unset, so + # post_process must not alter categorical output (only float cast). + ds = DirectSampling(self.ti2d, n_neighbors=4, scan_fraction=1.0) + processed = ds([self.x2d, self.y2d], seed=7, post_process=True) + raw = ds([self.x2d, self.y2d], seed=7, post_process=False) + np.testing.assert_array_equal(processed, raw) + self.assertTrue(set(np.unique(processed)).issubset({0.0, 1.0})) + + def test_conditioning_outside_grid_snaps_to_boundary(self): + # A conditioning point beyond the domain snaps to the nearest grid node + # (current behaviour: silent boundary snap, no error). + ds = DirectSampling(self.ti2d, n_neighbors=4, scan_fraction=1.0) + ds.set_condition([[100.0], [100.0]], [1.0]) # far outside the 6x6 grid + field = ds([self.x2d, self.y2d], seed=7) + self.assertAlmostEqual(field[5, 5], 1.0) + + def test_max_radius_below_one_random_fill(self): + # max_radius in (0, 1) excludes every neighbour (nearest cell is at + # distance 1.0), so each node falls back to a random TI sample. Must + # still run and produce valid values. + ds = DirectSampling( + self.ti2d, n_neighbors=4, scan_fraction=1.0, max_radius=0.5 + ) + field = ds([self.x2d, self.y2d], seed=7) + self.assertEqual(field.shape, (6, 6)) + self.assertFalse(np.any(np.isnan(field))) + self.assertTrue(set(np.unique(field)).issubset({0.0, 1.0})) + + def test_simulation_3d(self): + rng = np.random.default_rng(0) + ti = TrainingImage( + rng.integers(0, 2, (10, 10, 10)).astype(float), categorical=True + ) + ds = DirectSampling(ti, n_neighbors=6, scan_fraction=0.5) + field = ds([np.arange(6, dtype=float)] * 3, seed=7) + self.assertEqual(field.shape, (6, 6, 6)) + self.assertFalse(np.any(np.isnan(field))) + self.assertTrue(set(np.unique(field)).issubset({0.0, 1.0})) + + def test_continuous_2d_simulation(self): + rng = np.random.default_rng(0) + ti = TrainingImage( + rng.random((20, 20)), categorical=False, distance="l2" + ) + ds = DirectSampling( + ti, n_neighbors=6, scan_fraction=0.5, threshold=0.05 + ) + field = ds([np.arange(10, dtype=float)] * 2, seed=7) + self.assertEqual(field.shape, (10, 10)) + self.assertFalse(np.any(np.isnan(field))) + # values are copied from the TI, so they stay within the TI range + self.assertGreaterEqual(field.min(), ti.data.min()) + self.assertLessEqual(field.max(), ti.data.max()) + + def test_lp_distance_simulation(self): + # Exercises the vectorized Lp scan path through a full simulation + # (p != 1, 2), which the unit-level distance tests do not cover. + rng = np.random.default_rng(2) + ti = TrainingImage( + rng.random((20, 20)), categorical=False, distance="l3" + ) + ds = DirectSampling( + ti, n_neighbors=6, scan_fraction=0.5, threshold=0.05 + ) + field = ds([np.arange(10, dtype=float)] * 2, seed=7) + self.assertEqual(field.shape, (10, 10)) + self.assertFalse(np.any(np.isnan(field))) + self.assertGreaterEqual(field.min(), ti.data.min()) + self.assertLessEqual(field.max(), ti.data.max()) + + def test_variation_distance_simulation(self): + # The variation metric applies the mean-shift on assignment, so values + # may leave the raw TI range; they must stay finite and non-NaN. + rng = np.random.default_rng(1) + ti = TrainingImage( + rng.random((20, 20)), categorical=False, distance="variation" + ) + ds = DirectSampling( + ti, n_neighbors=6, scan_fraction=0.5, threshold=0.05 + ) + field = ds([np.arange(10, dtype=float)] * 2, seed=7) + self.assertEqual(field.shape, (10, 10)) + self.assertTrue(np.all(np.isfinite(field))) + + def test_gstools_namespace(self): + self.assertIs(gs.DirectSampling, DirectSampling) + self.assertIs(gs.TrainingImage, TrainingImage) + self.assertIs(gs.mps.DirectSampling, DirectSampling) + self.assertIs(gs.mps.TrainingImage, TrainingImage) + + +if __name__ == "__main__": + unittest.main()