├── .gitignore ├── LICENSE ├── PVT tables data manipulation.ipynb ├── README.rst ├── Tracer test global mass fraction.ipynb ├── accumulative_sum_benchmark.ipynb ├── block_matrix_with_kron_product.ipynb ├── continuous_transition.ipynb ├── eigen_broadcast_cheatsheet_(cpp).ipynb ├── eigen_broadcast_cheatsheet_(python).ipynb ├── einsum-notebook.ipynb ├── environment.yml ├── interpolation_to_a_structured_grid_from_a_cloud_of_points.ipynb ├── numpy_performance.ipynb ├── smooth_transition_between_analytic_functions.ipynb ├── test_transfinite_interpolation.ipynb └── test_write_strategy.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | *checkpoint.ipynb 2 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Arthur Soprano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PVT tables data manipulation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Read and manipulate some data in .pvt files" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 2, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import numpy as np\n", 17 | "import matplotlib.pyplot as plt" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 10, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Here we can implement some criteria in order to modify PVT values\n", 27 | "def calculate_new_property_value(P_value, T_value, previous_property_value):\n", 28 | " '''\n", 29 | " Here we can add some criteria in order to change values of\n", 30 | " a variable selected in \"property_index\"\n", 31 | " '''\n", 32 | " # P v = Z R T / M\n", 33 | " # rho = 1 / v = P M / (Z R T)\n", 34 | " R = 8.314 / (16.04 * 1e-3) # J/(kg.K)\n", 35 | " Z = 1.0\n", 36 | " new_property_value = P_value / (Z * R * (T_value + 273.15))\n", 37 | " return f\"{new_property_value:.6e}\"\n", 38 | "\n", 39 | " #if previous_property_value > 0.002:\n", 40 | " # new_property_value = 0.002\n", 41 | " # return f\"{new_property_value:.6e}\"\n", 42 | " #else:\n", 43 | " # return f\"{previous_property_value:.6e}\"\n", 44 | " \n", 45 | "import pathlib\n", 46 | "\n", 47 | "# INPUT:\n", 48 | "TABLE_NAME = \"Tabela_Tampliada_200\"\n", 49 | "\n", 50 | "# Filepath\n", 51 | "filename = pathlib.Path(\"W:\\\\\" + TABLE_NAME + \".tab\")\n", 52 | "\n", 53 | "# Read original table\n", 54 | "original_lines = [i.strip().split() for i in open(filename).readlines()]\n", 55 | "\n", 56 | "converted_lines = original_lines.copy()\n", 57 | "\n", 58 | "names = converted_lines[21][2][1:-1].split(',')\n", 59 | "\n", 60 | "# Variable name\n", 61 | "property_index = names.index(\"ROG\")\n", 62 | "\n", 63 | "pvt_points = [l for l in converted_lines[23:] if 'PVTTABLE' in l]\n", 64 | "converted_pvt_points = pvt_points.copy()\n", 65 | "\n", 66 | "some_property = []\n", 67 | "for i, (_, _, _, point) in enumerate(pvt_points):\n", 68 | " line_values = point[1:-1].split(',')\n", 69 | " \n", 70 | " def get_value(index):\n", 71 | " return float(line_values[index])\n", 72 | " \n", 73 | " P_value = get_value(names.index(\"PT\"))\n", 74 | " T_value = get_value(names.index(\"TM\"))\n", 75 | "\n", 76 | " property_value = get_value(property_index)\n", 77 | " property_value = calculate_new_property_value(P_value, T_value, property_value)\n", 78 | " line_values[property_index] = property_value\n", 79 | " \n", 80 | " some_property.append(float(property_value))\n", 81 | " converted_point = \"(\" + \",\".join(line_values) + \")\"\n", 82 | " converted_pvt_points[i][3] = converted_point\n", 83 | "\n", 84 | "converted_lines[23:] = converted_pvt_points\n", 85 | "\n", 86 | "out_filename = pathlib.Path(\"W:\\\\\" + TABLE_NAME + \"_modified.tab\")\n", 87 | "converte_file = open(out_filename, \"w\")\n", 88 | "with converte_file as f:\n", 89 | " for _list in converted_lines:\n", 90 | " f.write(\" \".join(_list) + '\\n')\n", 91 | " \n", 92 | "pressures = np.array([float(i) for i in original_lines[17][2][1:-1].split(',')])\n", 93 | "temperatures = np.array([float(i) for i in original_lines[18][2][1:-1].split(',')])\n", 94 | "bubble_point_pressures = np.array([float(i) for i in original_lines[19][2][1:-1].split(',')])\n", 95 | "bubble_point_temperatures = np.array([float(i) for i in original_lines[20][2][1:-1].split(',')])\n", 96 | "some_property = np.array(some_property).reshape((temperatures.size, pressures.size))\n", 97 | "T, P = np.meshgrid(temperatures, pressures, indexing='xy')" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 7, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "plt.figure()\n", 107 | "plt.scatter(T, P)\n", 108 | "plt.scatter(bubble_point_temperatures, bubble_point_pressures)\n", 109 | "#plt.xlim(-10, 60)\n", 110 | "#plt.ylim(-1e5, 1000e5)\n", 111 | "plt.show()" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 11, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "%matplotlib qt\n", 121 | "from mpl_toolkits.mplot3d import Axes3D\n", 122 | "import matplotlib.pyplot as plt\n", 123 | "from matplotlib import cm\n", 124 | "from matplotlib.ticker import LinearLocator, FormatStrFormatter\n", 125 | "import numpy as np\n", 126 | "\n", 127 | "\n", 128 | "fig = plt.figure()\n", 129 | "ax = fig.gca(projection='3d')\n", 130 | "\n", 131 | "\n", 132 | "# Plot the surface.\n", 133 | "surf = ax.plot_surface(T, P, some_property, cmap=cm.coolwarm,\n", 134 | " linewidth=0, antialiased=False)\n", 135 | "\n", 136 | "# Customize the z axis.\n", 137 | "#ax.set_xlim(-50, 100)\n", 138 | "#ax.set_xlim(-1.01, 1.01)\n", 139 | "#ax.zaxis.set_major_locator(LinearLocator(10))\n", 140 | "#ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))\n", 141 | "\n", 142 | "# Add a color bar which maps values to colors.\n", 143 | "fig.colorbar(surf, shrink=0.5, aspect=5)\n", 144 | "\n", 145 | "plt.show()" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [] 154 | } 155 | ], 156 | "metadata": { 157 | "kernelspec": { 158 | "display_name": "Python 3", 159 | "language": "python", 160 | "name": "python3" 161 | }, 162 | "language_info": { 163 | "codemirror_mode": { 164 | "name": "ipython", 165 | "version": 3 166 | }, 167 | "file_extension": ".py", 168 | "mimetype": "text/x-python", 169 | "name": "python", 170 | "nbconvert_exporter": "python", 171 | "pygments_lexer": "ipython3", 172 | "version": "3.6.7" 173 | } 174 | }, 175 | "nbformat": 4, 176 | "nbformat_minor": 2 177 | } 178 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://mybinder.org/badge_logo.svg 3 | :target: https://mybinder.org/v2/gh/ESSS/notebooks/master 4 | 5 | notebooks 6 | ========= 7 | 8 | This repo contains notebooks that explaining some cool stuff we learn every now and then. 9 | 10 | Index 11 | ----- 12 | 13 | * `Bounding Velocities `_: no description. 14 | * `Creating block matrix structures using numpy and scipy `_: This notebook intends to provide a consistent way to create a block matrix structure from a simplified non-block matrix structure.. 15 | * `Einstein Summation `_: Einstein Summation. 16 | * `Interpolate point cloud into structured grid `_: Interpolate point cloud into structured grid. 17 | * `Smooth transition between analytic functions `_: Create a smooth transition in a place where a function is discontinuous. 18 | * `Test Write Strategy `_: A simple test that emulates a simulation loop and write raw numpy data and an HDF5 file to compare sizes and times to output a large amount of data. 19 | * `Accumulative Sum Benchmark `_: a comparison between different implementations to deal with accumulative sum using Python native loops, numpy.cumsum and sci20 Array. 20 | * `Eigen Broadcast Cheatsheet `_: examples of how to do some not-so-simple broadcasting using Eigen. Check accompanying `Numpy version `_. 21 | 22 | How to run 23 | ---------- 24 | 25 | 1. Install `miniconda` 26 | 2. Install `nbformat` in the root environment: `conda install -n root nbformat` 27 | 3. Create an environment to run `jupyter notebook`: type `conda env create` in the root directory of this project (or `conda env update` if the `notebooks` environment was already created previously) 28 | 4. Activate the environment: type `activate notebooks` (Windows) or `source activate notebooks` (Unix) 29 | 5. Run and open it: `jupyter notebook` 30 | -------------------------------------------------------------------------------- /accumulative_sum_benchmark.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### In this sample script we present a simple benchmark that compares Numpy, sci20 and Python native loop approaches for calculating accumulative sum for a given array." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Let's start with imports. Make sure you have installed sci20 correctly." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 7, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import functools\n", 24 | "import timeit\n", 25 | "\n", 26 | "import numpy as np\n", 27 | "\n", 28 | "from sci20.core import Array, FromNumPy\n", 29 | "from sci20.core.unittest import AccumulateArrayReturning" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "#### The following blocks will be used to define the functions that computes the accumulative sum by the chosen three approaches." 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "Native implementation using Python for command" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 2, 49 | "metadata": { 50 | "collapsed": true 51 | }, 52 | "outputs": [], 53 | "source": [ 54 | "def native_acc(x):\n", 55 | " \"\"\"\n", 56 | " Calculate accumulative sum using Python native loop approach\n", 57 | " :param x: Numpy array\n", 58 | " :return: Accumulative sum (list)\n", 59 | " \"\"\"\n", 60 | " native_acc_sum = []\n", 61 | "\n", 62 | " sum_aux = 0\n", 63 | " for val in x:\n", 64 | " native_acc_sum.append(val + sum_aux)\n", 65 | " sum_aux += val\n", 66 | "\n", 67 | " return native_acc_sum" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "Just a binding for numpy.cumsum" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 3, 80 | "metadata": { 81 | "collapsed": true 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "def np_acc(x):\n", 86 | " \"\"\"\n", 87 | " Calculate accumulative sum using numpy.cumsum\n", 88 | " :param x: Numpy array\n", 89 | " :return: Accumulative sum (Numpy array)\n", 90 | " \"\"\"\n", 91 | " return np.cumsum(x)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "Now using sci20 implementation" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 4, 104 | "metadata": { 105 | "collapsed": true 106 | }, 107 | "outputs": [], 108 | "source": [ 109 | "def sci20_acc(x):\n", 110 | " \"\"\"\n", 111 | " Calculate accumulative sum using sci20 Array\n", 112 | " :param x: Numpy array\n", 113 | " :return: Accumulative sum (sci20 array)\n", 114 | " \"\"\"\n", 115 | " x_array = Array(FromNumPy(x))\n", 116 | " return AccumulateArrayReturning(x_array)\n" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "Here we use the timeit standard lib to obtain the elapsed time of each method" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "def do_benchmark(n=1000, k=10):\n", 133 | " \"\"\"\n", 134 | " Compute elapsed time for each accumulative sum implementation (Native loop, Numpy and sci20).\n", 135 | " :param n: Number of array elements. Default is 1000.\n", 136 | " :param k: Number of averages used for timing. Default is 10.\n", 137 | " :param dtype: Array data type. Default is np.double.\n", 138 | " :return: A tuple (dt_native, dt_sci20, dt_np) containing the elapsed time for each method.\n", 139 | " \"\"\"\n", 140 | "\n", 141 | " x = np.linspace(1, 100, n)\n", 142 | "\n", 143 | " dt_native = timeit.Timer(functools.partial(native_acc, x,)).timeit(k)\n", 144 | " dt_sci20 = timeit.Timer(functools.partial(sci20_acc, x,)).timeit(k)\n", 145 | " dt_np = timeit.Timer(functools.partial(np_acc, x,)).timeit(k)\n", 146 | "\n", 147 | " return dt_native, dt_sci20, dt_np" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "And finally we have the results. sci20 and Numpy implementations are almost equivalent, while native loop has demonstrated its inefficiency" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 6, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "name": "stdout", 164 | "output_type": "stream", 165 | "text": [ 166 | "Computing benchmark for n=10...\nNative: 0.00044343s / sci20: 0.00137527s / Numpy: 0.00004437s.\nComputing benchmark for n=100...\nNative: 0.00087931s / sci20: 0.00009992s / Numpy: 0.00003139s.\nComputing benchmark for n=1000...\nNative: 0.00690351s / sci20: 0.00017659s / Numpy: 0.00006822s.\nComputing benchmark for n=10000...\nNative: 0.05423526s / sci20: 0.00039181s / Numpy: 0.00030850s.\nComputing benchmark for n=100000...\n" 167 | ] 168 | }, 169 | { 170 | "name": "stdout", 171 | "output_type": "stream", 172 | "text": [ 173 | "Native: 0.56866048s / sci20: 0.00286645s / Numpy: 0.00264851s.\nComputing benchmark for n=1000000...\n" 174 | ] 175 | }, 176 | { 177 | "name": "stdout", 178 | "output_type": "stream", 179 | "text": [ 180 | "Native: 5.58011361s / sci20: 0.04359414s / Numpy: 0.04290680s.\nComputing benchmark for n=10000000...\n" 181 | ] 182 | }, 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | "Native: 56.68339517s / sci20: 0.45964209s / Numpy: 0.46603757s.\n" 188 | ] 189 | } 190 | ], 191 | "source": [ 192 | "\"\"\"\n", 193 | "Computes and prints the elapsed time for each accumulative sum implementation (Native loop, Numpy and sci20).\n", 194 | "\"\"\"\n", 195 | "n_values = [10**x for x in range(1, 8)] # [10, 100, ..., 10^7]\n", 196 | "\n", 197 | "for n in n_values:\n", 198 | " print(\"Computing benchmark for n={}...\".format(n))\n", 199 | " dt_native, dt_sci20, dt_np = do_benchmark(n)\n", 200 | " print(\"Native: {:.8f}s / sci20: {:.8f}s / Numpy: {:.8f}s.\".format(dt_native, dt_sci20, dt_np))" 201 | ] 202 | } 203 | ], 204 | "metadata": { 205 | "kernelspec": { 206 | "display_name": "Python 2", 207 | "language": "python", 208 | "name": "python2" 209 | }, 210 | "language_info": { 211 | "codemirror_mode": { 212 | "name": "ipython", 213 | "version": 2 214 | }, 215 | "file_extension": ".py", 216 | "mimetype": "text/x-python", 217 | "name": "python", 218 | "nbconvert_exporter": "python", 219 | "pygments_lexer": "ipython2", 220 | "version": "2.7.12" 221 | } 222 | }, 223 | "nbformat": 4, 224 | "nbformat_minor": 2 225 | } 226 | -------------------------------------------------------------------------------- /continuous_transition.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Bounding Velocities\n", 8 | "\n", 9 | "Using the approach from \n", 10 | "\n", 11 | " 2015 - Ashrafizadeh et al - A Jacobian-free Newton–Krylov method for thermalhydraulics simulations\n", 12 | "\n", 13 | "Eq. (41a) and (41b) from the article.\n", 14 | "\n", 15 | "The transition equation is\n", 16 | "\n", 17 | "$$\n", 18 | "\\eta = 0.5 \\left[ 1 + \\tanh \\left( \\frac{x - x_0}{\\epsilon} \\right) \\right]\n", 19 | "$$\n", 20 | "\n", 21 | "with the final interpolation obtained as\n", 22 | "\n", 23 | "$$\n", 24 | "y = (1 - \\eta) y_0 + \\eta y_1\n", 25 | "$$" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 28, 31 | "metadata": { 32 | "collapsed": true 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "import numpy as np\n", 37 | "def continuous_transition(x, x_threshold, y0, y1, smoothness):\n", 38 | " η = 0.5 * (1 + np.tanh( (x - x_threshold) / smoothness ))\n", 39 | " return (1 - η) * y0 + η * y1" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 31, 45 | "metadata": {}, 46 | "outputs": [ 47 | { 48 | "data": { 49 | "application/vnd.jupyter.widget-view+json": { 50 | "model_id": "fc94c8712b7f491fb298f6d6950a8fa1" 51 | } 52 | }, 53 | "metadata": {}, 54 | "output_type": "display_data" 55 | } 56 | ], 57 | "source": [ 58 | "import seaborn\n", 59 | "import matplotlib .pyplot as plt\n", 60 | "from ipywidgets import interact, widgets\n", 61 | "\n", 62 | "α = np.linspace(0, 1, num=500)\n", 63 | "\n", 64 | "@interact(\n", 65 | " U_G=(1.0, 10, 0.1),\n", 66 | " U_L=(1.0, 10, 0.1),\n", 67 | " α_min=widgets.FloatSlider(\n", 68 | " min=1e-5, \n", 69 | " max=0.5, \n", 70 | " value=0.25, \n", 71 | " step=1e-5, \n", 72 | " continuous_update=True,\n", 73 | " readout_format='.2e',\n", 74 | " ),\n", 75 | " smoothness=widgets.FloatSlider(\n", 76 | " min=1e-5, \n", 77 | " max=1e-1, \n", 78 | " value=1e-2, \n", 79 | " step=1e-5, \n", 80 | " continuous_update=True,\n", 81 | " readout_format='.2e',\n", 82 | " )\n", 83 | ")\n", 84 | "def plot_results(U_G=5, U_L=1, α_min=0.5, smoothness=1e-2):\n", 85 | " Umax = max(U_G, U_L)\n", 86 | " Umin = min(U_G, U_L)\n", 87 | " \n", 88 | " U = continuous_transition(x=α, x_threshold=α_min, y0=U_L, y1=U_G, smoothness=smoothness)\n", 89 | " plt.figure(figsize=(10,10))\n", 90 | " plt.plot(α, U)\n", 91 | " plt.xlim((-0.1, 1.1))\n", 92 | "\n", 93 | " plt.ylim((0.9*Umin, 1.1*Umax))\n", 94 | " plt.show()" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 30, 100 | "metadata": {}, 101 | "outputs": [ 102 | { 103 | "data": { 104 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkkAAAJMCAYAAADuTTKHAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3X+M5WddL/DPmTkz+3u72zKtCP2BQOOVewUhMYTeBtJ0\nuUowFuhCbbuNsYFArkmv/EGo1VbNBYNRYgxgFG/IrcaSSyCAjbEE9mruVeOtXFtALSouFlZuO122\nO7s7O3PO93u+94+Zc0bs6cxg9zzP8z3zeoWEdmdy5unT5rPvfZ7P9/PtNE3TBAAA32Em9wIAAEok\nJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCM0b3YH7i4ePZif2QShw/vjdOnl3MvY0ex5+nZ8/TseXr2\nPL027/nCwoFn/ZqTpHXd7mzuJew49jw9e56ePU/Pnqc3rXsuJAEAjCEkAQCMISQBAIwhJAEAjCEk\nAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEA\njCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwh\nJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQB\nAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCMISQBAIwhJAEAjCEkAQCM\nISQBAIwhJAEAjCEkAQCMISQBAIzR3c43vfnNb479+/dHRMQLX/jCeP/73z/RRQEA5LZlSOr1ehER\ncf/99098MQAApdjyuu2xxx6L5eXluPPOO+Mnf/In49FHH02xLgCArLY8Sdq9e3fceeedcfTo0fj6\n178eb3/72+Ohhx6KmRntTEB7rPSqeOrplTh7oR+9fh2r/Tp6/UEMmiaapokmIqKJ9f9f+/umee4/\nd//+XXHu3Opz/yC2zZ6nd+XzL4mXPn9/dDqd3Eu5qLYMSddcc01cffXVo78+dOhQLC4uxhVXXDH2\n+w8f3hvd7uzFXWUiCwsHci9hx7Hn6e2kPf/6t5bi8//n8fjLv30iTi6ey70cmGr//b7/FJce3J17\nGRfVliHpk5/8ZPzd3/1d3HffffHEE0/E+fPnY2Fh4Vm///Tp5Yu6wFQWFg7E4uLZ3MvYUex5ejtl\nz6t6EP/jf/5DfP4vvxkREbvmZuMHrjkclx/aEwf3zceuudmYn5uN+bmZmJ3pRCc6sf6/9f/vxPAP\nxJ1OJ57Ln40PHtwTS0sXnus/Et8Fe57eVS84FPVqPxYX+7mX8l3b7A+OW4akm2++Oe6+++649dZb\nY2ZmJt7//ve7agOKNWia+Ogf/E08/NiT8fzL9sbNr31x/Pvvuyzmunnq1k4JpiWx5+lN655vGZLm\n5ubiV3/1V1OsBeA5+/zD34iHH3syrn3hJfFf3vry2D2/rUknAM/gSAiYGmfOrcan//eJ2Le7G//5\nzf9BQAKeEyEJmBpf+L8nY6VXx03Xf18c2DufezlAywlJwFSoB4P4X1/659izqxv/8Qefn3s5wBQQ\nkoCp8LdfPx1nzvXi1S+7InbNtXMMCVAWIQmYCl858e2IiHjVtc8+ogTguyEkAVPhr098O+a7M/HS\nF16SeynAlBCSgNZ7+txqnHzqfFx71aGYa+nEf6A8QhLQeif+eSkiIq594aHMKwGmiZAEtN7jT669\nl+2qK3bOe+mAyROSgNb7xnpIuvLy/ZlXAkwTIQlovW88eTYO7J2LQ/sNkAQuHiEJaLULq1UsPr0S\nV16+PzqdTu7lAFNESAJa7YnTyxER8b2X7cu8EmDaCElAqz319EpERDzv0J7MKwGmjZAEtNpTZ9ZC\n0sIluzOvBJg2QhLQaotnLkRExGVCEnCRCUlAq51aP0l63iWu24CLS0gCWm3x6Quxb3c39u7u5l4K\nMGWEJKC1mqaJU2dWnCIBEyEkAa119kI/etVAPxIwEUIS0FpL53sREXHJPpO2gYtPSAJa6+x6SDqw\ndy7zSoBpJCQBrXVm2UkSMDlCEtBaS+f7ERFxUEgCJkBIAlpraXTdJiQBF5+QBLSWxm1gkoQkoLWW\n1nuSXLcBkyAkAa21dL4Xc92Z2D0/m3spwBQSkoDWWlruxcG9c9HpdHIvBZhCQhLQSk3TxNL5vqs2\nYGKEJKCVetUgqnoQ+/YYJAlMhpAEtNLyShUREXt3dTOvBJhWQhLQSsur6yFpt5MkYDKEJKCVLjhJ\nAiZMSAJaaXl17ZUke3cLScBkCElAK+lJAiZNSAJaaaMnSUgCJkNIAlppdJIkJAETIiQBrTQ6Sdrl\n6TZgMoQkoJWWVzRuA5MlJAGtpHEbmDQhCWil4XXbHiEJmBAhCWil5ZUq5rszMddVxoDJUF2AVlpe\nrWKPfiRggoQkoJWWVyr9SMBECUlAK63269g9LyQBkyMkAa0zGDTRrwaxa04JAyZHhQFaZ7VfR0Q4\nSQImSkgCWmeltxaSds3PZl4JMM2EJKB1hidJrtuASVJhgNZZHZ4kzbluAyZHSAJaZ3SS5LoNmCAh\nCWidUU+S6zZgglQYoHU83QakICQBrbPRk+S6DZgcIQloHT1JQApCEtA6K70qIpwkAZMlJAGts9of\nRETEbidJwAQJSUDr6EkCUhCSgNZZ7a9ftzlJAiZISAJaZ8VJEpCAkAS0Tm+9J0lIAiZJSAJaZ/R0\n27wSBkyOCgO0zmq/jrnuTMzOKGHA5KgwQOus9geu2oCJE5KA1lnt1THv5bbAhKkyQOv0qzrmuk6S\ngMkSkoDW6VWDmO8qX8BkqTJA6/SFJCABVQZolcGgiXrQxJyQBEyYKgO0Sq9am7Y97+k2YMKEJKBV\netXatO25WeULmCxVBmiV/vorSeaMAAAmTJUBWqVfr4UkjdvApKkyQKv0+ms9SeYkAZMmJAGt0q+c\nJAFpqDJAq4wat4UkYMJUGaBV+tXwuk35AiZLlQFaZeO6TU8SMFlCEtAqo+s2IwCACVNlgFbRuA2k\nosoArWIEAJCKkAS0St/TbUAiqgzQKq7bgFRUGaBVep5uAxIRkoBW6ZmTBCSiygCtMrpuMwIAmDBV\nBmiVXn+9cXtW+QImS5UBWqVfD4dJ6kkCJktIAlqlvz4nydNtwKSpMkCr9MxJAhJRZYBW6VeDmOl0\noqsnCZgwVQZolX41cIoEJKHSAK1S1YPoznZyLwPYAYQkoFX69SC6TpKABFQaoFWqehDdGaULmDyV\nBmiVqnKSBKSh0gCt0q+bmNOTBCQgJAGtUtcDj/8DSag0QKto3AZSUWmA1qgHg2gaL7cF0lBpgNao\nqiYiwnUbkIRKA7RGv157b5thkkAKQhLQGlXt5bZAOioN0BpVNTxJUrqAyVNpgNZw3QakJCQBrVHV\nGreBdFQaoDWq2nUbkI5KA7SGxm0gJZUGaA2N20BKKg3QGv1RT5LGbWDyhCSgNUbXbU6SgARUGqA1\nRo3bepKABFQaoDX6epKAhLZVaU6dOhWve93r4sSJE5NeD8CzqgyTBBLaMiRVVRX33Xdf7N69O8V6\nAJ6VYZJASltWmg984APxEz/xE3H55ZenWA/As9K4DaS0aaX51Kc+FZdddllcd9110TRNqjUBjKVx\nG0ip02ySfm6//fbodNbu/h977LF40YteFL/5m78Zl1122bN+YFXV0e3OXvyVAjve7z/0WDzwua/G\n+971mvjBlyzkXg4w5bqbffH3fu/3Rn997Nix+KVf+qVNA1JExOnTyxdnZYktLByIxcWzuZexo9jz\n9Nq+52eWViIi4vzZ1db8c7R9z9vInqfX5j1fWDjwrF/b9pn18EQJIJeN6zb1CJi8TU+S/qX7779/\nkusA2FK/NicJSEelAVpj+IJbT7cBKag0QGsMr9tmDZMEEhCSgNborw+TdJIEpKDSAK1Rm5MEJKTS\nAK2hcRtISaUBWqMevbtNTxIweUIS0BrD67YZc9uABIQkoDXqQROzMx3DbYEkhCSgNaq60Y8EJKPa\nAK1RDwYxO+MUCUhDSAJaox40BkkCyQhJQGvUdeMkCUhGSAJaY+26TdkC0lBtgNZYa9x2kgSkISQB\nrbHWk6RsAWmoNkBreLoNSElIAlpD4zaQkpAEtIYRAEBKQhLQCk3TRD1oouvpNiAR1QZohXrQREQ4\nSQKSEZKAVqjr9ZDkJAlIRLUBWqEeDCIiNG4DyQhJQCtU6ydJhkkCqQhJQCts9CQpW0Aaqg3QCnXt\nug1IS0gCWmF0kiQkAYkISUArVK7bgMRUG6AVhtdtXSdJQCJCEtAKhkkCqQlJQCsYJgmkptoArWCY\nJJCakAS0QuW6DUhMSAJaYdS47ek2IBHVBmiFjZ4kJ0lAGkIS0AqGSQKpCUlAK1TDxm3XbUAiqg3Q\nCsPrNsMkgVSEJKAVDJMEUhOSgFYYPt1mmCSQimoDtEKlcRtITEgCWmE0AsB1G5CIkAS0wvC1JIZJ\nAqmoNkArGCYJpCYkAa2gJwlITUgCWqE2TBJITLUBWsF1G5CakAS0wmjitpMkIBHVBmiF0XWbkyQg\nESEJaIXKa0mAxIQkoBX0JAGpCUlAKxgmCaSm2gCt4CQJSE1IAlqhHg2TVLaANFQboBWq0TBJJ0lA\nGkIS0Aqu24DUhCSgFeranCQgLSEJaIV60MTsTCc6HSEJSENIAlqhGjT6kYCkhCSgFeq68WQbkJSK\nA7RCPRhE10kSkJCQBLTC2kmSkASkIyQBrVAPBq7bgKRUHKAVNG4DqQlJQCu4bgNSE5KAVlhr3Fay\ngHRUHKAVnCQBqQlJQCvUepKAxIQkoHhN06y/lkTJAtJRcYDi1YMmIrzcFkhLSAKKV9drIUnjNpCS\nigMUrx4MIsJJEpCWkAQUrxpet2ncBhISkoDiDa/bnCQBKQlJQPHqeu26TU8SkJKKAxTP021ADkIS\nULyNniQlC0hHxQGKN7xuc5IEpCQkAcVz3QbkICQBxTNMEshBxQGKZ5gkkIOQBBTPMEkgByEJKJ5h\nkkAOQhJQvI3rNiULSEfFAYq30bjtJAlIR0gCilcNT5I83QYkpOIAxdOTBOQgJAHFM0wSyEFIAoo3\nfC2JYZJASioOULzKSRKQgZAEFG/Uk+TpNiAhIQkonjlJQA4qDlC82mtJgAyEJKB41XCYpJ4kICEh\nCShebZgkkIGKAxTPMEkgByEJKJ5hkkAOQhJQvI3GbSULSEfFAYpXjSZuO0kC0hGSgOLpSQJyEJKA\n4hkmCeSg4gDFM0wSyEFIAopXj4ZJKllAOioOULxqNEzSSRKQjpAEFE/jNpCDkAQUzzBJIAchCShe\nPRjE7EwnOh0hCUhHSAKKV9WNfiQgOSEJKF5dN2YkAcmpOkDxhtdtACkJSUDx6oHrNiA9IQkoXl03\nBkkCyak6QPGqwcBJEpCckAQUb61xW0gC0upu9Q2DwSB+7ud+Lk6cOBEzMzPxi7/4i/GSl7wkxdoA\nImK9J8l1G5DYllXn+PHj0el04oEHHoi77rorPvjBD6ZYF8BI7boNyGDLk6Qbb7wxbrjhhoiIOHny\nZFxyySUTXxTAv1TXTXSFJCCxLUNSRMTMzEy8973vjc9//vPxG7/xG5NeE8BI0zSu24AsOk3TNNv9\n5lOnTsXRo0fjD//wD2P37t1jv6eq6uh2Zy/aAoGdraoH8ab3/EG8/KXPi//6zutyLwfYQbY8SfrM\nZz4TTzzxRLzjHe+IXbt2xczMTMxs8ie606eXL+oCU1lYOBCLi2dzL2NHsefptXHPV/t1RETU9aB1\na49o5563nT1Pr817vrBw4Fm/tmVIev3rXx9333133H777VFVVdxzzz0xPz9/URcI8Gzqeu2w2zBJ\nILUtQ9KePXvi13/911OsBeAZqsEgIsLTbUBy/mgGFG14kmSYJJCakAQUrR6eJLluAxJTdYCi1YP1\nkyTXbUBiQhJQtFHj9qxyBaSl6gBFq+rhdZuTJCAtIQko2ui6TUgCEhOSgKLpSQJyEZKAotW1p9uA\nPFQdoGjVYNi47SQJSEtIAopmmCSQi5AEFM0wSSAXVQco2ugkyXUbkJiQBBRt+HRb13UbkJiQBBRt\nNEzSxG0gMVUHKJphkkAuQhJQNMMkgVyEJKBow2GSXU+3AYmpOkDRKk+3AZkISUDRzEkCclF1gKLp\nSQJyEZKAog2HSZqTBKQmJAFF2xgBoFwBaak6QNE2hkk6SQLSEpKAohkmCeQiJAFF22jcVq6AtFQd\noGgbwySdJAFpCUlA0Vy3AbkISUDRNhq3lSsgLVUHKJphkkAuQhJQNMMkgVyEJKBohkkCuag6QNEM\nkwRyEZKAonm6DchFSAKKVg8GMTvTiU5HSALSEpKAotV14xQJyEJIAopWDxr9SEAWQhJQtKoeeLIN\nyELlAYrmJAnIRUgCilbXjUGSQBZCElC0tafblCogPZUHKJrrNiAXIQkoWlU3TpKALFQeoGj1YOAk\nCchCSAKKpnEbyEVIAorVNM1aT5KQBGQgJAHFGr3cdlapAtJTeYBibYQkJ0lAekISUKy6XgtJXU+3\nARmoPECx6sEgIkJPEpCFkAQUy3UbkJOQBBSrqocnSUoVkJ7KAxTLSRKQk5AEFGujcVtIAtITkoBi\njU6SXLcBGag8QLFGT7e5bgMyEJKAYlW1niQgHyEJKFbt6TYgI5UHKNawJ0njNpCDkAQUywgAICch\nCSiWYZJATioPUKxa4zaQkZAEFEtPEpCTkAQUa2NOklIFpKfyAMUaXbc5SQIyEJKAYlWebgMyEpKA\nYg2HSXY93QZkoPIAxdp4wa2TJCA9IQko1sYwSaUKSE/lAYo1HCbZ1ZMEZCAkAcWqPN0GZCQkAcUa\nNW67bgMyUHmAYo0mbgtJQAYqD1Cs0Qtu9SQBGQhJQLH0JAE5CUlAsfQkATmpPECx9CQBOak8QLFG\nPUmu24AMhCSgWMOeJMMkgRyEJKBYXksC5KTyAMXyWhIgJyEJKNbw6baZjpAEpCckAcWqBk10ZzvR\nEZKADIQkoFh13ehHArJRfYBiVYNBdD3+D2QiJAHFqpwkARmpPkCx6nrgyTYgGyEJKFY9aKI7o0wB\neag+QLGqehCzTpKATIQkoFhV3cSskyQgE9UHKJaeJCAnIQkoVj1oouvpNiAT1Qco0qBp1kOSkyQg\nDyEJKFJdNxERMWuYJJCJkAQUqR6svdzWMEkgF9UHKFK1fpKkJwnIRfUBilTX6ydJrtuATIQkoEgb\nJ0lCEpCHkAQUSU8SkJvqAxRJTxKQm+oDFKnSkwRkJiQBRaoHepKAvIQkoEi16zYgM9UHKJLrNiA3\nIQkoUuXpNiAz1QcokjlJQG5CElCkUU/SjDIF5KH6AEUaDpN0kgTkIiQBRRo1butJAjJRfYAiDXuS\nPN0G5CIkAUUaniTNdZUpIA/VByhSVQ17kpQpIA/VByhSvxaSgLxUH6BI5iQBuXU3+2JVVfGzP/uz\ncfLkyej3+/HOd74zbrjhhlRrA3awykkSkNmmIemzn/1sHD58OH7lV34lzpw5EzfddJOQBCTRrzRu\nA3ltGpJ+9Ed/NH7kR34kIiIGg0F0u5t+O8BFM5q47SQJyGTT1LNnz56IiDh37lzcdddd8TM/8zNJ\nFgWw0bitJwnIY8ujoW9961vx0z/903H77bfHG97whi0/8PDhvdHtzl6UxaW2sHAg9xJ2HHueXlv2\nvDu3VkeuuPxgLFy6N/Nqnpu27Pk0sefpTeOebxqSnnrqqbjzzjvj3nvvjVe/+tXb+sDTp5cvysJS\nW1g4EIuLZ3MvY0ex5+m1ac/PnV+NiIilM8sxU9eZV/Nv16Y9nxb2PL027/lm4W7Ty/7f+q3fiqWl\npfjIRz4Sx44dizvuuCN6vd5FXyDAv9Y3TBLIbNOTpHvuuSfuueeeVGsBGBnOSZoTkoBMVB+gSKM5\nSV2N20AeQhJQpH49iE4nYnZGmQLyUH2AIlXVwFUbkJUKBBSpqgeatoGsVCCgSP26MUgSyEpIAopU\nVYPoem8bkJEKBBTJdRuQmwoEFKmqNW4DealAQJGqunGSBGSlAgFFquqBQZJAVkISUJxB00Q9aFy3\nAVmpQEBxKi+3BQqgAgHFGb23TUgCMlKBgOL06yYiwpwkICsVCCjO8LptzsRtICMhCSiO6zagBCoQ\nUJy+kAQUQAUCiuMkCSiBCgQUp6qGjdt6koB8hCSgOMPrNsMkgZxUIKA4tes2oAAqEFAcjdtACVQg\noDj94ZwkwySBjFQgoDjDkDQvJAEZqUBAcXrDk6Q5JQrIRwUCitPv1xERMTc7m3klwE4mJAHFGTZu\nzztJAjJSgYDi9Pp6koD8VCCgOBtPt7luA/IRkoDi9Kr1niQnSUBGKhBQnJ4RAEABVCCgOJVhkkAB\nVCCgOKOTpDk9SUA+QhJQHD1JQAlUIKA4/b7rNiA/FQgoTr8eRHe2EzOdTu6lADuYkAQUp9cfmJEE\nZCckAcXpV7XH/4HsVCGgOL1qoB8JyE4VAorTF5KAAqhCQHH61SDm9SQBmQlJQFGapoleVcfcnPIE\n5KUKAUWpB000TcTcrPIE5KUKAUXpe7ktUAhVCCjK8L1tc97bBmQmJAFF6ffX3tvmJAnITRUCijI6\nSRKSgMxUIaAofSEJKIQqBBRlo3FbTxKQl5AEFGW1Wu9JMicJyEwVAoqy2lsLSbs93QZkJiQBRRmG\npF3zQhKQl5AEFGV1fQTALidJQGZCElCUFSdJQCGEJKAow5MkPUlAbkISUJRhT9K8kyQgMyEJKIqT\nJKAUQhJQFD1JQCmEJKAoo5Ok+W7mlQA7nZAEFGVjBIDyBOSlCgFFWe3VMdPpRHdWeQLyUoWAoqz0\n6tg1PxudTif3UoAdTkgCirLar1y1AUVQiYCirPYHsUvTNlAAIQkoymqvNiMJKIKQBBRj0DSx2q9d\ntwFFUImAYvT7g4gI121AEYQkoBgrfdO2gXIISUAxVntVRHhvG1AGIQkoxui9bUISUAAhCSjGhdW1\nk6Q9u/UkAfkJSUAxllfWQtI+IQkogJAEFGN5/SRp7y4hCchPSAKKMTxJ2uskCSiAkAQUw0kSUBIh\nCSjGxknSXOaVAAhJQEGWV/sR4ek2oAxCElCM0UmS6zagAEISUIxhSNqzyzBJID8hCSjG8moVu+dn\nY3ZGaQLyU4mAYiyvVB7/B4ohJAHFWF6tYu8uT7YBZRCSgCIMmiZWVp0kAeUQkoAirKxW0YQn24By\nCElAEc55JQlQGCEJKMLS+V5ERBzcN595JQBrhCSgCKOQtFdIAsogJAFFWFpeC0mXOEkCCiEkAUUY\nniQd2GcEAFAGIQkogus2oDRCElCEYUhy3QaUQkgCirC03I9OROzf67oNKIOQBBRh6Xwv9u2Z83Jb\noBiqEVCEpfM9V21AUYQkILt+NYjl1SoOuGoDCiIkAdl9++xKRERcenB35pUAbBCSgOyeenotJD3v\nEiEJKIeQBGT31JkLERGxcGhP5pUAbBCSgOyeOuMkCSiPkARktxGSnCQB5RCSgOyeevpCzM504vCB\nXbmXAjAiJAHZLZ5ZiUsP7oqZmU7upQCMCElAVudX+rF0vhdXHN6beykA30FIArL65pPnIiLiysv3\nZ14JwHcSkoCsHn9iPSRdISQBZRGSgKy+MTpJOpB5JQDfSUgCsnr8ybMx152J77nU4/9AWYQkIJvl\nlSq++eT5uOqK/TE7oxwBZVGVgGy++vjpGDRNvOyaS3MvBeAZhCQgm698/dsREfEDQhJQICEJyGLQ\nNPGlf3gqds/Pxvd978HcywF4BiEJyOKvT3w7Ti2txg//u8ujO6sUAeVRmYAsvvDFb0ZExGtf8YLM\nKwEYT0gCkvvyP56KL33tVFx75aG45nvMRwLKtK2Q9Oijj8axY8cmvRZgB3jy6Qvx3x78m+h0Im69\n8aXR6XipLVCm7lbf8Du/8zvxmc98Jvbt25diPcCUqgeDePhvn4zf//zfx7kL/bjtyLVx1RVOkYBy\nbRmSrr766vjwhz8c73nPe1KsJ4tB08Q/njwTi0+d2/T7mmi294EX89u2/Vnb+8Zmm5+3vc96bh/2\n/5ZW48zTy+uftc2fud0P38YHbveztv8zt/Mt6f8b+pdbcclTy3HmzIXn9EO38++qiYiV1SrOXejH\n0nIvvvnk+fiHk2fi3IV+dGc7cfvrr40bXvnCbf08gFy2DElHjhyJkydPplhLNl/4y2/GA1/4+9zL\ngKl2aP983PDKF8Trf/iquPyQV5AA5dsyJH23Dh/eG93u7MX+2Im68dXXRBURdb31H5EvdvvEdvox\nsvzMbX/Ydr/t4v1zbn9tOX7mdr5le592Mf+9b7fvZ7s/cjsft2dXNw7sm48De+fjqisOxOGDu7f5\n6dNnYcG1Ymr2PL1p3PNth6TtXq2cPr38b15MLp2IuOMNPxCLi2dzL2VHWVg4YM8Ty7Xn1Wo/Fhf7\nyX9uCfx3np49T6/Ne75ZuNv2CABPoAAAO8m2QtILXvCC+PjHPz7ptQAAFMMwSQCAMYQkAIAxhCQA\ngDGEJACAMYQkAIAxhCQAgDGEJACAMYQkAIAxOs1zfZU7AMAUcpIEADCGkAQAMIaQBAAwhpAEADCG\nkAQAMIaQBAAwxo4LSU3TxH333Re33HJL3HHHHfGNb3zjO75+/PjxuPnmm+OWW26JT3ziE5lWOV22\n2vMHH3ww3vrWt8att94av/ALv5BnkVNmqz0fuvfee+ODH/xg4tVNp632/Etf+lLcdtttcdttt8Vd\nd90VvV4v00qnx1Z7/tnPfjbe/OY3x9GjR+OBBx7ItMrp8+ijj8axY8ee8etT+ftns8N87nOfa977\n3vc2TdM0jzzySPOud71r9LV+v98cOXKkOXv2bNPr9Zq3vOUtzalTp3ItdWpstucrKyvNkSNHmtXV\n1aZpmubd7353c/z48SzrnCab7fnQAw880LztbW9rfu3Xfi318qbSVnv+4z/+483jjz/eNE3TfOIT\nn2hOnDiReolTZ6s9v+6665qlpaWm1+s1R44caZaWlnIsc6p89KMfbd74xjc2b3vb277j16f1988d\nd5L0xS/3AIexAAADXUlEQVR+Ma6//vqIiHj5y18eX/nKV0Zf+9rXvhZXX3117N+/P+bm5uJVr3pV\nPPzww7mWOjU22/P5+fn4+Mc/HvPz8xERUVVV7Nq1K8s6p8lmex4R8Vd/9Vfx5S9/OW655ZYcy5tK\nm+35iRMn4tChQ/Gxj30sjh07FmfOnIlrrrkm00qnx1b/nX//939/nDlzJlZXVyMiotPpJF/jtLn6\n6qvjwx/+8DN+fVp//9xxIencuXNx4MCB0d93u90YDAZjv7Zv3744e/Zs8jVOm832vNPpxKWXXhoR\nEb/7u78bFy5ciNe85jVZ1jlNNtvzxcXF+NCHPhT33ntvNAbuXzSb7fnp06fjkUceiWPHjsXHPvax\n+LM/+7P4i7/4i1xLnRqb7XlExEtf+tJ4y1veEj/2Yz8Wr3vd62L//v05ljlVjhw5ErOzs8/49Wn9\n/XPHhaT9+/fH+fPnR38/GAxiZmZm9LVz586Nvnb+/Pk4ePBg8jVOm832PGKtr+ADH/hA/Pmf/3l8\n6EMfyrHEqbPZnv/RH/1RPP300/H2t789fvu3fzsefPDB+PSnP51rqVNjsz0/dOhQXHXVVfGiF70o\nut1uXH/99c849eC7t9mef/WrX40//uM/juPHj8fx48fj1KlT8dBDD+Va6tSb1t8/d1xIeuUrXxl/\n8id/EhERjzzySFx77bWjr734xS+Of/qnf4qlpaXo9Xrx8MMPxyte8YpcS50am+15RMTP//zPR7/f\nj4985COjazeem832/NixY/HJT34y7r///njHO94Rb3zjG+Omm27KtdSpsdmeX3nllbG8vDxqLP7i\nF78YL3nJS7Ksc5pstucHDhyIPXv2xPz8/OjEemlpKddSp86/PoWe1t8/u7kXkNqRI0fiT//0T0e9\nGL/8y78cDz74YFy4cCGOHj0ad999d/zUT/1UNE0TR48ejcsvvzzzittvsz1/2cteFp/61KfiVa96\nVRw7diw6nU7ccccdceONN2Zedbtt9d85F99We/6+970v3v3ud0dExA/90A/Fa1/72pzLnQpb7fnw\nqdn5+fm46qqr4k1velPmFU+PYX/XtP/+2Wk0JQAAPMOOu24DANgOIQkAYAwhCQBgDCEJAGAMIQkA\nYAwhCQBgDCEJAGAMIQkAYIz/D8ks+uORxhWDAAAAAElFTkSuQmCC\n", 105 | "text/plain": [ 106 | "" 107 | ] 108 | }, 109 | "metadata": {}, 110 | "output_type": "display_data" 111 | } 112 | ], 113 | "source": [ 114 | "plot_results()" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": { 121 | "collapsed": true 122 | }, 123 | "outputs": [], 124 | "source": [] 125 | } 126 | ], 127 | "metadata": { 128 | "kernelspec": { 129 | "display_name": "Python 3", 130 | "language": "python", 131 | "name": "python3" 132 | }, 133 | "language_info": { 134 | "codemirror_mode": { 135 | "name": "ipython", 136 | "version": 3 137 | }, 138 | "file_extension": ".py", 139 | "mimetype": "text/x-python", 140 | "name": "python", 141 | "nbconvert_exporter": "python", 142 | "pygments_lexer": "ipython3", 143 | "version": "3.5.3" 144 | } 145 | }, 146 | "nbformat": 4, 147 | "nbformat_minor": 2 148 | } 149 | -------------------------------------------------------------------------------- /eigen_broadcast_cheatsheet_(cpp).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#include \n", 10 | "#include \n", 11 | "#include \n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "using EigenArray2d = Eigen::Array;\n", 21 | "using EigenArray1d = Eigen::Array;" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "metadata": {}, 28 | "outputs": [ 29 | { 30 | "name": "stdout", 31 | "output_type": "stream", 32 | "text": [ 33 | "vec_ni:\n", 34 | "1\n", 35 | "2\n", 36 | "3\n", 37 | "\n", 38 | "vec_nj:\n", 39 | "40\n", 40 | "50\n", 41 | "\n", 42 | "mat_ni_nj:\n", 43 | " 600 700\n", 44 | " 800 900\n", 45 | "1000 1100\n", 46 | "\n" 47 | ] 48 | } 49 | ], 50 | "source": [ 51 | "constexpr auto ni = 3;\n", 52 | "constexpr auto nj = 2;\n", 53 | "\n", 54 | "auto vec_ni = EigenArray1d(ni);\n", 55 | "vec_ni << 1.0, 2.0, 3.0;\n", 56 | "\n", 57 | "auto vec_nj = EigenArray1d(nj);\n", 58 | "vec_nj << 40.0, 50.0;\n", 59 | "\n", 60 | "auto mat_ni_nj = EigenArray2d(ni, nj);\n", 61 | "mat_ni_nj <<\n", 62 | " 600.0, 700.0,\n", 63 | " 800.0, 900.0,\n", 64 | " 1000.0, 1100.0;\n", 65 | "\n", 66 | "std::cout << \"vec_ni:\\n\" << vec_ni << \"\\n\\n\";\n", 67 | "std::cout << \"vec_nj:\\n\" << vec_nj << \"\\n\\n\";\n", 68 | "std::cout << \"mat_ni_nj:\\n\" << mat_ni_nj << \"\\n\\n\";\n" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 4, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "name": "stdout", 78 | "output_type": "stream", 79 | "text": [ 80 | "vec_nj * mat_ni_nj:\n", 81 | "24000 35000\n", 82 | "32000 45000\n", 83 | "40000 55000\n", 84 | "\n" 85 | ] 86 | } 87 | ], 88 | "source": [ 89 | "std::cout << \"vec_nj * mat_ni_nj:\\n\"\n", 90 | " << mat_ni_nj.rowwise() * vec_nj.transpose() << \"\\n\\n\";" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 5, 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "name": "stdout", 100 | "output_type": "stream", 101 | "text": [ 102 | "vec_ni * mat_ni_nj.T:\n", 103 | " 600 1600 3000\n", 104 | " 700 1800 3300\n", 105 | "\n" 106 | ] 107 | } 108 | ], 109 | "source": [ 110 | "std::cout << \"vec_ni * mat_ni_nj.T:\\n\"\n", 111 | " << (mat_ni_nj.transpose().rowwise() * vec_ni.transpose()) << \"\\n\\n\";" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 6, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "name": "stdout", 121 | "output_type": "stream", 122 | "text": [ 123 | "np.sum(vec_nj * mat_ni_nj, axis=1):\n", 124 | "59000\n", 125 | "77000\n", 126 | "95000\n", 127 | "\n" 128 | ] 129 | } 130 | ], 131 | "source": [ 132 | "std::cout << \"np.sum(vec_nj * mat_ni_nj, axis=1):\\n\"\n", 133 | " << (mat_ni_nj.rowwise() * vec_nj.transpose()).rowwise().sum() << \"\\n\\n\";" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 7, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "auto mat_nj_ni = (mat_ni_nj.transpose().rowwise() * vec_ni.transpose()).eval();" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": 8, 148 | "metadata": {}, 149 | "outputs": [ 150 | { 151 | "name": "stdout", 152 | "output_type": "stream", 153 | "text": [ 154 | "mat_nj_ni / vec_ni:\n", 155 | " 600 800 1000\n", 156 | " 700 900 1100\n", 157 | "\n" 158 | ] 159 | } 160 | ], 161 | "source": [ 162 | "std::cout << \"mat_nj_ni / vec_ni:\\n\"\n", 163 | " << mat_nj_ni.rowwise() / vec_ni.transpose() << \"\\n\\n\"; " 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 9, 169 | "metadata": {}, 170 | "outputs": [ 171 | { 172 | "name": "stdout", 173 | "output_type": "stream", 174 | "text": [ 175 | "(vec_ni * mat_ni_nj.T) / vec_ni:\n", 176 | " 600 800 1000\n", 177 | " 700 900 1100\n", 178 | "\n" 179 | ] 180 | } 181 | ], 182 | "source": [ 183 | "// Replacing `mat_nj_ni` with it's expression\n", 184 | "\n", 185 | "std::cout << \"(vec_ni * mat_ni_nj.T) / vec_ni:\\n\"\n", 186 | " << (mat_ni_nj.transpose().rowwise() * vec_ni.transpose()).rowwise() / vec_ni.transpose() << \"\\n\\n\";" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": 10, 192 | "metadata": {}, 193 | "outputs": [ 194 | { 195 | "name": "stdout", 196 | "output_type": "stream", 197 | "text": [ 198 | "(vec_ni * mat_ni_nj.T) / np.sum(vec_nj * mat_ni_nj, axis=1):\n", 199 | "0.0101695 0.0207792 0.0315789\n", 200 | "0.0118644 0.0233766 0.0347368\n", 201 | "\n" 202 | ] 203 | } 204 | ], 205 | "source": [ 206 | "// Replacing rightmost `vec_ni` by the sum expression\n", 207 | "\n", 208 | "std::cout << \"(vec_ni * mat_ni_nj.T) / np.sum(vec_nj * mat_ni_nj, axis=1):\\n\"\n", 209 | " << (mat_ni_nj.transpose().rowwise() * vec_ni.transpose()).rowwise() / (mat_ni_nj.rowwise() * vec_nj.transpose()).rowwise().sum().transpose() << \"\\n\\n\";" 210 | ] 211 | } 212 | ], 213 | "metadata": { 214 | "kernelspec": { 215 | "display_name": "C++17", 216 | "language": "C++17", 217 | "name": "xeus-cling-cpp17" 218 | }, 219 | "language_info": { 220 | "codemirror_mode": "text/x-c++src", 221 | "file_extension": ".cpp", 222 | "mimetype": "text/x-c++src", 223 | "name": "c++", 224 | "version": "-std=c++17" 225 | } 226 | }, 227 | "nbformat": 4, 228 | "nbformat_minor": 2 229 | } 230 | -------------------------------------------------------------------------------- /eigen_broadcast_cheatsheet_(python).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "# empty" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 3, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "vec_ni:\n", 31 | " [1. 2. 3.] \n", 32 | "\n", 33 | "vec_nj:\n", 34 | " [40. 50.] \n", 35 | "\n", 36 | "mat_ni_nj:\n", 37 | " [[ 600. 700.]\n", 38 | " [ 800. 900.]\n", 39 | " [1000. 1100.]] \n", 40 | "\n" 41 | ] 42 | } 43 | ], 44 | "source": [ 45 | "ni = 3\n", 46 | "nj = 2\n", 47 | "\n", 48 | "vec_ni = np.array([1.0, 2.0, 3.0])\n", 49 | "vec_nj = np.array([40.0, 50.0])\n", 50 | "\n", 51 | "mat_ni_nj = np.array([\n", 52 | " [600.0, 700.0],\n", 53 | " [800.0, 900.0],\n", 54 | " [1000.0, 1100.0],\n", 55 | "])\n", 56 | "\n", 57 | "print('vec_ni:\\n', vec_ni, '\\n')\n", 58 | "print('vec_nj:\\n', vec_nj, '\\n')\n", 59 | "print('mat_ni_nj:\\n', mat_ni_nj, '\\n')" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 4, 65 | "metadata": {}, 66 | "outputs": [ 67 | { 68 | "data": { 69 | "text/plain": [ 70 | "array([[24000., 35000.],\n", 71 | " [32000., 45000.],\n", 72 | " [40000., 55000.]])" 73 | ] 74 | }, 75 | "execution_count": 4, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "vec_nj * mat_ni_nj" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 5, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "text/plain": [ 92 | "array([[ 600., 1600., 3000.],\n", 93 | " [ 700., 1800., 3300.]])" 94 | ] 95 | }, 96 | "execution_count": 5, 97 | "metadata": {}, 98 | "output_type": "execute_result" 99 | } 100 | ], 101 | "source": [ 102 | "vec_ni * mat_ni_nj.T" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 6, 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "data": { 112 | "text/plain": [ 113 | "array([59000., 77000., 95000.])" 114 | ] 115 | }, 116 | "execution_count": 6, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "np.sum(vec_nj * mat_ni_nj, axis=1)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 7, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "mat_nj_ni = vec_ni * mat_ni_nj.T" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 8, 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "data": { 141 | "text/plain": [ 142 | "array([[ 600., 800., 1000.],\n", 143 | " [ 700., 900., 1100.]])" 144 | ] 145 | }, 146 | "execution_count": 8, 147 | "metadata": {}, 148 | "output_type": "execute_result" 149 | } 150 | ], 151 | "source": [ 152 | "mat_nj_ni / vec_ni" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 9, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "data": { 162 | "text/plain": [ 163 | "array([[ 600., 800., 1000.],\n", 164 | " [ 700., 900., 1100.]])" 165 | ] 166 | }, 167 | "execution_count": 9, 168 | "metadata": {}, 169 | "output_type": "execute_result" 170 | } 171 | ], 172 | "source": [ 173 | "# Replacing `mat_nj_ni` with it's expression\n", 174 | "\n", 175 | "(vec_ni * mat_ni_nj.T) / vec_ni" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 10, 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "data": { 185 | "text/plain": [ 186 | "array([[0.01016949, 0.02077922, 0.03157895],\n", 187 | " [0.01186441, 0.02337662, 0.03473684]])" 188 | ] 189 | }, 190 | "execution_count": 10, 191 | "metadata": {}, 192 | "output_type": "execute_result" 193 | } 194 | ], 195 | "source": [ 196 | "# Replacing rightmost `vec_ni` by the sum expression\n", 197 | "\n", 198 | "(vec_ni * mat_ni_nj.T) / np.sum(vec_nj * mat_ni_nj, axis=1)" 199 | ] 200 | } 201 | ], 202 | "metadata": { 203 | "kernelspec": { 204 | "display_name": "Python 3", 205 | "language": "python", 206 | "name": "python3" 207 | }, 208 | "language_info": { 209 | "codemirror_mode": { 210 | "name": "ipython", 211 | "version": 3 212 | }, 213 | "file_extension": ".py", 214 | "mimetype": "text/x-python", 215 | "name": "python", 216 | "nbconvert_exporter": "python", 217 | "pygments_lexer": "ipython3", 218 | "version": "3.6.5" 219 | } 220 | }, 221 | "nbformat": 4, 222 | "nbformat_minor": 2 223 | } 224 | -------------------------------------------------------------------------------- /einsum-notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Einsum Notebook - Einstein Summation\n", 8 | "\n", 9 | "## Einstein Notation\n", 10 | "In mathematics, especially in applications of linear algebra to physics, the Einstein notation or Einstein summation convention is a notational convention that implies summation over a set of indexed terms in a formula, thus achieving notational brevity. It was introduced to physics by Albert Einstein in 1916.\n", 11 | "\n", 12 | "The following\n", 13 | "$$\n", 14 | "y = \\sum^3_{i=1} c_i x_i = c_1 x_1 + c_2 x_2 + c_3 x_3\n", 15 | "$$\n", 16 | "is reduced to\n", 17 | "$$\n", 18 | "y = c_i x_i\n", 19 | "$$\n", 20 | "\n", 21 | "### Inner Product\n", 22 | "$$\n", 23 | "\\mathbf{u} \\cdot \\mathbf{v} = u_i v_i\n", 24 | "$$\n", 25 | "\n", 26 | "### Matrix Product\n", 27 | "$$\n", 28 | "\\mathbf{C} = \\mathbf{A} \\mathbf{B} = A_{ij} B_{jk} = C_{ik}\n", 29 | "$$\n", 30 | "\n", 31 | "### Matrix Trace\n", 32 | "The trace of a matrix, $tr(A) = \\sum_i A_ii$ is calculated by\n", 33 | "$$\n", 34 | "tr(A) = A_{ii}\n", 35 | "$$\n", 36 | "\n", 37 | "### NumPy Einsum\n", 38 | "```\n", 39 | "numpy.einsum(subscripts, *operands, out=None, dtype=None, order='K', casting='safe')\n", 40 | "```" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 1, 46 | "metadata": { 47 | "collapsed": false 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "import numpy as np" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "Ok, let's try some stuff out. The example below shows how a dot product is represented using einsum:" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 2, 64 | "metadata": { 65 | "collapsed": false 66 | }, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "Dot using np.dot is 24.0\n", 73 | "Dot using np.einsum is 24.0\n", 74 | "[ 1. 2. 4. 3.]\n" 75 | ] 76 | } 77 | ], 78 | "source": [ 79 | "u = np.array([1.0, 2.0, 4.0, 3.0])\n", 80 | "v = np.array([3.0, 1.0, 1.0, 5.0])\n", 81 | "\n", 82 | "\n", 83 | "print('Dot using np.dot is', np.dot(u, v))\n", 84 | "print('Dot using np.einsum is', np.einsum('i,i', u, v))\n", 85 | "print(np.einsum('i',u))" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 3, 91 | "metadata": { 92 | "collapsed": false 93 | }, 94 | "outputs": [ 95 | { 96 | "name": "stdout", 97 | "output_type": "stream", 98 | "text": [ 99 | "Trace using np.trace is 3.1\n", 100 | "\n", 101 | "Trace using np.einsum is 3.1\n" 102 | ] 103 | } 104 | ], 105 | "source": [ 106 | "A = np.array([[1.0, 2.0, 4.0, 3.0],\n", 107 | " [1.0, 0.0, 2.0, 0.0],\n", 108 | " [1.0, 1.0, 1.1, 0.0],\n", 109 | " [2.0, 2.0, 1.0, 1.0]])\n", 110 | "\n", 111 | "B = np.array([[1.0, 2.0, 4.0, 3.0],\n", 112 | " [0.0, 1.0, 0.0, 0.0],\n", 113 | " [1.0, 1.0, 1.0, 1.0],\n", 114 | " [7.0, 8.0, 9.0, 1.0]])\n", 115 | "\n", 116 | "\n", 117 | "Cdot = np.dot(A, B)\n", 118 | "Cein = np.einsum('jw,wk', A, B)\n", 119 | "\n", 120 | "assert np.allclose(Cdot, Cein)\n", 121 | "\n", 122 | "print('Trace using np.trace is', np.trace(A))\n", 123 | "print('\\nTrace using np.einsum is',np.einsum('ii', A))\n", 124 | "#print np.einsum('ij', A)\n", 125 | "#print np.einsum('ji', A)" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 4, 131 | "metadata": { 132 | "collapsed": false 133 | }, 134 | "outputs": [ 135 | { 136 | "name": "stdout", 137 | "output_type": "stream", 138 | "text": [ 139 | "Matrix-vector product using np.dot is\n", 140 | " [ 30. 9. 7.4 13. ]\n", 141 | "\n", 142 | "Matrix-vector product using np.einsum is\n", 143 | " [ 30. 9. 7.4 13. ]\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "print('Matrix-vector product using np.dot is\\n', np.dot(A, u))\n", 149 | "print('\\nMatrix-vector product using np.einsum is\\n', np.einsum('ij,j', A, u))" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 5, 155 | "metadata": { 156 | "collapsed": false 157 | }, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "[ 13. 12. 15.4 6. ]\n", 164 | "[ 13. 12. 15.4 6. ]\n" 165 | ] 166 | } 167 | ], 168 | "source": [ 169 | "print(np.dot(A.T, u))\n", 170 | "print(np.einsum('qj,q', A, u))" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "## Broadcasting\n", 178 | "\n", 179 | "Broadcasting will use the \"`...`\" syntax. " 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 6, 185 | "metadata": { 186 | "collapsed": false 187 | }, 188 | "outputs": [ 189 | { 190 | "name": "stdout", 191 | "output_type": "stream", 192 | "text": [ 193 | "A is\n", 194 | " [[ 1. 2. 4. 3. ]\n", 195 | " [ 1. 0. 2. 0. ]\n", 196 | " [ 1. 1. 1.1 0. ]\n", 197 | " [ 2. 2. 1. 1. ]]\n", 198 | "\n", 199 | "U is\n", 200 | " [ 1. 2. 4. 3.]\n", 201 | "\n", 202 | "A*u is\n", 203 | " [[ 1. 4. 16. 9. ]\n", 204 | " [ 1. 0. 8. 0. ]\n", 205 | " [ 1. 2. 4.4 0. ]\n", 206 | " [ 2. 4. 4. 3. ]]\n", 207 | "\n", 208 | "np.einsum is\n", 209 | " [[ 1. 4. 16. 9. ]\n", 210 | " [ 1. 0. 8. 0. ]\n", 211 | " [ 1. 2. 4.4 0. ]\n", 212 | " [ 2. 4. 4. 3. ]]\n" 213 | ] 214 | } 215 | ], 216 | "source": [ 217 | "# Multiply each line of A by u pointwise\n", 218 | "print('A is\\n', A)\n", 219 | "print('\\nU is\\n', u)\n", 220 | "print('\\nA*u is\\n', A*u)\n", 221 | "print('\\nnp.einsum is\\n', np.einsum('...j,j->...j', A, u))" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 7, 227 | "metadata": { 228 | "collapsed": false 229 | }, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "\n", 236 | "np.einsum is\n", 237 | " [ 10. 3. 3.1 6. ]\n" 238 | ] 239 | } 240 | ], 241 | "source": [ 242 | "print('\\nnp.einsum is\\n', np.einsum('...j->...', A))" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "## Now let's check performance!" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": { 256 | "collapsed": false 257 | }, 258 | "outputs": [ 259 | { 260 | "name": "stdout", 261 | "output_type": "stream", 262 | "text": [ 263 | "Broadcasting\n", 264 | "\n", 265 | "A*b\n", 266 | "100 loops, best of 3: 2.95 ms per loop\n", 267 | "\n", 268 | "np.einsum('...j,j->...j', A, b)\n", 269 | "100 loops, best of 3: 3.44 ms per loop\n", 270 | "\n", 271 | "Matrix Vector Product\n", 272 | "\n", 273 | "np.dot(A, b)\n", 274 | "The slowest run took 12.83 times longer than the fastest. This could mean that an intermediate result is being cached.\n", 275 | "10000 loops, best of 3: 183 µs per loop\n", 276 | "\n", 277 | "np.einsum('...j,j', A, b)\n", 278 | "1000 loops, best of 3: 433 µs per loop\n", 279 | "\n", 280 | "Matrix Product\n", 281 | "\n", 282 | "np.dot(A, B)\n", 283 | "10 loops, best of 3: 35.6 ms per loop\n", 284 | "\n", 285 | "np.einsum('ij,jk', A, B)\n", 286 | "1 loop, best of 3: 442 ms per loop\n" 287 | ] 288 | } 289 | ], 290 | "source": [ 291 | "A = np.random.rand(1000,1000)\n", 292 | "B = np.random.rand(1000,1000)\n", 293 | "b = np.random.rand(1000)\n", 294 | "\n", 295 | "print ('Broadcasting')\n", 296 | "\n", 297 | "print ('\\nA*b')\n", 298 | "%timeit A*b\n", 299 | "\n", 300 | "print (\"\\nnp.einsum('...j,j->...j', A, b)\")\n", 301 | "%timeit np.einsum('...j,j->...j', A, b)\n", 302 | "\n", 303 | "print ('\\nMatrix Vector Product')\n", 304 | "\n", 305 | "print ('\\nnp.dot(A, b)')\n", 306 | "%timeit np.dot(A, b)\n", 307 | "\n", 308 | "print (\"\\nnp.einsum('...j,j', A, b)\")\n", 309 | "%timeit np.einsum('...j,j', A, b)\n", 310 | "\n", 311 | "print ('\\nMatrix Product')\n", 312 | "\n", 313 | "print ('\\nnp.dot(A, B)')\n", 314 | "%timeit np.dot(A, B)\n", 315 | "\n", 316 | "print (\"\\nnp.einsum('ij,jk', A, B)\")\n", 317 | "%timeit np.einsum('ij,jk', A, B)" 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": null, 323 | "metadata": { 324 | "collapsed": false 325 | }, 326 | "outputs": [ 327 | { 328 | "name": "stdout", 329 | "output_type": "stream", 330 | "text": [ 331 | "Using dot and python loop\n", 332 | "10 loops, best of 3: 19.4 ms per loop\n", 333 | "\n", 334 | "Using np.einsum('...jk,...k->...j', A_array, b_array)\n" 335 | ] 336 | } 337 | ], 338 | "source": [ 339 | "A_array = np.random.rand(10000, 3, 3) # [A1, A2, A3, ...]\n", 340 | "b_array = np.random.rand(10000, 3) # [b1, b2, b3, ...]\n", 341 | "x_array = np.zeros((10000, 3))\n", 342 | "\n", 343 | "# np.dot(A_array, b_array)\n", 344 | "\n", 345 | "\n", 346 | "def use_dot(x_array):\n", 347 | " for i in range(10000):\n", 348 | " x_array[i] = np.dot(A_array[i], b_array[i])\n", 349 | "\n", 350 | "print ('Using dot and python loop')\n", 351 | "%timeit use_dot(x_array)\n", 352 | "\n", 353 | "print (\"\\nUsing np.einsum('...jk,...k->...j', A_array, b_array)\")\n", 354 | "%timeit np.einsum('...jk,...k->...j', A_array, b_array)\n", 355 | "\n", 356 | "use_dot(x_array)\n", 357 | "x_einsum_array1 = np.einsum('...jk,...k->...j', A_array, b_array)\n", 358 | "assert np.allclose(x_array, x_einsum_array1)" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": null, 364 | "metadata": { 365 | "collapsed": false 366 | }, 367 | "outputs": [], 368 | "source": [ 369 | "np.einsum('...ij,...jk,...kl,...l->...i', A_array, A_array, A_array, b_array)" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "**NOTE**: If performance and not code style is your primary concern, you might still be better off with dot and the loop (depending on your specific data and system environment). In contrast to einsum, dot can take advantage of BLAS and will often multithread automatically.\n", 377 | "\n", 378 | "# Methods in the oven: `numpy.core.umath_tests`" 379 | ] 380 | }, 381 | { 382 | "cell_type": "code", 383 | "execution_count": null, 384 | "metadata": { 385 | "collapsed": false 386 | }, 387 | "outputs": [], 388 | "source": [ 389 | "#dir(np.core.umath_tests)\n", 390 | "#print np.version.version\n", 391 | "from numpy.core.umath_tests import inner1d\n", 392 | "dir(np.core.umath_tests)" 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "metadata": { 399 | "collapsed": false 400 | }, 401 | "outputs": [], 402 | "source": [ 403 | "a_array = np.random.rand(1000000, 3) # [a1, a2, a3, ...]\n", 404 | "b_array = np.random.rand(1000000, 3) # [b1, b2, b3, ...]\n", 405 | "\n", 406 | "print (\"np.einsum('...k,...k', a_array, b_array)\")\n", 407 | "%timeit np.einsum('...k,...k', a_array, b_array)\n", 408 | "\n", 409 | "# Another one, it seems really advanced... but also faster...\n", 410 | "print ('\\nnp.core.umath_tests.inner1d(a_array, b_array)')\n", 411 | "%timeit np.core.umath_tests.inner1d(a_array, b_array)\n", 412 | "\n", 413 | "assert np.allclose(np.einsum('...k,...k', a_array, b_array), np.core.umath_tests.inner1d(a_array, b_array))" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "metadata": {}, 419 | "source": [ 420 | "It uses the [Generalized Universal Function API](http://docs.scipy.org/doc/numpy/reference/c-api.generalized-ufuncs.html), but this will be for another book meeting.\n", 421 | "\n", 422 | ">There is a general need for looping over not only functions on scalars but also over **functions on vectors** (or arrays). This concept is realized in Numpy by generalizing the universal functions (ufuncs).\n", 423 | "\n", 424 | "New **hidden** methods in this [pull request](https://github.com/numpy/numpy/pull/3220/files). They all use the method `gufunc(..., **kw_args)`.\n", 425 | "\n", 426 | "In `numpy/linalg/_gufuncs_linalg.py`,\n", 427 | "```\n", 428 | "=======================\n", 429 | " gufuncs_linalg module\n", 430 | "=======================\n", 431 | "\n", 432 | "Linear Algebra functions implemented as gufuncs, so they broadcast.\n", 433 | "\n", 434 | "warning::** This module is only for testing, the functionality will be integrated into numpy.linalg proper.\n", 435 | "```" 436 | ] 437 | }, 438 | { 439 | "cell_type": "markdown", 440 | "metadata": {}, 441 | "source": [ 442 | "`matrix_multiply` wins for **matrix-matrix** vectorized." 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": null, 448 | "metadata": { 449 | "collapsed": false 450 | }, 451 | "outputs": [], 452 | "source": [ 453 | "A_array = np.random.rand(10000, 4, 3) # [A1, A2, A3, ...]\n", 454 | "B_array = np.random.rand(10000, 3, 4) # [A1, A2, A3, ...]\n", 455 | "\n", 456 | "print (\"np.einsum('...ij,...jk->...ik', A_array, B_array)\")\n", 457 | "%timeit np.einsum('...ij,...jk->...ik', A_array, B_array)\n", 458 | "\n", 459 | "# Another one, it seems really advanced... but also faster...\n", 460 | "print ('\\nnp.core.umath_tests.matrix_multiply(A_array, B_array)')\n", 461 | "%timeit np.core.umath_tests.matrix_multiply(A_array, B_array)\n", 462 | "\n", 463 | "assert np.allclose(np.einsum('...ij,...jk->...ik', A_array, B_array), np.core.umath_tests.matrix_multiply(A_array, B_array))" 464 | ] 465 | }, 466 | { 467 | "cell_type": "markdown", 468 | "metadata": {}, 469 | "source": [ 470 | "`einsum` wins for **matrix-vector** vectorized." 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": null, 476 | "metadata": { 477 | "collapsed": false 478 | }, 479 | "outputs": [], 480 | "source": [ 481 | "A_array = np.random.rand(10000, 4, 4) # [A1, A2, A3, ...]\n", 482 | "b_array = np.random.rand(10000, 4) # [b1, b2, b3, ...]\n", 483 | "\n", 484 | "print (\"\\nUsing np.einsum('...jk,...k->...j', A_array, b_array)\")\n", 485 | "%timeit np.einsum('...jk,...k->...j', A_array, b_array)\n", 486 | "\n", 487 | "print (\"\\nnp.core.umath_tests.matrix_multiply(A_array, b_array)\")\n", 488 | "%timeit np.core.umath_tests.matrix_multiply(A_array, b_array[:, :, np.newaxis])[:,:,0]\n", 489 | "\n", 490 | "x_einsum = np.einsum('...jk,...k->...j', A_array, b_array)\n", 491 | "x_matrix_multiply = np.core.umath_tests.matrix_multiply(A_array, b_array[:, :, np.newaxis])[:,:,0]\n", 492 | "assert np.allclose(x_einsum, x_matrix_multiply)" 493 | ] 494 | }, 495 | { 496 | "cell_type": "markdown", 497 | "metadata": {}, 498 | "source": [ 499 | "When there is only one operand, no axes are summed, and no output parameter is provided, a view into the operand is returned instead of a new array. Thus, taking the diagonal as `np.einsum('ii->i', a)` produces a view." 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": null, 505 | "metadata": { 506 | "collapsed": false 507 | }, 508 | "outputs": [], 509 | "source": [ 510 | "A = np.array([[1.0, 2.0, 4.0, 3.0],\n", 511 | " [1.0, 0.0, 2.0, 0.0],\n", 512 | " [1.0, 1.0, 1.1, 0.0],\n", 513 | " [2.0, 2.0, 1.0, 1.0]])\n", 514 | "d = np.einsum('ii->i', A)\n", 515 | "\n", 516 | "#d[0] = 1000 # Doesn't work, it's a view!\n", 517 | "print(d)\n", 518 | "print(A)" 519 | ] 520 | }, 521 | { 522 | "cell_type": "code", 523 | "execution_count": null, 524 | "metadata": { 525 | "collapsed": false 526 | }, 527 | "outputs": [], 528 | "source": [ 529 | "# Products with more than 2 arrays...\n", 530 | "\n", 531 | "A_array = np.random.rand(2, 3, 3) # [A1, A2, A3, ...]\n", 532 | "b_array = np.random.rand(2, 3) # [b1, b2, b3, ...]\n", 533 | "\n", 534 | "# \"A*A*b\" = [dot(dot(A1,A1), b1), dot(dot(A2,A2), b2), ...]\n", 535 | "val2 = np.einsum('...ij,...jk,...k->...ik', A_array, A_array, b_array)\n", 536 | "print(val2)" 537 | ] 538 | } 539 | ], 540 | "metadata": { 541 | "anaconda-cloud": {}, 542 | "kernelspec": { 543 | "display_name": "Python [conda env:petsc_debug]", 544 | "language": "python", 545 | "name": "conda-env-petsc_debug-py" 546 | }, 547 | "language_info": { 548 | "codemirror_mode": { 549 | "name": "ipython", 550 | "version": 3 551 | }, 552 | "file_extension": ".py", 553 | "mimetype": "text/x-python", 554 | "name": "python", 555 | "nbconvert_exporter": "python", 556 | "pygments_lexer": "ipython3", 557 | "version": "3.5.2" 558 | } 559 | }, 560 | "nbformat": 4, 561 | "nbformat_minor": 0 562 | } 563 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: notebooks 2 | channels: 3 | - https://conda.anaconda.org/conda-forge/ 4 | dependencies: 5 | # Packages used to start the jupyter server 6 | - python==3.6.5 7 | - notebook==6.0.1 8 | - ipython==7.8.0 9 | - nb_conda==2.2.1 10 | - ipykernel==5.1.2 11 | - ipywidgets==7.5.1 12 | - widgetsnbextension==3.5.1 13 | - jupyter_contrib_nbextensions==0.5.1 14 | # C++ Notebooks 15 | # NOTE: xeus-cling was pinned to build number 1000 because it wasn't working in MyBinder, check this issue: 16 | # https://github.com/QuantStack/xeus-cling/issues/194 17 | - xeus-cling==0.4.11 # =0.4.10=he860b03_1000 18 | - cling==0.5 19 | - xeus==0.18.1 20 | 21 | # Packages used by the kernel where notebooks are run 22 | # (They could be added to a separate env, but are kept together here for convenience) 23 | - scipy==1.3.1 24 | - seaborn==0.9.0 25 | - sympy==1.4 26 | - pandas==0.25.1 27 | - pytables==3.5.2 28 | 29 | # C++ packages 30 | - eigen==3.3.7 31 | - clangdev==5.0.0 # >5.0.0 isn't yet supported by `cling` in conda-forge 32 | -------------------------------------------------------------------------------- /numpy_performance.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import numpy as np" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## `np.zeros_like` vs `np.zeros`" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "Small size\n", 31 | "5.91 µs ± 406 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 32 | "2.06 µs ± 66.5 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 33 | "Large size\n", 34 | "800 µs ± 11.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 35 | "18.2 µs ± 1.17 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "print(\"Small size\")\n", 41 | "a = np.ones(shape=(4, 500))\n", 42 | "%timeit -n10000 np.zeros_like(a)\n", 43 | "%timeit -n10000 np.zeros(shape=a.shape)\n", 44 | "print(\"Large size\")\n", 45 | "a = np.ones(shape=(8, 50000))\n", 46 | "%timeit -n100 np.zeros_like(a)\n", 47 | "%timeit -n100 np.zeros(shape=a.shape)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "## `np.ones_like` vs `np.ones`" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "Small size\n", 67 | "4.13 µs ± 173 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 68 | "3.82 µs ± 84.5 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 69 | "Large size\n", 70 | "803 µs ± 29 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 71 | "806 µs ± 14.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 72 | ] 73 | } 74 | ], 75 | "source": [ 76 | "print(\"Small size\")\n", 77 | "a = np.ones(shape=(4, 500))\n", 78 | "%timeit -n10000 np.ones_like(a)\n", 79 | "%timeit -n10000 np.ones(shape=a.shape)\n", 80 | "print(\"Large size\")\n", 81 | "a = np.ones(shape=(8, 50000))\n", 82 | "%timeit -n100 np.ones_like(a)\n", 83 | "%timeit -n100 np.ones(shape=a.shape)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "## `np.full_like` vs `np.full`" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 4, 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "name": "stdout", 100 | "output_type": "stream", 101 | "text": [ 102 | "Small size\n", 103 | "4.12 µs ± 251 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 104 | "4.4 µs ± 112 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 105 | "Large size\n", 106 | "794 µs ± 16.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 107 | "796 µs ± 16.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "print(\"Small size\")\n", 113 | "a = np.ones(shape=(4, 500))\n", 114 | "%timeit -n10000 np.full_like(a, 1.1)\n", 115 | "%timeit -n10000 np.full(a.shape, 1.1)\n", 116 | "print(\"Large size\")\n", 117 | "a = np.ones(shape=(8, 50000))\n", 118 | "%timeit -n100 np.full_like(a, 1.1)\n", 119 | "%timeit -n100 np.full(a.shape, 1.1)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "## `np.empty_like` vs `np.empty`" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "name": "stdout", 136 | "output_type": "stream", 137 | "text": [ 138 | "Small size\n", 139 | "366 ns ± 47.7 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 140 | "542 ns ± 21.5 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 141 | "Large size\n", 142 | "15.9 µs ± 667 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 143 | "17.5 µs ± 1.71 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "print(\"Small size\")\n", 149 | "a = np.empty(shape=(4, 500))\n", 150 | "%timeit -n10000 np.empty_like(a)\n", 151 | "%timeit -n10000 np.empty(a.shape)\n", 152 | "print(\"Large size\")\n", 153 | "a = np.ones(shape=(8, 50000))\n", 154 | "%timeit -n100 np.empty_like(a)\n", 155 | "%timeit -n100 np.empty(a.shape)" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "## `np.r_` vs `np.concatenate` vs `vstack` vs `np.array`" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 6, 168 | "metadata": {}, 169 | "outputs": [ 170 | { 171 | "name": "stdout", 172 | "output_type": "stream", 173 | "text": [ 174 | "Small size\n", 175 | "22.4 µs ± 236 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 176 | "9.2 µs ± 82.6 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 177 | "12.6 µs ± 142 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 178 | "3.43 µs ± 66.8 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 179 | "4.85 µs ± 85.1 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 180 | "\n", 181 | "Large size\n", 182 | "3.79 ms ± 90.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 183 | "3.71 ms ± 42.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 184 | "4.33 ms ± 553 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 185 | "4.04 ms ± 312 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "print(\"Small size\")\n", 191 | "a = np.ones(500)\n", 192 | "%timeit -n10000 np.r_[[a], [a], [a], [a]]\n", 193 | "%timeit -n10000 np.concatenate(([a], [a], [a], [a]))\n", 194 | "%timeit -n10000 np.vstack((a, a, a, a))\n", 195 | "%timeit -n10000 np.array([a,a,a,a]) # another approach for this case\n", 196 | "%timeit -n10000 np.array([a, a, a, a]).reshape((-1, a.shape[-1]))\n", 197 | "assert np.allclose(np.r_[[a], [a], [a], [a]], np.concatenate(([a], [a], [a], [a])))\n", 198 | "assert np.allclose(np.r_[[a], [a], [a], [a]], np.vstack((a, a, a, a)))\n", 199 | "assert np.allclose(np.r_[[a], [a], [a], [a]], np.array([a,a,a,a]))\n", 200 | "assert np.allclose(np.r_[[a], [a], [a], [a]], np.array([a, a, a, a]).reshape((-1, a.shape[-1])))\n", 201 | "print(\"\\nLarge size\")\n", 202 | "a = np.random.rand(8, 50000)\n", 203 | "%timeit -n100 np.r_[a, a, a, a]\n", 204 | "%timeit -n100 np.concatenate((a, a, a, a))\n", 205 | "%timeit -n100 np.vstack((a, a, a, a))\n", 206 | "%timeit -n100 np.array([a, a, a, a]).reshape((-1, a.shape[1]))\n", 207 | "\n", 208 | "assert np.allclose(np.r_[a, a, a, a], np.concatenate((a, a, a, a)))\n", 209 | "assert np.allclose(np.r_[a, a, a, a], np.vstack((a, a, a, a)))\n", 210 | "assert np.allclose(np.r_[a, a, a, a], np.array([a, a, a, a]).reshape((-1, a.shape[1])))" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "## `np.c_` vs `np.hstack`" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 7, 223 | "metadata": {}, 224 | "outputs": [ 225 | { 226 | "name": "stdout", 227 | "output_type": "stream", 228 | "text": [ 229 | "Small size\n", 230 | "33.3 µs ± 592 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 231 | "7.04 µs ± 140 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 232 | "3.74 µs ± 102 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 233 | "\n", 234 | "Large size\n", 235 | "3.25 ms ± 54.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 236 | "3.19 ms ± 27.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 237 | "3.2 ms ± 60.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 238 | ] 239 | } 240 | ], 241 | "source": [ 242 | "print(\"Small size\")\n", 243 | "U = np.ones(shape=(2, 400))\n", 244 | "%timeit -n10000 np.c_[U[:, 0], U]\n", 245 | "%timeit -n10000 np.hstack((U[:, 0, np.newaxis], U))\n", 246 | "%timeit -n10000 Unew = np.empty(shape=(U.shape[0], U.shape[1] + 1)); Unew[:, 0] = U[:, 0]; Unew[:, 1:] = U;\n", 247 | "\n", 248 | "assert np.allclose(np.c_[U[:, 0], U], np.hstack((U[:, 0, np.newaxis], U)))\n", 249 | "Unew = np.empty(shape=(U.shape[0], U.shape[1] + 1)); Unew[:, 0] = U[:, 0]; Unew[:, 1:] = U;\n", 250 | "assert np.allclose(np.c_[U[:, 0], U], Unew)\n", 251 | "\n", 252 | "print(\"\\nLarge size\")\n", 253 | "U = np.ones(shape=(3, 400000))\n", 254 | "%timeit -n100 np.c_[U[:, 0], U]\n", 255 | "%timeit -n100 np.hstack((U[:, 0, np.newaxis], U))\n", 256 | "%timeit -n100 Unew = np.empty(shape=(U.shape[0], U.shape[1] + 1)); Unew[:, 0] = U[:, 0]; Unew[:, 1:] = U;\n", 257 | "assert np.allclose(np.c_[U[:, 0], U], np.hstack((U[:, 0, np.newaxis], U)))\n", 258 | "Unew = np.empty(shape=(U.shape[0], U.shape[1] + 1)); Unew[:, 0] = U[:, 0]; Unew[:, 1:] = U;\n", 259 | "assert np.allclose(np.c_[U[:, 0], U], Unew)" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": {}, 265 | "source": [ 266 | "## `np.diff` vs `U[..., 1:] - U[..., :-1]`" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 8, 272 | "metadata": {}, 273 | "outputs": [ 274 | { 275 | "name": "stdout", 276 | "output_type": "stream", 277 | "text": [ 278 | "Small size\n", 279 | "8.39 µs ± 428 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 280 | "3.81 µs ± 170 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n", 281 | "Large size\n", 282 | "457 µs ± 22.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n", 283 | "451 µs ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "print(\"Small size\")\n", 289 | "U = np.ones(shape=(4, 200))\n", 290 | "%timeit -n10000 np.diff(U, axis=-1)\n", 291 | "%timeit -n10000 U[..., 1:] - U[..., :-1]\n", 292 | "assert np.allclose(np.diff(U, axis=-1), U[..., 1:] - U[..., :-1])\n", 293 | "\n", 294 | "print(\"Large size\")\n", 295 | "U = np.ones(shape=(20, 10000))\n", 296 | "%timeit -n100 np.diff(U, axis=-1)\n", 297 | "%timeit -n100 U[..., 1:] - U[..., :-1]\n", 298 | "assert np.allclose(np.diff(U, axis=-1), U[..., 1:] - U[..., :-1])" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "## `a.copy()` vs `np.array(a)` vs `np.r_[a]`" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 9, 311 | "metadata": {}, 312 | "outputs": [ 313 | { 314 | "name": "stdout", 315 | "output_type": "stream", 316 | "text": [ 317 | "2.41 µs ± 893 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n", 318 | "1.67 µs ± 448 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n", 319 | "14 µs ± 5.85 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n", 320 | "2.2 µs ± 211 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n" 321 | ] 322 | } 323 | ], 324 | "source": [ 325 | "a = np.arange(5000)\n", 326 | "%timeit -n1000 a.copy()\n", 327 | "%timeit -n1000 np.array(a)\n", 328 | "%timeit -n1000 np.r_[a]\n", 329 | "%timeit -n1000 anew = np.empty_like(a); anew[:] = a[:];" 330 | ] 331 | } 332 | ], 333 | "metadata": { 334 | "kernelspec": { 335 | "display_name": "Python 3", 336 | "language": "python", 337 | "name": "python3" 338 | }, 339 | "language_info": { 340 | "codemirror_mode": { 341 | "name": "ipython", 342 | "version": 3 343 | }, 344 | "file_extension": ".py", 345 | "mimetype": "text/x-python", 346 | "name": "python", 347 | "nbconvert_exporter": "python", 348 | "pygments_lexer": "ipython3", 349 | "version": "3.5.3" 350 | } 351 | }, 352 | "nbformat": 4, 353 | "nbformat_minor": 2 354 | } 355 | -------------------------------------------------------------------------------- /smooth_transition_between_analytic_functions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Smooth transition between analytic functions\n", 8 | "\n", 9 | "The idea is to create a smooth transition in a place where a function is discontinuous.\n", 10 | "\n", 11 | "This way to create smoothness is useful because it doesn't have to evaluate the original piecewise functions outside their original domains.\n", 12 | "\n", 13 | "The original discontinuity usually happens because a function is already defined like this:\n", 14 | "\n", 15 | "$$\n", 16 | " f(x) = \\left\\{\n", 17 | " \\begin{array}{r}\n", 18 | " f_{left}(x) &: x < x_{threshold}\\\\\n", 19 | " f_{right}(x) &: x \\geq x_{threshold}\\\\\n", 20 | " \\end{array}\n", 21 | " \\right.\n", 22 | "$$\n", 23 | "\n", 24 | "For example:" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": { 31 | "collapsed": true 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "import sympy\n", 36 | "from sympy import Piecewise\n", 37 | "import numpy as np\n", 38 | "\n", 39 | "\n", 40 | "# For example:\n", 41 | "x_ = sympy.symbols('x', real=True)\n", 42 | "\n", 43 | "f_left_ = x_**1.2\n", 44 | "f_right_ = 10.0 / x_**0.2\n", 45 | "\n", 46 | "x_threshold = 10.0 ** (5 / 7)\n", 47 | "\n", 48 | "f_ = Piecewise(\n", 49 | " (f_left_, x_ < x_threshold),\n", 50 | " (f_right_, True)\n", 51 | ")\n", 52 | "\n", 53 | "f = sympy.lambdify(x_, f_)" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 2, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "data": { 63 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAdkAAAFJCAYAAADXIVdBAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xl0m9WdN/Dvo92yZEm25SXeYjtx9j1kYQtLILSFtiwB\nMhSmA9PDcJh26OkMFIZCKDSlZ2bKmeGUAnPad6bQl5bhhS5Tyr6kWUiczbEdO4kdx/si2ZJteZFt\n6b5/yFbixLacRNLz6NH3c46PY+ux/Lt5JH19r+5zrySEECAiIqKo08hdABERkVoxZImIiGKEIUtE\nRBQjDFkiIqIYYcgSERHFCEOWiIgoRnTRvkOXqz+q9+dwmOHxDEb1PuXCtiiPWtoBsC1KpJZ2AGzL\nTJxO67S3Kb4nq9Np5S4hatgW5VFLOwC2RYnU0g6AbblYig9ZIiKiRMWQJSIiihGGLBERUYwwZImI\niGKEIUtERBQjDFkiIqIYYcgSERHFCEOWiIgoRhiyREREMcKQJSIiihGGLF2SvoER7DrajsHhMblL\nISJSnKhvEEDJ5Zfv1uBofTfe+PgEtqwrxOY1BTCb+LAiIgLYk6VL4On342h9NwBAq9Hgd39pwKM/\n34M/7Gpgz5aICOzJ0iU4dMIFALjnhjJcvjQHnxxqwfv7m/G7XQ34oLwZN1xWgBvW5sNs0stcKRGR\nPBiydNEOHu8CAKwucyLFqMNXNs7Fdavz8enhVry3rwm/39WA9/c34ZqVebjhsgI4rEaZKyYiii+G\nLF2UXp8fx5u9KM1LmxSeKUYdvryhCNetzsOnh1vxQXkz3tvfhA8PNGPjkhzctL4QczJTZayciCh+\nIobs22+/jXfeeQcA4Pf7UVNTg927dyMtLS3mxZFyfVHVASGANWVZU95uMujwpfVF2LymAHurO/Dn\nfU3YVdmOXZXtWDU/E1/aUIR5ebY4V01EFF8RQ/a2227DbbfdBgB45plncPvttzNgCXsq2wAAaxY4\nZzxOr9Pg6hVzcOXyXBw+4caf9zXi8Ek3Dp90ozQvDZvXFGDNAid0Ws7BIyL1mfVwcWVlJerq6vD0\n00/Hsh5KAIPDozh60oXCbAuc9pRZ/YxGkrBmgROryzJxotmLP+9rwtH6btS3VsNuMeDa1fnYtHIO\n0syGGFdPRBQ/khBCzObAv//7v8c3vvENbNiwYcbjxsYC0Om0USmOlOmTA8144Y1D+MaXFuKuzQsu\n+n7aXD78aXcDPtzfhCH/WKjXuyoPt1xZgtJ8exQrJiKSx6x6sn19fWhoaIgYsADg8QxeclFnczqt\ncLn6o3qfclFLWz4tbwIALMq3XVJ79AC+fsVcbFmbj92V7fj4YAs+Lm/Gx+XNmJ9vw7Wr8rBmgRP6\nGP7RppZzArAtSqSWdgBsS6T7m86sQra8vBwbN26MWkGUuIb8Y6hq6EFRjhW5GdGZJZxi1GHz2gJc\ntyYfVad68NGBZlQ19OBkSy8sH+lx+dIcbFo5J2q/j4goXmYVsg0NDcjPz491LZQAjtZ3YywQxBXL\n50T9vjWShOWlGVhemoHOnkHsrGjDrsp2fFDejA/Km7GgwI5NK+fEvHdLRBQtswrZv/3bv411HZQg\nDowvQHF5DEL2bNnpZmy9dh6+flUJDp904fMjbahp9OB4sxepH+pwxbJcXLk8F/lOS0zrICK6FFyM\ngmbNPxJAZX03ctLNKMyxwu32xfx36nUarFuUjXWLstHpGcTOI5N7t0XZVly+LAfrF2dzZjIRKQ5D\nlmat8lQ3RsaCWLvQCUmS4v77sx2h3u2tV5fgyEk3dle2o/JUD9746CTe/KQOy0szcPnSXKyYl8Hr\nbolIERiyNGsTQ8VrF0y9ylO86LQarF2YhbULs9A7MIJ91R3YXdURXuQi1aTD+sXZuHxpLopzrbL8\nQUBEBDBkaZZGRgOoqO+G025CQZZy3ge1pRpw47pC3LiuEE2d/dhT1YEvqjvwyaFWfHKoFVn2FKxb\nnIV1i7L5/i0RxR1DlmaluqEH/pEA1q7KU2zPsDDbisJsK+64phRVDT3Yf6wTh0+68b97GvG/exqR\nl5mKdYuysG5xNrIdZrnLJaIkwJClWTlwPLR37NqF8g4Vz4ZOq8HKeZlYOS8T/pEAKurd2F/ThaP1\n3XjnLw145y8NmJtjxbpF2bjpihK5yyUiFWPIUkSjY0EcqXMjI82IuTnTr2yiREaDNjw7eXB4DIdP\nurCvphPHGjw43dGPNz+tQ3GuFavLnFizIAs56ezhElH0MGQpoprGHgz5x3DV8lzFDhXPhtkUur72\nimW56B8cwcHjLhxt6MHRk240tPfj/31+CnmZqeOB60RBliWh20tE8mPIUkThoWKZZxVHk9VswDWr\n8rD1xoVoaOpBRZ0bh064UNXQgz/uOY0/7jmNTJspHLileTZoGLhEdIEYsjSjsUAQh0+4YLcYUJKn\nzn2ELSn6cA93eGQMVad6cPCECxV17vCiF2lmPZaVZmBFaSaWFKcjxcinDhFFxlcKmtHxJi8Ghsdw\n/er8pOjJmQy68DW4o2NB1DT24OBxFyrqu7G7sgO7Kzug1UhYUGjHitJMrJiXgSzOVCaiaTBkaUYH\nJxagWOiUuZL40+s0WF6aieWlmQgKgcaOflTUuVFR141jpz04dtqDNz4+idwMM1bMy8SK0gzMy7dB\nq+FqU0QUwpClaQWDAgdPuJBm1mN+km+irpEkFOemoTg3DV+/qgSefj+O1k8Ebg/e29eE9/Y1IcWo\nw+IiB5aUpGPp3HRk2lPkLp2IZMSQpWmdaPaif3AU16zKg0aj/qHiC+GwGrFpZR42rczDyGgAtU0e\nVNR1o/JUNw6ecOHgidBksex0M5YWp2NpcToWFjpgNHCLPqJkwpClaU2sVbxmQfINFV8Ig14bHlYW\nQqDLM4Sqhh5UN/SgptGDjw+24OODLdBqJMzPt2FpSQaWFqcjP8uSFO9zEyUzhixNKShCQ8WWFD0W\nFCT3UPGFkCQJ2elmZKebcf2afIwFgqhr6Q2Hbm2TF7VNXrz1WT2sZj0WFjqwsMiBRUUOZDtSeF0u\nkcowZGlK9a296PWN4Krludw27hLotBosLAoF6R3XlKJvYATVp3tQdaoHtU0elNd2obw2NGJgtxiw\naPzYRYUOvp9LpAIMWZrSgdrQe4prVLQAhRKkpRqwcUkONi7JCQ8t1zR6UNPoQW2TB3urO7G3uhMA\nkGkzhXu5CwsdcFiNMldPRBeKIUvnEULg4Imu0EzZuQ65y1Gts4eWr1mVByEEWt0DocBt9OB4kxe7\njrZj19F2AECWIwVl+XbML7ChrMCOLDuHl4mUjiFL52lo70dPnx+XL83hUHEcSZKEfKcF+U4Lblhb\ngGBQoLnLF+7lnmzxYldlO3ZVhkLXlmrA/AI7yvJDoZuewf1yiZSGIUvnmZhVrKa1ihORRiOhKMeK\nohwrblpfiGBQoMXlw4lmL0609OJksxcHartwYPw9XbNJh5I5aSjLt6OswI7iXCv0Ol4yRCQnhixN\nIoTAweNdMBq0WFLMoWIl0Wik8Mb0m9cWQAgBl3cIJ5p7caLFi1Ntfag6FZpUBQA6rYSibCtK5thQ\nmpeG0jk2pKcZOcRMFEcMWZqkqdMHl3cY6xdnsxekcJIkIcthRpbDjCuX58LptKKuwY2TLb040ezF\nydZenO7oR31bHz48EPoZu8WA0jk2lObZUDInDXNzrDDoeZ6JYoUhS5OcGSrmAhSJyGYxhjc4AAD/\naACNHf2ob+tFfWsf6lt7J61IpdVIKMiyoDTPhtI5aSjNsyHTZmJvlyhKGLIUJoTAgeMuGPQaLC3J\nkLscigKjXouygtB7tEDoHHf3DeNUWx/qWntxqq0PjR39ON3Rj48Phn7GkqLH3FwrinPSQp9z02C3\n8PIhoovBkKWwVvcAOnsGsXaBE0YOIaqSJEnItKUg05aCdYuyAQCjYwE0dvpwqrUXdW19ON0++b1d\nIDTMPDcnDcW5VszNDQ0zW80GuZpBlDAYshQ2MUt1YqiRkoNep8W8PBvm5dlw4/j3+gdH0NjRj4aO\nfpxu78Ppjn4cqXPjSJ07/HOZNhPm5oR6unPHZ0GbTXp5GkGkUAxZCiuv7YJep8EyDhUnPavZENrI\n4KzHgtfnx+n2fpzuCIVuQ3sfDhx34cBxV/iYTJspNAM6yzI+E9oCh5Uzmil5MWQJANDq8qG9exBr\nypxIMfJhQeezW4xYOd+IlfMzAYTe3+3p8+N0Rx8a2vvR2NmPps5+HDrhwqETZ4LXkqJHQZYFReOh\nW5BtRW66mdsnUlKY1avpK6+8gk8++QSjo6PYtm0btm7dGuu6KM4mFqm/bBGHiml2JElChs2EDJsp\nvMa1EAJe3wiau/rR2OlDU2c/mjt94fWZJxh0GuQ5LSgaD93CbAvyMlNhMvAPPFKXiI/offv24fDh\nw3jjjTcwNDSEX/7yl/Goi+JICBEeKl5eyqFiuniSJMFhNcJhNWJ5aWb4+4PDY2ju6kdT15ngbeoM\nDTmfzWk3Id9pQVlROhypeuQ7LchOT4FWw+U9KTFFDNldu3ahrKwMDz/8MHw+Hx599NF41EVx1Ooe\nCA0VL3CyJ0ExYTbpsKDQgQWFZ1YRGx0Los09gKauUOi2uHxocQ3g8Ek3Dp88M8FKp9VgToYZeU4L\n8rNSw+s72y0GvtdLihfxFdXj8aCtrQ0vv/wyWlpa8NBDD+G9997jg1tFymvGh4o5q5jiSK/ThNdm\nniCEQN/ACPpHgqiuc6HF5UOra2A8jH1A9ZmfTzXpQsHrDAVvnjMVuRmpsKRwhjMpR8SQtdvtKCkp\ngcFgQElJCYxGI3p6epCRMfWwosNhhi7Ky/E5ndbIByUIpbVFCIFDJ90w6LW4bv3cC5r0pLS2XCy1\ntANQR1sm/tRbddYGFYGgQGf3AE6396GxvQ+nO0Kf61q8ONHsnfTzdqsRhdlW5I/PcC7IsaIgywq7\nTLOc1XBOJrAtFy7iK+qaNWvwq1/9Cn/zN3+Drq4uDA0NwW63T3u8xzMY1QKdTitcrv6o3qdclNiW\nli4fWl0+rF3ghK9vCL5Z/pwS23Ix1NIOQP1t0QOYn2vF/FwrgDwAwMhoAG3dA2jpGkBbd6jH2+Ye\nQGWdG0fPuqYXCPV8czNTMSfDjDkZqZiTGer5xnLTBLWfk0QV7bbMFNgRQ/baa69FeXk57rjjDggh\n8NRTT0Gr5WpAarGfC1BQAjPotZibk4a5OWmTvu8fDaCjezAcvO3dg2hzD+BUax/qWnonHWs0aJGb\nbsaczFDw5qSbkZ1uRpY9BXodJ1zRpZnV2CAnO6nTxKxig06DFWfNBCVKdEa99rz3e4HQZKtOz2A4\ndNvcoR5wi8uH0x2TezaSFFpcIyc9FLw56SnhAOYCGzRbnEqaxFpc42sVL8yC0cDRCVI/vU4Tnp18\ntkAwCJd3GO3uAXT0DIY/OnsGUXmqG5Wnuicdb9Rrke1IQU6GORy8OeMfXMyFzsZHQxIrr+0EwFnF\nRFqNJhyS5xocHkVHzxA6egbGP4fCt6NnMDTj+Ry2VEM4dEvy7Ug1aEL7/tpT+MdsEmLIJqnQUHFo\nW7vlXKuYaFpmkx4lc/QomTP5fd+gEPD2+9E+EbrdZ3rAJ5tDs553VrRN+hlbqgFZjpTQhz0lFL7j\nX6dycwVVYsgmqeYuH4eKiS6BRpKQnmZCepoJS+amT7ptdCyALs8Q/EHgZGMPujxD6PIMoss7hLrW\nXpw8Z/IVEJr9nOUwI9uRAqc9FLzZDjOcjhSkmfV8DzhBMWST1MRaxes4VEwUdXqdFnlOC5xOK0qy\nJ7//OxYIort3GJ2eIbi8Q+j0DI6H8BCau85fahIIzYDOtofCN9NuQqYtBc7xz5k2Ewzc/1mxGLJJ\nSAiBA7VdMOg1WMa1ioniSqfVIHt8stS5gkGBnv7hUOh6h8Lh2+UZRIdn6veAgdAwdKbdBKftTAhn\n2kzItKcg3WqETstLkeTCkE1CzV0+dHqGcNnCLBj5FzCRYmg00nhApmDxObcJIdA3OAq3dwiu3iG4\nvcNw9w7BNf75dHs/6lvP7wVrxjdtCPd8zwljm8UADYeiY4Yhm4TC29pxqJgoYUiSBFuqAbZUA0rz\nbOfdHggG4en3w+0dnhzCvcNwe4dQ2+QF4D3v5/Q6DdLTTMhIMyIjzRT6sIXea86wmdgTvkQM2SQT\nXoCCQ8VEqqLVaMK94IVwnHf76FgA7t7h0If3TPi6eodD7xH3TL0krgTAZjEgOyMVNrN+PJDPhHFG\nmhFmzoyeFkM2yTR1+tDlGcK6RRwqJkomep0WuRmh9Zqn4h8JoKc/FLjdfeMfvX509w2jp28Ydc1e\nBIJiyp9NMWrPC9/0NCMy01KQnmaEzWJI2j2BGbJJhkPFRDQVo2HmEE7PsKCuwX1WAA+jp88/6etW\n18CUP6uRJNgsBqRbjXBYjXBYTXBYjUhPMyJ9/N82i0GVw9IM2SQyMavYqNdiGRegIKILoNWcuS54\n/hS3CyEw5B+De4rw9fj88PT5cbqjH/Vt50/OAkLD0mkWA9KtpjNhnBb6PPE9ewK+P8yQTSJNnT50\neUNDxbyujoiiSZIkmE16FJr0KMyeeuu3oBDoGxiBp9+Pnj4/PP3D8PT7Q1/3+9HTNzzttcIT0lIN\n48E73gseD2KHJRTCDotRUQvsMGSTyH6uVUxEMtJIEuwWI+wWI4pzpz4mKAR8g6PjwXtWCPcNh8O4\nzT2Axo7p94NNMepgtxgmha/dEgpju8UIu+P8a5RjhSGbJDhUTESJQCNJSEs1IC3VcN5WhROEEBgY\nHpsUvN5+Pzy+0GevLxTM7d1Tz5jeuCwX3/rKolg2I4whmyQaO/vh8g5j/eJsDhUTUUKTJAmWFD0s\nKdMPTQPAyGgA3oGRScHr9flxxcr8uNXKkE0S5TWhWcVrF3ComIiSg0GvDe12ZE+Z9H2n0wqXa/rh\n5mhKrGladFEmFqAwGrRYVpIe+QeIiCgqGLJJ4HRHP9y9w1g5L5NDxUREccSQTQJcgIKISB4MWZUT\nQqC8pgsmgxZLizlUTEQUTwxZlTvV1ofuvmGsmu/kUDERUZwxZFVuX01oAYp1izhUTEQUbwxZFQsG\nQ7OKU006LOFQMRFR3DFkVexkixe9vhGsWeBMuEW1iYjUgK+8KrZvfAGKyxZly1wJEVFyYsiqVCAY\nxIHaLqSZ9VhYaJe7HCKipMSQVamaRg98Q6NYuzALWg1PMxGRHPjqq1L7j4WGitdxqJiISDYMWRUa\nHQvi0AkXHFYj5uXb5C6HiChpzWoXnltvvRUWiwUAkJ+fjx//+McxLYouTXVDDwb9Y7hyeS40kiR3\nOURESStiyPr9fggh8Nprr8WjHoqC/eMLUKxfzKFiIiI5RRwurq2txdDQEO6//37cd999OHLkSDzq\noovkHw3gcJ0bTrsJc3Om38yYiIhiTxJCiJkOOH78OCoqKrB161acPn0a3/rWt/Dee+9Bp5u6Ezw2\nFoBOxzVy5bK7og3P/6ocW6+fj/u+vFjucoiIklrE4eLi4mIUFRVBkiQUFxfDbrfD5XIhNzd3yuM9\nnsGoFhjPHexjLR5t+XDfaQDA0iJHTH+XWs6LWtoBsC1KpJZ2AGxLpPubTsTh4rfeegvPP/88AKCz\nsxM+nw9OpzNqxVH0DPnHcLS+G7kZZuQ7U+Uuh4go6UXsyd5xxx14/PHHsW3bNkiShB07dkw7VEzy\nOlLnxuhYEOsWZUPirGIiItlFTEuDwYB/+7d/i0ctdIn2H+O2dkRESsLFKFRiYHgUVQ09KMyyIDeD\nQ8VERErAkFWJQ8ddCAQFLmMvlohIMRiyKjGxAAXXKiYiUg6GrAr0DYzgWKMHJXPS4LSnyF0OERGN\nY8iqwMHjXRACWLeQQ8VERErCkFWBfTVdkABcxqFiIiJFYcgmOE+/HyebvZhfYIfDapS7HCIiOgtD\nNsGV13ZBgNfGEhEpEUM2we2v6YQkAWsXMGSJiJSGIZvAXN4hnGrrw6IiB9JSDXKXQ0RE52DIJrB9\nx7g5OxGRkjFkE5QQAl8c64ROq8GaMg4VExEpEUM2QbW4BtDmHsCK0gyYTdwViYhIiRiyCeqLYx0A\nOFRMRKRkDNkEFBQC+491IsWoxfLSDLnLISKiaTBkE1BdSy+6+/xYXeaEQa+VuxwiIpoGQzYBTcwq\n3rA4R+ZKiIhoJgzZBDMWCKK8tgtpqQYsLLLLXQ4REc2AIZtgjp32wDc0inULs6DV8PQRESkZX6UT\nzD7OKiYiShgM2QTiHw3g0Ak3Mm0mlMxJk7scIiKKgCGbQCrq3PCPBrBhSTYkSZK7HCIiioAhm0C+\nqJ5Yq5iziomIEgFDNkH4hkZReaobBVkW5GWmyl0OERHNAkM2QRw83oVAUGADJzwRESUMhmyCmFiA\nYt0ihiwRUaJgyCaAnr5hHG/yoizfhgybSe5yiIholhiyCWB/TRcEgPVLOOGJiCiRMGQTwL6aTmg1\nEtYucMpdChERXQCGrMK1dw+gsaMfS4rTYTUb5C6HiIguwKxCtru7G5s2bUJ9fX2s66FzTEx44jKK\nRESJJ2LIjo6O4qmnnoLJxAk38SaEwL5jnTDoNFg1P1PucoiI6AJFDNmf/OQnuPvuu5GVlRWPeugs\npzv60ekZwsr5mTAZdHKXQ0REF2jGV+63334b6enpuOqqq/Dqq6/O6g4dDjN0Om1UipvgdFqjen9y\nupC2/H5PIwBgy8ZiRf4fKLGmi6GWdgBsixKppR0A23IxJCGEmO7Ge+65B5IkQZIk1NTUYO7cufj5\nz38Op3P6Wa4uV39UC3Q6rVG/T7lcSFuCQYHvvbQbY2NBvPDtK6HTKmuOmlrOi1raAbAtSqSWdgBs\nS6T7m86MPdlf//rX4X/fe++92L59+4wBS9FT0+RBr28Em1bOUVzAEhHR7PDVW6H2VoU2Z9/IBSiI\niBLWrGfTvPbaa7Gsg87iHwng4AkXMm0mzMu3yV0OERFdJPZkFehwnQv+kdDm7Bpuzk5ElLAYsgq0\ntyq0AAWHiomIEhtDVmF6B0ZQ3dCDuTlW5GZwc3YiokTGkFWY/TWdCAqBjUvZiyUiSnQMWYXZW9UB\njSRxc3YiIhVgyCpIe/cATo/vuGNL5Y47RESJjiGrIHurx6+NXcpeLBGRGjBkFSIoBL6o7oTRoMWq\n+VxVi4hIDRiyClHX0gt37zDWlDlh1Ed3gwUiIpIHQ1YhzgwVc1YxEZFaMGQVYHQsiPKaLtgtBiwq\ndMhdDhERRQlDVgGO1rsx6B/DhsU50Gi4jCIRkVowZBVgb3VoGcUNSzirmIhITRiyMvMNjeJovRt5\nzlQUZFnkLoeIiKKIISuzA7VdGAsIXL4kBxJ33CEiUhWGrMz2VndAArB+MYeKiYjUhiErI5d3CCdb\nerGwyIH0NJPc5RARUZQxZGX0xfi1sZzwRESkTgxZmQghsLe6E3qdBmsXZMldDhERxQBDViYN7f3o\n6BnEynmZSDHq5C6HiIhigCErk91V7QCAK5ZxGUUiIrViyMpgdCyI/cc6YUs1YElxutzlEBFRjDBk\nZVBR58bA8Bg2LMmGVsNTQESkVnyFl8GeqtCs4iuW5spcCRERxRJDNs76BkZQeaobhdkW5HMZRSIi\nVWPIxtkXxzoRCAr2YomIkgBDNs72VLZDq5GwngtQEBGpHkM2jhraetHU5cOykgykmQ1yl0NERDHG\nkI2jTw40A+C1sUREyYIhGyeBYBCfHWpBqkmH5aWZcpdDRERxEHE9v0AggCeffBINDQ2QJAnPPPMM\nysrK4lGbqlSd6oG334/rVudBr+PfNkREySDiq/2nn34KAPjNb36DRx55BC+88ELMi1Kj3RPXxi7j\nrGIiomQRsSe7efNmXHPNNQCAtrY2pKWlxbom1RkYHsWRky4UZFswN8cqdzlERBQns9r+RafT4bHH\nHsOHH36I//iP/5jxWIfDDJ1OG5XiJjidiR1MB/Y0YCwgcN3aQmRlqeePlEQ/LxPU0g6AbVEitbQD\nYFsuhiSEELM92OVy4c4778Sf/vQnmM3maY7pj1pxQOg/Itr3GW8/+tUBnGrvw//5wY0IjozJXU5U\nqOG8AOppB8C2KJFa2gGwLZHubzoR35P93e9+h1deeQUAkJKSAkmSoOGi9rPW3j2A+rY+LJ6bjgxb\nitzlEBFRHEUcLr7xxhvx+OOP45577sHY2BieeOIJmEymeNSmCmc2A+C1sUREySZiyJrNZvz7v/97\nPGpRnaAQ2FvdAZNBi1VlTrnLISKiOOO4bwzVNnrQ0+fHZQuzYNRHdzIYEREpH0M2hnZX8tpYIqJk\nxpCNkSH/GA6e6ILTbsL8fJvc5RARkQwYsjFSXtuFkdEgrlw+B5IkyV0OERHJgCEbI3+paIMEziom\nIkpmDNkYaHWHro1dUpKO9DRe7kRElKwYsjGw+2g7AOCq5XNkroSIiOTEkI2ysUAQe6rakWrSYeU8\n7htLRJTMGLJRVlnfjb7BUWxcksN9Y4mIkhxTIMr+Mj5UfOVyXhtLRJTsGLJR1Ovz42h9N4qyrSjM\nVs+WUEREdHEYslG0p7oDQSHYiyUiIgAM2agRQmDX0XbotBqsX5wtdzlERKQADNkoqW/rQ3v3IFaX\nZcKSope7HCIiUgCGbJTsOtoGgNfGEhHRGQzZKPCPBLCvpgsZaUYsKnLIXQ4RESkEQzYKDhzvgn8k\ngCuW5UKj4WYAREQUwpCNgolrY7lvLBERnY0he4k6ewZxotmLRUUOOO0pcpdDREQKwpC9RLsqucIT\nERFNjSF7CYJBgd2V7Ugx6rCmzCl3OUREpDAM2UtQeaobXt8I1i/KgkGvlbscIiJSGIbsJfj8SOja\n2E0r82SuhIiIlIghe5E8/Wc2AyjK4WYARER0PobsRdp1tA1BIbBpJVd4IiKiqTFkL0JQCOysaIdB\nz80AiIhoegzZi3CsoQfdfcNYtygbKUad3OUQEZFCMWQvwucVExOeOFRMRETTY8heoN6BERw56Ua+\nMxUluWlyl0NERArGkL1AuyvbEQgKbFqZB0niZgBERDS9Gd9QHB0dxRNPPIHW1laMjIzgoYcewvXX\nXx+v2hTVKU51AAAPX0lEQVQnNOGpDXqdBhuWcMITERHNbMaQ/cMf/gC73Y5/+Zd/gdfrxde//vWk\nDtnjjR50eYZw+dIcpJr0cpdDREQKN2PI3nTTTdiyZQsAQAgBrTa5lw6cmPB09QpOeCIiosgkIYSI\ndJDP58NDDz2EO++8E7fccsuMx46NBaDTqS+Me31+fPOHHyA304yf/dN1fD+WiIgiiniRZ3t7Ox5+\n+GH81V/9VcSABQCPZzAqhU1wOq1wufqjep8X4/39TRgLBHHFkhy43b6Lug+ltCUa1NIWtbQDYFuU\nSC3tANiWSPc3nRlD1u124/7778dTTz2FjRs3Rq2gRCOEwOdH2qDTSti4NEfucoiIKEHMeAnPyy+/\njL6+Prz00ku49957ce+992J4eDhetSnGyZZedPQMYs2CLFjNBrnLISKiBDFjT/bJJ5/Ek08+Ga9a\nFOvzI60AOOGJiIguDBejiKB/cATltV3ITjdjYaFd7nKIiCiBMGQj2FXZjrGAwLWruMITERFdGIbs\nDIJC4LPDrTDoNLhiGSc8ERHRhWHIzqC6oQcub2hLO67wREREF4ohO4NPD4UmPF27Ok/mSoiIKBEx\nZKfR3TuMino35uZYUcwt7YiI6CIwZKfxeUUbhACuXcVeLBERXRyG7BTGAkHsrGiD2ajDusXc0o6I\niC4OQ3YKh0640DcwgsuX5cCoV99mB0REFB8M2Sl8dnh8whOHiomI6BIwZM/R5h5AbZMXi4ocyM1I\nlbscIiJKYAzZc7AXS0RE0cKQPYt/JIDdVR2wWQxYOT9T7nKIiCjBMWTPsq+mE0P+MWxaMQc6Lf9r\niIjo0jBJxgkh8MmhFkgSt7QjIqLoYMiOO9nSi6ZOH1aXOZGeZpK7HCIiUgGG7LiPD7YAADavyZe5\nEiIiUguGLICevmEcPO5CvtOCsgJuzE5ERNHBkAXw6eFWBIXA5rX53JidiIiiJulDdnQsgM+PtCHV\npMMGrlNMRERRlPQhu+9YF3xDo7h65RwYuE4xERFFUVKHrBACHx1shiRxhSciIoq+pA7Zutbxy3bm\nO5FpS5G7HCIiUpmkDtmPDoQu27mel+0QEVEMJG3InrlsJxULCnnZDhERRV/ShuxnRyYu2yngZTtE\nRBQTSRmyZ1+2s56X7RARUYwkZcjure5E/2Dosh0jL9shIqIYSbqQDQqB9/c3QauRsHlNgdzlEBGR\nis0qZCsqKnDvvffGupa4qDrVg/buQaxblAWH1Sh3OUREpGK6SAf853/+J/7whz8gJUUd15G+v78J\nALBlXaHMlRARkdpF7MkWFhbixRdfjEctMdfU2Y+aRg8WFTlQmG2VuxwiIlK5iD3ZLVu2oKWlZdZ3\n6HCYodNFdzKR0xmdQHz9o5MAgK2by6J2nxdKrt8bC2ppi1raAbAtSqSWdgBsy8WIGLIXyuMZjOr9\nOZ1WuFz9l3w/nn4/Pj/UgtwMMwozzVG5zwsVrbYogVraopZ2AGyLEqmlHQDbEun+ppM0s4s/OdSC\nQFDgxssKoOHiE0REFAdJEbL+kQA+O9wKq1mPjUty5C6HiIiSxKxCNj8/H2+++Wasa4mZXZXtGBge\nw7Wr8rhnLBERxY3qe7KBYBAfljdDp9XgutXcbYeIiOJH9SF7oNaFLu8QrliWg7RUg9zlEBFRElF1\nyAoh8O4XjZAk4EvrufgEERHFl6pDtvJUD5q7fLhsYRayHGa5yyEioiSj6pB9d+9pAMCXNxTJWgcR\nESUn1YbsyRYvTrT0YnlpBpdQJCIiWag2ZN/d2wiAvVgiIpKPKkO2pcuHivpuzMu3oazALnc5RESU\npFQZsu9+EerFfoW9WCIikpHqQrbLO4R9NZ3Id6ZieWmG3OUQEVESU13Ivrv3NIQIvRcrcSMAIiKS\nkapC1uUdwu7KDuSkm7FuUbbc5RARUZJTVcj+757TCAQFvnrFXGg07MUSEZG8VBOyXd4h7KnqQG4G\ne7FERKQMqgnZP433Ym9hL5aIiBRCFSE7qRe7kL1YIiJSBlWELHuxRESkRAkfsh09g9hdyV4sEREp\nT8KH7Ns7TyEoBG67uoS9WCIiUpSEDtlTbX04UNuFkjlpWF3mlLscIiKiSRI2ZIUQeOuzOgDA1mtK\nuboTEREpTsKGbFVDD2qbvFhWkoEFhQ65yyEiIjpPQoZsUAi89Vk9JAC3byqRuxwiIqIpJWTI7qns\nQHOXDxuWZKMw2yp3OURERFNKuJAdHB7DW5/Xw6DT4PZNpXKXQ0RENK2EC9k/7mlA38AIvrKxCOlp\nJrnLISIimlZChWx79wA+OtCCTJsJN60vlLscIiKiGSVMyAoh8PoHJxAICmy7fj70Oq3cJREREc0o\nYUJ2Z0Ubaho9WFGagZXzM+Uuh4iIKKKECNmevmG8+WkdUoxa3HfTQi48QURECUEX6YBgMIjt27fj\n+PHjMBgMeO6551BUVBSP2sZ/v8D/+XMthvwBfPNLC+GwGuP2u4mIiC5FxJ7sRx99hJGREfz2t7/F\n9773PTz//PPxqCvstx+dQHVDD5aVZOCq5blx/d1ERESXImLIHjx4EFdddRUAYOXKlaiqqop5URN2\nV7bj/75fi/Q0I751y2IOExMRUUKJOFzs8/lgsVjCX2u1WoyNjUGnm/pHHQ4zdFGa+dvacwqZNhN+\n+ODlKFDJyk5OpzraAainLWppB8C2KJFa2gGwLRcjYshaLBYMDAyEvw4Gg9MGLAB4PIPRqQzA1quL\n8eCty+DpGYDL1R+1+5WL02lVRTsA9bRFLe0A2BYlUks7ALYl0v1NJ+Jw8erVq7Fz504AwJEjR1BW\nVha1wiKRJAk6bUJMgCYiIjpPxJ7sDTfcgN27d+Puu++GEAI7duyIR11EREQJL2LIajQa/PCHP4xH\nLURERKrCsVgiIqIYYcgSERHFCEOWiIgoRhiyREREMcKQJSIiihGGLBERUYwwZImIiGKEIUtERBQj\nDFkiIqIYkYQQQu4iiIiI1Ig9WSIiohhhyBIREcUIQ5aIiChGGLJEREQxwpAlIiKKEYYsERFRjETc\ntD0egsEgtm/fjuPHj8NgMOC5555DUVFR+PZPPvkEP/vZz6DT6XD77bfjzjvvlLHamY2OjuKJJ55A\na2srRkZG8NBDD+H6668P3/5f//Vf+J//+R+kp6cDAJ555hmUlJTIVW5Et956KywWCwAgPz8fP/7x\nj8O3JdJ5efvtt/HOO+8AAPx+P2pqarB7926kpaUBSIzzUlFRgX/913/Fa6+9hsbGRnz/+9+HJEmY\nP38+nn76aWg0Z/5mjvScktvZbampqcGzzz4LrVYLg8GAn/zkJ8jMzJx0/EyPQ7md3ZZjx47hwQcf\nxNy5cwEA27Ztw5e//OXwsYl0Xr773e/C7XYDAFpbW7FixQq88MILk45X2nmZ6vV33rx58j5XhAK8\n//774rHHHhNCCHH48GHxd3/3d+HbRkZGxObNm4XX6xV+v1/cdtttwuVyyVVqRG+99ZZ47rnnhBBC\neDwesWnTpkm3f+973xOVlZUyVHbhhoeHxde+9rUpb0u083K27du3i9/85jeTvqf08/Lqq6+Km2++\nWWzdulUIIcSDDz4ovvjiCyGEED/4wQ/EBx98MOn4mZ5Tcju3Lffcc484duyYEEKIN954Q+zYsWPS\n8TM9DuV2blvefPNN8Ytf/GLa4xPpvEzwer3iq1/9qujs7Jz0fSWel6lef+V+rihiuPjgwYO46qqr\nAAArV65EVVVV+Lb6+noUFhbCZrPBYDBgzZo1KC8vl6vUiG666Sb8wz/8AwBACAGtVjvp9urqarz6\n6qvYtm0bXnnlFTlKnLXa2loMDQ3h/vvvx3333YcjR46Eb0u08zKhsrISdXV1uOuuuyZ9X+nnpbCw\nEC+++GL46+rqaqxbtw4AcPXVV2PPnj2Tjp/pOSW3c9vy05/+FIsWLQIABAIBGI3GScfP9DiU27lt\nqaqqwmeffYZ77rkHTzzxBHw+36TjE+m8THjxxRfxjW98A1lZWZO+r8TzMtXrr9zPFUWErM/nCw85\nAIBWq8XY2Fj4NqvVGr4tNTX1vAeukqSmpsJiscDn8+E73/kOHnnkkUm3f+UrX8H27dvx3//93zh4\n8CA+/fRTmSqNzGQy4YEHHsAvfvELPPPMM/jHf/zHhD0vE1555RU8/PDD531f6edly5Yt0OnOvLsj\nhIAkSQBC//f9/f2Tjp/pOSW3c9sy8eJ96NAhvP766/jmN7856fiZHodyO7cty5cvx6OPPopf//rX\nKCgowM9+9rNJxyfSeQGA7u5u7N27F7fddtt5xyvxvEz1+iv3c0URIWuxWDAwMBD+OhgMhk/2ubcN\nDAxMenFXovb2dtx333342te+hltuuSX8fSEE/vqv/xrp6ekwGAzYtGkTjh07JmOlMysuLsZXv/pV\nSJKE4uJi2O12uFwuAIl5Xvr6+tDQ0IANGzZM+n6inRcAk95TGhgYCL+3PGGm55QSvfvuu3j66afx\n6quvht8XnzDT41BpbrjhBixdujT873MfR4l2Xt577z3cfPPN543IAco9L+e+/sr9XFFEyK5evRo7\nd+4EABw5cgRlZWXh20pLS9HY2Aiv14uRkREcOHAAq1atkqvUiNxuN+6//3780z/9E+64445Jt/l8\nPtx8880YGBiAEAL79u0LPyGV6K233sLzzz8PAOjs7ITP54PT6QSQeOcFAMrLy7Fx48bzvp9o5wUA\nFi9ejH379gEAdu7cibVr1066fabnlNL8/ve/x+uvv47XXnsNBQUF590+0+NQaR544AEcPXoUALB3\n714sWbJk0u2JdF6AUBuuvvrqKW9T4nmZ6vVX7ueKIv6EuuGGG7B7927cfffdEEJgx44d+OMf/4jB\nwUHcdddd+P73v48HHngAQgjcfvvtyM7Olrvkab388svo6+vDSy+9hJdeegkAsHXrVgwNDeGuu+7C\nd7/7Xdx3330wGAzYuHEjNm3aJHPF07vjjjvw+OOPY9u2bZAkCTt27MCf//znhDwvANDQ0ID8/Pzw\n12c/xhLpvADAY489hh/84Af46U9/ipKSEmzZsgUA8Oijj+KRRx6Z8jmlRIFAAD/60Y+Qm5uLb3/7\n2wCAyy67DN/5znfCbZnqcajU3t/27dvx7LPPQq/XIzMzE88++yyAxDsvExoaGs77w0fJ52Wq199/\n/ud/xnPPPSfbc4W78BAREcWIIoaLiYiI1IghS0REFCMMWSIiohhhyBIREcUIQ5aIiChGGLJEREQx\nwpAlIiKKEYYsERFRjPx/AdfRPCwwZfkAAAAASUVORK5CYII=\n", 64 | "text/plain": [ 65 | "" 66 | ] 67 | }, 68 | "metadata": {}, 69 | "output_type": "display_data" 70 | } 71 | ], 72 | "source": [ 73 | "import seaborn\n", 74 | "import matplotlib.pyplot as plt\n", 75 | "from ipywidgets import interact, widgets\n", 76 | "\n", 77 | "x = np.linspace(0.001, 20, 1000)\n", 78 | "y = f(x)\n", 79 | "\n", 80 | "plt.plot(x, y)\n", 81 | "plt.show()" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "What we want is $C^1$ continuity, i.e., first-derivatives are continuous, without needing values of `f_left` and `f_right` outside where they are defined.\n", 89 | "\n", 90 | "To accomplish this, we'll join the second derivatives of both functions by a line, around a region $[x_0, x_1]$ that contains the threshold:" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 3, 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "data": { 100 | "image/png": "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\n4zp58qSWL18eWO55nr70pS/pk5/8pB599FF//t/+7d+qvr5emzdv1tmzZ7VgwYKKjfMDqCyncEz7\nOkmW8zzEnk+eAAAHQ0lEQVQNDeU0mMn6Q7bpoZwGh7L6+aV+JT9MFYfkdUJ0sDCcO5TVYCbYa630\n8U/bslQVHQm3m+ZERwXeNQEZcRQZ7rFGXFvz5tYqPTgUCNDocPC6bjBAo8Pr3miAAqWUFdqtra16\n/PHH1draqkgkon379kmS/uEf/kGLFi1SLpfTf/zHfyidTusnP/mJJGnbtm169NFH9dhjj+nVV1+V\n4zjavXt35SoBUFGOkw+aA9//mTypKHgrHaaObQ0HYD4Ia6ur/BDMD/Hml1VFRg3tRhxVXbN85Kfj\nDw9XjQpYd5yzc0sJa88Ns4fleWbf7mgqd5DZuAPOxpok6poJryZ+oX/80TnZljUqFAvHL0fCs9Dj\n9Kcj+R7pUDoz/PyREL1e4BZOLDKdye/XZFCXWSo+PA5g9lvZ9Lta8fHfKWtoN6x/LAHTmf/RFsCM\n4VgsYBZCGwCAkCC0AQAICUIbAICQILQBAAgJQhsAgJAgtAEACAlCGwCAkCC0AQAICUIbAICQILQB\nAAgJQhsAgJAgtAEACAlCGwCAkCC0AQAICcvzPG+mGwEAAMZHTxsAgJAgtAEACAlCGwCAkCC0AQAI\nCUIbAICQILQBAAgJd6YbMJV+/etf6/Of/7y+/e1va+nSpf78V155RS+88IIcx1FjY6M6Ojpk27bW\nrFmjWCwmSVq4cKF27949U00v6Xp1Pf/883r55Zc1f/58SdITTzyh2267TR0dHTp37pyi0aieeuop\nNTQ0zFTTSxqrrmQyqW3btvnPeeedd7R9+3a1traG4v0q1cbjx4/r7/7u7+S6rtauXauHH35YuVwu\nFO9XqbrCvH+VamNY96/r1RT2feuZZ57R8ePHNTQ0pNbWVq1bt85fFuZ9a1zeLJVOp70vfelL3h/8\nwR94//mf/+nP7+/v9z7zmc94fX19nud53tatW71jx455AwMD3uc+97mZau6EXa8uz/O87du3ez/7\n2c8C8370ox95jz/+uOd5nvfGG294X/ziF6etrTeiVF0FP/3pT722tjYvk8mE4v0q1cZ0Ou199rOf\n9a5cueINDg56n//8571kMhmK96tUXWHev8ZrYxj3r4n+3sO2b/37v/+794UvfMHLZrNeKpXy/uZv\n/sZfFuZ9ayJm7fD43r17tX79ev3Wb/1WYH40GtWLL76ompoaSVImk1FVVZXOnj2r/v5+bdy4Ue3t\n7UokEjPR7HFdry5Jeuutt/Tss8+qtbVVzzzzjCTpzJkzWrFihSSpqalJPT0909reiSpVlyR5nqcn\nn3xSHR0dchwnFO9XqTa+++67WrRokebOnatoNKrly5frtddeC8X7VaquMO9f47UxjPvXRH7vYdy3\n/vVf/1WNjY36sz/7M33xi1/U3Xff7S8L8741EbNyePz73/++5s+frxUrVujZZ58NLLNtW7fccosk\nqaurS319fbrrrrt0/vx5bdq0SevWrdP777+vzZs36+jRo3Jdc35FpeqSpPvuu0+PPPKIYrGYtmzZ\nohMnTiiVSvnDXJLkOI4ymUyo6pLyw1233367lixZIkmqrq42/v0q1cZUKqW6ujr/uXPmzFEqlQrF\n+1WqrjDvX+P9nwrj/jWR/SSM+9bly5f1y1/+Ut/85jf1wQcf6E//9E919OhRWZYV6n1rIsLV2gn6\n3ve+J8uy9G//9m9655139Pjjj+vgwYOKx+OSpFwup6efflrvvfee9u/fL8uytHjxYjU0NPiP6+vr\nlUwmtWDBghmuZkSpujzP04YNG/z/rCtXrtTbb7+tWCym3t5efxu5XM64/6TjvV+SdOTIEbW3t/vT\nYXi/SrXx2velt7dXdXV1oXi/xvvdh3X/KtXGsO5fE/m9h3Hfqq+v15IlSxSNRrVkyRJVVVXp0qVL\nuvnmm0O9b03ErBwe/853vqN/+qd/UldXlz72sY9p7969gQDYuXOnBgcHdeDAAX8Y7/Dhw9qzZ48k\n6eLFi0qlUoF1TFCqrlQqpfvvv1+9vb3yPE+nT5/WsmXL1NzcrJMnT0qSEomEGhsbZ7KEMY33fklS\nT0+Pmpub/ekwvF+l2rh06VJduHBBV65cUTqd1uuvv65PfOIToXi/xvvdh3X/KtXGsO5fE/m9h3Hf\nWr58uX7yk5/I8zxdvHhR/f39qq+vlxTufWsiZv0XhrS1tamjo0Nvv/22+vr6tGzZMq1du1Z33HGH\nLMuSJLW3t2vlypXasWOHfvnLX8qyLH35y18O/Ec2zbV1tbS06J//+Z/V1dWlaDSqO++8U3/xF3/h\nnzF5/vx5eZ6nzs7OwBnnphmrrkuXLumP//iP9YMf/MB/XjqdNv79GquNv/jFL/y6Cme4ep6ntWvX\n6o/+6I9C8X6VqivM+9d471cY96/xagrrviVJX/3qV3X69Gl5nqetW7fqypUrod+3JmLWhzYAALPF\nrBweBwBgNiK0AQAICUIbAICQILQBAAgJQhsAgJAgtAEACAlCGwCAkCC0AQAIif8PkcO5FDmv9uQA\nAAAASUVORK5CYII=\n", 101 | "text/plain": [ 102 | "" 103 | ] 104 | }, 105 | "metadata": {}, 106 | "output_type": "display_data" 107 | } 108 | ], 109 | "source": [ 110 | "d_dx_f_left_ = sympy.diff(f_left_, x_, 1)\n", 111 | "d_dx_f_right_ = sympy.diff(f_right_, x_, 1)\n", 112 | "\n", 113 | "d_dx_f_ = Piecewise(\n", 114 | " (d_dx_f_left_, x_ < x_threshold),\n", 115 | " (d_dx_f_right_, True)\n", 116 | ")\n", 117 | "\n", 118 | "d_dx_f = sympy.lambdify(x_, d_dx_f_)\n", 119 | "\n", 120 | "\n", 121 | "d_dx_f_left = sympy.lambdify(x_, d_dx_f_left_)\n", 122 | "d_dx_f_right = sympy.lambdify(x_, d_dx_f_right_)\n", 123 | "\n", 124 | "plt.plot(x, d_dx_f(x))\n", 125 | "plt.xlim((x_threshold * 0.8, x_threshold * 1.2))\n", 126 | "plt.ylim((-0.4, 1.8))\n", 127 | "plt.show()" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "The derivative is indeed very discontinuous! Let's fix this!\n", 135 | "\n", 136 | "They do not need to be symmetric around the threshold" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 4, 142 | "metadata": { 143 | "collapsed": true 144 | }, 145 | "outputs": [], 146 | "source": [ 147 | "x_0 = x_threshold * 0.8\n", 148 | "x_1 = x_threshold * 1.2\n", 149 | "\n", 150 | "d_dx_at_x_0 = d_dx_f_left_.evalf(subs={x_: x_0})\n", 151 | "d_dx_at_x_1 = d_dx_f_right_.evalf(subs={x_: x_1})\n", 152 | "\n", 153 | "d_dx_f_center_ = d_dx_at_x_0 + ((x_ - x_0) / (x_1 - x_0)) * (d_dx_at_x_1 - d_dx_at_x_0)\n", 154 | "\n", 155 | "d_dx_f_smooth_ = Piecewise(\n", 156 | " (d_dx_f_left_, x_ < x_0),\n", 157 | " (d_dx_f_center_, x_ < x_1),\n", 158 | " (d_dx_f_right_, True)\n", 159 | ")\n", 160 | "\n", 161 | "d_dx_f_smooth = sympy.lambdify(x_, d_dx_f_smooth_)\n" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 5, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "data": { 171 | "image/png": "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\n8la5AAAAAElFTkSuQmCC\n", 172 | "text/plain": [ 173 | "" 174 | ] 175 | }, 176 | "metadata": {}, 177 | "output_type": "display_data" 178 | } 179 | ], 180 | "source": [ 181 | "plt.plot(x, d_dx_f_smooth(x))\n", 182 | "plt.xlim((x_threshold * 0.6, x_threshold * 1.4))\n", 183 | "plt.ylim((-0.4, 1.8))\n", 184 | "plt.show()" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "So now we just have to integrate `d_dx_f_center` and adjust it's integral constant to create a better piecewise `f_smooth`\n", 192 | "\n" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 6, 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [ 201 | "f_center_ = sympy.integrate(d_dx_f_center_, x_)\n", 202 | "\n", 203 | "f_left = sympy.lambdify(x_, f_left_)\n", 204 | "f_center = sympy.lambdify(x_, f_center_)\n", 205 | "\n", 206 | "# f_center(x0) == f_left(x0)\n", 207 | "f_center_ = f_center_ + (f_left(x_0) - f_center(x_0))\n", 208 | "\n", 209 | "f_smooth_ = Piecewise(\n", 210 | " (f_left_, x_ < x_0),\n", 211 | " (f_center_, x_ < x_1),\n", 212 | " (f_right_, True)\n", 213 | ")\n", 214 | "\n", 215 | "f_smooth = sympy.lambdify(x_, f_smooth_)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 7, 221 | "metadata": {}, 222 | "outputs": [ 223 | { 224 | "data": { 225 | "image/png": "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\n/dXV1XjllVewfft2vPzyy1KUOGO1tbXw+Xx44IEHcP/996Oqqip8X7wdlzEnT55EfX097r777gm3\ny/24FBYW4oUXXgh/X11djTVr1gAANmzYgE8//XTC9tM9p6R2cVt+/OMfY8GCBQCAQCAAvV4/Yfvp\nHodSu7gtp06dwkcffYR7770XTz75JDwez4Tt4+m4jHnhhRdw3333ITMzc8Ltcjwuk73+Sv1ckUXI\nejyecJcDAKjVaoyMjITvM5lM4ftSUlIueeDKSUpKCoxGIzweD77+9a/j0UcfnXD/F77wBezcuRO/\n+MUvcOTIEezevVuiSiMzGAx48MEH8fOf/xzPPPMM/v7v/z5uj8uYl19+GY888sglt8v9uGzZsgUa\nzYV3d0RRhCAIAEJ/e7fbPWH76Z5TUru4LWMv3kePHsVrr72Gv/7rv56w/XSPQ6ld3JaKigo89thj\neP3111FQUICf/exnE7aPp+MCAD09Pdi/fz/uuOOOS7aX43GZ7PVX6ueKLELWaDTC6/WGvw8Gg+GD\nffF9Xq93wou7HHV0dOD+++/H7bffjltvvTV8uyiK+PKXvwyLxQKdToeNGzfi9OnTElY6veLiYtx2\n220QBAHFxcUwm81wOBwA4vO49Pf3o6GhAWvXrp1we7wdFwAT3lPyer3h95bHTPeckqM//vGP+O53\nv4tXXnkl/L74mOkeh3KzefNmLF68OPz1xY+jeDsu7777Lm655ZZLeuQA+R6Xi19/pX6uyCJkV6xY\ngb179wIAqqqqUF5eHr6vtLQUTU1N6O3txdDQEA4fPozly5dLVWpE3d3deOCBB/AP//AP2Lp164T7\nPB4PbrnlFni9XoiiiIMHD4afkHL01ltv4fnnnwcA2O12eDwe2Gw2APF3XACgsrIS69atu+T2eDsu\nALBw4UIcPHgQALB3716sWrVqwv3TPafk5re//S1ee+01vPrqqygoKLjk/ukeh3Lz4IMP4sSJEwCA\n/fv3Y9GiRRPuj6fjAoTasGHDhknvk+Nxmez1V+rniiz+hdq8eTP27duHe+65B6IoYteuXfj973+P\ngYEB3H333fj2t7+NBx98EKIo4s4770RWVpbUJU/ppZdeQn9/P1588UW8+OKLAIBt27bB5/Ph7rvv\nxje/+U3cf//90Ol0WLduHTZu3ChxxVPbunUrnnjiCWzfvh2CIGDXrl3405/+FJfHBQAaGhqQn58f\n/n78YyyejgsAPP744/jOd76DH//4xygpKcGWLVsAAI899hgeffTRSZ9TchQIBPBP//RPyMnJwde+\n9jUAwOrVq/H1r3893JbJHodyPfvbuXMnnn32WWi1WlitVjz77LMA4u+4jGloaLjkHx85H5fJXn//\n8R//Ec8995xkzxWuwkNERBQjsuguJiIiUiKGLBERUYwwZImIiGKEIUtERBQjDFkiIqIYYcgSERHF\nCEOWiIgoRhiyREREMfL/ASWnq3g6YJ3cAAAAAElFTkSuQmCC\n", 226 | "text/plain": [ 227 | "" 228 | ] 229 | }, 230 | "metadata": {}, 231 | "output_type": "display_data" 232 | } 233 | ], 234 | "source": [ 235 | "plt.plot(x, f_smooth(x))\n", 236 | "plt.show()" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "Much better!\n", 244 | "Now let's generalize a way to create `f_center`:" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 8, 250 | "metadata": {}, 251 | "outputs": [ 252 | { 253 | "data": { 254 | "image/png": "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\n+KjMtvsAAAAASUVORK5CYII=\n", 255 | "text/latex": [ 256 | "$$\\frac{x^{2} \\left(df_{0} - df_{1}\\right)}{2 x_{0} - 2 x_{1}} + \\frac{x}{x_{0} - x_{1}} \\left(- df_{0} x_{1} + df_{1} x_{0}\\right)$$" 257 | ], 258 | "text/plain": [ 259 | " 2 \n", 260 | "x ⋅(df₀ - df₁) x⋅(-df₀⋅x₁ + df₁⋅x₀)\n", 261 | "────────────── + ────────────────────\n", 262 | " 2⋅x₀ - 2⋅x₁ x₀ - x₁ " 263 | ] 264 | }, 265 | "execution_count": 8, 266 | "metadata": {}, 267 | "output_type": "execute_result" 268 | } 269 | ], 270 | "source": [ 271 | "sympy.init_printing()\n", 272 | "\n", 273 | "x_0_, x_1_, f_0_ = sympy.symbols('x_0,x_1,f_{left}(x_0)', real=True)\n", 274 | "\n", 275 | "# df_0 is f_left'(x_0)\n", 276 | "# df_1 is f_right'(x_1)\n", 277 | "\n", 278 | "df_0_, df_1_ = sympy.symbols('df_0,df_1', real=True)\n", 279 | "\n", 280 | "a = (x_ - x_0_) / (x_1_ - x_0_)\n", 281 | "\n", 282 | "# d_dx_f_center_ = d_dx_at_x_0 + a * (d_dx_at_x_1 - d_dx_at_x_0)\n", 283 | "d_dx_f_center_ = (1 - a) * df_0_ + a * df_1_\n", 284 | "f_center_ = sympy.integrate(d_dx_f_center_, x_)\n", 285 | "\n", 286 | "# f_center_ = f_center_ + f_0_ - f_center_.subs(x_, x_0_)\n", 287 | "\n", 288 | "f_center_" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": 9, 294 | "metadata": {}, 295 | "outputs": [ 296 | { 297 | "name": "stdout", 298 | "output_type": "stream", 299 | "text": [ 300 | "-0.4387286573389664 5.230431342173345 -8.63388046349694\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "# Simplifying f_center to be an equation like x * (a*x + b) + c:\n", 306 | "\n", 307 | "def get_f_center_coefficients(x0, x1, f_left, df_left, df_right):\n", 308 | " dx = x1 - x0\n", 309 | " df0 = df_left(x0)\n", 310 | " df1 = df_right(x1)\n", 311 | " a = 0.5 * (df1 - df0) / dx\n", 312 | " b = (df0*x1 - df1*x0) / dx\n", 313 | " c = f_left(x0) - (x0 * (a*x0 + b))\n", 314 | " return a, b, c\n", 315 | "\n", 316 | "x_0 = x_threshold * 0.8\n", 317 | "x_1 = x_threshold * 1.2\n", 318 | "\n", 319 | "# So, for example:\n", 320 | "a, b, c = get_f_center_coefficients(\n", 321 | " x_0,\n", 322 | " x_1,\n", 323 | " f_left=lambda x: x**1.2,\n", 324 | " df_left=lambda x: 1.2 * x**0.2,\n", 325 | " df_right=lambda x: -2.0*x**-1.2,\n", 326 | ")\n", 327 | "\n", 328 | "print (a, b, c)" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 10, 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "def f_smooth(x):\n", 338 | " return np.piecewise(x, [x < x_0, (x_0 <= x) & (x <= x_1), x > x_1], [\n", 339 | " lambda x: x**1.2,\n", 340 | " lambda x: x * (-0.4387286573389664 * x + 5.230431342173345) - 8.63388046349694,\n", 341 | " lambda x: 10.0 / x**0.2,\n", 342 | " ])\n" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 11, 348 | "metadata": {}, 349 | "outputs": [ 350 | { 351 | "data": { 352 | "image/png": "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\n/dXV1XjllVewfft2vPzyy1KUOGO1tbXw+Xx44IEHcP/996Oqqip8X7wdlzEnT55EfX097r777gm3\ny/24FBYW4oUXXgh/X11djTVr1gAANmzYgE8//XTC9tM9p6R2cVt+/OMfY8GCBQCAQCAAvV4/Yfvp\nHodSu7gtp06dwkcffYR7770XTz75JDwez4Tt4+m4jHnhhRdw3333ITMzc8Ltcjwuk73+Sv1ckUXI\nejyecJcDAKjVaoyMjITvM5lM4ftSUlIueeDKSUpKCoxGIzweD77+9a/j0UcfnXD/F77wBezcuRO/\n+MUvcOTIEezevVuiSiMzGAx48MEH8fOf/xzPPPMM/v7v/z5uj8uYl19+GY888sglt8v9uGzZsgUa\nzYV3d0RRhCAIAEJ/e7fbPWH76Z5TUru4LWMv3kePHsVrr72Gv/7rv56w/XSPQ6ld3JaKigo89thj\neP3111FQUICf/exnE7aPp+MCAD09Pdi/fz/uuOOOS7aX43GZ7PVX6ueKLELWaDTC6/WGvw8Gg+GD\nffF9Xq93wou7HHV0dOD+++/H7bffjltvvTV8uyiK+PKXvwyLxQKdToeNGzfi9OnTElY6veLiYtx2\n220QBAHFxcUwm81wOBwA4vO49Pf3o6GhAWvXrp1we7wdFwAT3lPyer3h95bHTPeckqM//vGP+O53\nv4tXXnkl/L74mOkeh3KzefNmLF68OPz1xY+jeDsu7777Lm655ZZLeuQA+R6Xi19/pX6uyCJkV6xY\ngb179wIAqqqqUF5eHr6vtLQUTU1N6O3txdDQEA4fPozly5dLVWpE3d3deOCBB/AP//AP2Lp164T7\nPB4PbrnlFni9XoiiiIMHD4afkHL01ltv4fnnnwcA2O12eDwe2Gw2APF3XACgsrIS69atu+T2eDsu\nALBw4UIcPHgQALB3716sWrVqwv3TPafk5re//S1ee+01vPrqqygoKLjk/ukeh3Lz4IMP4sSJEwCA\n/fv3Y9GiRRPuj6fjAoTasGHDhknvk+Nxmez1V+rniiz+hdq8eTP27duHe+65B6IoYteuXfj973+P\ngYEB3H333fj2t7+NBx98EKIo4s4770RWVpbUJU/ppZdeQn9/P1588UW8+OKLAIBt27bB5/Ph7rvv\nxje/+U3cf//90Ol0WLduHTZu3ChxxVPbunUrnnjiCWzfvh2CIGDXrl3405/+FJfHBQAaGhqQn58f\n/n78YyyejgsAPP744/jOd76DH//4xygpKcGWLVsAAI899hgeffTRSZ9TchQIBPBP//RPyMnJwde+\n9jUAwOrVq/H1r3893JbJHodyPfvbuXMnnn32WWi1WlitVjz77LMA4u+4jGloaLjkHx85H5fJXn//\n8R//Ec8995xkzxWuwkNERBQjsuguJiIiUiKGLBERUYwwZImIiGKEIUtERBQjDFkiIqIYYcgSERHF\nCEOWiIgoRhiyREREMfL/ASWnq3g6YJ3cAAAAAElFTkSuQmCC\n", 353 | "text/plain": [ 354 | "" 355 | ] 356 | }, 357 | "metadata": {}, 358 | "output_type": "display_data" 359 | } 360 | ], 361 | "source": [ 362 | "plt.plot(x, f_smooth(x))\n", 363 | "plt.show()" 364 | ] 365 | } 366 | ], 367 | "metadata": { 368 | "anaconda-cloud": {}, 369 | "kernelspec": { 370 | "display_name": "Python [conda env:notebooks]", 371 | "language": "python", 372 | "name": "conda-env-notebooks-py" 373 | }, 374 | "language_info": { 375 | "codemirror_mode": { 376 | "name": "ipython", 377 | "version": 3 378 | }, 379 | "file_extension": ".py", 380 | "mimetype": "text/x-python", 381 | "name": "python", 382 | "nbconvert_exporter": "python", 383 | "pygments_lexer": "ipython3", 384 | "version": "3.5.3" 385 | } 386 | }, 387 | "nbformat": 4, 388 | "nbformat_minor": 2 389 | } 390 | -------------------------------------------------------------------------------- /test_write_strategy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Test Write Strategy\n", 8 | "Currently HDF5 is our standard file format to store simulation results and this notebook aims to check if playing with raw data yields better perfomance (in terms of both, speed and file size).\n", 9 | "\n", 10 | "The idea is to emulate an ALFASim simulation that generates a large amount of data (profiles and trends). We are saving all pipes variables as profiles and for each pipe the trends are defined as the values at the first position just for easiness.\n" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "Set some constants to determine the problem size, frequencies and where to store the data to be saved" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 145, 23 | "metadata": { 24 | "collapsed": true 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "number_of_pipes = 10\n", 29 | "number_of_cells = 500\n", 30 | "total_simulation_time = 6 * 60 * 60 # [s], 6 hours of simulation\n", 31 | "\n", 32 | "trend_output_frequency = 2 # [s]\n", 33 | "profile_output_frequency = 5 * 60 # [s]\n", 34 | "\n", 35 | "initial_time_step = 1.0e-2 # [s]\n", 36 | "time_step_increase_factor = 1.1\n", 37 | "maximum_time_step = 2 # [s]\n", 38 | "\n", 39 | "import tempfile\n", 40 | "import os\n", 41 | "import json\n", 42 | "import numpy\n", 43 | "\n", 44 | "root_path = os.getcwd()" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "Let's create a list of pipes, each one with some output properies and random values.\n", 52 | "During the simulation the data for each pipe will change but for our pourposes it can be always the same." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 146, 58 | "metadata": { 59 | "collapsed": true 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "from collections import namedtuple\n", 64 | "import numpy as np\n", 65 | "\n", 66 | "output_variables_per_pipe = [\n", 67 | " 'pressure', \n", 68 | " 'temperature', \n", 69 | " 'gas_velocity', \n", 70 | " 'liquid_velocity'\n", 71 | "]\n", 72 | "\n", 73 | "positions = number_of_pipes * [0]\n", 74 | "\n", 75 | "class Pipe:\n", 76 | " __slots__ = list(output_variables_per_pipe)\n", 77 | "\n", 78 | "\n", 79 | "pipes = []\n", 80 | "for i in range(number_of_pipes):\n", 81 | " pipes.append(Pipe())\n", 82 | " for var in output_variables_per_pipe:\n", 83 | " setattr(pipes[-1], var, np.random.rand(number_of_cells))" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": { 89 | "collapsed": true 90 | }, 91 | "source": [ 92 | "Time step sizes are not constant during the simulation, so emulate a growing time step size up to the point when it reaches the maximum value." 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 147, 98 | "metadata": { 99 | "collapsed": true 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "def new_time_step(time_step, time_step_increase_factor, maximum_time_step):\n", 104 | " new_time_step = time_step * time_step_increase_factor\n", 105 | " if new_time_step > maximum_time_step:\n", 106 | " return maximum_time_step\n", 107 | " else:\n", 108 | " return new_time_step" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "## Raw data from NumPy and metadata\n", 116 | "Crate a folder to store the simulation results. This would have the project name associated to it but here simplename is enough, just check whether it exists or not and clear the folder content if necessary. A folder inside of the main one is also created to store the metadata." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 148, 122 | "metadata": { 123 | "collapsed": true 124 | }, 125 | "outputs": [], 126 | "source": [ 127 | "def create_dirs():\n", 128 | "\n", 129 | " project_name = 'my_project_folder'\n", 130 | " project_path = os.path.join(root_path, project_name)\n", 131 | " meta_path = os.path.join(project_path, 'meta')\n", 132 | "\n", 133 | " if not os.path.exists(project_path):\n", 134 | " os.makedirs(project_path)\n", 135 | " else:\n", 136 | " # clear directory content\n", 137 | " # in real life need to ask the user what to do, if delete and rerun simulation or stop\n", 138 | " import shutil\n", 139 | " for file in os.listdir(project_path):\n", 140 | " file_path = os.path.join(project_path, file)\n", 141 | "\n", 142 | " if os.path.isfile(file_path):\n", 143 | " os.unlink(file_path)\n", 144 | " elif os.path.isdir(file_path): \n", 145 | " shutil.rmtree(file_path, ignore_errors=True)\n", 146 | " \n", 147 | " os.makedirs(meta_path)\n", 148 | " \n", 149 | " return project_path, meta_path\n", 150 | " " 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "Some auxiliary methods to actually write the data..." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 149, 163 | "metadata": { 164 | "collapsed": true 165 | }, 166 | "outputs": [], 167 | "source": [ 168 | "def write_profiles(pipes, tmp_path):\n", 169 | " from numpy import ndarray\n", 170 | " \n", 171 | " profile_path = os.path.join(tmp_path, 'profiles')\n", 172 | " os.makedirs(profile_path)\n", 173 | " \n", 174 | " metadata = []\n", 175 | " pipe_index = 0\n", 176 | " profile_index = 0\n", 177 | " \n", 178 | " for pipe in pipes:\n", 179 | " pipe_name = 'pipe_' + '{:03d}'.format(pipe_index)\n", 180 | " \n", 181 | " for var in sorted(output_variables_per_pipe):\n", 182 | " md = {}\n", 183 | " output_file = 'profile_' + '{:03d}'.format(profile_index) + '.binary'\n", 184 | " data = getattr(pipe, var)\n", 185 | " data.tofile(os.path.join(profile_path, output_file))\n", 186 | " #np.savez_compressed(os.path.join(profile_path, output_file), a=data)\n", 187 | " \n", 188 | " md['property_id'] = var\n", 189 | " md['edge'] = pipe_name\n", 190 | " md['fn'] = output_file\n", 191 | " md['size'] = len(data)\n", 192 | " \n", 193 | " profile_index += 1\n", 194 | " metadata.append(md)\n", 195 | " \n", 196 | " pipe_index += 1\n", 197 | "\n", 198 | " return metadata" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 150, 204 | "metadata": { 205 | "collapsed": true 206 | }, 207 | "outputs": [], 208 | "source": [ 209 | "def write_trends(pipes, positions, tmp_path):\n", 210 | " from numpy import array\n", 211 | " \n", 212 | " trends_path = os.path.join(tmp_path, 'trends')\n", 213 | " os.makedirs(trends_path)\n", 214 | " \n", 215 | " metadata = []\n", 216 | " trend_data = []\n", 217 | " i = 0\n", 218 | " \n", 219 | " output_file = 'trends.binary'\n", 220 | " \n", 221 | " for pipe, position in zip(pipes, positions):\n", 222 | " pipe_name = 'pipe_' + '{:03d}'.format(i)\n", 223 | " \n", 224 | " for var in sorted(output_variables_per_pipe):\n", 225 | " md = {}\n", 226 | " \n", 227 | " full_data = getattr(pipe, var)\n", 228 | " trend_data.append(full_data[position])\n", 229 | " \n", 230 | " md['property_id'] = var\n", 231 | " md['edge'] = pipe_name\n", 232 | " md['fn'] = output_file\n", 233 | " md['position'] = position\n", 234 | " \n", 235 | " metadata.append(md)\n", 236 | "\n", 237 | " i += 1\n", 238 | "\n", 239 | " trend_data = array(trend_data)\n", 240 | " trend_data.tofile(os.path.join(trends_path, output_file))\n", 241 | "\n", 242 | " return metadata" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 151, 248 | "metadata": { 249 | "collapsed": true 250 | }, 251 | "outputs": [], 252 | "source": [ 253 | "def write_metadata(folder_path, metadata, filename):\n", 254 | "\n", 255 | " with open(os.path.join(folder_path, filename), 'w') as file:\n", 256 | " json.dump(obj=metadata, fp=file, indent=4)" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": 152, 262 | "metadata": { 263 | "collapsed": true 264 | }, 265 | "outputs": [], 266 | "source": [ 267 | "def write_timeset(folder_path, timeset):\n", 268 | " ts = numpy.array(timeset)\n", 269 | " ts_values_file = os.path.join(folder_path, 'timeset.values.binary')\n", 270 | " ts.tofile(ts_values_file)\n", 271 | " \n", 272 | " content = dict({'unit': 's', 'count': len(timeset), 'fn': 'timeset.values.npy'})\n", 273 | " with open(os.path.join(folder_path, 'timeset.json'), 'w') as file:\n", 274 | " json.dump(obj=content, fp=file, indent=4)" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 153, 280 | "metadata": { 281 | "collapsed": true 282 | }, 283 | "outputs": [], 284 | "source": [ 285 | "def write_metadata_and_timeset(metadata_path, folder_name, profile_metadata, trend_metadata, timeset):\n", 286 | " tmp_path = tempfile.mkdtemp(dir=metadata_path)\n", 287 | " write_metadata(tmp_path, profile_metadata, 'profiles.json')\n", 288 | " write_metadata(tmp_path, trend_metadata, 'trends.json')\n", 289 | " write_timeset(tmp_path, timeset)\n", 290 | " os.rename(tmp_path, os.path.join(metadata_path, folder_name))" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 154, 296 | "metadata": { 297 | "collapsed": true 298 | }, 299 | "outputs": [], 300 | "source": [ 301 | "def update_timeset_file(timeset, meta_path):\n", 302 | "\n", 303 | " ts = numpy.array(timeset)\n", 304 | " tmp_values_file = os.path.join(meta_path, 'tmp.values.binary')\n", 305 | " ts.tofile(tmp_values_file)\n", 306 | " \n", 307 | " tmp_meta_file = os.path.join(meta_path, 'tmp.json')\n", 308 | " content = dict({'unit': 's', 'count': len(timeset), 'fn': 'timeset.values.binary'})\n", 309 | " with open(tmp_meta_file, 'w') as file:\n", 310 | " json.dump(obj=content, fp=file, indent=4) \n", 311 | " \n", 312 | " ts_values_file = os.path.join(meta_path, 'timeset.values.binary')\n", 313 | " ts_meta_file = os.path.join(meta_path, 'timeset.json')\n", 314 | " \n", 315 | " if os.path.exists(ts_values_file):\n", 316 | " os.remove(ts_values_file)\n", 317 | " if os.path.exists(ts_meta_file):\n", 318 | " os.remove(ts_meta_file)\n", 319 | " \n", 320 | " os.rename(tmp_values_file, ts_values_file)\n", 321 | " os.rename(tmp_meta_file, ts_meta_file)" 322 | ] 323 | }, 324 | { 325 | "cell_type": "markdown", 326 | "metadata": {}, 327 | "source": [ 328 | "This is the actual simulation loop encapsulated into a function just to facilitate timming it below:" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 155, 334 | "metadata": { 335 | "collapsed": true 336 | }, 337 | "outputs": [], 338 | "source": [ 339 | "def solve(): \n", 340 | " \n", 341 | " time_step_index = 0\n", 342 | " t = 0\n", 343 | " time_step = initial_time_step\n", 344 | "\n", 345 | " elapsed_time_since_last_trend_output = 0.0\n", 346 | " elapsed_time_since_last_profile_output = 0.0\n", 347 | "\n", 348 | " timeset = []\n", 349 | "\n", 350 | " # create basic directory structire if it doesn't exist, if it exits clear content\n", 351 | " project_path, meta_path = create_dirs()\n", 352 | " meta_folder_name = ''\n", 353 | "\n", 354 | " last_profile_metadata = profile_metadata = None\n", 355 | " last_trend_metadata = trend_metadata = None\n", 356 | "\n", 357 | " while t < total_simulation_time:\n", 358 | "\n", 359 | " ts_folder_name = 'TS_' + '{:08d}'.format(time_step_index)\n", 360 | " \n", 361 | " timeset.append(t)\n", 362 | "\n", 363 | " if time_step_index == 0:\n", 364 | " # always save initial condition, profiles and trends\n", 365 | " tmp_path = tempfile.mkdtemp(dir=project_path)\n", 366 | "\n", 367 | " profile_metadata = write_profiles(pipes, tmp_path)\n", 368 | " trend_metadata = write_trends(pipes, positions, tmp_path)\n", 369 | " os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 370 | " \n", 371 | " # also create the metadata folder if everyting goes fine until here\n", 372 | " meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 373 | " write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 374 | " \n", 375 | " last_profile_metadata = profile_metadata\n", 376 | " last_trend_metadata = trend_metadata\n", 377 | " \n", 378 | " # call solve methods or in this case just use the values in the pipes list\n", 379 | "\n", 380 | " elapsed_time_since_last_trend_output += time_step\n", 381 | " elapsed_time_since_last_profile_output += time_step\n", 382 | "\n", 383 | " if elapsed_time_since_last_trend_output > trend_output_frequency or \\\n", 384 | " elapsed_time_since_last_profile_output > profile_output_frequency:\n", 385 | "\n", 386 | " tmp_path = tempfile.mkdtemp(dir=project_path)\n", 387 | "\n", 388 | " if elapsed_time_since_last_trend_output > trend_output_frequency:\n", 389 | " trend_metadata = write_trends(pipes, positions, tmp_path)\n", 390 | " elapsed_time_since_last_trend_output = 0.0\n", 391 | "\n", 392 | " if elapsed_time_since_last_profile_output > profile_output_frequency:\n", 393 | " profile_metadata = write_profiles(pipes, tmp_path)\n", 394 | " elapsed_time_since_last_profile_output = 0.0\n", 395 | "\n", 396 | " os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 397 | " \n", 398 | " # update timeset file \n", 399 | " update_timeset_file(timeset, os.path.join(meta_path, meta_folder_name))\n", 400 | " \n", 401 | " # if metadata changed, create a new directory and store the updated information\n", 402 | " if last_profile_metadata != profile_metadata or last_trend_metadata != trend_metadata:\n", 403 | " meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 404 | " write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 405 | "\n", 406 | " \n", 407 | " time_step = new_time_step(time_step, time_step_increase_factor, maximum_time_step)\n", 408 | " t += time_step\n", 409 | " time_step_index += 1\n" 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": 156, 415 | "metadata": {}, 416 | "outputs": [ 417 | { 418 | "name": "stdout", 419 | "output_type": "stream", 420 | "text": [ 421 | "36.3 s ± 420 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 422 | ] 423 | } 424 | ], 425 | "source": [ 426 | "%%timeit\n", 427 | "solve()" 428 | ] 429 | }, 430 | { 431 | "cell_type": "markdown", 432 | "metadata": {}, 433 | "source": [ 434 | "__First implementation was using a `.json` file to store the timeset, but as it can become quite big (all simulated time steps, not only the saved ones) the dump to the `.json` file was a bottleneck: `93.7%` of the time was spent just in `update_timeset_file` method:__\n", 435 | "\n", 436 | "```\n", 437 | "%lprun -f solve solve()\n", 438 | "\n", 439 | "Timer unit: 3.01859e-07 s\n", 440 | "\n", 441 | "Total time: 378.498 s\n", 442 | "File: \n", 443 | "Function: solve at line 1\n", 444 | "\n", 445 | "Line # Hits Time Per Hit % Time Line Contents\n", 446 | "==============================================================\n", 447 | " 1 def solve(): \n", 448 | " 2 \n", 449 | " 3 1 6 6.0 0.0 time_step_index = 0\n", 450 | " 4 1 3 3.0 0.0 t = 0\n", 451 | " 5 1 3 3.0 0.0 time_step = initial_time_step\n", 452 | " 6 \n", 453 | " 7 1 2 2.0 0.0 elapsed_time_since_last_trend_output = 0.0\n", 454 | " 8 1 2 2.0 0.0 elapsed_time_since_last_profile_output = 0.0\n", 455 | " 9 \n", 456 | " 10 1 3 3.0 0.0 timeset = []\n", 457 | " 11 \n", 458 | " \n", 459 | " 12 1 29782315 29782315.0 2.4 project_path, meta_path = create_dirs()\n", 460 | " 13 \n", 461 | " 14 1 10 10.0 0.0 last_profile_metadata = None\n", 462 | " 15 1 5 5.0 0.0 last_trend_metadata = None\n", 463 | " 16 \n", 464 | " 17 10846 38824 3.6 0.0 while t < total_simulation_time:\n", 465 | " 18 \n", 466 | " \n", 467 | " \n", 468 | " 19 10845 210044 19.4 0.0 ts_folder_name = 'TS_' + '{:08d}'.format(time_step_index)\n", 469 | " 20 \n", 470 | " 21 10845 50351 4.6 0.0 timeset.append(t)\n", 471 | " 22 \n", 472 | " 23 10845 32718 3.0 0.0 if time_step_index == 0:\n", 473 | " 24 # always save initial condition \n", 474 | " 25 1 2049 2049.0 0.0 tmp_path = tempfile.mkdtemp(dir=project_path)\n", 475 | " 26 \n", 476 | " 27 1 107427 107427.0 0.0 profile_metadata = write_profiles(pipes, tmp_path)\n", 477 | " 28 1 4265 4265.0 0.0 trend_metadata = write_trends(pipes, positions, tmp_path)\n", 478 | " 29 1 1919 1919.0 0.0 os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 479 | " 30 \n", 480 | " 31 # also create the metadata folder if everyting goes fine until here\n", 481 | " 32 1 26 26.0 0.0 meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 482 | " 33 1 42555 42555.0 0.0 write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 483 | " 34 \n", 484 | " 35 1 7 7.0 0.0 last_profile_metadata = profile_metadata\n", 485 | " 36 1 2 2.0 0.0 last_trend_metadata = trend_metadata\n", 486 | " 37 \n", 487 | " 38 # set up and run the model but here this is not necessary as there is no simulation,\n", 488 | " 39 # we just use the values in the pipes list\n", 489 | " 40 \n", 490 | " 41 10845 36874 3.4 0.0 elapsed_time_since_last_trend_output += time_step\n", 491 | " 42 10845 33156 3.1 0.0 elapsed_time_since_last_profile_output += time_step\n", 492 | " 43 \n", 493 | " 44 10845 42624 3.9 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency or elapsed_time_since_last_profile_output > profile_output_frequency:\n", 494 | " 45 \n", 495 | " 46 5437 5030271 925.2 0.4 tmp_path = tempfile.mkdtemp(dir=project_path)\n", 496 | " 47 \n", 497 | " 48 5437 57617 10.6 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency:\n", 498 | " 49 5402 23472668 4345.2 1.9 trend_metadata = write_trends(pipes, positions, tmp_path)\n", 499 | " 50 5402 37765 7.0 0.0 elapsed_time_since_last_trend_output = 0.0\n", 500 | " 51 \n", 501 | " 52 5437 34341 6.3 0.0 if elapsed_time_since_last_profile_output > profile_output_frequency:\n", 502 | " 53 71 7417821 104476.4 0.6 profile_metadata = write_profiles(pipes, tmp_path)\n", 503 | " 54 71 685 9.6 0.0 elapsed_time_since_last_profile_output = 0.0\n", 504 | " 55 \n", 505 | " 56 5437 11762379 2163.4 0.9 os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 506 | " 57 \n", 507 | " 58 # update timeset file \n", 508 | " 59 5437 1174911177 216095.5 93.7 update_timeset_file(timeset, os.path.join(meta_path, meta_folder_name))\n", 509 | " 60 \n", 510 | " 61 # if metadata changed, create a new directory and store the updated information\n", 511 | " 62 5437 548502 100.9 0.0 if last_profile_metadata != profile_metadata or last_trend_metadata != trend_metadata:\n", 512 | " 63 meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 513 | " 64 write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 514 | " 65 \n", 515 | " 66 \n", 516 | " 67 10845 143380 13.2 0.0 time_step = new_time_step(time_step, time_step_increase_factor, maximum_time_step)\n", 517 | " 68 10845 48121 4.4 0.0 t += time_step\n", 518 | " 69 10845 39312 3.6 0.0 time_step_index += 1\n", 519 | "```\n", 520 | "\n", 521 | "__In more detail:__\n", 522 | "```\n", 523 | "%lprun -f update_timeset_file solve()\n", 524 | "\n", 525 | "Timer unit: 3.01859e-07 s\n", 526 | "\n", 527 | "Total time: 356.392 s\n", 528 | "File: \n", 529 | "Function: update_timeset_file at line 1\n", 530 | "\n", 531 | "Line # Hits Time Per Hit % Time Line Contents\n", 532 | "==============================================================\n", 533 | " 1 def update_timeset_file(timeset, meta_path):\n", 534 | " 2 \n", 535 | " \n", 536 | " 3 5437 369593 68.0 0.0 tmp_file = os.path.join(meta_path, 'tmp.json')\n", 537 | " \n", 538 | " \n", 539 | " 4 5437 320227 58.9 0.0 timeset_file = os.path.join(meta_path, 'timeset.json')\n", 540 | " 5 \n", 541 | " 6 5437 64538 11.9 0.0 content = dict({'unit': 's', 'values': timeset})\n", 542 | " 7 5437 3746910 689.2 0.3 with open(tmp_file, 'w') as file:\n", 543 | " 8 5437 1156926499 212787.7 98.0 json.dump(obj=content, fp=file, indent=4) \n", 544 | " \n", 545 | " \n", 546 | " 9 \n", 547 | " \n", 548 | " 10 5437 2704180 497.4 0.2 if os.path.exists(timeset_file):\n", 549 | " \n", 550 | " \n", 551 | " 11 5437 6365348 1170.7 0.5 os.remove(timeset_file)\n", 552 | " 12 \n", 553 | " \n", 554 | " 13 5437 10157657 1868.2 0.9 os.rename(tmp_file, timeset_file)\n", 555 | "```\n", 556 | "\n", 557 | "__The decision was to split the information and save two files, one containing the timeset metadata `timeset.json` and other with the values using a numpy dump `timeset.values`.__\n", 558 | "```\n", 559 | "%lprun -f solve solve()\n", 560 | "\n", 561 | "Timer unit: 3.01859e-07 s\n", 562 | "\n", 563 | "Total time: 35.2779 s\n", 564 | "File: \n", 565 | "Function: solve at line 1\n", 566 | "\n", 567 | "Line # Hits Time Per Hit % Time Line Contents\n", 568 | "==============================================================\n", 569 | " 1 def solve(): \n", 570 | " 2 \n", 571 | " 3 1 6 6.0 0.0 time_step_index = 0\n", 572 | " 4 1 3 3.0 0.0 t = 0\n", 573 | " 5 1 3 3.0 0.0 time_step = initial_time_step\n", 574 | " 6 \n", 575 | " 7 1 2 2.0 0.0 elapsed_time_since_last_trend_output = 0.0\n", 576 | " 8 1 3 3.0 0.0 elapsed_time_since_last_profile_output = 0.0\n", 577 | " 9 \n", 578 | " 10 1 2 2.0 0.0 timeset = []\n", 579 | " 11 \n", 580 | " 12 # create basic directory structire if it doesn't exist, if it exits clear content\n", 581 | " 13 1 197836 197836.0 0.2 project_path, meta_path = create_dirs()\n", 582 | " 14 1 7 7.0 0.0 meta_folder_name = ''\n", 583 | " 15 \n", 584 | " 16 1 3 3.0 0.0 last_profile_metadata = profile_metadata = None\n", 585 | " 17 1 2 2.0 0.0 last_trend_metadata = trend_metadata = None\n", 586 | " \n", 587 | " 18 \n", 588 | " 19 10846 39773 3.7 0.0 while t < total_simulation_time:\n", 589 | " 20 \n", 590 | " 21 10845 165287 15.2 0.1 ts_folder_name = 'TS_' + '{:08d}'.format(time_step_index)\n", 591 | " 22 \n", 592 | " 23 10845 47141 4.3 0.0 timeset.append(t)\n", 593 | " 24 \n", 594 | " 25 10845 31701 2.9 0.0 if time_step_index == 0:\n", 595 | " 26 # always save initial condition, profiles and trends\n", 596 | " 27 1 953 953.0 0.0 tmp_path = tempfile.mkdtemp(dir=project_path)\n", 597 | " 28 \n", 598 | " 29 1 103800 103800.0 0.1 profile_metadata = write_profiles(pipes, tmp_path)\n", 599 | " 30 1 3713 3713.0 0.0 trend_metadata = write_trends(pipes, positions, tmp_path)\n", 600 | " 31 1 1826 1826.0 0.0 os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 601 | " 32 \n", 602 | " 33 # also create the metadata folder if everyting goes fine until here\n", 603 | " 34 1 18 18.0 0.0 meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 604 | " 35 1 41405 41405.0 0.0 write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 605 | " 36 \n", 606 | " 37 1 6 6.0 0.0 last_profile_metadata = profile_metadata\n", 607 | " 38 1 3 3.0 0.0 last_trend_metadata = trend_metadata\n", 608 | " 39 \n", 609 | " \n", 610 | " 40 # call solve methods or in this case just use the values in the pipes list\n", 611 | " 41 \n", 612 | " 42 10845 37281 3.4 0.0 elapsed_time_since_last_trend_output += time_step\n", 613 | " 43 10845 32066 3.0 0.0 elapsed_time_since_last_profile_output += time_step\n", 614 | " 44 \n", 615 | " 45 10845 41421 3.8 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency or elapsed_time_since_last_profile_output > profile_output_frequency:\n", 616 | " 46 \n", 617 | " 47 5437 4479454 823.9 3.8 tmp_path = tempfile.mkdtemp(dir=project_path)\n", 618 | " 48 \n", 619 | " 49 5437 46541 8.6 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency:\n", 620 | " 50 5402 22644276 4191.8 19.4 trend_metadata = write_trends(pipes, positions, tmp_path)\n", 621 | " 51 5402 36690 6.8 0.0 elapsed_time_since_last_trend_output = 0.0\n", 622 | " 52 \n", 623 | " 53 5437 32757 6.0 0.0 if elapsed_time_since_last_profile_output > profile_output_frequency:\n", 624 | " 54 71 7820215 110143.9 6.7 profile_metadata = write_profiles(pipes, tmp_path)\n", 625 | " 55 71 714 10.1 0.0 elapsed_time_since_last_profile_output = 0.0\n", 626 | " 56 \n", 627 | " 57 5437 12242185 2251.6 10.5 os.rename(tmp_path, os.path.join(project_path, ts_folder_name))\n", 628 | " 58 \n", 629 | " 59 # update timeset file \n", 630 | " 60 5437 68224529 12548.2 58.4 update_timeset_file(timeset, os.path.join(meta_path, meta_folder_name))\n", 631 | " 61 \n", 632 | " 62 # if metadata changed, create a new directory and store the updated information\n", 633 | " 63 5437 392792 72.2 0.3 if last_profile_metadata != profile_metadata or last_trend_metadata != trend_metadata:\n", 634 | " 64 meta_folder_name = 'meta_' + '{:08d}'.format(time_step_index)\n", 635 | " 65 write_metadata_and_timeset(meta_path, meta_folder_name, profile_metadata, trend_metadata, timeset)\n", 636 | " 66 \n", 637 | " 67 \n", 638 | " 68 10845 122973 11.3 0.1 time_step = new_time_step(time_step, time_step_increase_factor, maximum_time_step)\n", 639 | " 69 10845 45225 4.2 0.0 t += time_step\n", 640 | " 70 10845 36260 3.3 0.0 time_step_index += 1\n", 641 | "\n", 642 | "```\n", 643 | "\n", 644 | "__Again in more detail:__\n", 645 | "```\n", 646 | "%lprun -f update_timeset_file solve() \n", 647 | " \n", 648 | "Timer unit: 3.01859e-07 s\n", 649 | "\n", 650 | "Total time: 20.5189 s\n", 651 | "File: \n", 652 | "Function: update_timeset_file at line 1\n", 653 | "\n", 654 | "Line # Hits Time Per Hit % Time Line Contents\n", 655 | "==============================================================\n", 656 | " 1 def update_timeset_file(timeset, meta_path):\n", 657 | " 2 \n", 658 | " 3 5437 6109988 1123.8 9.0 ts = numpy.array(timeset)\n", 659 | " 4 5437 370292 68.1 0.5 tmp_values_file = os.path.join(meta_path, 'tmp.values')\n", 660 | " 5 5437 14579577 2681.5 21.4 ts.dump(tmp_values_file)\n", 661 | " 6 \n", 662 | " 7 5437 455100 83.7 0.7 tmp_meta_file = os.path.join(meta_path, 'tmp.json')\n", 663 | " \n", 664 | " 8 5437 63703 11.7 0.1 content = dict({'unit': 's', 'count': len(timeset), 'fn': 'timeset.values'})\n", 665 | " 9 5437 3681250 677.1 5.4 with open(tmp_meta_file, 'w') as file:\n", 666 | " 10 5437 9022392 1659.4 13.3 json.dump(obj=content, fp=file, indent=4) \n", 667 | " 11 \n", 668 | " 12 5437 455519 83.8 0.7 ts_values_file = os.path.join(meta_path, 'timeset.values')\n", 669 | " 13 5437 314816 57.9 0.5 ts_meta_file = os.path.join(meta_path, 'timeset.json')\n", 670 | " 14 \n", 671 | " 15 5437 2227507 409.7 3.3 if os.path.exists(ts_values_file):\n", 672 | " 16 5437 5394110 992.1 7.9 os.remove(ts_values_file)\n", 673 | " 17 5437 2048175 376.7 3.0 if os.path.exists(ts_meta_file):\n", 674 | " 18 5437 4809399 884.6 7.1 os.remove(ts_meta_file)\n", 675 | " 19 \n", 676 | " 20 5437 9412867 1731.3 13.8 os.rename(tmp_values_file, ts_values_file)\n", 677 | " 21 5437 9030373 1660.9 13.3 os.rename(tmp_meta_file, ts_meta_file) \n", 678 | "```\n", 679 | "\n", 680 | "__Before: 378.498 s__\n", 681 | "\n", 682 | "__After: 35.2779 s__" 683 | ] 684 | }, 685 | { 686 | "cell_type": "code", 687 | "execution_count": 157, 688 | "metadata": {}, 689 | "outputs": [ 690 | { 691 | "name": "stdout", 692 | "output_type": "stream", 693 | "text": [ 694 | "The line_profiler extension is already loaded. To reload it, use:\n", 695 | " %reload_ext line_profiler\n" 696 | ] 697 | } 698 | ], 699 | "source": [ 700 | "#%load_ext line_profiler" 701 | ] 702 | }, 703 | { 704 | "cell_type": "markdown", 705 | "metadata": { 706 | "collapsed": true 707 | }, 708 | "source": [ 709 | "## HDF5 with Pandas\n", 710 | "http://glowingpython.blogspot.com.br/2014/08/quick-hdf5-with-pandas.html\n", 711 | "\n", 712 | "The inspiration comes from the above link, it seems Pandas' `DataFrame` can be used to easily represent the `HDF` groups, creating and appending data where necessary. Let's try it then..." 713 | ] 714 | }, 715 | { 716 | "cell_type": "code", 717 | "execution_count": 158, 718 | "metadata": { 719 | "collapsed": true 720 | }, 721 | "outputs": [], 722 | "source": [ 723 | "import numpy as np\n", 724 | "from pandas import HDFStore, DataFrame, set_option\n", 725 | "\n", 726 | "set_option('io.hdf.default_format','table')" 727 | ] 728 | }, 729 | { 730 | "cell_type": "code", 731 | "execution_count": 159, 732 | "metadata": { 733 | "collapsed": true 734 | }, 735 | "outputs": [], 736 | "source": [ 737 | "def write_profiles_2_hdf(pipes, output_file, time_step_index, time):\n", 738 | "\n", 739 | " hdf = HDFStore(output_file, complevel=9, complib='blosc')\n", 740 | " \n", 741 | " pipe_index = 0\n", 742 | " \n", 743 | " for pipe in pipes:\n", 744 | " pipe_name = 'pipe_' + '{:03d}'.format(pipe_index)\n", 745 | " folder_name = 'profiles/time_' + '{:06d}'.format(time_step_index) + '/' + pipe_name\n", 746 | " local={}\n", 747 | " \n", 748 | " for var in output_variables_per_pipe:\n", 749 | " data = getattr(pipe, var)\n", 750 | " local[var] = data\n", 751 | "\n", 752 | " df = DataFrame(local)\n", 753 | " hdf.put(folder_name, df)\n", 754 | " pipe_index += 1\n", 755 | " \n", 756 | " hdf.close()" 757 | ] 758 | }, 759 | { 760 | "cell_type": "code", 761 | "execution_count": 160, 762 | "metadata": { 763 | "collapsed": true 764 | }, 765 | "outputs": [], 766 | "source": [ 767 | "def write_trends_2_hdf(pipes, positions, output_file, time_step_index, time):\n", 768 | "\n", 769 | " hdf = HDFStore(output_file, complevel=9, complib='blosc')\n", 770 | " \n", 771 | " pipe_index = 0\n", 772 | "\n", 773 | " for pipe in pipes:\n", 774 | " pipe_name = 'pipe_' + '{:03d}'.format(pipe_index)\n", 775 | " folder_name = 'trends/' + pipe_name\n", 776 | " local={}\n", 777 | " \n", 778 | " for var, pos in zip(output_variables_per_pipe, positions):\n", 779 | " data = getattr(pipe, var)\n", 780 | " local[var] = data[pos]\n", 781 | "\n", 782 | " df = DataFrame(local, index=[time_step_index])\n", 783 | " if time_step_index == 0:\n", 784 | " hdf.put(folder_name, df, index=False)\n", 785 | " else:\n", 786 | " hdf.append(folder_name, df, index=False)\n", 787 | " pipe_index += 1\n", 788 | "\n", 789 | "\n", 790 | " hdf.close()" 791 | ] 792 | }, 793 | { 794 | "cell_type": "code", 795 | "execution_count": 161, 796 | "metadata": { 797 | "collapsed": true 798 | }, 799 | "outputs": [], 800 | "source": [ 801 | "def update_timeset_2_hdf(timeset, time_step_index, output_file):\n", 802 | " hdf = HDFStore(output_file, complevel=9, complib='blosc')\n", 803 | " folder_name = 'timeset' \n", 804 | " \n", 805 | " hdf.put(folder_name, DataFrame(timeset))\n", 806 | "\n", 807 | " hdf.close() " 808 | ] 809 | }, 810 | { 811 | "cell_type": "code", 812 | "execution_count": 162, 813 | "metadata": { 814 | "collapsed": true 815 | }, 816 | "outputs": [], 817 | "source": [ 818 | "def solve_hdf(): \n", 819 | " \n", 820 | " time_step_index = 0\n", 821 | " t = 0\n", 822 | " time_step = initial_time_step\n", 823 | "\n", 824 | " elapsed_time_since_last_trend_output = 0.0\n", 825 | " elapsed_time_since_last_profile_output = 0.0\n", 826 | "\n", 827 | " timeset = []\n", 828 | "\n", 829 | " output_file = os.path.join(root_path, 'hdf_results_file.h5')\n", 830 | " if os.path.exists(output_file):\n", 831 | " os.remove(output_file)\n", 832 | "\n", 833 | " last_profile_metadata = profile_metadata = None\n", 834 | " last_trend_metadata = trend_metadata = None\n", 835 | "\n", 836 | " while t < total_simulation_time:\n", 837 | " \n", 838 | " timeset.append(t)\n", 839 | "\n", 840 | " if time_step_index == 0:\n", 841 | " # always save initial condition, profiles and trends\n", 842 | " write_profiles_2_hdf(pipes, output_file, time_step_index, t)\n", 843 | " write_trends_2_hdf(pipes, positions, output_file, time_step_index, t)\n", 844 | " \n", 845 | " # call solve methods or in this case just use the values in the pipes list\n", 846 | "\n", 847 | " elapsed_time_since_last_trend_output += time_step\n", 848 | " elapsed_time_since_last_profile_output += time_step\n", 849 | "\n", 850 | " if elapsed_time_since_last_trend_output > trend_output_frequency or \\\n", 851 | " elapsed_time_since_last_profile_output > profile_output_frequency:\n", 852 | "\n", 853 | " if elapsed_time_since_last_trend_output > trend_output_frequency:\n", 854 | " write_trends_2_hdf(pipes, positions, output_file, time_step_index, t)\n", 855 | " elapsed_time_since_last_trend_output = 0.0\n", 856 | "\n", 857 | " if elapsed_time_since_last_profile_output > profile_output_frequency:\n", 858 | " write_profiles_2_hdf(pipes, output_file, time_step_index, t)\n", 859 | " elapsed_time_since_last_profile_output = 0.0\n", 860 | " \n", 861 | " # update timeset file \n", 862 | " update_timeset_2_hdf(timeset, time_step_index, output_file)\n", 863 | " \n", 864 | " time_step = new_time_step(time_step, time_step_increase_factor, maximum_time_step)\n", 865 | " t += time_step\n", 866 | " time_step_index += 1" 867 | ] 868 | }, 869 | { 870 | "cell_type": "code", 871 | "execution_count": 163, 872 | "metadata": { 873 | "collapsed": true 874 | }, 875 | "outputs": [], 876 | "source": [ 877 | "%%timeit \n", 878 | "solve_hdf()" 879 | ] 880 | }, 881 | { 882 | "cell_type": "markdown", 883 | "metadata": { 884 | "collapsed": true 885 | }, 886 | "source": [ 887 | "Using compression makes the file size drops from 62,7 Mb to 24,3 Mb.\n", 888 | "\n", 889 | "The use of `index=False` while appending data made the total time drops from ~780 to ~400 seconds. \n", 890 | "\n", 891 | "Results are almost the same in terms of file size but in terms of time, dumping the raw numpy data showed much faster. The main cause is that appending data to the trends is an expensive operation. \n", 892 | "If instead of appending trend data, a similar structure as for the profiles is built (save trend data every time), the file gets bigger, around 335 Mb, and the time increases as more elements are added to the hdf file." 893 | ] 894 | }, 895 | { 896 | "cell_type": "markdown", 897 | "metadata": {}, 898 | "source": [ 899 | "```\n", 900 | "lprun -f solve_hdf solve_hdf()\n", 901 | "\n", 902 | "Timer unit: 3.01859e-07 s\n", 903 | "\n", 904 | "Total time: 561.247 s\n", 905 | "File: \n", 906 | "Function: solve_hdf at line 1\n", 907 | "\n", 908 | "Line # Hits Time Per Hit % Time Line Contents\n", 909 | "==============================================================\n", 910 | " 1 def solve_hdf(): \n", 911 | " 2 \n", 912 | " 3 1 7 7.0 0.0 time_step_index = 0\n", 913 | " 4 1 3 3.0 0.0 t = 0\n", 914 | " 5 1 3 3.0 0.0 time_step = initial_time_step\n", 915 | " 6 \n", 916 | " 7 1 4 4.0 0.0 elapsed_time_since_last_trend_output = 0.0\n", 917 | " 8 1 2 2.0 0.0 elapsed_time_since_last_profile_output = 0.0\n", 918 | " 9 \n", 919 | " 10 1 3 3.0 0.0 timeset = []\n", 920 | " 11 \n", 921 | " 12 1 140 140.0 0.0 output_file = os.path.join(root_path, 'hdf_results_file.h5')\n", 922 | " 13 1 2035 2035.0 0.0 if os.path.exists(output_file):\n", 923 | " 14 1 9709 9709.0 0.0 os.remove(output_file)\n", 924 | " 15 \n", 925 | " 16 1 9 9.0 0.0 last_profile_metadata = profile_metadata = None\n", 926 | " 17 1 5 5.0 0.0 last_trend_metadata = trend_metadata = None\n", 927 | " 18 \n", 928 | " 19 10846 38872 3.6 0.0 while t < total_simulation_time:\n", 929 | " 20 \n", 930 | " 21 10845 39093 3.6 0.0 timeset.append(t)\n", 931 | " 22 \n", 932 | " 23 10845 29691 2.7 0.0 if time_step_index == 0:\n", 933 | " 24 # always save initial condition, profiles and trends\n", 934 | " 25 1 615936 615936.0 0.0 write_profiles_2_hdf(pipes, output_file, time_step_index, t)\n", 935 | " 26 1 250852 250852.0 0.0 write_trends_2_hdf(pipes, positions, output_file, time_step_index, t)\n", 936 | " 27 \n", 937 | " 28 # call solve methods or in this case just use the values in the pipes list\n", 938 | " 29 \n", 939 | " 30 10845 33713 3.1 0.0 elapsed_time_since_last_trend_output += time_step\n", 940 | " 31 10845 31333 2.9 0.0 elapsed_time_since_last_profile_output += time_step\n", 941 | " 32 \n", 942 | " 33 10845 42973 4.0 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency or elapsed_time_since_last_profile_output > profile_output_frequency:\n", 943 | " 34 \n", 944 | " 35 5437 14523 2.7 0.0 if elapsed_time_since_last_trend_output > trend_output_frequency:\n", 945 | " 36 5402 1437442723 266094.5 77.3 write_trends_2_hdf(pipes, positions, output_file, time_step_index, t)\n", 946 | " 37 5402 44686 8.3 0.0 elapsed_time_since_last_trend_output = 0.0\n", 947 | " 38 \n", 948 | " 39 5437 50935 9.4 0.0 if elapsed_time_since_last_profile_output > profile_output_frequency:\n", 949 | " 40 71 38007387 535315.3 2.0 write_profiles_2_hdf(pipes, output_file, time_step_index, t)\n", 950 | " 41 71 593 8.4 0.0 elapsed_time_since_last_profile_output = 0.0\n", 951 | " 42 \n", 952 | " 43 # update timeset file \n", 953 | " 44 5437 382398143 70332.6 20.6 timeset = update_timeset_2_hdf(timeset, time_step_index, output_file)\n", 954 | " 45 \n", 955 | " 46 10845 167670 15.5 0.0 time_step = new_time_step(time_step, time_step_increase_factor, maximum_time_step)\n", 956 | " 47 10845 44826 4.1 0.0 t += time_step\n", 957 | " 48 10845 34471 3.2 0.0 time_step_index += 1\n", 958 | "```" 959 | ] 960 | }, 961 | { 962 | "cell_type": "markdown", 963 | "metadata": {}, 964 | "source": [ 965 | "```\n", 966 | "lprun -f write_trends_2_hdf solve_hdf()\n", 967 | "\n", 968 | "Timer unit: 3.01859e-07 s\n", 969 | "\n", 970 | "Total time: 466.429 s\n", 971 | "File: \n", 972 | "Function: write_trends_2_hdf at line 1\n", 973 | "\n", 974 | "Line # Hits Time Per Hit % Time Line Contents\n", 975 | "==============================================================\n", 976 | " 1 def write_trends_2_hdf(pipes, positions, output_file, time_step_index, time):\n", 977 | " 2 \n", 978 | " 3 5403 26576072 4918.8 1.7 hdf = HDFStore(output_file, complevel=9, complib='blosc')\n", 979 | " 4 \n", 980 | " 5 5403 33586 6.2 0.0 pipe_index = 0\n", 981 | " 6 \n", 982 | " 7 59433 254140 4.3 0.0 for pipe in pipes:\n", 983 | " 8 54030 1539888 28.5 0.1 pipe_name = 'pipe_' + '{:03d}'.format(pipe_index)\n", 984 | " 9 54030 233666 4.3 0.0 folder_name = 'trends/' + pipe_name\n", 985 | " 10 54030 317760 5.9 0.0 local={}\n", 986 | " 11 \n", 987 | " 12 270150 1168972 4.3 0.1 for var, pos in zip(output_variables_per_pipe, positions):\n", 988 | " 13 216120 1076875 5.0 0.1 data = getattr(pipe, var)\n", 989 | " 14 216120 1114198 5.2 0.1 local[var] = data[pos]\n", 990 | " 15 \n", 991 | " 16 54030 246591786 4564.0 16.0 df = DataFrame(local, index=[time_step_index])\n", 992 | " 17 54030 319697 5.9 0.0 if time_step_index == 0:\n", 993 | " 18 10 214424 21442.4 0.0 hdf.put(folder_name, df, index=False)\n", 994 | " 19 else:\n", 995 | " 20 54020 1226677630 22707.8 79.4 hdf.append(folder_name, df, index=False)\n", 996 | " 21 54030 471009 8.7 0.0 pipe_index += 1\n", 997 | " 22 \n", 998 | " 23 \n", 999 | " 24 5403 38597675 7143.7 2.5 hdf.close()\n", 1000 | "```" 1001 | ] 1002 | }, 1003 | { 1004 | "cell_type": "markdown", 1005 | "metadata": { 1006 | "collapsed": true 1007 | }, 1008 | "source": [ 1009 | "There is still the possibiity of using numpy compressed format `numpy.savez_compressed`, in this case:\n", 1010 | "\n", 1011 | "**Before: ** 26.2Mb and 46 s\n", 1012 | "\n", 1013 | "**After: ** 13.9 Mb and 104 s\n", 1014 | "\n", 1015 | "If we try to save the compressed format only for the profiles we can achieve a better total file size (43% reduction) with less impact in time (14% increase):\n", 1016 | "\n", 1017 | "**14.9 Mb and 52.4 s**\n", 1018 | "\n", 1019 | "Since we have now `.npz` and `.npy` (compressed and uncompressed) files, it would be good to have this information in the meta-data files as well as an item even if the same method to load the data can be used `numpy.load`." 1020 | ] 1021 | }, 1022 | { 1023 | "cell_type": "markdown", 1024 | "metadata": { 1025 | "collapsed": true 1026 | }, 1027 | "source": [ 1028 | "Despite `numpy.savez_compressed` provides some facility, the file format together with the data introduces some metadata already. It was decided then to use the rawest format as possible, so `ndarray.tofile` was also tested.\n", 1029 | "**11.3Mb and ~40 s**" 1030 | ] 1031 | }, 1032 | { 1033 | "cell_type": "markdown", 1034 | "metadata": {}, 1035 | "source": [ 1036 | "# Reading data\n", 1037 | "In the same way we had some questions about writing data, reading is important also, so let's try retrieving the data from the file(s)..." 1038 | ] 1039 | }, 1040 | { 1041 | "cell_type": "code", 1042 | "execution_count": 164, 1043 | "metadata": { 1044 | "collapsed": true 1045 | }, 1046 | "outputs": [], 1047 | "source": [ 1048 | "def read_raw_profile(desired_properties):\n", 1049 | " project_name = 'my_project_folder'\n", 1050 | " project_path = os.path.join(root_path, project_name)\n", 1051 | " meta_path = os.path.join(project_path, 'meta')\n", 1052 | "\n", 1053 | " if not os.path.exists(project_path):\n", 1054 | " raise(IOError, 'Can not find the project folder: %s' % project_path)\n", 1055 | " if not os.path.exists(meta_path):\n", 1056 | " raise(IOError, 'Can not find the metadata folder: %s' % meta_path)\n", 1057 | "\n", 1058 | " # in this case there is a single metafile, but we could have more than one\n", 1059 | " profiles_meta_file = os.path.join(os.path.join(meta_path, 'meta_00000000'), 'profiles.json')\n", 1060 | " with open(profiles_meta_file) as file: \n", 1061 | " profiles_meta = json.load(file)\n", 1062 | "\n", 1063 | " for desired_property, desired_edge in desired_properties:\n", 1064 | " meta = None\n", 1065 | "\n", 1066 | " for pf in profiles_meta:\n", 1067 | " if pf['edge'] == desired_edge and pf['property_id'] == desired_property:\n", 1068 | " meta = pf\n", 1069 | " break\n", 1070 | "\n", 1071 | " data_fn = meta['fn']\n", 1072 | "\n", 1073 | " surface = []\n", 1074 | " for subdir, dirs, files in os.walk(project_path):\n", 1075 | " for dir in dirs:\n", 1076 | " if dir.startswith('TS'):\n", 1077 | " dir_path = os.path.join(project_path, dir)\n", 1078 | " profile_path = os.path.join(dir_path, 'profiles')\n", 1079 | " if os.path.exists(profile_path):\n", 1080 | " file_path = os.path.join(profile_path, data_fn)\n", 1081 | " surface.append(np.fromfile(file_path))\n", 1082 | " \n", 1083 | " # now use the array to do whathever you want..." 1084 | ] 1085 | }, 1086 | { 1087 | "cell_type": "code", 1088 | "execution_count": 165, 1089 | "metadata": { 1090 | "collapsed": true 1091 | }, 1092 | "outputs": [], 1093 | "source": [ 1094 | "single_property = [\n", 1095 | " ('pressure', 'pipe_002')\n", 1096 | "]" 1097 | ] 1098 | }, 1099 | { 1100 | "cell_type": "code", 1101 | "execution_count": 166, 1102 | "metadata": {}, 1103 | "outputs": [ 1104 | { 1105 | "name": "stdout", 1106 | "output_type": "stream", 1107 | "text": [ 1108 | "1.83 s ± 12.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 1109 | ] 1110 | } 1111 | ], 1112 | "source": [ 1113 | "%%timeit\n", 1114 | "# time to read one profile\n", 1115 | "read_raw_profile(single_property)" 1116 | ] 1117 | }, 1118 | { 1119 | "cell_type": "code", 1120 | "execution_count": 167, 1121 | "metadata": {}, 1122 | "outputs": [], 1123 | "source": [ 1124 | "#lprun -f read_raw_profile read_raw_profile(single_property)" 1125 | ] 1126 | }, 1127 | { 1128 | "cell_type": "code", 1129 | "execution_count": 168, 1130 | "metadata": {}, 1131 | "outputs": [], 1132 | "source": [ 1133 | "all_properties = []\n", 1134 | "\n", 1135 | "for i in range(number_of_pipes):\n", 1136 | " pipe_name = 'pipe_' + '{:03d}'.format(i)\n", 1137 | " for prop in output_variables_per_pipe:\n", 1138 | " all_properties.append((prop, pipe_name))\n", 1139 | " " 1140 | ] 1141 | }, 1142 | { 1143 | "cell_type": "code", 1144 | "execution_count": 169, 1145 | "metadata": {}, 1146 | "outputs": [ 1147 | { 1148 | "name": "stdout", 1149 | "output_type": "stream", 1150 | "text": [ 1151 | "1min 13s ± 664 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 1152 | ] 1153 | } 1154 | ], 1155 | "source": [ 1156 | "%%timeit\n", 1157 | "# time to read several profiles\n", 1158 | "read_raw_profile(all_properties)" 1159 | ] 1160 | }, 1161 | { 1162 | "cell_type": "markdown", 1163 | "metadata": {}, 1164 | "source": [ 1165 | "Reading one profile takes ~2 seconds while reading 40 profiles (4 variables x 10 pipes) takes ~80 seconds, fairly linear relation, which is good..." 1166 | ] 1167 | }, 1168 | { 1169 | "cell_type": "code", 1170 | "execution_count": 170, 1171 | "metadata": { 1172 | "collapsed": true 1173 | }, 1174 | "outputs": [], 1175 | "source": [ 1176 | "def read_hdf_profile(desired_properties):\n", 1177 | " from pandas import read_hdf\n", 1178 | " import pandas\n", 1179 | "\n", 1180 | " with pandas.HDFStore('hdf_results_file.h5') as hdf:\n", 1181 | " for desired_property, desired_edge in desired_properties:\n", 1182 | " surface = []\n", 1183 | " for key in hdf.keys():\n", 1184 | " if 'profiles' in key and desired_edge in key:\n", 1185 | " data = read_hdf('hdf_results_file.h5', key)\n", 1186 | " surface.append(data[desired_property].values)\n", 1187 | " \n" 1188 | ] 1189 | }, 1190 | { 1191 | "cell_type": "code", 1192 | "execution_count": 171, 1193 | "metadata": {}, 1194 | "outputs": [ 1195 | { 1196 | "name": "stdout", 1197 | "output_type": "stream", 1198 | "text": [ 1199 | "4.58 s ± 20.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 1200 | ] 1201 | } 1202 | ], 1203 | "source": [ 1204 | "%%timeit\n", 1205 | "read_hdf_profile(single_property)" 1206 | ] 1207 | }, 1208 | { 1209 | "cell_type": "code", 1210 | "execution_count": 172, 1211 | "metadata": {}, 1212 | "outputs": [ 1213 | { 1214 | "name": "stdout", 1215 | "output_type": "stream", 1216 | "text": [ 1217 | "3min 8s ± 5.36 s per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 1218 | ] 1219 | } 1220 | ], 1221 | "source": [ 1222 | "%%timeit\n", 1223 | "read_hdf_profile(all_properties)" 1224 | ] 1225 | }, 1226 | { 1227 | "cell_type": "code", 1228 | "execution_count": 173, 1229 | "metadata": { 1230 | "collapsed": true 1231 | }, 1232 | "outputs": [], 1233 | "source": [ 1234 | "#lprun -f read_hdf_profile read_hdf_profile(single_property)" 1235 | ] 1236 | }, 1237 | { 1238 | "cell_type": "markdown", 1239 | "metadata": {}, 1240 | "source": [ 1241 | "Read one property in the HDF takes ~4.5 seconds and to read 40 properties ~190 seconds (~42 times more), also fairly linear..." 1242 | ] 1243 | }, 1244 | { 1245 | "cell_type": "code", 1246 | "execution_count": null, 1247 | "metadata": { 1248 | "collapsed": true 1249 | }, 1250 | "outputs": [], 1251 | "source": [] 1252 | } 1253 | ], 1254 | "metadata": { 1255 | "kernelspec": { 1256 | "display_name": "Python 3", 1257 | "language": "python", 1258 | "name": "python3" 1259 | }, 1260 | "language_info": { 1261 | "codemirror_mode": { 1262 | "name": "ipython", 1263 | "version": 3 1264 | }, 1265 | "file_extension": ".py", 1266 | "mimetype": "text/x-python", 1267 | "name": "python", 1268 | "nbconvert_exporter": "python", 1269 | "pygments_lexer": "ipython3", 1270 | "version": "3.5.3" 1271 | } 1272 | }, 1273 | "nbformat": 4, 1274 | "nbformat_minor": 2 1275 | } 1276 | --------------------------------------------------------------------------------