diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5c8f8644..b5a2acf00 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,7 @@ test: # install the requirements - conda install --file conda_requirements.txt + - pip install mapbox-earcut - cd ./py_gnome - python ./setup.py install diff --git a/conda_requirements.txt b/conda_requirements.txt index 2adf0b9cd..140a88cef 100644 --- a/conda_requirements.txt +++ b/conda_requirements.txt @@ -38,6 +38,11 @@ regex unidecode>=0.04.19 pyshp=1.2.12 +#for SpatialRelease +trimesh +shapely +pyproj +mapbox_earcut # from NOAA_ORR_ERD channel # NOAA maintained packages gridded=0.2.5 diff --git a/experiments/gnome_weathering/viscosity_with_temp.ipynb b/experiments/gnome_weathering/viscosity_with_temp.ipynb new file mode 100644 index 000000000..d848b660a --- /dev/null +++ b/experiments/gnome_weathering/viscosity_with_temp.ipynb @@ -0,0 +1,609 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Viscosity as a function of temperature\n", + "\n", + "The basic equation is an exponential decay of viscosity with an increase in temperature.\n", + "\n", + "1. $ \\nu_0 = a \\cdot e^{\\left( \\frac {k_{\\nu 2}} T \\right)} $\n", + "\n", + "2. $ a = \\left( \\nu_{ref} \\cdot e^{-k_{\\nu 2} / T_{ref}} \\right) $\n", + "\n", + "If you have a reference viscsity, then this leads to:\n", + "\n", + "3. $ \\nu_0 = \\nu_{ref} \\cdot exp \\left(k_{\\nu 2} \\cdot \\left[ \\frac{1}{T_w} - \\frac{1}{T_{ref}} \\right] \\right)$\n", + "\n", + "Eq. 1 has two contstants: if we have the viscosity at two reference temperatures, then we can compute these two constants:\n", + "\n", + "4. $k_{\\nu 2} = \\frac{\\ln(\\nu_1 / \\nu_1)}{t_1^{-1} - t_2^{-1}}$\n", + "\n", + "and\n", + "\n", + "2. $ a = \\left( \\nu_{ref} \\cdot e^{-k_{\\nu 2} / T_{ref}} \\right) $" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import print_function, division\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## computing the decay constant\n", + "\n", + "Paine:\n", + "k_v2 = 9000 # from Paine\n", + "\n", + "### Abu-Eishah 1999\n", + "\n", + "kν2 = exp(5.471 + 0.00342 · Tb50) (Abu-Eishah 1999)\n", + "\n", + "(Tb50 is the temp at which 50% boils)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### compute both contants from two data points:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# compute both contants from two data points:\n", + " \n", + "def comp_a_k_v2(v1, t1, v2, t2):\n", + " k_v2 = (np.log(v1 / v2)) / ((1/t1) - (1/t2))\n", + " a1 = v1 * np.exp(-k_v2 / t1)\n", + " a2 = v2 * np.exp(-k_v2 / t2)\n", + " \n", + " return k_v2, a1, a2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ANS constants:\n", + "(3072.9432107061007, 0.0002664936187290487, 0.00026649361872904876)\n", + "kv_2 from Abu-Eishah 1999: 1929.5961281980653\n" + ] + } + ], + "source": [ + "# example data:\n", + "\n", + "# Alaska North Slope (2015)\n", + "#Dynamic Viscosity\n", + "# 17.92 cP\t0 °C\n", + "# 9.852 cP\t15 °C\n", + "\n", + "# Density\tTemperature\n", + "# 0.875 g/cm³\t0 °C\n", + "# 0.864 g/cm³\t15 °C\n", + "\n", + "v1 = 17.92 / 0.875\n", + "t1 = 273.16\n", + "\n", + "v2 = 9.852 / 0.864\n", + "t2 = 288.16\n", + "\n", + "# computing for sample data:\n", + "print(\"ANS constants:\")\n", + "\n", + "print(comp_a_k_v2(v1, t1, v2, t2))\n", + "\n", + "## computing the decay constant\n", + "# k_v2 = 9000 # from Paine\n", + "\n", + "# For ANS TB50 = 339.1 C = 612.3K\n", + "#\n", + "k_v2 = np.exp(5.471 + 0.00342 * 612.3) # (Abu-Eishah 1999),\n", + "print(\"kv_2 from Abu-Eishah 1999: \", k_v2)\n", + "\n", + "k_v2, a1, a2 = comp_a_k_v2(v1, t1, v2, t2)\n", + "\n", + "assert np.allclose(a1, a2)\n", + "\n", + "\n", + "# a1 = v1 * np.exp(-k_v2 / t1)\n", + "# a2 = v2 * np.exp(-k_v2 / t2)\n", + "\n", + "# a = v1\n", + "\n", + "def kvisc(t, a):\n", + " return a * np.exp(k_v2 / t)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "T = np.linspace(273, 303, 10) # environmentallly realistic range of temps.\n", + "kv1 = kvisc(T, a1)\n", + "kv2 = kvisc(T, a2)\n", + "\n", + "plt.plot(T, kv1)\n", + "plt.plot(T, kv2, '.')\n", + "plt.plot(t1, v1, 'o')\n", + "plt.plot(t2, v2, 'o')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More than two data points\n", + "\n", + "There are (at least) three options here:\n", + "\n", + "1) log-interpolate between each pair or points\n", + "\n", + "3) Do a least squares fit to all the data\n", + "\n", + "4) use the two points at the span of environmentally relevent data\n", + "\n", + "(1) is kind of a pain, so let's explore the other options" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Least squares fit\n", + "\n", + "#### Sample data: \n", + "HOOPS BLEND, ExxonMobil\n", + "\n", + "\n", + "| Viscosity | Temperature |\n", + "|------------|-------------|\n", + "| 19.6 cSt | 4.85 °C |\n", + "| 13.2 cSt | 15.85 °C |\n", + "| 9.85 cSt | 24.85 °C |\n", + "| 9.39 cSt | 26.85 °C |\n", + "| 6.99 cSt | 37.85 °C |\n", + "| 6.62 cSt | 39.85 °C |\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# least squares fit to data:\n", + "\n", + "def lstsq_k_a(viscs, temps):\n", + " viscs = np.asarray(viscs)\n", + " temps = np.asarray(temps)\n", + " A = np.c_[np.ones_like(viscs), 1.0 / temps]\n", + " b = np.log(viscs)\n", + " x = x, residuals, rank, s = np.linalg.lstsq(A, b)\n", + " K = x[1]\n", + " a = np.exp(x[0])\n", + " print(\"solution:\", K, a)\n", + " return K, a\n", + " \n", + "\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "solution: 2684.0463074485856 1.2345627928465315e-09\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chris.barker/miniconda3/envs/gnome/lib/python2.7/site-packages/ipykernel_launcher.py:8: FutureWarning: `rcond` parameter will change to the default of machine precision times ``max(M, N)`` where M and N are the input matrix dimensions.\n", + "To use the future default and silence this warning we advise to pass `rcond=None`, to keep using the old, explicitly pass `rcond=-1`.\n", + " \n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0.5,1,'HOOPS BLEND, ExxonMobil')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "viscs = np.array([19.6, 13.2, 9.85, 9.39, 6.99, 6.62]) * 1e-6\n", + "temps = np.array([4.85, 15.85, 24.85, 26.85, 37.85, 39.85]) + 273.16\n", + "\n", + "k_v2, A = lstsq_k_a(viscs, temps)\n", + "\n", + "\n", + "T = np.linspace(min(temps), max(temps), 20)\n", + "\n", + "fit_visc = A * np.exp(k_v2 / T)\n", + "\n", + "plt.plot(T, fit_visc)\n", + "plt.plot(temps, viscs, 'o')\n", + "plt.title('HOOPS BLEND, ExxonMobil')\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: a refined product:\n", + "\n", + "MARINE INTERMEDIATE FUEL OIL\n", + "\n", + "API: 12.9\n", + "\n", + "```\n", + "Dynamic Viscosity:\n", + "Viscosity\tTemperature\n", + "64000 cP\t-0.15 °C\n", + "31500 cP\t4.85 °C\n", + "16000 cP\t9.85 °C\n", + "8200 cP\t14.85 °C\n", + "```\n", + "\n", + "```\n", + "Density:\n", + "Density\tTemperature\n", + "0.991 g/cm³\t-0.15 °C\n", + "0.987 g/cm³\t4.85 °C\n", + "0.983 g/cm³\t9.85 °C\n", + "0.979 g/cm³\t14.85 °C\n", + "```\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "solution: 10695.492500499755 6.258824740751891e-13\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chris.barker/miniconda3/envs/gnome/lib/python2.7/site-packages/ipykernel_launcher.py:8: FutureWarning: `rcond` parameter will change to the default of machine precision times ``max(M, N)`` where M and N are the input matrix dimensions.\n", + "To use the future default and silence this warning we advise to pass `rcond=None`, to keep using the old, explicitly pass `rcond=-1`.\n", + " \n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0.5,1,'MARINE INTERMEDIATE FUEL OIL')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYcAAAEICAYAAAC0+DhzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi41LCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvSM8oowAAIABJREFUeJzt3Xd8FWXa//HPlULooQUEEggIFkBqQESwgO6ya8GCiGvBssta17I+lt/uqus+7iOru7qsfW2ga0FsWNC1ixrBANJBQFoASWihBwLX748z6OEkpJHknCTf9+t1XmfmPnPPXGdgcp2573tmzN0REREJFxftAEREJPYoOYiISCFKDiIiUoiSg4iIFKLkICIihSg5iIhIIUoOIiJSiJJDDWNmy81st5m1iCj/1szczNIjyu8KyvtFlF9qZnvNbJuZbTGzWWZ2etjn6UG9hGD+2cj1mFknM/Ow+U/NbFewzv2vtw7yPS41sy8ivtc6M2sQVvbrYJ3tItbpZrY9bH5QEN/uiOVmRXyX/eXLzey28uzXMm5nnZm9bWanFrGtUyLKTgrq3hJWNihsXdsj1r0t2C9l3ed7I5Z9KOzf7tdFxJQdNh+537ftjzf4f/Z8UdstIg4zs/8xs8VmttPMVprZvWaWFLbMs2b2vxH7NaE065fSUXKomZYBF+yfMbNjgHqRC5mZARcDG4FRRawn090bAk2AR4CXzKxJMdvdCPxvCbFd6+4Nw15nlLB8uATg+shCd18Zvs6guEdY2ZSg7G8R2+4RsaomQf3hwJ8i/2hTyv1ahu30AD4AXjezS0v47qOI+Hdy9ylh37lr+LqD18qgrCz7PDNi2WtLiCtSj4j6fytjfYCxwGjgEqAR8AtgMDChHOuSclJyqJmeI3Rg7TcKGF/EcoOANoT+4I40szpFrczd9wXrbAB0Lma744DuZnZieYIuhfuAm0tIUIfM3bOAeUDPiI9Ku19Lu50f3P2fwF3AGDMr8ng0s/qEEtY1QGczyyjvNmOdmXUGrgYudPdMdy9w93nAucBQMxsc3QhrDyWHmulroLGZHW1m8cD5QFGn9KOAt4CXg/nTi1iGYB2XAXuAFcVsdwfwV+CecsZdkizgU+DmSlo/AGbWH+gGLIn4qLT7taxeA1oCRx7k83OBbcArwPscmKBqmiFAtrtPCy9091WE9n/k2ZxUEiWHmmv/r9xTgYXA6vAPg1+j5wEvuPseYCKFm5b6m9lmYBdwP3CRu+eUsN3HgXZm9ouDfD7WzDaHvf5Spm8FdwDXmVlKGetB6KwjfNvjIj5fb2Y7gUxCzWhvFLGOYvdrKbcTaU3w3uwgn48CXnb3vcALwAVmlljCOsOVZZ/3j1i2fxm2AzAjov7Py1i/BbD2IJ+tDT6XKqAOnJrrOeBzoANFN32cDRQA7wbz/wE+NLMUd88Nyr5294Fm1hB4ilAzVLHtvu6eH/zx+Qth7fNhfufuT5b52/y0/rlm9jZwG7CgjNXvd/c/FvN5C8CBGwjFngjsjlimpP1amu1Eahu8b4z8wMzSgJOB24OiN4EngNMoOnkVpSz7/Gt3H1hEeQGh/REukdDZZLje7h55xlUW64HWB/msNaF+H6kCOnOoodx9BaED6ZeEmi0ijQIaAivN7AdCTRaJFPEH3d23EWoHvtjMepVi888AyYQSUGW4E/gNP/1RrTDuvtfd/07obOnqIj4vab+Wx9lADrCoiM8uJnScvhX8O30P1KXqm5ZWAukRZR0ovpmxPD4G0qzw6Lk0oD/wUQVvTw5CyaFmuwIY7O7bwwvNrC2htt3TCXW69iQ0cmYMRY9awt03AE8SatYplrsXEOpkvfUQYi9u/UsI9ZP8rjLWH7gXuMXM6hbxWZH7tazMrJWZXUso2d0edPxHugT4Mz/9O/Uk1Adxmpk1P5Ttl9HLwGVm1i8YanoEcCPwUhnWEWdmdcNeSZELuPt3wGPAf8ysv5nFm1lX4FXgQ3f/sJj1J0WsX3/fDoF2Xg3m7kuDkTeRLga+dff/BiNmfnD3HwgNIexuZt0OssoHgV+aWfdSbP5Fim47fihiHPz0Un2Zwu4mNHqqLG6J2Pb6YpZ9B9hE6AzlAMXs19JuZ7OZbQfmEDoDOc/dn45cSdDenw48HP7v5O6TCHWWF9VsV5RD3ufu/j6hprxngDxCzZHjCDVxhZsVsa0Hwz67ANgZ9lp6kM1dS+iHyPOEOuLfIzQQ4dwSwtwWsX6NbDoEpof9iIhIJJ05iIhIIUoOIiJSiJKDiIgUouQgIiKFVNuL4Fq0aOHp6enRDkNEpFqZPn36encv8Q4D1TY5pKenk5VV3GhCERGJZGalunBRzUoiIlKIkoOIiBSi5CAiIoUoOYiISCFKDiIiUkjtSg6zJ8AD3eCuJqH32XokrYhIUartUNYymz0B3vod7NkZms9bFZoH6D4ienGJiMSg2nPm8NHdPyWG/fbsDJWLiMgBak9yyMsuW7mISC1We5JDcmrZykVEarHakxyG3AGJ9Q4oyrckfEiJT70UEal1ak9y6D4CzhgLyWmAsTWpNf+TfwVv+8BoRyYiEnNqz2glCCWIYGRSvb37WPFYJndOmsdxhzenRcNCzzoXEam1as+ZQ4SE+DjuH96dbbsKuPPNedEOR0QkptTa5ADQuVUjrj+lM+/MWcvkOWujHY6ISMyo1ckBYPQJHenWtjF/enMuG7fvjnY4IiIxodYnh8T4OO4b3oO8nXv481tqXhIRASUHAI5u3ZhrTu7Em9+u4YP566IdjohI1Ck5BK4+qRNHHdaIP7w+h7wde6IdjohIVCk5BOokxHH/eT3YsH03d789P9rhiIhElZJDmG5tk7nqxMN5dUY2nyzKiXY4IiJRU6rkYGZNzGyimS00swVmdpyZNTOzD8xscfDeNGz5281siZktMrOfh5X3MbM5wWdjzcyC8iQzezkon2pm6RX9RUvruiGdOKJVQ25/dQ5bdql5SURqp9KeOfwTeM/djwJ6AAuA24CP3L0z8FEwj5l1AUYCXYGhwCNmFh+s51FgNNA5eA0Nyq8ANrl7J+ABYMwhfq9yS0qI577hPcjZuou/vrMgWmGIiERVicnBzBoDJwBPAbj7bnffDAwDxgWLjQPOCqaHAS+5e767LwOWAP3MrDXQ2N0z3d2B8RF19q9rIjBk/1lFNPRIa8JvTujIS9+sYsri3GiFISISNaU5c+gI5ALPmNlMM3vSzBoArdx9LUDw3jJYvi2wKqx+dlDWNpiOLD+gjrsXAHlA83J9owpy4ylH0DGlAbe9Oodt+QXRDEVEpMqVJjkkAL2BR929F7CdoAnpIIr6xe/FlBdX58AVm402sywzy8rNrdxf9HUT47lveHfW5O3k3slqXhKR2qU0ySEbyHb3qcH8RELJYl3QVETwnhO2fFpY/VRgTVCeWkT5AXXMLAFIBjZGBuLuT7h7hrtnpKSklCL0Q9OnfTMuP74Dz3+9kq+Wrq/07YmIxIoSk4O7/wCsMrMjg6IhwHxgEjAqKBsFvBlMTwJGBiOQOhDqeJ4WND1tNbP+QX/CJRF19q9rOPBx0C8RdTf/7EjSm9fntlfnsGO3mpdEpHYo7Wil64D/mNlsoCfwV+Be4FQzWwycGszj7vOACYQSyHvANe6+N1jPVcCThDqplwKTg/KngOZmtgS4ieKbrapUvTrxjDm3Oys37uBv7y2KdjgiIlXCYuQHepllZGR4VlZWlW3vrknzGJe5nAm/PY6+6c2qbLsiIhXJzKa7e0ZJy+kK6VK6ZeiRpDatxy0TZ7Nz996SK4iIVGNKDqVUv04CY87tzrL12/n7f9W8JCI1m5JDGQw4vAUXHtuOp75cxldLNHpJRGouJYcy+n+/PJqOLRpw/cvfkrs1P9rhiIhUCiWHMmqQlMDDF/Zmy8493DThW/btq54d+iIixVFyKIejDmvMXWd2Zcri9Tz62dJohyMiUuGUHMppZN80zujRhn988B3fLC90MbeISLWm5FBOZsZfz+5GWtN6XPfCTDZu3x3tkEREKoySwyFoVDeRh37Vm43bd3PzK7OorhcUiohEUnI4RN3aJvOH047m44U5PDllWbTDERGpEEoOFeCS49oztOthjHlvITNXbop2OCIih0zJoQKYGWOGd+ew5Lpc+8JM8nbo2dMiUr0pOVSQ5HqJ/OuCXqzbsotbXlX/g4hUb0oOFahXu6bcOvQo3p+3jvGZK6IdjohIuSk5VLArBnZg8FEtueedBcxdnRftcEREykXJoYLFxRl/P68HzRvW4ZoXZrB1l/ofRKT6UXKoBE0b1GHsBb3I3rST21+bo/4HEal2lBwqSd/0Ztx06hG8PXstL32zKtrhiIiUiZJDJbrqxMMZ1LkFd02ax4K1W6IdjohIqSk5VKK4OOMfI3rSuF4i174wg+35BdEOSUSkVJQcKllKoyT+eX5Pvl+/nTvenBftcERESkXJoQoM6NSC6wZ35tUZ2Uycnh3tcERESqTkUEWuH9KZ/h2b8ac35rIkZ2u0wxERKZaSQxWJjzP+ObIX9erEc81/ZrJjt/ofRCR2KTlUoVaN6/Lg+T1ZnLOVG1/W86dFJHYpOVSxE45I4Q+ndeH9eev4+weLoh2OiEiREqIdQG10+fHpLMnZysOfLKVTy4ac3Ss12iGJiBygVGcOZrbczOaY2bdmlhWUNTOzD8xscfDeNGz5281siZktMrOfh5X3CdazxMzGmpkF5Ulm9nJQPtXM0iv2a8YWM+PPZ3ajf8dm3DpxDtNX6AFBIhJbytKsdLK793T3jGD+NuAjd+8MfBTMY2ZdgJFAV2Ao8IiZxQd1HgVGA52D19Cg/Apgk7t3Ah4AxpT/K1UPdRLiePTCPrRpUpffPpdF9qYd0Q5JRORHh9LnMAwYF0yPA84KK3/J3fPdfRmwBOhnZq2Bxu6e6aE70Y2PqLN/XROBIfvPKmqypg3q8OSovuQX7OPX47LYpiuoRSRGlDY5OPBfM5tuZqODslbuvhYgeG8ZlLcFwu80lx2UtQ2mI8sPqOPuBUAe0DwyCDMbbWZZZpaVm5tbytBjW6eWDXnkwt4sztnGDS/NZK9GMIlIDChtcjje3XsDvwCuMbMTilm2qF/8Xkx5cXUOLHB/wt0z3D0jJSWlpJirjUGdU7jzjC58uCCHv723MNrhiIiULjm4+5rgPQd4HegHrAuaigjec4LFs4G0sOqpwJqgPLWI8gPqmFkCkAxsLPvXqb4uOS6di/u35/HPv2dClm7xLSLRVWJyMLMGZtZo/zTwM2AuMAkYFSw2CngzmJ4EjAxGIHUg1PE8LWh62mpm/YP+hEsi6uxf13DgY6+FT8i544wuDOzUgj+8Podpy2pVbhSRGFOaM4dWwBdmNguYBrzj7u8B9wKnmtli4NRgHnefB0wA5gPvAde4+95gXVcBTxLqpF4KTA7KnwKam9kS4CaCkU+1TWJ8HA//qjdpTevz2+eyWLlBI5hEJDqsuv5Az8jI8KysrGiHUSmWrd/OWQ9/SctGSbx29QAa1U2MdkgiUkOY2fSwSxIOSrfPiEEdWjTg0Qt7s2z9dq57USOYRKTqKTnEqAGdWvDnYV35dFEuf313QbTDEZFaRvdWimEXHtuexeu28dQXy+jUsiEX9GsX7ZBEpJbQmUOM++NpR3PiESn86Y25ZC7dEO1wRKSWUHKIcQnxcfzrV71Ib9GAq/4zneXrt0c7JBGpBZQcqoHGdRN5alQGBlwx7hvydu6JdkgiUsMpOVQT7Zs34LGL+rBy4w6ufWEGe/bui3ZIIlKDKTlUI8d2bM49Zx3DlMXr+f2EWRriKiKVRqOVqpkRfdPYsH03Y95bSMO6CdxzVjdqwd3NRaSKKTlUQ1eddDhbd+3hkU+X0qhuArcNPUoJQkQqlJJDNfU/Pz+SrbsKePyz72lcN5FrTu4U7ZBEpAZRcqimQs+h7sq2/ALue38RDZMSGDUgPdphiUgNoeRQjcXFGfcN7862/ALunDSPhkkJnNsnteSKIiIl0Gilai4hPo5/XdCL4zs155ZXZ/Pe3B+iHZKI1ABKDjVA3cR4nrg4g+6pyfzuxZlMWVwznq8tItGj5FBDNEhK4NlL+9ExpQGjx09n+opN0Q5JRKoxJYcaJLl+Is9dcSytGidx2TPTmL9mS7RDEpFqSsmhhklplMTzvz6WhkkJXPL0VL7P3RbtkESkGlJyqIFSm9bnuV8fiztc9ORUVm/eGe2QRKSaUXKooQ5Pacj4K/qxNb+Ai56cSu7W/GiHJCLViJJDDda1TTLPXtaXH/J2cfFTU8nboVt9i0jpKDnUcH3aN+OJS/rwfe52Ln12GtvzC6IdkohUA0oOtcCgzimMvaAXs7PzGP1cFrv27I12SCIS45Qcaomh3Q7jb+d258slG7juxZnsLtDDgkTk4JQcapFz+6Ty5zO78sH8dVz5/HSdQYjIQSk51DKjBqRzz9nd+GRRDpc98436IESkSKVODmYWb2YzzeztYL6ZmX1gZouD96Zhy95uZkvMbJGZ/TysvI+ZzQk+G2vBE2rMLMnMXg7Kp5pZesV9RYl04bHteWBET6Yt38hFGsUkIkUoy5nD9cCCsPnbgI/cvTPwUTCPmXUBRgJdgaHAI2YWH9R5FBgNdA5eQ4PyK4BN7t4JeAAYU65vI6V2Vq+2PHJhb+at3sL5T2TqOggROUCpkoOZpQKnAU+GFQ8DxgXT44Czwspfcvd8d18GLAH6mVlroLG7Z7q7A+Mj6uxf10RgiOm5l5Xu510P46lLM1ixYQfnP57JGl1JLSKB0p45PAjcAoQPcWnl7msBgveWQXlbYFXYctlBWdtgOrL8gDruXgDkAc0jgzCz0WaWZWZZubm6LXVFGNQ5hfFX9CN3az7nPZbJ8vXbox2SiMSAEpODmZ0O5Lj79FKus6hf/F5MeXF1Dixwf8LdM9w9IyUlpZThSEn6pjfjxdH92bG7gPMez2TRD1ujHZKIRFlpzhyOB840s+XAS8BgM3seWBc0FRG85wTLZwNpYfVTgTVBeWoR5QfUMbMEIBnYWI7vI+XUrW0yE357HHEG5z+RyezszdEOSUSiqMTk4O63u3uqu6cT6mj+2N0vAiYBo4LFRgFvBtOTgJHBCKQOhDqepwVNT1vNrH/Qn3BJRJ396xoebKPQmYNUrs6tGvHKbwfQMCmBX/17KtOWKT+L1FaHcp3DvcCpZrYYODWYx93nAROA+cB7wDXuvv9qq6sIdWovAZYCk4Pyp4DmZrYEuIlg5JNUvXbN6zPxygG0apzEJU9P5dNFOSVXEpEax6rrD/SMjAzPysqKdhg11vpt+Vzy1DQW52xl7Mhe/OKY1tEOSUQqgJlNd/eMkpbTFdJSpBYNk3hxdH+OaZvMNS/M4NXp2SVXEpEaQ8lBDiq5XuiZ1Mcd3pzfvzKL5zKXRzskEakiSg5SrAZJCTw1qi+nHN2KP705j0c/XRrtkESkCig5SInqJsbz6EW9GdazDWPeW8h97y+kuvZViUjpJEQ7AKkeEuPj+MeIntSvk8DDnyxlzeZd3HvuMSQlxJdcWUSqHSUHKbX4OOOvZ3ejTXJd/v7Bd6zauIPHL+5D84ZJ0Q5NRCqYmpWkTMyM64Z05uFf9WbO6jzOeuRLFq/T7TZEaholBymX07q35uXfHseuPfs455Gv+Ow73QhRpCZRcpBy65nWhDevOZ7UZvW57JlpjM9cHu2QRKSCKDnIIWnTpB4TrzyOwUe14o4353Hnm3Mp2Luv5IoiEtOUHOSQNUhK4PGL+zD6hI6My1zB5eOy2LJLjx4Vqc6UHKRCxMcZ/++XR3PvOcfw1ZL1nPvIV6zauCPaYYlIOSk5SIUa2a8d46/oR87WfIY9/CXfLNdtv0WqIyUHqXADDm/BG9ccT3K9RC7891Rem6Gb9olUN0oOUik6tGjA61cPoE/7ptw0YRb3v7+Ifft0yw2R6kLJQSpNk/p1GH9FP0b2TeOhT5Zw7Ysz2Ll7b8kVRSTqlBykUiXGx/F/5xzDH087mslzf+D8JzLJ2bIr2mGJSAmUHKTSmRm/HtSRf1+cwZKcbZz50JfMWLkp2mGJSDGUHKTKnNKlFROvHEBigjHisUz+/fn3uvW3SIxScpAq1aVNY96+bhCnHN2Ke95dwG/GZ7E96wV4oBvc1ST0PntCtMMUqfV0y26pcsn1Enn0ot6M+2o5syb/m7hlTwL5oQ/zVsFbvwtNdx8RtRhFajudOUhUmBmXHt+BMclvUG9/Ythvz0746O7oBCYigJKDRFmd7WuK/iBPF86JRJOSg0RXcmqRxbsbtKniQEQknJKDRNeQOyCx3gFFO0ni1ryzeOqLZRrNJBIlSg4SXd1HwBljITkNMEhOw0//J9uOOIe/vD2f3z43nbwduv23SFWzkn6ZmVld4HMgidDoponufqeZNQNeBtKB5cAId98U1LkduALYC/zO3d8PyvsAzwL1gHeB693dzSwJGA/0ATYA57v78uLiysjI8KysrLJ/Y6kW3J2nv1zO/727gMOS6/LQr3rTM61JtMMSqfbMbLq7Z5S0XGnOHPKBwe7eA+gJDDWz/sBtwEfu3hn4KJjHzLoAI4GuwFDgETOLD9b1KDAa6By8hgblVwCb3L0T8AAwplTfUmosM+OKgR145crjcIfzHvuKp9XMJFJlSkwOHrItmE0MXg4MA8YF5eOAs4LpYcBL7p7v7suAJUA/M2sNNHb3TA8d4eMj6uxf10RgiJnZoX01qQl6tWvKO78byIlHtOTut+dz5fPTydupZiaRylaqPgczizezb4Ec4AN3nwq0cve1AMF7y2DxtsCqsOrZQVnbYDqy/IA67l4A5AHNi4hjtJllmVlWbm5u6b6hVHtN6tfh35f04Y+nHc1HC3I4bewUpi3TQ4REKlOpkoO773X3nkAqobOAbsUsXtQvfi+mvLg6kXE84e4Z7p6RkpJSUthSg+y/ed+EK48jzozzn8jk7rfm6xbgIpWkTKOV3H0z8CmhvoJ1QVMRwXtOsFg2kBZWLRVYE5SnFlF+QB0zSwCSAf00lEJ6t2vK5OsHcXH/9jz95TJ+OXYKWXoUqUiFKzE5mFmKmTUJpusBpwALgUnAqGCxUcCbwfQkYKSZJZlZB0Idz9OCpqetZtY/6E+4JKLO/nUNBz529TzKQTRISuDuYd144TfHsmfvPs57PJP/fXs+u/boLEKkopTmxnutgXHBiKM4YIK7v21mmcAEM7sCWAmcB+Du88xsAjAfKACucff9R+1V/DSUdXLwAngKeM7MlhA6YxhZEV9OarYBh7fgvRtO4P/eXcCTXyzj44U53HdeD/q0bxrt0ESqvRKvc4hVus5Bwn2xeD23vjqbtXk7+c2gjtx46hHUTYwvuaJILVOR1zmIxLyBnVvw3g2DOL9vOx7//HtOGzuFmXranEi5KTlIjdGobiL/d84xjL+8Hzt37+XcR79izHsLyS9QX4RIWSk5SI1zwhEpvHfjCZzXJ41HP13KGf/6gtnZm6Mdlki1ouQgNVLjuomMGd6dZy7ry5adBZz9yFfc//4inUWIlJKSg9RoJx/ZkvdvPIGze7XloU+WcOa/vmTu6rxohyUS85QcpMZLrpfI/ef14OlLM9i0YzfDHv6Sv7w9n627dI8mkYNRcpBaY/BRrfjgxhMZkZHG018uY/DfP+ONmat1p1eRIig5SK2SXD80oumNq4+nTXJdbnj5W85/4msW/rAl2qGJxBQlB6mVeqQ14fWrj+fec45h8bqtnDb2C/781jy2qKlJBFBykFosLs4Y2a8dn9x8EiP7pvHsV8sZfP9nvDo9W01NUuspOUit16R+He45+xgmXTOQ1Kb1+P0rszjvsUzmr1FTk9ReSg4igWNSk3ntqgH87dzufL9+O6f/awp3vjlXT56TWknJQSRMXJwxom8an/z+JC7q357nvl7B4Ps/ZULWKvbtU1OT1B5KDiJFSK6fyN3DujHp2oG0b16fWybOZvhjX+kCOqk1lBxEitGtbTITrxzAfcO7s2LDDs586Av+9MZcNmzLj3ZoIpVKyUGkBHFxxnkZaXx880lcclw6/5m6ghPv+5R/friYbfkF0Q5PpFLoYT8iZbQkZxv3v7+I9+b9QPMGdbhucCd+dWx76iTot5bEPj3sR6SSdGrZkMcu7sPrVw+gc6uG3PXWfIb841PemLlandZSYyg5iJRTr3ZNefE3/Rl3eT8aJSVyw8vf8suxU/hkYY4uopNqT8lB5BCYGScekcLb1w3knyN7smP3Xi579hvOf+Jrpq/QY0ql+lJyEKkAcXHGsJ5t+fCmE/nLsK58n7udcx/9it+Mz2Lxuq3RDk+kzNQhLVIJtucX8MyXy3j8s+/ZvruAc3qncuOpR9C2Sb1ohya1XGk7pJUcRCrRxu27eeSTJYzPXAHAxce155qTO9GsQZ0oRya1lZKDSAxZvXknD37wHa/OyKZ+nQQuOa49lw/sQIuGSdEOTWoZJQeRGPTduq08+OF3TJ77A0kJcYzs247RJ3SkjZqbpIooOYjEsKW523js06W8PnM1AOf0bsuVJx5Ox5SGUY5MaroKuwjOzNLM7BMzW2Bm88zs+qC8mZl9YGaLg/emYXVuN7MlZrbIzH4eVt7HzOYEn401MwvKk8zs5aB8qpmll+dLi1QXh6c05L7zevDZLSdz4bHtePPbNQz5x2dc88IM5q3Rzf0k+kozlLUA+L27Hw30B64xsy7AbcBH7t4Z+CiYJ/hsJNAVGAo8YmbxwboeBUYDnYPX0KD8CmCTu3cCHgDGVMB3E4l5bZvU48/DuvHFrYO58sTD+WxRLqeN/YLLnpnG9BUbox2e1GIlJgd3X+vuM4LprcACoC0wDBgXLDYOOCuYHga85O757r4MWAL0M7PWQGN3z/RQW9b4iDr71zURGLL/rEKkNkhplMStQ4/iy9sGc/PPjmBWdh7nPprJ+Y9n8vl3ubriWqpcmS6CC5p7egFTgVbuvhZCCQRoGSzWFlgVVi07KGsbTEeWH1DH3QuAPKB5EdsfbWZZZpaVm5tbltBFqoXkeolcO7gzX9x6Mnec3oUVG3ZwydPTOPOhL3lv7lrdu0mqTKmTg5k1BF4FbnD34h6uW9Qvfi/CjcQ4AAAOPUlEQVSmvLg6Bxa4P+HuGe6ekZKSUlLIItVW/ToJXD6wA5/dchL3nnMMW3bt4crnZ/CzBz/n1enZ7C7YF+0QpYYrVXIws0RCieE/7v5aULwuaCoieM8JyrOBtLDqqcCaoDy1iPID6phZApAMqMFVar2khHhG9mvHRzedyNgLepEQZ/z+lVkcP+ZjHvzwO3K27op2iFJDlWa0kgFPAQvc/R9hH00CRgXTo4A3w8pHBiOQOhDqeJ4WND1tNbP+wToviaizf13DgY9djawiP0qIj+PMHm2YfP0gnr2sL93aNObBDxdz/L0fc/1LM5m5Ujf5k4pV4nUOZjYQmALMAfafy/4/Qv0OE4B2wErgPHffGNT5A3A5oZFON7j75KA8A3gWqAdMBq5zdzezusBzhPozNgIj3f374uLSdQ5S2y1bv53xmcuZmJXN1vwCeqQmM2pAOqd1b01SQnyJ9aV20kVwIrXEtvwCXpuRzbivlrM0dzstGtbhgn7tuPDY9hyWXDfa4UmMUXIQqWXcnS+WrGfcV8v5aGEO8WYM7XYYlw5Ip0/7pmh0uEDpk0NCVQQjIpXPzBjUOYVBnVNYuWEH4zOX83LWKt6evZaubRpz6YB0zujRhrqJanKSkunMQaQG27G7gNdnrmbcV8v5bt02mjWow8i+aYzs2452zetHOzyJAjUriciP3J3MpRt49qvlfLhgHfsc+ndsxoiMNH7RrTX16uhsorZQchCRIq3ZvJPXZmTzyvRsVmzYQcOkBM7o0ZrzMtLoldYk1DcxewJ8dDfkZUNyKgy5A7qPiHboUgGUHESkWO7OtGUbmZCVzbtz1rJzz14OT2nA7alzGLL4Hqxg508LJ9aDM8YqQdQASg4iUmrb8gt4Z/YaJmRl88+1F5Mat77wQslpcOPcqg9OKlSFPc9BRGq+hkkJnN+3Ha9eNYC2cRuKXMbzsossl5pJyUFEDmDJqUWWr97XnGEPf8nzX69g0/bdVRyVVDUlBxE50JA7Qn0MYTyhHt91u5Fdu/fyxzfm0veeD7n0mWm8Oj2bLbv2RClQqUy6CE5EDrS/0zlstJINuYPB3Udwsjvz1mzhrdlreHvWWn7/yizqvB7HSUekcHqPNpxydEvq19GflZpAHdIiUi7uzsxVm3lr1hrenbOWdVvyqZcYz+CjW3JG9zacdGSKrsaOQRqtJCJVZt8+55vlG3lr9homz/mBDdt30zApgZ91acXpPVozsFMKdRLUih0LlBxEJCoK9u4j8/sNvD1rLZPnrmXLrgKS6yUytOthnN6jNcd1bE5CvBJFtCg5iEjU7S7YxxdLcnl71lr+O38d2/ILaN6gDqcc3YpTurRiYKcWunVHFVNyEJGYsmvPXj5dlMs7c9by6cIctuYXUDcxjoGdUji1S0sGH9WKlEZJ0Q6zxtMtu0UkptRNjGdot8MY2u0wdhfsY9qyjXy4YB0fzF/HhwvWYTaHnmlNOLVLK049uhWdWjbUMyiiSGcOIhJV7s7CH7b+mCRmZ+cB0L55fU4Nmp8y2jdVP0UFUbOSiFRLP+Tt4sMFoUTx1ZIN7N67j+R6iQw+qiWndmnFCUek0DBJjR7lpeQgItXetvwCpnyXywcL1vHxwhw279hDYryR0b4ZJxyRwqDOLejSujFxcWp+Ki0lBxGpUQr27mPGys18uGAdn3+Xy8IftgLQvEEdBnZuwaDOKZzQuQUtG9eNcqSxTclBRGq0nC27+GLJej7/Lpcvlqxn/bbQzQCPOqwRg4Jk0a9DM12lHUHJQURqjX37nAU/bGHK4vVMWZzLN8s2sXvvPuokxHFsh2ac0DmFQUe04MhWjWr9CCglBxGptXbsLmDqso1M+S6ULBbnbAOgZaMkBnZuwXEdm9O/Y3NSm9ardclC1zmISK1Vv04CJx/ZkpOPbAnA2rydTPluPZ8vzuWThTm8NmM1AG2S63Jsx+Yc26EZx3ZsTnrz+rUuWRyMzhxEpFbZt8/5LmcrU7/fyLRlG5m6bMOP/RUtGyXRL0gU/Ts0q5EX4lVYs5KZPQ2cDuS4e7egrBnwMpAOLAdGuPum4LPbgSuAvcDv3P39oLwP8CxQD3gXuN7d3cySgPFAH2ADcL67Ly8pcCUHEakI7s7S3O1MXbaBqd+HksW6LflAaCRUvw7NfjyzOLJVo2o/bLYik8MJwDZgfFhy+Buw0d3vNbPbgKbufquZdQFeBPoBbYAPgSPcfa+ZTQOuB74mlBzGuvtkM7sa6O7uV5rZSOBsdz+/pMCVHESkMrg7KzbsCEsWG1m9eScATeon0je9GX3aN6V3u6Yc0za52t04sML6HNz9czNLjygeBpwUTI8DPgVuDcpfcvd8YJmZLQH6mdlyoLG7ZwbBjQfOAiYHde4K1jUReMjMzKtre5eIVGtmRnqLBqS3aMD5fdsBsGrjjh+boKYu28gH89cBkBBnHN26Mb3aNaF3u6b0ateEds1qRr9FeTukW7n7WgB3X2tmLYPytoTODPbLDsr2BNOR5fvrrArWVWBmeUBzYH3kRs1sNDAaoF27duUMXUSkbNKa1SetWX3O7ZMKwIZt+cxcuZmZqzYxY8VmJk7PZnzmCiDUFNWrXRN6BcmiR2oTGlTD231UdMRFpUsvpry4OoUL3Z8AnoBQs1J5AhQROVTNGyZxSpfQTQEB9u5zFv2w9cdkMXPVJj5ckANAnMGRhzWmd5AwerdrQnrzBjHfd1He5LDOzFoHZw2tgZygPBtIC1suFVgTlKcWUR5eJ9vMEoBkYGM54xIRqXLxcUaXNo3p0qYxFx7bHoBN23fzbfZmZq7YxMxVm5n07Rr+M3UlAI2SEujatjHd2iRzTGoy3dom0yHGEkZ5k8MkYBRwb/D+Zlj5C2b2D0Id0p2BaUGH9FYz6w9MBS4B/hWxrkxgOPCx+htEpLpr2qDOAdda7N3nLM3dxsyVm5izOo85q7cw/usV7C7YB0CDOvF0bRNKFMekhhJHx5SGxIcnjNkT4KO7IS8bklNhyB3QfUSlxF+a0UovEup8bgGsA+4E3gAmAO2AlcB57r4xWP4PwOVAAXCDu08OyjP4aSjrZOC6YChrXeA5oBehM4aR7v59SYFrtJKIVHd79u5j8bptzF2Tx9zVecxZnceCtVvYtSeUMOrXiadL68Z0a5vML3wKfefcRVzBzp9WkFgPzhhbpgSh22eIiFRDBXv3sTR3O3NWhxLG3NV5zFuzhQ/sGlLjCo3TgeQ0uHFuqdev22eIiFRDCfFxHHlYI448rBHDg9FRe/c5cXdvKLpCXnbR5YdIz90TEYlx8XGGJacW/eHByg+RkoOISHUw5I5QH0O4xHqh8kqg5CAiUh10HxHqfE5OAyz0XsbO6LJQn4OISHXRfUSlJYNIOnMQEZFClBxERKQQJQcRESlEyUFERApRchARkUKq7e0zzCwXWBHtOAItKOL5EzFGMR66WI8PYj/GWI8PYj/GQ42vvbunlLRQtU0OscTMskpzr5JoUoyHLtbjg9iPMdbjg9iPsariU7OSiIgUouQgIiKFKDlUjCeiHUApKMZDF+vxQezHGOvxQezHWCXxqc9BREQK0ZmDiIgUouQgIiKFKDmUgpmlmdknZrbAzOaZ2fVB+ctm9m3wWm5m30bUa2dm28zs5liLz8y6m1lmsPyc4FneMROjmSWa2bggtgVmdnuU4utpZl8H8WWZWb+wOreb2RIzW2RmP6/M+MoTo5mdambTg3043cwGx1J8YfWq5Dgpb4wxdKwc7N+5co4Vd9erhBfQGugdTDcCvgO6RCzzd+COiLJXgVeAm2MpPkK3ap8N9AjmmwPxMRbjr4CXgun6wHIgvarjA/4L/CIo/yXwaTDdBZgFJAEdgKXR2ofFxNgLaBNMdwNWx1J8YfWq5Dgp5z6MmWOlmBgr5VjR8xxKwd3XAmuD6a1mtgBoC8wHMDMDRgA//jIzs7OA74HtMRjfz4DZ7j4rqHOQh9NGNUYHGphZAlAP2A1siUJ8DjQOFksG1gTTwwgdkPnAMjNbAvQDMmMlRnefGVZ9HlDXzJKCmKMeH1TtcVLOGGPpWDlYjJVzrFR2pq5pLyAdWAk0Dis7AcgKm29A6I9EQ+AuquAXURnjuwF4DngfmAHcEoP7MBF4Ccgl9IdjdDTiA44OplcBqwndegDgIeCisDpPAcNjKcaI5YcDH8ZSfNE8TsoQY8wcK8XEWCnHivocysDMGhI6Bb7B3cMz8wXAi2HzfwYecPdtMRpfAjAQuDB4P9vMhsRYjP2AvUAbQs02vzezjlGI7yrgRndPA24klAQArIjqVTIuvAwx7l++KzAG+G2MxReV46SMMcbSsXKwGCvnWKnKLFidX4Sy8/vATRHlCcA6IDWsbAqhdr/lwGZgI3BtDMU3Eng2bP5PwP/E2D58GLg4bP5pYERVxwfk8dP1QAZsCaZvB24PW+594Lho7MODxRjMpxJqsz6+smMrxz6s8uOkHDHGzLFSTIyVcqzozKEUgvbwp4AF7v6PiI9PARa6e/b+Ancf5O7p7p4OPAj81d0fipX4CP2n625m9YN2yhMJ2v5jKMaVwGALaQD0BxZGIb41hPYPhPpDFgfTk4CRZpZkZh2AzsC0yoqvPDGaWRPgHUJJ7MvKjK088VX1cVKeGImtY+VgMVbOsVIVvyaq+4vQ6aQTGrXwbfD6ZfDZs8CVxdS9i8ofrVTm+ICLCHVSzgX+Fmv7kFA79CtBjPOp5F9rB4svKJ9OaGTSVKBPWJ0/EBqltIhgFEksxQj8kVAb9Ldhr5axEl9E3Uo/Tg7h3zkmjpVi/p0r5VjR7TNERKQQNSuJiEghSg4iIlKIkoOIiBSi5CAiIoUoOYiISCFKDiIiUoiSg4iIFPL/AcOev32ZQtkhAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dviscs = np.array([64000, 31500, 16000, 8200])\n", + "densities = np.array([.991, .987, .983, .979])\n", + "viscs = dviscs / densities\n", + "temps = np.array([-0.15, 4.85, 9.85, 14.85]) + 273.16\n", + "\n", + "k_v2, A = lstsq_k_a(viscs, temps)\n", + "\n", + "\n", + "T = np.linspace(min(temps), max(temps), 20)\n", + "\n", + "fit_visc = A * np.exp(k_v2 / T)\n", + "\n", + "plt.plot(T, fit_visc)\n", + "plt.plot(temps, viscs, 'o')\n", + "plt.title('MARINE INTERMEDIATE FUEL OIL')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## One data point\n", + "\n", + "If we only have one data point, we need to assume a value for the decay constant. For now, we're using the mode of the data:\n", + "\n", + "2100 K\n", + "\n", + "For an oil with a single value:\n", + "kvisc = 9.85 cSt\n", + "T = 24.85 C\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.32e-05 289.01\n", + "A: 9.223804126010874e-09\n", + "value at 273C: 2.0213282964723807e-05\n", + "value at 300C: 1.0115129451432752e-05\n" + ] + } + ], + "source": [ + "k_v2 = 2100\n", + "visc_0 = 13.2 * 1e-6\n", + "T_0 = 15.85 + 273.16\n", + "A = visc_0 * np.exp(-k_v2 / T_0)\n", + "\n", + "print(visc_0, T_0)\n", + "print(\"A:\", A)\n", + "# a few data points:\n", + "\n", + "print(\"value at 273C:\", A * np.exp(k_v2 / 273))\n", + "print(\"value at 300C:\", A * np.exp(k_v2 / 300))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "T = np.linspace(273, 303, 10) # environmentallly realistic range of temps.\n", + "visc = A * np.exp(k_v2 / T)\n", + "plt.plot(T, visc)\n", + "plt.plot(T_0, visc_0, 'o')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GNOME Code\n", + "\n", + "This is using the code in GNOME" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kvis_at_temp called\n", + "(1.32e-05, 289.01, array([273. , 276.33333333, 279.66666667, 283. ,\n", + " 286.33333333, 289.66666667, 293. , 296.33333333,\n", + " 299.66666667, 303. ]), 2100.0)\n", + "('A:', 9.223804126010867e-09)\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "\n", + "from gnome.weatherers.oil import Oil\n", + "\n", + "# Make an oil to test with:\n", + "oil = Oil(name='empty_oil',\n", + " api=None,\n", + " pour_point=None,\n", + " solubility=None, # kg/m^3\n", + " # emulsification properties\n", + " bullwinkle_fraction=None,\n", + " bullwinkle_time=None,\n", + " emulsion_water_fraction_max=None,\n", + " densities=None,\n", + " density_ref_temps=None,\n", + " density_weathering=None,\n", + " kvis=[],\n", + " kvis_ref_temps=[],\n", + " kvis_weathering=[],\n", + " # PCs:\n", + " mass_fraction=[],\n", + " boiling_point=[],\n", + " molecular_weight=[],\n", + " component_density=[],\n", + " sara_type=[],\n", + " flash_point=[],\n", + " adios_oil_id=''\n", + " )\n", + "\n", + "# Set it up with one KVIS\n", + "oil.kvis = [1.32e-05]\n", + "oil.kvis_ref_temps = [289.01]\n", + "oil.kvis_weathering = [0.0]\n", + "\n", + "# compute and plot:\n", + "T = np.linspace(273, 303, 10) # environmentallly realistic range of temps.\n", + "visc = oil.kvis_at_temp(T)\n", + "plt.plot(T, visc)\n", + "plt.plot(oil.kvis_ref_temps, oil.kvis, 'o')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.15" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/lib_gnome/ComponentMover_c.h b/lib_gnome/ComponentMover_c.h index c66aa79c7..0eb7b88a4 100644 --- a/lib_gnome/ComponentMover_c.h +++ b/lib_gnome/ComponentMover_c.h @@ -13,6 +13,7 @@ #include "Basics.h" #include "TypeDefs.h" #include "CurrentMover_c.h" +#include "CATSMover_c.h" #ifdef pyGNOME #include "GridVel_c.h" #else @@ -95,6 +96,7 @@ class DLL_API ComponentMover_c : virtual public CurrentMover_c { virtual WorldPoint3D GetMove(const Seconds& model_time, Seconds timeStep,long setIndex,long leIndex,LERec *theLE,LETYPE leType); virtual Boolean VelocityStrAtPoint(WorldPoint3D wp, char *diagnosticStr); + virtual WorldRect GetGridBounds(){return pattern1->GetGridBounds();} //void SetRefPosition (WorldPoint p) { refP = p;} //void GetRefPosition (WorldPoint *p) { (*p) = refP;} diff --git a/lib_gnome/CurrentCycleMover_c.h b/lib_gnome/CurrentCycleMover_c.h index fb8a3dfaf..bebe5c01c 100644 --- a/lib_gnome/CurrentCycleMover_c.h +++ b/lib_gnome/CurrentCycleMover_c.h @@ -64,6 +64,7 @@ class DLL_API CurrentCycleMover_c : virtual public GridCurrentMover_c { VelocityRec GetPatValue (WorldPoint p); VelocityRec GetScaledPatValue(const Seconds& model_time, WorldPoint p,Boolean * useEddyUncertainty);//JLM 5/12/99 + virtual WorldRect GetGridBounds(){return timeGrid->GetGridBounds();} /*virtual OSErr GetStartTime(Seconds *startTime); virtual OSErr GetEndTime(Seconds *endTime);*/ //virtual double GetStartUVelocity(long index); diff --git a/lib_gnome/MakeDagTree.cpp b/lib_gnome/MakeDagTree.cpp index c2dc10102..0a83ea18e 100644 --- a/lib_gnome/MakeDagTree.cpp +++ b/lib_gnome/MakeDagTree.cpp @@ -12,7 +12,7 @@ #include "Replacements.h" #endif -DAGTreeStruct gDagTree; +DAGTreeStruct gDagTree; WORLDPOINTDH gCoord; Side_List **gSidesList; long gAllocatedDagLength = 0; @@ -31,19 +31,19 @@ char gErrStr[256]; long FindThirdVertex(long triNum, TopologyHdl THdl, long pointA, long pointB) { long pointC; - + if (triNum == -1) {pointC = -1;} - else if ((*THdl)[triNum].vertex1 == pointA && (*THdl)[triNum].vertex2 == pointB) + else if ((*THdl)[triNum].vertex1 == pointA && (*THdl)[triNum].vertex2 == pointB) {pointC = (*THdl)[triNum].vertex3;} - else if ((*THdl)[triNum].vertex2 == pointA && (*THdl)[triNum].vertex3 == pointB) + else if ((*THdl)[triNum].vertex2 == pointA && (*THdl)[triNum].vertex3 == pointB) {pointC = (*THdl)[triNum].vertex1;} - else if ((*THdl)[triNum].vertex3 == pointA && (*THdl)[triNum].vertex1 == pointB) + else if ((*THdl)[triNum].vertex3 == pointA && (*THdl)[triNum].vertex1 == pointB) {pointC = (*THdl)[triNum].vertex2;} - else if ((*THdl)[triNum].vertex1 == pointB && (*THdl)[triNum].vertex2 == pointA) + else if ((*THdl)[triNum].vertex1 == pointB && (*THdl)[triNum].vertex2 == pointA) {pointC = (*THdl)[triNum].vertex3;} - else if ((*THdl)[triNum].vertex2 == pointB && (*THdl)[triNum].vertex3 == pointA) + else if ((*THdl)[triNum].vertex2 == pointB && (*THdl)[triNum].vertex3 == pointA) {pointC = (*THdl)[triNum].vertex1;} - else if ((*THdl)[triNum].vertex3 == pointB && (*THdl)[triNum].vertex1 == pointA) + else if ((*THdl)[triNum].vertex3 == pointB && (*THdl)[triNum].vertex1 == pointA) {pointC = (*THdl)[triNum].vertex2;} return (pointC); } @@ -58,20 +58,20 @@ int SideListCompare(void const *x1, void const *x2) Side_List *p1,*p2; p1 = (Side_List*)x1; p2 = (Side_List*)x2; - + if (p1->triRight < p2->triRight) - return -1; + return -1; else if (p1->triRight > p2->triRight) return 1; else return 0; - + } /////////////////////////////////////////////////////////////////////////////////// -Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) +Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) { - + Side_List **sideListHdl; // What we want to get. //Side_List tempSide; // Temporary storage for a side during sorting. long sideCount; // The total number of sides @@ -80,35 +80,35 @@ Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) // The maximum number of sides for a given number of triangles. // The first +1 is for the single triangle case, the second +1 is // for the array index (I do not define the zero segment) - //long numBoundarySeg; // The total number of boundary segments in the domain. + //long numBoundarySeg; // The total number of boundary segments in the domain. long i; // index & counter sideListHdl = (Side_List**)_NewHandleClear((maxSides+1)*sizeof(Side_List)); - if(!sideListHdl) + if(!sideListHdl) { strcpy(errStr,"Out of memory in BuildSideList"); return (nil); } - sideCount = 0; + sideCount = 0; // The triangle (triangle[0]) is outside. This case // does not need to go through the loop. for (i=0;i maxSides) { strcpy(errStr,"MaxSides exceeded in BuildSideList"); - if(sideListHdl) DisposeHandle((Handle)sideListHdl); + if(sideListHdl) DisposeHandle((Handle)sideListHdl); return (nil); } (*sideListHdl)[sideCount].p1 = (*topoHdl)[i].vertex1; // Move around always in the same direction // as the numbering scheme: 1->2->3->1->2 ... (*sideListHdl)[sideCount].p2 = (*topoHdl)[i].vertex2; (*sideListHdl)[sideCount].triLeft = i; //By definition in our geometry, the triangle to - // the left of our segment will always be the + // the left of our segment will always be the // the triangle that we are in. (*sideListHdl)[sideCount].triRight = (*topoHdl)[i].adjTri3; // By our geometry, the triangle adj on // the right will be the traiangle across from the @@ -116,12 +116,12 @@ Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) (*sideListHdl)[sideCount].triLeftP3 = (*topoHdl)[i].vertex3; (*sideListHdl)[sideCount].triRightP3 = FindThirdVertex((*topoHdl)[i].adjTri3,topoHdl,(*topoHdl)[i].vertex1,(*topoHdl)[i].vertex2); (*sideListHdl)[sideCount].topoIndex = (i)*6; - + sideCount++; //Increment the counter for a new side. } if ((*topoHdl)[i].adjTri1 < i) { - // safety check + // safety check if(sideCount > maxSides){strcpy(errStr,"MaxSides exceeded in BuildSideList"); if(sideListHdl) DisposeHandle((Handle)sideListHdl); return (nil);} (*sideListHdl)[sideCount].p1 = (*topoHdl)[i].vertex2; @@ -135,11 +135,11 @@ Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) } if ((*topoHdl)[i].adjTri2 < i) { - // safety check + // safety check if(sideCount > maxSides) { strcpy(errStr,"MaxSides exceeded in BuildSideList"); - if(sideListHdl) DisposeHandle((Handle)sideListHdl); + if(sideListHdl) DisposeHandle((Handle)sideListHdl); return (nil); } (*sideListHdl)[sideCount].p1 = (*topoHdl)[i].vertex3; @@ -154,29 +154,29 @@ Side_List** BuildSideList(TopologyHdl topoHdl, char *errStr) } // Count the number of boundary segments. // numBoundarySeg = 0; -// for (i=0;i Width) ? 1/Height : 1/Width; tx = scalefactor * Width/dLong; ty = scalefactor * Height/dLat; - + for(i= 0; i < npoints ; i++) { (*coord)[i].pLong = tx * ((*coord)[i].pLong - xmin); @@ -211,10 +211,10 @@ OSErr LatLongTransform(LongPointHdl vertices)// may need to read in the doubles void ConvertSegNoToTopIndex(Side_List **sl, DAGTreeStruct dagTree) { long nnodes = dagTree.numBranches,i; - DAG** dagHdl; - + DAG** dagHdl; + dagHdl = dagTree.treeHdl; - + for(i=0;icountTotal < p2->countTotal) + + if (p1->countTotal < p2->countTotal) return -1; // first less than second else if (p1->countTotal > p2->countTotal) return 1; else return 0;// same,equivalent - + } ////////////////////////////////////////////////////////////////////////// @@ -334,8 +334,8 @@ void IncrementGDagTree(void) { // then allocate more memory long newNumber = gAllocatedDagLength + NUMTOALLOCATE; _SetHandleSize((Handle)gDagTree.treeHdl,newNumber* sizeof(DAG)); - - err = _MemError(); + + err = _MemError(); if (err) { strcpy(gErrStr,"Not enough memory to expand the DAG tree structure"); @@ -362,17 +362,17 @@ long newNodexx(Side_List **sides_node) long numBoundSides_node = 0; Test tN[NUMTESTS]; long numTests = NUMTESTS; - long location; // result of right or left routine + long location = 0; // result of right or left routine long testNodeP1, testNodeP2; // points in test node segment - long i,j; // Counters + long i, j; // Counters //char str[512]; - //long start, end; - + //long start, end; + for (i=0; i 0.) location = 0; diff --git a/lib_gnome/Shio.h b/lib_gnome/Shio.h index b404cf735..0ce84e0c7 100644 --- a/lib_gnome/Shio.h +++ b/lib_gnome/Shio.h @@ -380,7 +380,7 @@ short GetReferenceCurve(CONSTITUENT *constituent, // Amplitude-phase array struc short GetJulianDayHr(short day, // day of month (1 - 31) short month, // month (1- 12) - short year, // year (1993 - 2020) + short year, // year (1993 - 2025) double *hour); // returns hours from beginning of year short GetTideHeight( DateTimeRec *BeginDate,DateTimeRec *EndDate, diff --git a/lib_gnome/ShioHeight.cpp b/lib_gnome/ShioHeight.cpp index 17ba2bef3..cd55cafa5 100644 --- a/lib_gnome/ShioHeight.cpp +++ b/lib_gnome/ShioHeight.cpp @@ -1145,7 +1145,7 @@ short GetJulianDayHr(short day, // day of month (1 - 31) if( (day<1) || (day>DaysInMonth[month] ) ){ err=4; goto Error; } // Bad day - if( (year<1904) || (year>2020) ){ err=5; goto Error; } // Bad year + if( (year<1904) || (year>2025) ){ err=5; goto Error; } // Bad year // Compute the hour now for(i=1;i180,0] = points[:,0]-360 + except ValueError: + pass + return points + class MapFromBNA(RasterMap): """ A raster land-water map, created from file with polygons in it. @@ -1066,6 +1090,7 @@ def __init__(self, raster_size=4096 * 4096, map_bounds=None, spillable_area=None, + shift_lons=0, **kwargs): """ Creates a RasterMap from a data file. @@ -1080,8 +1105,12 @@ def __init__(self, raster -- the actual size will match the aspect ratio of the bounding box of the land :type raster_size: integer + + :param shiftLons: shift longitudes to be in -180 to 180 coords or 0 to 360. + 180, or 360 are valid inputs + :type shiftLons: integer - Optional arguments (kwargs): + Optional arguments (kwargs): :param refloat_halflife: the half-life (in hours) for the re-floating. @@ -1096,6 +1125,7 @@ def __init__(self, """ self.filename = filename self._raster_size = raster_size + self.shift_lons = shift_lons # fixme: do some file type checking here. polygons = haz_files.ReadBNA(filename, 'PolygonSet') @@ -1111,6 +1141,12 @@ def __init__(self, land_polys = PolygonSet() # and lakes.... spillable_area_bna = PolygonSet() + + #add if based on input param + if shift_lons == 360: + polygons.TransformData(ShiftLon360) + elif shift_lons == 180: + polygons.TransformData(ShiftLon180) for p in polygons: if p.metadata[1].lower().replace(' ', '') == 'spillablearea': @@ -1160,6 +1196,7 @@ def __init__(self, **kwargs) return None + def build_raster(self, land_polys=None, BB=None): """ Build the underlying raster used for the map diff --git a/py_gnome/gnome/model.py b/py_gnome/gnome/model.py index dbec13f8d..86c06c43a 100644 --- a/py_gnome/gnome/model.py +++ b/py_gnome/gnome/model.py @@ -1041,9 +1041,10 @@ def step(self): raise StopIteration("Run complete for {0}".format(self.name)) else: - self.setup_time_step() + #self.setup_time_step() #release half the LEs for this time interval self.release_elements(self.time_step/2, self.model_time) + self.setup_time_step() self.move_elements() self.weather_elements() self.step_is_done() @@ -1526,6 +1527,17 @@ def check_inputs(self): msgs.append('warning: ' + self.__class__.__name__ + ': ' + msg) # isValid = False + map_bounding_box = self.map.get_map_bounding_box() + for mover in self.movers: + bounds = mover.get_bounds() + # check longitude is within map bounds + if (bounds[1][0] < map_bounding_box[0][0] or bounds[0][0] > map_bounding_box[1][0] or + bounds[1][1] < map_bounding_box[0][1] or bounds[0][1] > map_bounding_box[1][1]): + msg = ('One of the movers - {0} - is outside of the map bounds. ' + .format(mover.name)) + self.logger.warning(msg) # for now make this a warning + msgs.append('warning: ' + self.__class__.__name__ + ': ' + msg) + return (msgs, isValid) def validate(self): diff --git a/py_gnome/gnome/movers/current_movers.py b/py_gnome/gnome/movers/current_movers.py index 0a9992d20..61120ceb9 100644 --- a/py_gnome/gnome/movers/current_movers.py +++ b/py_gnome/gnome/movers/current_movers.py @@ -124,6 +124,14 @@ def get_points(self): return points + def get_bounds(self): + ''' + Right now the cython mover only gets the triangular center points. + ''' + bounds = self.mover._get_bounds() + current_bounds = ((bounds["loLong"] / 1e6, bounds["loLat"] / 1e6), (bounds["hiLong"] / 1e6, bounds["hiLat"] / 1e6)) + return current_bounds + class CatsMoverSchema(CurrentMoversBaseSchema): '''static schema for CatsMover''' diff --git a/py_gnome/gnome/movers/movers.py b/py_gnome/gnome/movers/movers.py index 9e801b038..a1b108d8c 100644 --- a/py_gnome/gnome/movers/movers.py +++ b/py_gnome/gnome/movers/movers.py @@ -193,6 +193,13 @@ def get_move(self, sc, time_step, model_time_datetime): return delta + def get_bounds(self): + ''' + Return a bounding box surrounding the grid data. + ''' + + return ((-360, -90), (360, 90)) + class PyMover(Mover): def __init__(self, default_num_method='RK2', diff --git a/py_gnome/gnome/movers/py_current_movers.py b/py_gnome/gnome/movers/py_current_movers.py index d50485b5e..1330f3fc6 100644 --- a/py_gnome/gnome/movers/py_current_movers.py +++ b/py_gnome/gnome/movers/py_current_movers.py @@ -4,8 +4,8 @@ from colander import (SchemaNode, Bool, Float, drop) from gnome.basic_types import oil_status -from gnome.basic_types import (world_point_type, - status_code_type) +# from gnome.basic_types import (world_point_type, +# status_code_type) from gnome.utilities.projections import FlatEarthProjection @@ -58,6 +58,7 @@ def __init__(self, uncertain_across=.25, uncertain_cross=.25, default_num_method='RK2', + grid_topology=None, **kwargs ): """ @@ -86,7 +87,8 @@ def __init__(self, Default: RK2 """ - (super(PyCurrentMover, self).__init__(default_num_method=default_num_method, **kwargs)) + (super(PyCurrentMover, self).__init__(default_num_method=default_num_method, + **kwargs)) self.filename = filename self.current = current @@ -95,6 +97,7 @@ def __init__(self, raise ValueError("must provide a filename or current object") else: self.current = GridCurrent.from_netCDF(filename=self.filename, + grid_topology=grid_topology, **kwargs) self.scale_value = scale_value @@ -174,6 +177,39 @@ def get_grid_data(self): return np.column_stack((lons.reshape(-1), lats.reshape(-1))) + def get_lat_lon_data(self): + """ + function for getting lat lon data from the mover + """ + if isinstance(self.current.grid, Grid_U): + grid_data = self.current.grid.nodes[self.current.grid.faces[:]] + dtype = grid_data.dtype.descr + unstructured_type = dtype[0][1] + lons = (grid_data + .view(dtype=unstructured_type) + .reshape(-1, len(dtype))[0::2, 0]) + lats = (grid_data + .view(dtype=unstructured_type) + .reshape(-1, len(dtype))[1::2, 0]) + + return lons, lats + else: + lons = self.current.grid.node_lon + lats = self.current.grid.node_lat + + return lons.reshape(-1), lats.reshape(-1) + + def get_bounds(self): + ''' + Return a bounding box surrounding the grid data. + ''' + longs, lats = self.get_lat_lon_data() + + left, right = longs.min(), longs.max() + bottom, top = lats.min(), lats.max() + + return ((left, bottom), (right, top)) + def get_center_points(self): if (hasattr(self.current.grid, 'center_lon') and self.current.grid.center_lon is not None): diff --git a/py_gnome/gnome/movers/py_wind_movers.py b/py_gnome/gnome/movers/py_wind_movers.py index 0ffe8b372..e3bd3997f 100644 --- a/py_gnome/gnome/movers/py_wind_movers.py +++ b/py_gnome/gnome/movers/py_wind_movers.py @@ -182,6 +182,48 @@ def get_grid_data(self): return np.column_stack((lons.reshape(-1), lats.reshape(-1))) + def get_lat_lon_data(self): + """ + The main function for getting grid data from the mover + """ + if isinstance(self.wind.grid, Grid_U): + #return self.wind.grid.nodes[self.wind.grid.faces[:]] + grid_data = self.wind.grid.nodes[self.wind.grid.faces[:]] + #grid_data = self.wind.grid.nodes[self.wind.grid.faces[:]] + dtype = grid_data.dtype.descr + print "dtype = ", dtype, len(dtype) + #print "grid_data shape = ", grid_data.shape() + print "grid_data = ", grid_data[:,0] + unstructured_type = dtype[0][1] + lons = (grid_data + .view(dtype=unstructured_type) + .reshape(-1, len(dtype))[0::2, 0]) + lats = (grid_data + .view(dtype=unstructured_type) + .reshape(-1, len(dtype))[1::2, 0]) + + print "lens = ", len(lons), len(lats) + return lons, lats + else: + lons = self.wind.grid.node_lon + print "lon len = ", len(lons) + lats = self.wind.grid.node_lat + print "lat len = ", len(lats) + + #return np.column_stack((lons.reshape(-1), lats.reshape(-1))) + return lons.reshape(-1), lats.reshape(-1) + + def get_bounds(self): + ''' + Return a bounding box surrounding the grid data. + ''' + longs, lats = self.get_lat_lon_data() + + left, right = longs.min(), longs.max() + bottom, top = lats.min(), lats.max() + + return ((left, bottom), (right, top)) + def get_center_points(self): if (hasattr(self.wind.grid, 'center_lon') and self.wind.grid.center_lon is not None): diff --git a/py_gnome/gnome/outputters/netcdf.py b/py_gnome/gnome/outputters/netcdf.py index 8eb7eb0b3..5e935f890 100644 --- a/py_gnome/gnome/outputters/netcdf.py +++ b/py_gnome/gnome/outputters/netcdf.py @@ -3,12 +3,13 @@ ''' import os from datetime import datetime +import zipfile import netCDF4 as nc import numpy as np -from colander import SchemaNode, String, drop, Int, Bool +from colander import SchemaNode, String, Boolean, drop, Int, Bool from gnome import __version__ from gnome.basic_types import oil_status, world_point_type @@ -174,6 +175,9 @@ class NetCDFOutputSchema(BaseOutputterSchema): _middle_of_run = SchemaNode( Bool(), missing=drop, save=True, read_only=True, test_equal=False ) + zip_output = SchemaNode( + Boolean(), missing=drop, save=True, update=True + ) class NetCDFOutput(Outputter, OutputterFilenameMixin): @@ -268,6 +272,7 @@ class NetCDFOutput(Outputter, OutputterFilenameMixin): def __init__(self, filename, + zip_output=False, which_data='standard', compress=True, # FIXME: this should not be default, but since we don't have @@ -284,6 +289,8 @@ def __init__(self, store the NetCDF data. :type filename: str. or unicode + :param zip_output=True: whether to zip up the output netcdf files + :param which_data='standard': If 'standard', write only standard data. If 'most' means, write everything except the attributes we know are @@ -326,10 +333,16 @@ def __init__(self, name, ext = os.path.splitext(self.filename) self._u_filename = '{0}_uncertain{1}'.format(name, ext) + self.forecast_filename = self.filename + self.zip_filename = '{0}.{1}'.format(name, 'zip') # fixme: move to base class? self.name = os.path.split(filename)[1] + self.zip_output = zip_output + if self.zip_output is True: + self.filename = self.zip_filename + if which_data.lower() in self.which_data_lu: self._which_data = which_data.lower() else: @@ -493,6 +506,7 @@ def _update_arrays_to_output(self, sc): def prepare_for_model_run(self, model_start_time, spills, + uncertain = False, **kwargs): """ .. function:: prepare_for_model_run(model_start_time, @@ -536,21 +550,24 @@ def prepare_for_model_run(self, use super to pass model_start_time, cache=None and remaining kwargs to base class method """ - super(NetCDFOutput, self).prepare_for_model_run(model_start_time, - spills, **kwargs) if not self.on: return + super(NetCDFOutput, self).prepare_for_model_run(model_start_time, + spills, **kwargs) + # this should have been called by the superclass version # self.clean_output_files() + self.uncertain = uncertain + self._update_var_attributes(spills) for sc in self.sc_pair.items(): if sc.uncertain: file_ = self._u_filename else: - file_ = self.filename + file_ = self.forecast_filename self._file_exists_error(file_) @@ -628,20 +645,26 @@ def _create_nc_var(self, grp, var_name, dtype, shape, chunksz): dtype = 'u1' try: - if var_name != "non_weathering": - # fixme: TOTAL Kludge -- - # failing with bad chunksize error for this particular varaible - # I have no idea why!!!! - var = grp.createVariable(var_name, - dtype, - shape, - zlib=self._compress, - chunksizes=chunksz) - else: - var = grp.createVariable(var_name, - dtype, - shape, - zlib=self._compress) + var = grp.createVariable(var_name, + dtype, + shape, + zlib=self._compress, + chunksizes=chunksz) +# this should be fixed now since non_weathering is initialized +# if var_name != "non_weathering": +# # fixme: TOTAL Kludge -- +# # failing with bad chunksize error for this particular varaible +# # I have no idea why!!!! +# var = grp.createVariable(var_name, +# dtype, +# shape, +# zlib=self._compress, +# chunksizes=chunksz) +# else: +# var = grp.createVariable(var_name, +# dtype, +# shape, +# zlib=self._compress) except RuntimeError as err: msg = ("\narguments are:\n" "\tvar_name: {}\n" @@ -687,7 +710,7 @@ def write_output(self, step_num, islast_step=False): if sc.uncertain and self._u_filename is not None: file_ = self._u_filename else: - file_ = self.filename + file_ = self.forecast_filename time_stamp = sc.current_time_stamp @@ -726,12 +749,34 @@ def write_output(self, step_num, islast_step=False): ) grp.variables[key][idx] = val + if islast_step: + if self.zip_output is True: + self._zip_output_files() + self._start_idx = _end_idx # set _start_idx for the next timestep return {'filename': (self.filename, self._u_filename), 'time_stamp': time_stamp} + def _zip_output_files(self): + zfilename = self.zip_filename + zipf = zipfile.ZipFile(zfilename, 'w') + + forcst_file = self.forecast_filename + dir, file_to_zip = os.path.split(forcst_file) + zipf.write(forcst_file, + arcname=file_to_zip) + os.remove(forcst_file) + if self.uncertain is True: + uncrtn_file = self._u_filename + dir, file_to_zip = os.path.split(uncrtn_file) + zipf.write(uncrtn_file, + arcname=file_to_zip) + os.remove(uncrtn_file) + + zipf.close() + def clean_output_files(self): ''' deletes output files that may be around @@ -746,6 +791,10 @@ def clean_output_files(self): os.remove(self._u_filename) except OSError: pass # it must not be there + try: + os.remove(self.forecast_filename) + except OSError: + pass # it must not be there def rewind(self): ''' diff --git a/py_gnome/gnome/outputters/shape.py b/py_gnome/gnome/outputters/shape.py index ff3f12873..c0194429e 100644 --- a/py_gnome/gnome/outputters/shape.py +++ b/py_gnome/gnome/outputters/shape.py @@ -47,7 +47,7 @@ def __init__(self, filename, zip_output=True, surface_conc="kde", filename = filename.split(".zip")[0].split(".shp")[0] if "." in os.path.split(filename)[-1]: - # anything after a doit gets removed + # anything after a dot gets removed # I *think* pyshp is doing that, but not sure. raise ValueError("shape files can't have a dot in the filename") @@ -62,6 +62,7 @@ def __init__(self, filename, zip_output=True, surface_conc="kde", def prepare_for_model_run(self, model_start_time, spills, + uncertain = False, **kwargs): """ .. function:: prepare_for_model_run(model_start_time, @@ -102,6 +103,8 @@ def prepare_for_model_run(self, **kwargs) + self.uncertain = uncertain + # shouldn't be required if prepare_for_model_ run cleaned them out. self._file_exists_error(self.filename + '.zip') @@ -151,6 +154,10 @@ def write_output(self, step_num, islast_step=False): output_info = {'time_stamp': sc.current_time_stamp.isoformat(), 'output_filename': output_filename} + if islast_step: + if self.uncertain is True: + self._zip_output_files() + return output_info def _record_shape_entries(self, sc): @@ -212,6 +219,27 @@ def _save_and_archive_shapefiles(self, sc): zipf.close() + def _zip_output_files(self): + if self.zip_output is True: + zfilename_temp = self.filename + '_temp' + '.zip' + zfilename = self.filename + '.zip' + zipf = zipfile.ZipFile(zfilename_temp, 'w') + + forcst_file = zfilename + dir, file_to_zip = os.path.split(forcst_file) + zipf.write(forcst_file, + arcname=file_to_zip) + os.remove(forcst_file) + if self.uncertain is True: + uncrtn_file = self.filename + '_uncert' + '.zip' + dir, file_to_zip = os.path.split(uncrtn_file) + zipf.write(uncrtn_file, + arcname=file_to_zip) + os.remove(uncrtn_file) + + zipf.close() + os.rename(zfilename_temp, zfilename) + def rewind(self): ''' reset a few parameter and call base class rewind to reset diff --git a/py_gnome/gnome/scripting/__init__.py b/py_gnome/gnome/scripting/__init__.py index ba5afb339..5ed3fe7cf 100644 --- a/py_gnome/gnome/scripting/__init__.py +++ b/py_gnome/gnome/scripting/__init__.py @@ -1,34 +1,40 @@ -""" -Scripting package for GNOME with assorted utilities that make it easier to -write scripts. +# """ +# Scripting package for GNOME with assorted utilities that make it easier to +# write scripts. -The ultimate goal is to be able to run py_gnome for the "common" use cases -with only functions available in this module +# The ultimate goal is to be able to run py_gnome for the "common" use cases +# with only functions available in this module -Classes and helper functions are imported from various py_gnome modules -(spill, environment, movers etc). +# Classes and helper functions are imported from various py_gnome modules +# (spill, environment, movers etc). -we recommend that this module be used like so:: +# we recommend that this module be used like so:: - import gnome.scripting import gs +# import gnome.scripting import gs -Then you will have easy access to most of the stuff you need to write -py_gnome scripts with, e.g.:: +# Then you will have easy access to most of the stuff you need to write +# py_gnome scripts with, e.g.:: - model = gs.Model(start_time="2018-04-12T12:30", - duration=gs.days(2), - time_step=gs.minutes(15)) +# model = gs.Model(start_time="2018-04-12T12:30", +# duration=gs.days(2), +# time_step=gs.minutes(15)) - model.map = gs.MapFromBNA('coast.bna', refloat_halflife=0.0) # seconds +# model.map = gs.MapFromBNA('coast.bna', refloat_halflife=0.0) # seconds - model.spills += gs.point_line_release_spill(num_elements=1000, - start_position=(-163.75, - 69.75, - 0.0), - release_time="2018-04-12T12:30") -""" +# model.spills += gs.point_line_release_spill(num_elements=1000, +# start_position=(-163.75, +# 69.75, +# 0.0), +# release_time="2018-04-12T12:30") +# """ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +# import gnome -import gnome from gnome.model import Model from gnome.basic_types import oil_status_map @@ -58,7 +64,7 @@ spatial_release_spill, ) -from gnome.environment.wind import constant_wind +from gnome.environment.wind import Wind, constant_wind from gnome.movers.wind_movers import (constant_wind_mover, wind_mover_from_file, ) @@ -68,6 +74,7 @@ KMZOutput, OilBudgetOutput, ShapeOutput, + WeatheringOutput, ) from gnome.maps.map import MapFromBNA, GnomeMap diff --git a/py_gnome/gnome/spill/release.py b/py_gnome/gnome/spill/release.py index d29b9ea6f..0c904331c 100644 --- a/py_gnome/gnome/spill/release.py +++ b/py_gnome/gnome/spill/release.py @@ -3,19 +3,30 @@ is composed of a release object and an ElementType ''' -import copy import math +import warnings +import numpy as np +import shapefile as shp +import trimesh +import geojson +import zipfile +from math import ceil from datetime import datetime, timedelta + +from shapely.geometry import Polygon, Point, MultiPoint + +from pyproj import Proj, transform +import pyproj + from gnome.utilities.time_utils import asdatetime +from gnome.utilities.geometry.geo_routines import random_pt_in_tri -import numpy as np -from colander import (iso8601, - SchemaNode, SequenceSchema, - drop, Bool, Int, Float) +from colander import (String, SchemaNode, SequenceSchema, drop, Int, Float, + Boolean) -from gnome.persist.base_schema import ObjTypeSchema, WorldPoint, WorldPointNumpy -from gnome.persist.extend_colander import LocalDateTime +from gnome.persist.base_schema import ObjTypeSchema, WorldPoint +from gnome.persist.extend_colander import LocalDateTime, FilenameSchema from gnome.persist.validators import convertible_to_seconds from gnome.basic_types import world_point_type @@ -27,7 +38,6 @@ from gnome.environment.timeseries_objects_base import TimeseriesData,\ TimeseriesVector from gnome.environment.gridded_objects_base import Time -from math import ceil class BaseReleaseSchema(ObjTypeSchema): @@ -63,22 +73,6 @@ class PointLineReleaseSchema(BaseReleaseSchema): description = 'PointLineRelease object schema' -class StartPositions(SequenceSchema): - start_position = WorldPoint() - - -class SpatialReleaseSchema(BaseReleaseSchema): - ''' - Contains properties required by UpdateWindMover and CreateWindMover - TODO: also need a way to persist list of element_types - ''' - description = 'SpatialRelease object schema' - start_position = StartPositions( - save=True, update=True - ) - release_mass = SchemaNode(Float()) - - class Release(GnomeId): """ base class for Release classes. @@ -91,10 +85,12 @@ def __init__(self, release_time=None, num_elements=0, release_mass=0, + end_release_time=None, **kwargs): self.num_elements = num_elements self.release_time = asdatetime(release_time) + self.end_release_time = asdatetime(end_release_time) self.release_mass = release_mass self.rewind() super(Release, self).__init__(**kwargs) @@ -141,9 +137,37 @@ def num_elements(self, val): @property def release_duration(self): ''' - return value in seconds + duration over which particles are released in seconds + ''' + if self.end_release_time is None: + return 0 + else: + return (self.end_release_time - self.release_time).total_seconds() + + @property + def end_release_time(self): + if self._end_release_time is None: + return self.release_time + else: + return self._end_release_time + + @end_release_time.setter + def end_release_time(self, val): + ''' + Set end_release_time. + If end_release_time is None or if end_release_time == release_time, + it is an instantaneous release. + + Also update reference to set_newparticle_positions - if this was + previously an instantaneous release but is now timevarying, we need + to update this method ''' - return 0 + val = asdatetime(val) + if val is not None and self.release_time > val: + raise ValueError('end_release_time must be greater than ' + 'release_time') + + self._end_release_time = val def LE_timestep_ratio(self, ts): ''' @@ -228,6 +252,7 @@ def __init__(self, if num_elements is None and num_per_timestep is None: num_elements = 1000 super(PointLineRelease, self).__init__(release_time=release_time, + end_release_time=end_release_time, num_elements=num_elements, release_mass = release_mass, **kwargs) @@ -240,7 +265,6 @@ def __init__(self, # initializes internal variables: _end_release_time, _start_position, # _end_position - self.end_release_time = asdatetime(end_release_time) self.start_position = start_position self.end_position = end_position @@ -269,41 +293,6 @@ def is_pointsource(self): return False - @property - def release_duration(self): - ''' - duration over which particles are released in seconds - ''' - if self.end_release_time is None: - return 0 - else: - return (self.end_release_time - self.release_time).total_seconds() - - @property - def end_release_time(self): - if self._end_release_time is None: - return self.release_time - else: - return self._end_release_time - - @end_release_time.setter - def end_release_time(self, val): - ''' - Set end_release_time. - If end_release_time is None or if end_release_time == release_time, - it is an instantaneous release. - - Also update reference to set_newparticle_positions - if this was - previously an instantaneous release but is now timevarying, we need - to update this method - ''' - val = asdatetime(val) - if val is not None and self.release_time > val: - raise ValueError('end_release_time must be greater than ' - 'release_time') - - self._end_release_time = val - @property def num_per_timestep(self): return self._num_per_timestep @@ -472,7 +461,7 @@ def initialize_LEs(self, to_rel, data, current_time, time_step): time_step = integer seconds ''' if(time_step == 0): - time_step = 1 #to deal with initializing position in instantaneous release case + time_step = 1 #to deal with initializing position in instantaneous release case sl = slice(-to_rel, None, 1) start_position = self._pos_ts.at(None, current_time, extrapolate=True) @@ -493,61 +482,426 @@ def initialize_LEs(self, to_rel, data, current_time, time_step): data['init_mass'][sl] = self._mass_per_le +class StartPositions(SequenceSchema): + start_position = WorldPoint() + + +class SpatialReleaseSchema(BaseReleaseSchema): + ''' + Contains properties required by UpdateWindMover and CreateWindMover + TODO: also need a way to persist list of element_types + ''' + start_position = WorldPoint( + save=False, update=False, test_equal=False + ) + end_position = WorldPoint( + save=False, update=False, test_equal=False + ) + random_distribute = SchemaNode(Boolean()) + filename = FilenameSchema(save=False, missing=drop, isdatafile=False, update=False, test_equal=False) + #json_file = FilenameSchema(save=True, missing=drop, isdatafile=True, update=False, test_equal=False) + # Because file generation on save isn't supported yet + json_file = SchemaNode(String(), save=True, update=False, test_equal=False, missing=drop) + custom_positions = StartPositions(save=True, update=True) + + class SpatialRelease(Release): """ - A simple release class -- a release of floating non-weathering particles, - with their initial positions pre-specified + A release of elements with their initial positions pre-specified + + They can be specified as a set of coordinates (custom_positions) + Or as polygons that will be randomly filled with particles """ _schema = SpatialReleaseSchema def __init__(self, - release_time=None, - start_position=None, - release_mass=0, + filename=None, + polygons=None, + weights=None, + thicknesses=None, + json_file=None, + custom_positions=None, + random_distribute=True, + num_elements=1000, **kwargs): """ - :param release_time: time the LEs are released - :type release_time: datetime.datetime + :param filename: NESDIS shapefile + :type filename: string or list of strings -- should be a zip file - :param start_positions: locations the LEs are released - :type start_positions: (num_elements, 3) numpy array of float64 - -- (long, lat, z) + :param polygons: polygons to use in this release + :type polygons: list of shapely.Polygon - num_elements and release_time passed to base class __init__ using super - See base :class:`Release` documentation - """ - super(SpatialRelease, self).__init__(release_time=release_time,**kwargs) - self.start_position = (np.asarray(start_position, - dtype=world_point_type) - .reshape((-1, 3))) - self.num_elements = len(self.start_position) - self.release_mass = release_mass + :param weights: probability weighting for each polygon. Must be the same + length as the polygons kwarg, and must sum to 1. If None, weights are + generated based on area proportion. - def num_elements_after_time(self, current_time, time_step): + :param start_positions: Nx3 array of release coordinates (lon, lat, z) + :type start_positions: np.ndarray + + :param random_distribute: If True, all LEs will always be distributed + among all release locations. Otherwise, LEs will be equally distributed, + and only remainder will be placed randomly + + :param num_elements: If passed as None, number of elements will be equivalent + to number of start positions. For backward compatibility. """ - return number of particles released in current_time + time_step + # We really should clean this up! + kwargs.pop('start_position', None) + kwargs.pop('end_position', None) + super(SpatialRelease, self).__init__( + **kwargs + ) + self.filename = None + if filename is not None and json_file is not None: + raise ValueError('May only provide filename or json_file to SpatialRelease') + elif filename is not None: + polygons, weights, thicknesses = self.__class__.load_shapefile(filename) + self.filename = filename + elif json_file is not None: + polygons, weights, thicknesses = self.__class__.load_geojson(json_file) + + self.polygons = polygons + if weights is None and self.polygons is not None: + weights = self.gen_default_weights(self.polygons) + if self.polygons is not None and len(weights) != len(self.polygons): + raise ValueError('Weights must be equal in length to provided Polygons') + + self.thicknesses = thicknesses + self.weights = weights + self.random_distribute = random_distribute + if custom_positions is not None: + self.custom_positions = np.array(custom_positions) + # num_elements = len(self.custom_positions) + # print("setting num_elements to:", num_elements) + else: + self.custom_positions = None + self.num_elements = num_elements + self._start_positions = self.gen_start_positions() + + @classmethod + def load_geojson(cls, filename): + #gj = geojson.load(filename) + # Currently (9/16/2020), the json_file init parameter and this filename parameter + # should always contain the raw GeoJSON. This is in lieu of developing a new + # system to generate files when saving + + fc = geojson.FeatureCollection(geojson.loads(filename)) + weights = fc.weights + thicknesses = fc.thicknesses + polygons = None + if fc.features is not None: + polygons = map(lambda f: Polygon(f.coordinates[0]), fc.features) + return polygons, weights, thicknesses + + @staticmethod + def load_shapefile(filename): """ - # call base class method to check if start_time is valid - if (current_time + timedelta(seconds=time_step) <= self.release_time): - return 0 + load up a spatial release from shapefiles + + shapefiles should be in a zip file + """ + with zipfile.ZipFile(filename, 'r') as zsf: + basename = ''.join(filename.split('.')[:-1]) + shpfile = filter(lambda f: f.split('.')[-1] == 'shp', zsf.namelist()) + if len(shpfile) > 0: + shpfile = zsf.open(shpfile[0], 'r') + else: + raise ValueError('No .shp file found') + dbffile = filter(lambda f: f.split('.')[-1] == 'dbf', zsf.namelist())[0] + dbffile = zsf.open(dbffile, 'r') + sf = shp.Reader(shp=shpfile, dbf=dbffile) + shapes = sf.shapes() + oil_polys = [] + oil_amounts = [] + field_names = [field[0] for field in sf.fields[1:]] + date_id = field_names.index('DATE') + time_id = field_names.index('TIME') + type_id = field_names.index('OILTYPE') + area_id = field_names.index('AREA_GEO') + im_date = sf.record()[date_id] + im_time = sf.record()[time_id] + all_oil_polys = [] + all_oil_weights = [] + all_oil_thicknesses = [] + shape_oil_thickness = [] + + oil_amounts = [] + for i, shape in enumerate(shapes): + oil_type = sf.records()[i][type_id] + oil_area = sf.records()[i][area_id] * 1000**2 # area in m2 + if oil_type.lower() == "thin": + thickness = 5e-6 + elif oil_type.lower() == "thick": + thickness = 200e-6 + else: + raise ValueError('Unknown oil classification: "{}". Should be one of:' + '"Thick" or "Thin"'.format(oil_type)) + oil_amounts.append(thickness * oil_area) # oil amount in cubic meters + shape_oil_thickness.append(thickness) + + # percentage of mass in each Shape. + # Later this is further broken down per Polygon + oil_amount_weights = map(lambda w: w / sum(oil_amounts), oil_amounts) + + # Each Shape contains multiple Polygons. The following extracts these Polygons + # and determines the per Polygon weighting out of the total + for shape, weight, thickness in zip(shapes, oil_amount_weights, shape_oil_thickness): + shape_polys = [] + shape_amounts = [] + shape_poly_area_weights = [] + shape_poly_thickness = [] + total_poly_area = 0 + for i, start_idx in enumerate(shape.parts): + sl = None + if i < len(shape.parts) - 1: + sl = slice(start_idx, shape.parts[i + 1]) + else: + sl = slice(start_idx, None) + points = shape.points[sl] + # kludge to get around version differences in pyproj + Proj1 = Proj2 = pts = None + if int(pyproj.__version__[0]) < 2: + Proj1 = Proj(init='epsg:3857') + Proj2 = Proj(init='epsg:4326') + pts = map(lambda pt: transform(Proj1, Proj2, pt[0], pt[1]), points) + else: + transformer = pyproj.Transformer.from_crs("epsg:3857", "epsg:4326", always_xy=True) + pts = map(lambda pt: transformer.transform(pt[0],pt[1]), points) + poly = Polygon(pts) + shape_polys.append(poly) + shape_poly_thickness.append(thickness) + + total_poly_area += poly.area + areas = map(lambda s: s.area, shape_polys) + # percentage of area each poly contributes to total shape area + shape_poly_area_weights = map(lambda s: s / total_poly_area, areas) + # percentage of mass each poly contributes to total mass + oil_poly_weights = map(lambda w: w * weight, shape_poly_area_weights) + all_oil_polys.extend(shape_polys) + all_oil_weights.extend(oil_poly_weights) + all_oil_thicknesses.extend(shape_poly_thickness) + + return all_oil_polys, all_oil_weights, all_oil_thicknesses + + @classmethod + def from_shapefile(cls, + filename=None, + **kwargs): + polys, weights, thicknesses = cls.load_shapefile(filename) + return cls( + polygons=polys, + weights=weights, + thicknesses=thicknesses, + **kwargs + ) + + @property + def json_file(self): + #Placeholder value for the serialization system + return None + + @property + def start_positions(self): + if not hasattr(self, "_start_positions") or self._start_positions is None: + self._start_positions = self.gen_start_positions() + return self._start_positions + + @start_positions.setter + def start_positions(self, val): + self._start_positions = val + + @property + def start_position(self): + if hasattr(self, '_start_positions'): + ctr = MultiPoint(self.gen_combined_start_positions()).centroid + return np.array([ctr.x, ctr.y, 0]) + else: + return np.array([0, 0, 0]) + + @property + def end_position(self): + return self.start_position + + @start_position.setter + def start_position(self, val): + ''' + dummy setter for web client + ''' + pass + + @property + def end_release_time(self): + if not hasattr(self, '_end_release_time') or self._end_release_time is None: + return self.release_time + else: + return self._end_release_time + + @end_release_time.setter + def end_release_time(self, val): + ''' + Set end_release_time. + If end_release_time is None or if end_release_time == release_time, + it is an instantaneous release. + + Also update reference to set_newparticle_positions - if this was + previously an instantaneous release but is now timevarying, we need + to update this method + ''' + val = asdatetime(val) + if val is not None and self.release_time > val: + raise ValueError('end_release_time must be greater than ' + 'release_time') + + self._end_release_time = val + + @property + def num_per_timestep(self): + return None - return self.num_elements + @num_per_timestep.setter + def num_per_timestep(self, val): + raise TypeError('num_per_timestep not supported on SpatialRelease') + + def LE_timestep_ratio(self, ts): + ''' + Returns the ratio + ''' + return 1.0 * self.num_elements / self.get_num_release_time_steps(ts) + + def gen_default_weights(self, polygons): + if polygons is None: + return + tot_area = sum(map(lambda p: p.area, polygons)) + weights = map(lambda p: p.area/tot_area, polygons) + return weights + + def gen_start_positions(self): + if self.polygons is None: + return + if self.weights is None: + self.weights = self.gen_default_weights(self.polygons) + #generates the start positions for this release. Must be called before usage in a model + def gen_release_pts_in_poly(num_pts, poly): + pts, tris = trimesh.creation.triangulate_polygon(poly, engine='earcut') + tris = map(lambda k: Polygon(k), pts[tris]) + areas = map(lambda s: s.area, tris) + t_area = sum(areas) + weights = map(lambda s: s/t_area, areas) + rv = map(random_pt_in_tri, np.random.choice(tris, num_pts, p=weights)) + rv = map(lambda pt: np.append(pt, 0), rv) #add Z coordinate + return rv + num_pts = self.num_elements + weights = self.weights + polys = self.polygons + pts_per_poly = map(lambda w: int(math.ceil(w*num_pts)), weights) + release_pts = [] + for n, poly in zip(pts_per_poly, polys): + release_pts.extend(gen_release_pts_in_poly(n, poly)) + return np.array(release_pts) def rewind(self): self._prepared = False self._mass_per_le = 0 + self._release_ts = None + self._combined_positions = None + #self._pos_ts = None + + + def gen_combined_start_positions(self): + self.start_positions #generates start_positions if not done already via property + + if self.start_positions is None: + if self.custom_positions is None: + raise ValueError('No polygons or custom positions specified, unable to generate release positions') + else: + return self.custom_positions + else: + if self.custom_positions is None: + return self.start_positions + else: + return np.vstack((self.start_positions, self.custom_positions)) + def prepare_for_model_run(self, ts): ''' - :param ts: integer seconds - :param amount: integer kilograms + :param ts: timestep as integer seconds ''' if self._prepared: self.rewind() - max_release = self.num_elements + if self.LE_timestep_ratio(ts) < 1: + raise ValueError('Not enough LEs: Number of LEs must at least \ + be equal to the number of timesteps in the release') + + num_ts = self.get_num_release_time_steps(ts) + max_release = 0 + if self.num_per_timestep is not None: + max_release = self.num_per_timestep * num_ts + else: + max_release = self.num_elements + + self.generate_release_timeseries(num_ts, max_release, ts) + + if self.weights is None: + self.weights = self.gen_default_weights(self.polygons) + + self._combined_positions = self.gen_combined_start_positions() + + if self.start_positions is None: + if self.custom_positions is None: + raise ValueError('No polygons or custom positions specified, unable to generate release positions') + else: + self._combined_positions = self.custom_positions + else: + if self.custom_positions is None: + self._combined_positions = self.start_positions + else: + self._combined_positions = np.vstack((self.start_positions, self.custom_positions)) - self._prepared = True self._mass_per_le = self.release_mass*1.0 / max_release + self._prepared = True + + def generate_release_timeseries(self, num_ts, max_release, ts): + ''' + Release timeseries describe release behavior as a function of time. + _release_ts describes the number of LEs that should exist at time T + SpatialRelease does not have a _pos_ts because it uses start_positions only + All use TimeseriesData objects. + ''' + t = None + if num_ts == 1: + # This is a special case, when the release is short enough a single + # timestep encompasses the whole thing. + if self.release_duration == 0: + t = Time([self.release_time, + self.end_release_time + timedelta(seconds=1)]) + else: + t = Time([self.release_time, self.end_release_time]) + else: + t = Time([self.release_time + timedelta(seconds=ts * step) + for step in range(0, num_ts + 1)]) + t.data[-1] = self.end_release_time + if self.release_duration == 0: + self._release_ts = TimeseriesData(name=self.name+'_release_ts', + time=t, + data=np.full(t.data.shape, max_release).astype(int)) + else: + self._release_ts = TimeseriesData(name=self.name+'_release_ts', + time=t, + data=np.linspace(0, max_release, num_ts + 1).astype(int)) + + def num_elements_after_time(self, current_time, time_step): + ''' + Returns the number of elements expected to exist at current_time+time_step. + Returns 0 if prepare_for_model_run has not been called. + :param ts: integer seconds + :param amount: integer kilograms + ''' + if not self._prepared: + return 0 + if current_time < self.release_time: + return 0 + return int(math.ceil(self._release_ts.at(None, current_time + timedelta(seconds=time_step), extrapolate=True))) + def initialize_LEs(self, to_rel, data, current_time, time_step): """ @@ -556,12 +910,60 @@ def initialize_LEs(self, to_rel, data, current_time, time_step): .. note:: this releases all the elements at their initial positions at the release_time """ + + num_locs = len(self._combined_positions) + if to_rel < num_locs: + warnings.warn("{0} is releasing fewer LEs than number of start positions at time: {1}".format(self, current_time)) + sl = slice(-to_rel, None, 1) - data['positions'][sl] = self.start_position + if self.random_distribute or to_rel < num_locs: + data['positions'][sl] = self._combined_positions[np.random.randint(0,len(self._combined_positions), to_rel)] + else: + qt = num_locs / to_rel #number of times to tile self.start_positions + rem = num_locs % to_rel #remaining LES to distribute randomly + qt_pos = np.tile(self.start_positions, (qt, 1)) + rem_pos = self._combined_positions[np.random.randint(0,len(self._combined_positions), rem)] + pos = np.vstack((qt_pos, rem_pos)) + assert len(pos) == to_rel + data['positions'][sl] = pos + data['mass'][sl] = self._mass_per_le data['init_mass'][sl] = self._mass_per_le + def to_dict(self, json_=None): + dct = super(SpatialRelease, self).to_dict(json_=json_) + if json_ == 'save': + #stick the geojson in the file for now + fc = geojson.FeatureCollection(self.polygons) + fc.weights = self.weights + fc.thicknesses = self.thicknesses + dct['json_file'] = geojson.dumps(fc) + return dct + + + def get_polygons(self): + ''' + Returns an array of lengths, and a list of line arrays. + The first array sequentially indexes the second array. + When the second array is split up using the first array + and the resulting lines are drawn, you should end up with a picture of + the polygons. + ''' + polycoords = map(lambda p: np.array(p.exterior.xy).T.astype(np.float32), self.polygons) + lengths = np.array(map(len, polycoords)).astype(np.int32) + # weights = self.weights if self.weights is not None else [] + # thicknesses = self.thicknesses if self.thicknesses is not None else [] + return lengths, polycoords + + def get_metadata(self): + return {'weights': self.weights, 'thicknesses': self.thicknesses} + + def get_start_positions(self): + #returns all combined start positions in binary form for the API + return np.ascontiguousarray(self.gen_combined_start_positions().astype(np.float32)) + + def GridRelease(release_time, bounds, resolution): """ @@ -574,7 +976,7 @@ def GridRelease(release_time, bounds, resolution): (max_lon, max_lat)) :type bounds: 2x2 numpy array or equivalent - :param resolution: resolution of grid -- it will be a resoluiton X resolution grid + :param resolution: resolution of grid -- it will be a resolution X resolution grid :type resolution: integer """ lon = np.linspace(bounds[0][0], bounds[1][0], resolution) @@ -583,7 +985,9 @@ def GridRelease(release_time, bounds, resolution): positions = np.c_[lon.flat, lat.flat, np.zeros((resolution * resolution),)] return SpatialRelease(release_time=release_time, - start_position=positions) + custom_positions=positions, + num_elements=len(positions), + ) class ContinuousSpatialRelease(SpatialRelease): @@ -616,6 +1020,11 @@ def __init__(self, num_elements and release_time passed to base class __init__ using super See base :class:`Release` documentation """ + super(self, SpatialRelease).__init__( + release_time=release_time, + num_elements=num_elements, + end_release_time=end_release_time + ) Release.__init__(release_time, num_elements, **kwargs) @@ -623,6 +1032,22 @@ def __init__(self, self._start_positions = (np.asarray(start_positions, dtype=world_point_type).reshape((-1, 3))) + @property + def release_duration(self): + ''' + duration over which particles are released in seconds + ''' + if self.end_release_time is None: + return 0 + else: + return (self.end_release_time - self.release_time).total_seconds() + + def LE_timestep_ratio(self, ts): + ''' + Returns the ratio + ''' + return 1.0 * self.num_elements / self.get_num_release_time_steps(ts) + def num_elements_to_release(self, current_time, time_step): ''' @@ -841,4 +1266,4 @@ def release_from_splot_data(release_time, filename): start_positions = np.repeat(pos, num_per_pos, axis=0) return SpatialRelease(release_time=release_time, - start_position=start_positions) + custom_positions=start_positions) diff --git a/py_gnome/gnome/spill/spill.py b/py_gnome/gnome/spill/spill.py index 3385fdac7..6dc4bb58b 100644 --- a/py_gnome/gnome/spill/spill.py +++ b/py_gnome/gnome/spill/spill.py @@ -549,23 +549,15 @@ def grid_spill(bounds, resolution grid :type resolution: integer - :param release_time: time the LEs are released (datetime object) - :type release_time: datetime.datetime - - :param end_position=None: Optional. For moving source, the end position - If None, then release is from a point source - :type end_position: 3-tuple of floats (long, lat, z) - - :param end_release_time=None: optional -- for a time varying release, - the end release time. If None, then release is instantaneous - :type end_release_time: datetime.datetime - :param substance=None: Type of oil spilled. :type substance: str or OilProps - + :param float amount=None: mass or volume of oil spilled :param str units=None: units for amount spilled + + :param release_time: time the LEs are released (datetime object) + :type release_time: datetime.datetime :param tuple windage_range=(.01, .04): Percentage range for windage. Active only for surface particles @@ -576,6 +568,7 @@ def grid_spill(bounds, randomly reset on this time scale :param str name='Surface Point/Line Release': a name for the spill ''' + release = GridRelease(release_time, bounds, resolution) diff --git a/py_gnome/gnome/utilities/appearance.py b/py_gnome/gnome/utilities/appearance.py index 380a052a0..83f2f3c80 100644 --- a/py_gnome/gnome/utilities/appearance.py +++ b/py_gnome/gnome/utilities/appearance.py @@ -135,4 +135,7 @@ class GridAppearance(Appearance): _schema = AppearanceSchema class MoverAppearance(Appearance): + _schema = AppearanceSchema + +class SpatialReleaseSchema(Appearance): _schema = AppearanceSchema \ No newline at end of file diff --git a/py_gnome/gnome/utilities/geometry/geo_routines.py b/py_gnome/gnome/utilities/geometry/geo_routines.py new file mode 100644 index 000000000..633ffd293 --- /dev/null +++ b/py_gnome/gnome/utilities/geometry/geo_routines.py @@ -0,0 +1,23 @@ +from shapely.geometry import Polygon +import numpy as np +import random + +#tri is a Shapely.Polygon, or 3x2 array of coords +#returns a 2D coordinate +def random_pt_in_tri(tri): + coords = None + if isinstance(tri, Polygon): + coords = tri.exterior.coords + else: + coords = tri + coords = np.array(coords) + R = random.random() + S = random.random() + if R + S >= 1: + R = 1 - R + S = 1 - S + A = coords[0] + AB = coords[1] - coords[0] + AC = coords[2] - coords[0] + RPP = A + R*AB + S*AC + return RPP \ No newline at end of file diff --git a/py_gnome/gnome/utilities/remote_data.py b/py_gnome/gnome/utilities/remote_data.py index c72837b2a..40a691a77 100644 --- a/py_gnome/gnome/utilities/remote_data.py +++ b/py_gnome/gnome/utilities/remote_data.py @@ -13,33 +13,37 @@ CHUNKSIZE = 1024 * 1024 -def get_datafile(file_): +def get_datafile(filename): """ - Function looks to see if file_ exists in local directory. If it exists, - then it simply returns the 'file_' back as a string. - If 'file_' does not exist in local filesystem, then it tries to download it - from the gnome server (http://gnome.orr.noaa.gov/py_gnome_testdata). + Looks to see if filename exists in local directory. If it exists, + then it simply returns the 'filename' back as a string. + + If 'filename' does not exist in the local filesystem, then it tries to + download it from the gnome server (http://gnome.orr.noaa.gov/py_gnome_testdata). If it successfully downloads the file, it puts it in the user specified - path given in file_ and returns the 'file_' string. + path given in filename and returns the 'filename' string. - If file is not found or server is down, it rethrows the HTTPError raised + If file is not found or server is down, it re-throws the HTTPError raised by urllib2.urlopen - :param file_: path to the file including filename - :type file_: string + :param filename: path to the file including filename + :type filename: string + :exception: raises urllib2.HTTPError if server is down or file not found on server - :returns: returns the string 'file_' once it has been downloaded to + + :returns: returns the string 'filename' once it has been downloaded to user specified location + """ - if os.path.exists(file_): - return file_ + if os.path.exists(filename): + return filename else: - # download file, then return file_ path + # download file, then return filename path - (path_, fname) = os.path.split(file_) + (path_, fname) = os.path.split(filename) if path_ == '': path_ = '.' # relative to current path @@ -69,7 +73,7 @@ def get_datafile(file_): os.makedirs(path_) sz_read = 0 - with open(file_, 'wb') as fh: + with open(filename, 'wb') as fh: # while sz_read < resp.info().getheader('Content-Length') # goes into infinite recursion so break loop for len(data) == 0 while True: @@ -85,4 +89,4 @@ def get_datafile(file_): pbar.update(CHUNKSIZE) pbar.finish() - return file_ + return filename diff --git a/py_gnome/gnome/utilities/save_updater.py b/py_gnome/gnome/utilities/save_updater.py index a283e0920..27405804f 100644 --- a/py_gnome/gnome/utilities/save_updater.py +++ b/py_gnome/gnome/utilities/save_updater.py @@ -162,7 +162,7 @@ def Substance_from_ElementType(et_json, water): def extract_zipfile(zip_file, to_folder='.'): - with zipfile.ZipFile(zip_file, 'r') as zf: + def work(zf): folders = [name for name in zf.namelist() if name.endswith('/') and not name.startswith('__MACOSX')] prefix=None if len(folders) == 1: @@ -201,6 +201,12 @@ def extract_zipfile(zip_file, to_folder='.'): with open(jsonfile, 'w') as jf: jf.write(contents) + if isinstance(zip_file, zipfile.ZipFile): + work(zip_file) + else: + with zipfile.ZipFile(zip_file, 'r') as zf: + work(zf) + def sanitize_filename(fname): ''' diff --git a/py_gnome/gnome/weatherers/oil.py b/py_gnome/gnome/weatherers/oil.py index 802066427..42ca20cd4 100644 --- a/py_gnome/gnome/weatherers/oil.py +++ b/py_gnome/gnome/weatherers/oil.py @@ -9,7 +9,6 @@ import json import numpy as np -from scipy.optimize import curve_fit from colander import (SchemaNode, Int, String, Float, drop) @@ -30,19 +29,19 @@ def __repr__(self): .format(self)) -class KVis(): +# class KVis(): - def __init__(self, m_2_s, ref_temp_k, weathering=0): - self.m_2_s = m_2_s - self.ref_temp_k = ref_temp_k - self.weathering = weathering +# def __init__(self, m_2_s, ref_temp_k, weathering=0): +# self.m_2_s = m_2_s +# self.ref_temp_k = ref_temp_k +# self.weathering = weathering - def __repr__(self): - return ('' - .format(self)) +# def __repr__(self): +# return ('' +# .format(self)) - def __getitem__(self, item): - return getattr(self, item) +# def __getitem__(self, item): +# return getattr(self, item) def density_at_temp(ref_density, ref_temp_k, temp_k, k_rho_t=0.0008): @@ -71,7 +70,7 @@ def vol_expansion_coeff(rho_0, t_0, rho_1, t_1): return k_rho_t -def kvis_at_temp(ref_kvis, ref_temp_k, temp_k, k_v2=2416.0): +def kvis_at_temp(ref_kvis, ref_temp_k, temp_k, k_v2=2100): ''' Source: Adios2 @@ -79,11 +78,14 @@ def kvis_at_temp(ref_kvis, ref_temp_k, temp_k, k_v2=2416.0): then we can estimate what its viscosity might be at another temperature. - Note: Bill's most recent viscosity document, and an analysis of the + Note: An analysis of the multi-KVis oils in our oil library suggest that a value of - 2416.0 (Abu Eishah 1999) would be a good default value for k_v2. + 2100 would be a good default value for k_v2. ''' - return ref_kvis * np.exp(k_v2 / temp_k - k_v2 / ref_temp_k) + print("kvis_at_temp called") + print(ref_kvis, ref_temp_k, temp_k, k_v2) + print("A:", ref_kvis * np.exp(-k_v2 / ref_temp_k)) + return ref_kvis * np.exp((k_v2 / temp_k) - (k_v2 / ref_temp_k)) class OilSchema(ObjTypeSchema): @@ -214,8 +216,7 @@ def __init__(self, self.bull_time = bullwinkle_time self._pour_point = pour_point self._flash_point = flash_point - self.solubility = solubility - #self._k_v2 = 2416.0 + self.solubility = solubility self._k_v2 = None # set the PC properties @@ -230,6 +231,10 @@ def __init__(self, self._bullwinkle = None self._bulltime = None + + # these will be initialized when used + self._k_v2 = None # decay constant for viscosity curve + self._visc_A = None # constant for viscosity curve #self.product_type = "Refined" @classmethod @@ -270,26 +275,26 @@ def get_dict(self): processing is done to each attribute. They are presented as is. """ - data = {'name':self.name, - 'api':self.api, - 'adios_oil_id':self.adios_oil_id, - 'pour_point':self._pour_point, - 'flash_point':self._flash_point, - 'solubility':self.solubility, - 'bullwinkle_fraction':self.bullwinkle, - 'bullwinkle_time':self.bulltime, - 'densities':self.densities, - 'density_ref_temps':self.density_ref_temps, - 'density_weathering':self.density_weathering, - 'kvis':self.kvis, - 'kvis_ref_temps':self.kvis_ref_temps, - 'kvis_weathering':self.kvis_weathering, - 'emulsion_water_fraction_max':self.emulsion_water_fraction_max, - 'mass_fraction':self.mass_fraction.tolist(), - 'boiling_point':self.boiling_point.tolist(), - 'molecular_weight':self.molecular_weight.tolist(), - 'component_density':self.component_density.tolist(), - 'sara_type':self.sara_type} + data = {'name': self.name, + 'api': self.api, + 'adios_oil_id': self.adios_oil_id, + 'pour_point': self._pour_point, + 'flash_point': self._flash_point, + 'solubility': self.solubility, + 'bullwinkle_fraction': self.bullwinkle, + 'bullwinkle_time': self.bulltime, + 'densities': self.densities, + 'density_ref_temps': self.density_ref_temps, + 'density_weathering': self.density_weathering, + 'kvis': self.kvis, + 'kvis_ref_temps': self.kvis_ref_temps, + 'kvis_weathering': self.kvis_weathering, + 'emulsion_water_fraction_max': self.emulsion_water_fraction_max, + 'mass_fraction': self.mass_fraction.tolist(), + 'boiling_point': self.boiling_point.tolist(), + 'molecular_weight': self.molecular_weight.tolist(), + 'component_density': self.component_density.tolist(), + 'sara_type': self.sara_type} return data @@ -307,27 +312,27 @@ def to_dict(self, json_=None): json_ = super(Oil, self).to_dict(json_=json_) - data = {'name':self.name, - 'api':self.api, - 'adios_oil_id':self.adios_oil_id, - #'pour_point':self.pour_point(), - 'pour_point':self._pour_point, - 'flash_point':self._flash_point, - 'solubility':self.solubility, - 'bullwinkle_fraction':self.bullwinkle, - 'bullwinkle_time':self.bulltime, - 'densities':self.densities, - 'density_ref_temps':self.density_ref_temps, - 'density_weathering':self.density_weathering, - 'kvis':self.kvis, - 'kvis_ref_temps':self.kvis_ref_temps, - 'kvis_weathering':self.kvis_weathering, - 'emulsion_water_fraction_max':self.emulsion_water_fraction_max, - 'mass_fraction':self.mass_fraction.tolist(), - 'boiling_point':self.boiling_point.tolist(), - 'molecular_weight':self.molecular_weight.tolist(), - 'component_density':self.component_density.tolist(), - 'sara_type':self.sara_type} + data = {'name': self.name, + 'api': self.api, + 'adios_oil_id': self.adios_oil_id, + #'pour_point': self.pour_point(), + 'pour_point': self._pour_point, + 'flash_point': self._flash_point, + 'solubility': self.solubility, + 'bullwinkle_fraction': self.bullwinkle, + 'bullwinkle_time': self.bulltime, + 'densities': self.densities, + 'density_ref_temps': self.density_ref_temps, + 'density_weathering': self.density_weathering, + 'kvis': self.kvis, + 'kvis_ref_temps': self.kvis_ref_temps, + 'kvis_weathering': self.kvis_weathering, + 'emulsion_water_fraction_max': self.emulsion_water_fraction_max, + 'mass_fraction': self.mass_fraction.tolist(), + 'boiling_point': self.boiling_point.tolist(), + 'molecular_weight': self.molecular_weight.tolist(), + 'component_density': self.component_density.tolist(), + 'sara_type': self.sara_type} data.update(json_) return data @@ -414,6 +419,7 @@ def get_densities(self): ''' return a list of densities for the oil at a specified state of weathering. + #fixme: this should not happen here! We include the API as a density if: - the specified weathering is 0 - the culled list of densities does not contain a measurement @@ -424,9 +430,11 @@ def get_densities(self): weathering = [d for d in self.density_weathering] new_densities = [] - for x in range(0,len(densities)): - if weathering[x] == 0: #also check None - new_densities.append(Density(kg_m_3=densities[x],ref_temp_k=density_ref_temps[x],weathering=0.0)) + for x in range(0, len(densities)): + if weathering[x] == 0: # also check None + new_densities.append(Density(kg_m_3=densities[x], + ref_temp_k=density_ref_temps[x], + weathering=0.0)) return sorted(new_densities, key=lambda d: d.ref_temp_k) @@ -435,7 +443,7 @@ def density_at_temp(self, temperature=288.15): Get the oil density at a temperature or temperatures. Note: there is a catch-22 which prevents us from getting - the min_temp in all casees: + the min_temp in all cases: - to estimate pour point, we need viscosities - if we need to convert dynamic viscosities to kinematic, we need density at 15C @@ -474,7 +482,7 @@ def density_at_temp(self, temperature=288.15): k_rho_t = self._vol_expansion_coeff(densities, temperature) rho_t = density_at_temp(ref_density, ref_temp_k, - temperature, k_rho_t) + temperature, k_rho_t) if len(rho_t) == 1: return rho_t[0] @@ -485,6 +493,7 @@ def density_at_temp(self, temperature=288.15): @property def standard_density(self): + # fixme: this should simply be a set value ''' Standard density is simply the density at 15C, which is the default temperature for density_at_temp() @@ -566,6 +575,7 @@ def closest_to_temperature(cls, obj_list, temperature, num=1): We accept only a scalar temperature or a sequence of temperatures ''' + # fixme: this is NOT how to do this! if hasattr(temperature, '__iter__'): # we like to deal with numpy arrays as opposed to simple iterables temperature = np.array(temperature) @@ -613,111 +623,69 @@ def aggregate_kvis(self): def kvis_at_temp(self, temp_k=288.15, weathering=0.0): - shape = None + """ + Compute the kinematic viscosity of the oil as a function of temperature - if hasattr(temp_k, '__iter__'): - # we like to deal with numpy arrays as opposed to simple iterables - temp_k = np.array(temp_k) - shape = temp_k.shape - temp_k = temp_k.reshape(-1) + :param temp_k: temperatures to compute at: can be scalar or array of values. + should be in Kelvin - kvis = [d for d in self.kvis] - kvis_ref_temps = [d for d in self.kvis_ref_temps] - weathering = [d for d in self.kvis_weathering] - kvis_list = [] + :param weathering: fraction weathered -- currently not implemented - for x in range(0,len(kvis)): - if weathering[x] == 0: #also check None - kvis_list.append(KVis(m_2_s=kvis[x],ref_temp_k=kvis_ref_temps[x],weathering=0.0)) + viscosity as a function of temp is given by: + v = A exp(k_v2 / T) - # agg = dict(kvis_list) + with constants determined from measured data + """ + if weathering != 0.0: + raise NotImplementedError("computing viscosity of weathered oil" + "is not implemented yet") - #new_kvis_list = zip(*[(KVis(m_2_s=k, ref_temp_k=t, weathering=w), e) - # for (t, w), (k, e) in sorted(agg.iteritems())]) - #kvis_list = [kv for kv in self.aggregate_kvis()[0] - #if (kv.weathering == weathering)] - closest_kvis = self.closest_to_temperature(kvis_list, temp_k) + temp_k = np.asarray(temp_k) - if closest_kvis is not None: - try: - # treat as a list - ref_kvis, ref_temp_k = zip(*[(kv[0].m_2_s, kv[0].ref_temp_k) - for kv in closest_kvis]) - if len(closest_kvis) > 1: - ref_kvis = np.array(ref_kvis).reshape(temp_k.shape) - ref_temp_k = np.array(ref_temp_k).reshape(temp_k.shape) - else: - ref_kvis, ref_temp_k = ref_kvis[0], ref_temp_k[0] - except TypeError: - # treat as a scalar - ref_kvis, ref_temp_k = (closest_kvis[0].m_2_s, - closest_kvis[0].ref_temp_k) - else: - return None - - if self._k_v2 is None: - self.determine_k_v2(kvis_list) + if self._k_v2 is None or self._visc_A is None: + self.determine_visc_constants() - kvis_t = kvis_at_temp(ref_kvis, ref_temp_k, temp_k, self._k_v2) + return self._visc_A * np.exp(self._k_v2 / temp_k) - if shape is not None: - return kvis_t.reshape(shape) - else: - return kvis_t - def determine_k_v2(self, kvis_list=None): - ''' - The value k_v2 is the coefficient of exponential decay used - when calculating kinematic viscosity as a function of - temperature. - - If the oil contains two or more viscosity measurements, then - we will make an attempt at determining k_v2 using a least - squares fit. - - Otherwise we will need to choose a reasonable average default - value. Bill's most recent viscosity document, and an - analysis of the multi-KVis oils in our oil library suggest that - a value of 2416.0 (Abu Eishah 1999) would be a good default - value. + def determine_visc_constants(self): ''' - self._k_v2 = 2416.0 + viscosity as a function of temp is given by: - def exp_func(temp_k, a, k_v2): - return a * np.exp(k_v2 / temp_k) + v = A exp(k_v2 / T) - if kvis_list is None: - kvis_list = [kv for kv in self.aggregate_kvis()[0] - if (kv.weathering in (None, 0.0))] + The constants, A and k_v2 are determined from the viscosity data: - if len(kvis_list) < 2: - return + If only one data point, a default value for k_vs is used: + 2100 K, based on analysis of data in the ADIOS database as of 2018 - ref_temp_k, ref_kvis = zip(*[(k.ref_temp_k, k.m_2_s) - for k in kvis_list]) + If two data points, the two constants are directly computed - for k in np.logspace(3.6, 4.5, num=8): - # k = log range from about 5000-32000 - a_coeff = ref_kvis[0] * np.exp(-k / ref_temp_k[0]) - - try: - popt, pcov = curve_fit(exp_func, ref_temp_k, ref_kvis, - p0=(a_coeff, k), maxfev=2000) - - # - we want our covariance to be a reasonably small number, - # but it can get into the thousands even for a good fit. - # So we will only check for inf values. - # - for sample sizes < 3, the covariance is unreliable. - if len(ref_kvis) > 2 and np.any(pcov == np.inf): - print 'covariance too high.' - continue + If three or more, the constants are computed by a least squares fit. + ''' + # find viscosity measurements with zero weathering - if popt[1] <= 1.0: - continue + # this sets: + self._k_v2 = None # decay constant for viscosity curve + self._visc_A = None - self._k_v2 = popt[1] - break - except (ValueError, RuntimeError): - continue + kvis = [k for k, w in zip(self.kvis, self.kvis_weathering) if w == 0.0] + kvis_ref_temps = [t for t, w in zip(self.kvis_ref_temps, + self.kvis_weathering) if w == 0.0] + if len(kvis) == 1: # use default k_v2 + self._k_v2 = 2100.0 + self._visc_A = kvis[0] * np.exp(-self._k_v2 / kvis_ref_temps[0]) + else: + # do a least squares fit to the data + # viscs = np.array(kvis) + # temps = np.array(kvis_ref_temps) + b = np.log(kvis) + A = np.c_[np.ones_like(b), 1.0 / np.array(kvis_ref_temps)] + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + self._k_v2 = x[1] + self._visc_A = np.exp(x[0]) + return def get(self, prop): 'get oil props' @@ -741,11 +709,11 @@ def bulltime(self): # check for user input value, otherwise set to -999 as a flag bulltime = -999. - if self._bulltime is not None: + if self._bulltime is not None: return self._bulltime else: if self.bull_time is not None: - return self.bull_time + return self.bull_time else: return bulltime #return bulltime diff --git a/py_gnome/gnome/weatherers/weathering_data.py b/py_gnome/gnome/weatherers/weathering_data.py index 725230a19..86b506945 100644 --- a/py_gnome/gnome/weatherers/weathering_data.py +++ b/py_gnome/gnome/weatherers/weathering_data.py @@ -89,7 +89,7 @@ def prepare_for_model_run(self, sc): water temperature which is constant for now. ''' # nothing released yet - set everything to 0.0 - for key in ('avg_density', 'floating', 'amount_released', + for key in ('avg_density', 'floating', 'amount_released', 'non_weathering', 'avg_viscosity'): sc.mass_balance[key] = 0.0 diff --git a/py_gnome/requirements.txt b/py_gnome/requirements.txt index a5a20a347..0aa526ff2 100644 --- a/py_gnome/requirements.txt +++ b/py_gnome/requirements.txt @@ -22,6 +22,7 @@ repoze.lru colander gsw # Thermodynamic Equations Of Seawater - density computation pyshp +mapbox-earcut #for SpatialRelease #gridded diff --git a/py_gnome/scripts/.gitignore b/py_gnome/scripts/.gitignore index 5f2fbd31e..b0ece522f 100644 --- a/py_gnome/scripts/.gitignore +++ b/py_gnome/scripts/.gitignore @@ -3,3 +3,40 @@ Mississippi_images TwoLE_LI_sound_images /script*/images/* script_surface_concentration/surface_concentration.* +script_TAP/120.0RK2.nc +script_TAP/120RK2.nc +script_TAP/arctic_coast3.bna +script_grid_spill/CAROMS.nc +script_grid_spill/whale_run_windage_3.0.nc +script_ice/arctic_avg2_0001_gnome.nc +script_ice/arctic_avg2_0002_gnome.nc +script_mariana/HYCOM.nc +script_mariana/HYCOM.nc.dat +script_mariana/mariana_island.bna +script_mississippi_river/LMiss.CUR +script_mississippi_river/LowerMississippiMap.bna +script_mississippi_river/script_lower_mississippi.nc +script_mississippi_river/script_lower_mississippi_uncertain.nc +script_ny_plume/nyharbor.bna +script_ny_plume/script_ny_plume.nc +script_passamaquoddy/EstesHead.txt +script_passamaquoddy/PQBayCur.nc4 +script_passamaquoddy/PassamaquoddyMap.bna +script_passamaquoddy/PassamaquoddyTOP.dat +script_passamaquoddy/script_passamaquoddy.nc +script_regular_grid/ +script_san_juan/script_san_juan.nc +script_sf_wind/WindSpeedDirSubset.nc +script_sf_wind/WindSpeedDirSubsetTop.dat +script_sf_wind/coastSF.bna +script_sf_wind/script_sf_bay.nc +script_sf_wind/script_sf_bay_uncertain.nc +script_tamoc/script_tamoc_old.py +script_weatherers/Model.gnome +script_weatherers/script_weatherers.nc +script_weatherers/script_weatherers_uncertain.nc +script_weathering_run/GNOME_oil_budget.csv +script_weathering_run/Model.gnome +script_weathering_run/WeatheringRun +script_weathering_run/WeatheringRun.gnome +script_weathering_run/WeatheringRun.zip diff --git a/py_gnome/scripts/run_all.py b/py_gnome/scripts/run_all.py index b7a077587..c0376d199 100755 --- a/py_gnome/scripts/run_all.py +++ b/py_gnome/scripts/run_all.py @@ -14,20 +14,26 @@ def run_all_with_script_runner(to_skip=[]): scripts = glob.glob(os.path.join(os.path.dirname(__file__), 'script_*/script_*.py')) - print scripts - default_skip = ['script_ny_plume/script_ny_plume.py', 'script_ny_roms/script_ny_roms.py', - 'script_tamoc/script_tamoc.py', 'script_tamoc/script_arctic_tamoc.py', - 'script_tamoc/script_gulf_tamoc.py', 'script_TAP/script_old_TAP.py'] + print(scripts) + + default_skip = ['script_ny_plume/script_ny_plume.py', + 'script_ny_roms/script_ny_roms.py', + 'script_tamoc/script_tamoc.py', + 'script_tamoc/script_arctic_tamoc.py', + 'script_tamoc/script_gulf_tamoc.py', + 'script_TAP/script_old_TAP.py', + 'script_ice/script_ice.py' + ] for script in to_skip: default_skip = [s for s in default_skip if script not in s] to_skip.extend(default_skip) for script in to_skip: scripts = [s for s in scripts if script not in s] - print scripts + print(scripts) for script in scripts: - print 'Begin processing script: {0}'.format(script) + print('Begin processing script: {0}'.format(script)) # clean directories first # script_runner.clean(os.path.dirname(script)) @@ -39,12 +45,12 @@ def run_all_with_script_runner(to_skip=[]): (model, imp_script) = script_runner.load_model(script, image_dir) script_runner.run(model) - print 'completed model run for: {0}'.format(script) + print('completed model run for: {0}'.format(script)) if hasattr(imp_script, 'post_run'): imp_script.post_run(model) - print 'completed post model run for: {0}'.format(script) + print('completed post model run for: {0}'.format(script)) # save model _state @@ -67,28 +73,48 @@ def run_all_with_script_runner(to_skip=[]): # print ('Exception in script_runner.run_from_save(saveloc)' # '\n\t{0}'.format(ex)) + def run_all_alone(): - # fixme -- needs to be finished... + """ + Runs all the scripts, each in a subprocess + + Then reports success and failures + """ + scripts = glob.glob(os.path.join(os.path.dirname(__file__), 'script_*/script_*.py')) - print scripts + # print(scripts) + # fixme: it would be good to keep track of the errors + successes = [] + failures = [] ## this could (and probably should) be made much smarter ## should it use subprocess?? for script in scripts: - print "**************************" - print "*" - print "* Running: %s"%script - print "*" - print "**************************" - subprocess.check_call(["python", script], shell=False) + print("**************************") + print("*") + print("* Running: %s"%script) + print("*") + print("**************************") + try: + subprocess.check_call(["python", script], shell=False) + successes.append(script) + except subprocess.CalledProcessError: + failures.append(script) + return successes, failures if __name__ == "__main__": - print sys.argv + print(sys.argv) to_skip = sys.argv[1:] - run_all_with_script_runner(to_skip) - + # run_all_with_script_runner(to_skip) + successes, failures = run_all_alone() + print("Successful scripts:") + for s in successes: + print(s) + print("Scripts with Errors:") + for s in failures: + print(s) diff --git a/py_gnome/scripts/script_shore_param/script_shore_param.py b/py_gnome/scripts/script_shore_param/script_shore_param.py index 76841b07a..2e077aff6 100644 --- a/py_gnome/scripts/script_shore_param/script_shore_param.py +++ b/py_gnome/scripts/script_shore_param/script_shore_param.py @@ -3,25 +3,19 @@ Script to test GNOME with HYCOM data in Mariana Islands region. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import os -from datetime import datetime, timedelta -from gnome import basic_types +import gnome.scripting as gs -from gnome import scripting from gnome import utilities -from gnome.utilities.remote_data import get_datafile - -from gnome.model import Model -from gnome.map import ParamMap -from gnome.spill import point_line_release_spill -from gnome.movers import RandomMover, constant_wind_mover, GridCurrentMover +from gnome.maps.map import ParamMap -from gnome.outputters import (Renderer, - # NetCDFOutput - ) -from gnome.basic_types import numerical_methods NUM_ELEMENTS = 10000 @@ -30,30 +24,34 @@ def make_model(img_dir=os.path.join(base_dir, 'images')): - print 'initializing the model' - - start_time = datetime(2013, 5, 18, 0) + print('initializing the model') - model = Model(start_time=start_time, duration=timedelta(days=3), - time_step=3600, uncertain=False) + start_time = "2013-05-18T00:00:00" + model = gs.Model(start_time=start_time, duration=gs.days(3), + time_step=3600, uncertain=False) - print 'adding the map' - p_map = model.map = ParamMap(center = (0,0), distance=20, bearing = 20, units='km' ) # hours + print('adding the map') + p_map = model.map = ParamMap(center=(0, 0), + distance=20, + bearing=20, + units='km', + ) # hours # # Add the outputters -- render to images, and save out as netCDF # - print 'adding renderer' - rend = Renderer( output_dir=img_dir, - image_size=(800, 600), - map_BB=p_map.get_map_bounds(), - land_polygons=p_map.get_land_polygon(), - ) - + print('adding renderer') + rend = gs.Renderer(output_dir=img_dir, + image_size=(800, 600), + map_BB=p_map.get_map_bounds(), + land_polygons=p_map.get_land_polygon(), + ) + rend.graticule.set_DMS(True) model.outputters += rend + # draw_back_to_fore=True) # print "adding netcdf output" @@ -65,13 +63,13 @@ def make_model(img_dir=os.path.join(base_dir, 'images')): # Set up the movers: # - print 'adding a RandomMover:' - model.movers += RandomMover(diffusion_coef=100000) + print('adding a RandomMover:') + model.movers += gs.RandomMover(diffusion_coef=100000) - print 'adding a simple wind mover:' - model.movers += constant_wind_mover(10, 225, units='m/s') + print('adding a simple wind mover:') + model.movers += gs.constant_wind_mover(10, 225, units='m/s') - print 'adding a current mover:' + print('adding a current mover:') # # # this is HYCOM currents # curr_file = get_datafile(os.path.join(base_dir, 'HYCOM.nc')) @@ -82,24 +80,25 @@ def make_model(img_dir=os.path.join(base_dir, 'images')): # # Add some spills (sources of elements) # # - print 'adding four spill' - model.spills += point_line_release_spill(num_elements=NUM_ELEMENTS // 4, - start_position=(0.0,0.0, - 0.0), - release_time=start_time) + print('adding four spill') + model.spills += gs.point_line_release_spill(num_elements=NUM_ELEMENTS // 4, + start_position=(0.0, + 0.0, + 0.0), + release_time=start_time) return model if __name__ == '__main__': - scripting.make_images_dir() + gs.make_images_dir() model = make_model() rend = model.outputters[0] for step in model: # rend.zoom(0.9) if (step['step_num'] == 33): pass - - print "step: %.4i -- memuse: %fMB" % (step['step_num'], - utilities.get_mem_use()) + + print("step: %.4i -- memuse: %fMB" % (step['step_num'], + utilities.get_mem_use())) # model.full_run(log=True) \ No newline at end of file diff --git a/py_gnome/scripts/script_weatherers/script_weatherers.py b/py_gnome/scripts/script_weatherers/script_weatherers.py old mode 100644 new mode 100755 index 92803763f..841b6a25f --- a/py_gnome/scripts/script_weatherers/script_weatherers.py +++ b/py_gnome/scripts/script_weatherers/script_weatherers.py @@ -3,31 +3,19 @@ Script to test GNOME with all weatherers and response options """ -import os -import shutil -from datetime import datetime, timedelta - -import numpy -np = numpy +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals -from gnome import scripting -from gnome.basic_types import datetime_value_2d +import os -from gnome.utilities.remote_data import get_datafile -from gnome.utilities.inf_datetime import InfDateTime +from gnome import scripting as gs -from gnome.model import Model -from gnome.maps import MapFromBNA -from gnome.environment import Wind -from gnome.spill import point_line_release_spill -from gnome.movers import RandomMover, WindMover +from gnome.environment import constant_wind, Water, Waves -from gnome.outputters import Renderer -from gnome.outputters import NetCDFOutput -from gnome.outputters import WeatheringOutput -from gnome.environment import constant_wind, Water, Waves from gnome.weatherers import (Emulsification, Evaporation, NaturalDispersion, @@ -36,110 +24,103 @@ Skimmer, WeatheringData) -from gnome.persist import load # define base directory base_dir = os.path.dirname(__file__) - water = Water(280.928) wind = constant_wind(20., 117, 'knots') waves = Waves(wind, water) + def make_model(images_dir=os.path.join(base_dir, 'images')): - print 'initializing the model' + print('initializing the model') - start_time = datetime(2015, 5, 14, 0, 0) + # start_time = datetime(2015, 5, 14, 0, 0) + start_time = gs.asdatetime("2015-05-14") # 1 day of data in file # 1/2 hr in seconds - model = Model(start_time=start_time, - duration=timedelta(days=1.75), - time_step=60 * 60, - uncertain=True) - -# mapfile = get_datafile(os.path.join(base_dir, './ak_arctic.bna')) -# -# print 'adding the map' -# model.map = MapFromBNA(mapfile, refloat_halflife=1) # seconds -# -# # draw_ontop can be 'uncertain' or 'forecast' -# # 'forecast' LEs are in black, and 'uncertain' are in red -# # default is 'forecast' LEs draw on top -# renderer = Renderer(mapfile, images_dir, image_size=(800, 600), -# output_timestep=timedelta(hours=2), -# draw_ontop='forecast') -# -# print 'adding outputters' -# model.outputters += renderer - - model.outputters += WeatheringOutput('.\\') + model = gs.Model(start_time=start_time, + duration=gs.days(1.75), + time_step=60 * 60, + uncertain=True) - netcdf_file = os.path.join(base_dir, 'script_weatherers.nc') - scripting.remove_netcdf(netcdf_file) - model.outputters += NetCDFOutput(netcdf_file, which_data='all', - output_timestep=timedelta(hours=1)) + print('adding outputters') + + model.outputters += gs.WeatheringOutput(os.path.join(base_dir, 'output')) - print 'adding a spill' + netcdf_file = os.path.join(base_dir, 'script_weatherers.nc') + gs.remove_netcdf(netcdf_file) + model.outputters += gs.NetCDFOutput(netcdf_file, + which_data='all', + output_timestep=gs.hours(1), + surface_conc=None, + ) + + print('adding a spill') # for now subsurface spill stays on initial layer # - will need diffusion and rise velocity # - wind doesn't act # - start_position = (-76.126872, 37.680952, 5.0), - end_time = start_time + timedelta(hours=24) - spill = point_line_release_spill(num_elements=100, - start_position=(-164.791878561, - 69.6252597267, 0.0), - release_time=start_time, - end_release_time=end_time, - amount=1000, - substance='ALASKA NORTH SLOPE (MIDDLE PIPELINE, 1997)', - units='bbl') + end_time = start_time + gs.hours(24) + spill = gs.point_line_release_spill(num_elements=100, + start_position=(-164.791878561, + 69.6252597267, 0.0), + release_time=start_time, + end_release_time=end_time, + amount=1000, + substance='ALASKA NORTH SLOPE (MIDDLE PIPELINE, 1997)', + units='bbl') # set bullwinkle to .303 to cause mass goes to zero bug at 24 hours (when continuous release ends) spill.substance._bullwinkle = .303 model.spills += spill - print 'adding a RandomMover:' - # model.movers += RandomMover(diffusion_coef=50000) + print('adding a RandomMover:') + model.movers += gs.RandomMover(diffusion_coef=50000) - print 'adding a wind mover:' + print('adding a wind mover:') - series = np.zeros((2,), dtype=datetime_value_2d) - series[0] = (start_time, (20, 0)) - series[1] = (start_time + timedelta(hours=23), (20, 0)) + # series = np.zeros((2,), dtype=datetime_value_2d) + # series[0] = (start_time, (20, 0)) + # series[1] = (start_time + timedelta(hours=23), (20, 0)) - wind2 = Wind(timeseries=series, units='knot') + # wind2 = gs.Wind(timeseries=series, units='knot') - w_mover = WindMover(wind) + w_mover = gs.WindMover(wind) model.movers += w_mover - print 'adding weatherers and cleanup options:' + print('adding weatherers and cleanup options:') # define skimmer/burn cleanup options - skim1_start = start_time + timedelta(hours=15.58333) - skim2_start = start_time + timedelta(hours=16) + skim1_start = start_time + gs.hours(15.58333) + skim2_start = start_time + gs.hours(16) - skim1_active_range = (skim1_start, skim1_start + timedelta(hours=8.)) - skim2_active_range = (skim2_start, skim2_start + timedelta(hours=12.)) + skim1_active_range = (skim1_start, skim1_start + gs.hours(8)) + skim2_active_range = (skim2_start, skim2_start + gs.hours(12)) units = spill.units skimmer1 = Skimmer(80, units=units, efficiency=0.36, - active_range=skim1_active_range) + active_range=skim1_active_range) skimmer2 = Skimmer(120, units=units, efficiency=0.2, - active_range=skim2_active_range) + active_range=skim2_active_range) + + burn_start = start_time + gs.hours(36) - burn_start = start_time + timedelta(hours=36) burn = Burn(1000., .1, - active_range=(burn_start, InfDateTime('inf')), efficiency=.2) + active_range=(burn_start, gs.InfTime()), + efficiency=.2) - chem_start = start_time + timedelta(hours=24) - chem_active_range = (chem_start, chem_start + timedelta(hours=8)) +# chem_start = start_time + gs.hours(24) +# chem_active_range = (chem_start, chem_start + gs.hours(8)) # c_disp = ChemicalDispersion(0.5, efficiency=0.4, # active_start=chem_start, -# active_stop=chem_start + timedelta(hours=8)) - +# active_stop=chem_start + gs.hours(8)) - model.environment += [Water(280.928), wind, waves] + model.environment += water + model.environment += wind + model.environment += waves model.weatherers += Evaporation(water, wind) model.weatherers += Emulsification(waves) @@ -153,7 +134,7 @@ def make_model(images_dir=os.path.join(base_dir, 'images')): if __name__ == "__main__": - scripting.make_images_dir() + gs.make_images_dir() model = make_model() model.full_run() model.save('.') diff --git a/py_gnome/scripts/script_weathering_run/script_weathering_run.py b/py_gnome/scripts/script_weathering_run/script_weathering_run.py index 87978abb2..2b7e9b05d 100644 --- a/py_gnome/scripts/script_weathering_run/script_weathering_run.py +++ b/py_gnome/scripts/script_weathering_run/script_weathering_run.py @@ -4,10 +4,15 @@ This is a "just weathering" run -- no land, currents, etc. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import os from gnome import scripting as gs -print "I am running!" +print("I am running!") # define base directory base_dir = os.path.dirname(__file__) @@ -15,15 +20,15 @@ def make_model(): - print 'initializing the model' + print('initializing the model') model = gs.Model(duration=gs.days(5)) - print 'adding outputters' + print('adding outputters') budget_file = os.path.join(base_dir, 'GNOME_oil_budget.csv') model.outputters += gs.OilBudgetOutput(budget_file) - print 'adding a spill' + print('adding a spill') # We need a spill at the very least spill = gs.point_line_release_spill(num_elements=10, # no need for a lot of elements for a instantaneous release start_position=(0.0, 0.0, 0.0), @@ -34,10 +39,10 @@ def make_model(): model.spills += spill - print 'adding a RandomMover:' + print('adding a RandomMover:') model.movers += gs.RandomMover() - print 'adding a wind mover:' + print('adding a wind mover:') model.movers += gs.constant_wind_mover(speed=10, direction=0, units="m/s") @@ -47,7 +52,7 @@ def make_model(): waves = gs.Waves() model.environment += waves - print 'adding the standard weatherers' + print('adding the standard weatherers') model.add_weathering() return model @@ -55,6 +60,6 @@ def make_model(): if __name__ == "__main__": model = make_model() - print "running the model" + print("running the model") model.full_run() model.save(saveloc=save_loc) diff --git a/py_gnome/tests/conftest.py b/py_gnome/tests/conftest.py index d16e111cc..4a5864178 100644 --- a/py_gnome/tests/conftest.py +++ b/py_gnome/tests/conftest.py @@ -27,6 +27,17 @@ def pytest_addoption(parser): 'used to run tests skipped by xdist')) +def pytest_configure(config): + """ + adding stuff to configuration + + in this case, the "run" marker + """ + config.addinivalue_line( + "markers", "slow: mark test as slow, to run only when --runslow flag is used" + ) + + def pytest_runtest_setup(item): ''' pytest builtin hook diff --git a/py_gnome/tests/unit_tests/conftest.py b/py_gnome/tests/unit_tests/conftest.py index 6c2df2fc4..5bf9d6466 100644 --- a/py_gnome/tests/unit_tests/conftest.py +++ b/py_gnome/tests/unit_tests/conftest.py @@ -560,7 +560,7 @@ def sample_spatial_release_spill(): (-15, 12, 4.0), (80, -80, 100.0)) - rel = SpatialRelease(datetime(2012, 1, 1, 1), start_positions) + rel = SpatialRelease(release_time=datetime(2012, 1, 1, 1), custom_positions=start_positions) sp = gnome.spill.Spill(release=rel) return (sp, start_positions) diff --git a/py_gnome/tests/unit_tests/test_model.py b/py_gnome/tests/unit_tests/test_model.py index a2135ee66..f60ae888b 100644 --- a/py_gnome/tests/unit_tests/test_model.py +++ b/py_gnome/tests/unit_tests/test_model.py @@ -69,7 +69,7 @@ def model(sample_model_fcn, tmpdir): # print start_points - release = SpatialRelease(start_position=line_pos, + release = SpatialRelease(custom_positions=line_pos, release_time=model.start_time) model.spills += Spill(release, substance=test_oil) @@ -365,7 +365,7 @@ def test_simple_run_with_image_output(tmpdir): start_points[:, 1] = np.linspace(47.93, 48.1, N) # print start_points - spill = Spill(release=SpatialRelease(start_position=start_points, + spill = Spill(release=SpatialRelease(custom_positions=start_points, release_time=start_time)) model.spills += spill @@ -422,7 +422,7 @@ def test_simple_run_with_image_output_uncertainty(tmpdir): start_points[:, 1] = np.linspace(47.93, 48.1, N) # print start_points - release = SpatialRelease(start_position=start_points, + release = SpatialRelease(custom_positions=start_points, release_time=start_time) model.spills += Spill(release) diff --git a/py_gnome/tests/unit_tests/test_outputters/test_current_outputter.py b/py_gnome/tests/unit_tests/test_outputters/test_current_outputter.py index de112905a..8cac58cbd 100644 --- a/py_gnome/tests/unit_tests/test_outputters/test_current_outputter.py +++ b/py_gnome/tests/unit_tests/test_outputters/test_current_outputter.py @@ -47,7 +47,7 @@ def model(sample_model, output_dir): release_time=model.start_time, end_position=rel_end_pos) - release = SpatialRelease(start_position=line_pos, + release = SpatialRelease(custom_positions=line_pos, release_time=model.start_time) model.spills += Spill(release) diff --git a/py_gnome/tests/unit_tests/test_outputters/test_ice_image_outputter.py b/py_gnome/tests/unit_tests/test_outputters/test_ice_image_outputter.py index d55fb0971..c58c9bdef 100644 --- a/py_gnome/tests/unit_tests/test_outputters/test_ice_image_outputter.py +++ b/py_gnome/tests/unit_tests/test_outputters/test_ice_image_outputter.py @@ -22,7 +22,7 @@ from pprint import PrettyPrinter pp = PrettyPrinter(indent=2, width=120) -pytest.mark.skip("ice image outputter not usefull -- tests slow") +pytest.mark.skip("ice image outputter not useful -- tests slow") curr_file = testdata['IceMover']['ice_curr_curv'] diff --git a/py_gnome/tests/unit_tests/test_outputters/test_ice_json_outputter.py b/py_gnome/tests/unit_tests/test_outputters/test_ice_json_outputter.py index 64d0d1911..b4342fb25 100644 --- a/py_gnome/tests/unit_tests/test_outputters/test_ice_json_outputter.py +++ b/py_gnome/tests/unit_tests/test_outputters/test_ice_json_outputter.py @@ -41,7 +41,7 @@ def model(sample_model, output_dir): release_time=model.start_time, end_position=start_pos) - release = SpatialRelease(start_position=line_pos, + release = SpatialRelease(custom_positions=line_pos, release_time=model.start_time) model.spills += Spill(release) diff --git a/py_gnome/tests/unit_tests/test_outputters/test_ice_outputter.py b/py_gnome/tests/unit_tests/test_outputters/test_ice_outputter.py index f48e11a3e..bc219c545 100644 --- a/py_gnome/tests/unit_tests/test_outputters/test_ice_outputter.py +++ b/py_gnome/tests/unit_tests/test_outputters/test_ice_outputter.py @@ -16,6 +16,9 @@ from ..conftest import testdata +pytest.mark.skip("ice outputter not currently useful -- tests slow") + + curr_file = testdata['IceMover']['ice_curr_curv'] topology_file = testdata['IceMover']['ice_top_curv'] c_ice_mover = IceMover(curr_file, topology_file) @@ -42,7 +45,7 @@ def model(sample_model, output_dir): release_time=model.start_time, end_position=start_pos) - release = SpatialRelease(start_position=line_pos, + release = SpatialRelease(custom_positions=line_pos, release_time=model.start_time) model.spills += Spill(release) diff --git a/py_gnome/tests/unit_tests/test_spill/data_for_tests/NESDIS_files.zip b/py_gnome/tests/unit_tests/test_spill/data_for_tests/NESDIS_files.zip new file mode 100644 index 000000000..cf0193b4f Binary files /dev/null and b/py_gnome/tests/unit_tests/test_spill/data_for_tests/NESDIS_files.zip differ diff --git a/py_gnome/tests/unit_tests/test_spill/test_release.py b/py_gnome/tests/unit_tests/test_spill/test_release.py index 9ecf5c73a..bd1905313 100644 --- a/py_gnome/tests/unit_tests/test_spill/test_release.py +++ b/py_gnome/tests/unit_tests/test_spill/test_release.py @@ -15,6 +15,7 @@ from gnome.spill import (Release, PointLineRelease, + SpatialRelease, GridRelease) from gnome.spill.release import release_from_splot_data from gnome.spill.le import LEData @@ -40,7 +41,7 @@ def test_init(self): # """ # bounds = ((0, 10), (2, 12)) # release = GridRelease(datetime.now(), bounds, 3) -# + # assert np.array_equal(release.start_position, [[0., 10., 0.], # [1., 10., 0.], # [2., 10., 0.], @@ -52,7 +53,6 @@ def test_init(self): # [2., 12., 0.]]) # todo: add other release to this test - need schemas for all - rel_time = datetime(2012, 8, 20, 13) rel_type = [PointLineRelease(release_time=rel_time, num_elements=5, @@ -250,6 +250,97 @@ def test_LE_initialization(self, r1, r3): assert pos[d] >= r._pos_ts.at(None, r.release_time + timedelta(seconds=ts/4))[d] assert pos[d] <= r._pos_ts.at(None, r.release_time + timedelta(seconds=ts/2))[d] +from shapely.geometry import Polygon +custom_positions=np.array([[5,6,7], [8,9,10]]) +polys = [Polygon([[0,0],[0,1],[1,0]])] + +@pytest.fixture('function') +def sr1(): + #150 minute continuous release + return SpatialRelease(release_time=rel_time, + end_release_time=rel_time + timedelta(seconds=900)*10, + num_elements=1000, + release_mass=5000, + polygons=polys, + custom_positions=custom_positions) + +@pytest.fixture('function') +def sr2(): + return SpatialRelease(release_time=rel_time, + num_elements=1000, + polygons=polys) + + +class TestSpatialRelease: + + def test_LE_timestep_ratio(self, sr1): + sr1.end_release_time = rel_time + timedelta(seconds=1000)*10 + #timestep of 10 seconds. 10,000 second release, min 1000 elements exactly + assert sr1.LE_timestep_ratio(10) == 1 + assert sr1.LE_timestep_ratio(20) == 2 + + def test_get_num_release_time_steps(self, sr1): + assert sr1.get_num_release_time_steps(9000) == 1 + assert sr1.get_num_release_time_steps(8999) == 2 + assert sr1.get_num_release_time_steps(900) == 10 + assert sr1.get_num_release_time_steps(899) == 11 + + def test_prepare_for_model_run(self, sr1, sr2): + sr1.prepare_for_model_run(900) + assert len(sr1._release_ts.data) == 11 + assert sr1._release_ts.at(None, sr1.release_time) == 0 + assert sr1._release_ts.at(None, sr1.end_release_time) == 1000 + assert np.all(sr1._release_ts.data == np.linspace(0,1000, len(sr1._release_ts.data))) + assert sr1._mass_per_le == 5 + assert sr1.get_num_release_time_steps(900) == 10 + + #combined positions must exist, and last entries must be custom positions + assert sr1._combined_positions is not None and np.all(sr1._combined_positions[-1] == [8,9,10]) + + sr1.rewind() + assert sr1._combined_positions is None + sr1.release_mass = 2500 + sr1.prepare_for_model_run(450) + assert len(sr1._release_ts.data) == 21 + assert sr1._release_ts.at(None, sr1.release_time) == 0 + assert sr1._release_ts.at(None, sr1.end_release_time) == 1000 + assert np.all(sr1._release_ts.data == np.linspace(0,1000, len(sr1._release_ts.data))) + assert sr1._mass_per_le == 2.5 + assert np.isclose(sum(sr1.weights),1) + + #No end_release time. Timeseries must be 2 entries, 1 second apart + + sr2.prepare_for_model_run(900) + assert len(sr2._release_ts.data) == 2 + assert sr2._release_ts.at(None, sr2.release_time) == 1000 + assert sr2._release_ts.at(None, sr2.release_time - timedelta(seconds=1)) == 1000 + assert sr2._release_ts.at(None, sr2.release_time + timedelta(seconds=1)) == 1000 + assert sr2._release_ts.at(None, sr2.release_time + timedelta(seconds=2)) == 1000 + assert np.all(sr2._release_ts.data == np.linspace(1000,1000, len(sr2._release_ts.data))) + assert sr2._mass_per_le == 0 + assert np.isclose(sum(sr2.weights),1) + + def test_rewind(self, sr1): + sr1.prepare_for_model_run(900) + assert sr1._prepared == True + assert sr1._release_ts is not None + sr1.rewind() + assert sr1._prepared == False + assert sr1._release_ts is None + + def test__eq__(self, sr1, sr2): + assert sr1 != sr2 + assert sr1 == sr1 + + def test_serialization(self, sr1): + ser = sr1.serialize() + deser = SpatialRelease.deserialize(ser) + assert deser == sr1 + + sr1.prepare_for_model_run(900) + ser = sr1.serialize() + deser = SpatialRelease.deserialize(ser) + assert deser == sr1 def test_release_from_splot_data(): ''' @@ -266,15 +357,12 @@ def test_release_from_splot_data(): exp = np.asarray((44.909252, 44.909252, 30.546749), dtype=int) - exp_num_elems = exp.sum() rel = release_from_splot_data(datetime(2015, 1, 1), td_file) - assert rel.num_elements == exp_num_elems - assert len(rel.start_position) == exp_num_elems cumsum = np.cumsum(exp) for ix in xrange(len(cumsum) - 1): - assert np.all(rel.start_position[cumsum[ix]] == - rel.start_position[cumsum[ix]:cumsum[ix + 1]]) - assert np.all(rel.start_position[0] == rel.start_position[:cumsum[0]]) + assert np.all(rel.custom_positions[cumsum[ix]] == + rel.custom_positions[cumsum[ix]:cumsum[ix + 1]]) + assert np.all(rel.custom_positions[0] == rel.custom_positions[:cumsum[0]]) os.remove(td_file) diff --git a/py_gnome/tests/unit_tests/test_spill/test_spatial_release.py b/py_gnome/tests/unit_tests/test_spill/test_spatial_release.py new file mode 100644 index 000000000..9ca40402b --- /dev/null +++ b/py_gnome/tests/unit_tests/test_spill/test_spatial_release.py @@ -0,0 +1,52 @@ +""" +tests for the spatial release from polygons: + +e.g. from the NESDIS MPSR reports +""" +from __future__ import print_function + +import os +import numpy as np + +from gnome.spill.release import SpatialRelease + +data_dir = os.path.join(os.path.split(__file__)[0], "data_for_tests") + +sample_shapefile = os.path.join(data_dir, "NESDIS_files.zip") + + +def check_valid_polygon(poly): + """ + checks that a shapely Polygon object at least has valid values for coordinates + """ + for point in poly.exterior.coords: + assert -360 < point[0] < 360 + assert -90 < point[0] < 90 + + +def test_load_shapefile(): + (all_oil_polys, + all_oil_weights, + all_oil_thicknesses) = SpatialRelease.load_shapefile(sample_shapefile) + + assert len(all_oil_polys) == 8 + assert len(all_oil_weights) == 8 + assert len(all_oil_thicknesses) == 8 + + for poly in all_oil_polys: + check_valid_polygon(poly) + + # NOTE: these values are pulled from running the code + # they may not be correct, but this will let us catch changes + assert np.allclose(all_oil_weights, [0.0019291097691711862, 0.0018247639782104231, + 0.09568991387647877, 0.00017874329138003873, + 9.309062636361091e-05, 0.005663950543120452, + 0.001098505440460224, 0.8935219224748153 + ], rtol=1e-12) + + assert np.allclose(all_oil_thicknesses, [5e-06, 5e-06, 5e-06, 5e-06, + 5e-06, 5e-06, 5e-06, 0.0002 + ], rtol=1e-12) + + + diff --git a/py_gnome/tests/unit_tests/test_spill/test_spill.py b/py_gnome/tests/unit_tests/test_spill/test_spill.py index 6bdd8ee1a..318c92e30 100644 --- a/py_gnome/tests/unit_tests/test_spill/test_spill.py +++ b/py_gnome/tests/unit_tests/test_spill/test_spill.py @@ -7,20 +7,25 @@ import pytest + @pytest.fixture('function') def sp(): return Spill() + rel_time = datetime(2014, 1, 1, 0, 0) end_rel_time = rel_time + timedelta(seconds=9000) pos = (0, 1, 2) -end_release_pos = (1,2,3) +end_release_pos = (1, 2, 3) + + @pytest.fixture('function') def inst_point_spill(): release = PointLineRelease(rel_time, pos) return Spill(release=release, amount=5000) + @pytest.fixture('function') def inst_point_line_spill(): release = PointLineRelease(rel_time, @@ -29,6 +34,7 @@ def inst_point_line_spill(): return Spill(release=release, amount=5000) + @pytest.fixture('function') def cont_point_spill(): release = PointLineRelease(rel_time, @@ -37,6 +43,7 @@ def cont_point_spill(): return Spill(release=release, amount=5000) + @pytest.fixture('function') def cont_point_line_spill(): release = PointLineRelease(rel_time, @@ -46,6 +53,7 @@ def cont_point_line_spill(): return Spill(release=release, amount=5000) + @pytest.fixture('function') def cont_point_spill_le_per_ts(): release = PointLineRelease(rel_time, @@ -62,9 +70,9 @@ class TestSpill(object): def test__init(self): sp = Spill() - #assert default construction assert sp.substance and isinstance(sp.substance, NonWeatheringSubstance) - assert sp.release and isinstance(sp.release, PointLineRelease) + assert sp.release and isinstance(sp.release, PointLineRelease) + def test_num_per_timestep_release_elements(self): 'release elements in the context of a spill container' @@ -85,7 +93,7 @@ def test_num_per_timestep_release_elements(self): to_rel = sp.release_elements(sc, model_time, 900) if model_time < sp.end_release_time: assert to_rel == 250 - assert len(sc['spill_num']) == min((ix+1) * 250, 1000) + assert len(sc['spill_num']) == min((ix + 1) * 250, 1000) else: assert to_rel == 0 @@ -109,7 +117,9 @@ def test_units(self, sp): with pytest.raises(ValueError): sp.units = 'inches' - #These are for when SpillContainer is removed + + # These are for when SpillContainer is removed + # NOTE: you can not parametrize on fixtures like this @pytest.mark.xfail() @pytest.mark.parametrize('spill', [inst_point_spill, inst_point_line_spill, diff --git a/py_gnome/tests/unit_tests/test_weatherers/test_oil.py b/py_gnome/tests/unit_tests/test_weatherers/test_oil.py new file mode 100644 index 000000000..84b786ce8 --- /dev/null +++ b/py_gnome/tests/unit_tests/test_weatherers/test_oil.py @@ -0,0 +1,129 @@ +""" +tests for the GNOME Oil object + +WARNING: very incomplete! +""" + +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +import pytest +import numpy as np + +from gnome.weatherers.oil import Oil + +# NOTE: maybe better to have test oils defined here? +from gnome.spill.sample_oils import _sample_oils + +@pytest.fixture +def empty_oil(): + return Oil(name='empty_oil', + api=None, + pour_point=None, + solubility=None, # kg/m^3 + # emulsification properties + bullwinkle_fraction=None, + bullwinkle_time=None, + emulsion_water_fraction_max=None, + densities=None, + density_ref_temps=None, + density_weathering=None, + kvis=[], + kvis_ref_temps=[], + kvis_weathering=[], + # PCs: + mass_fraction=[], + boiling_point=[], + molecular_weight=[], + component_density=[], + sara_type=[], + flash_point=[], + adios_oil_id='' + ) + +# from crude sample: + # 'kvis': [0.0005, 0.0006, 8.3e-05, 8.53e-05], + # 'kvis_ref_temps': [273.0, 288.0, 293.0, 311.0], + +def test_can_init(): + """ + can we initialize from a complete sample + """ + + oil = Oil(**_sample_oils['oil_crude']) + + print(oil) + + +def test_kvis_at_temp_single(empty_oil): + oil = empty_oil + + oil.kvis = [0.0006] + oil.kvis_ref_temps = [288.0] + oil.kvis_weathering = [0.0] + + kv = oil.kvis_at_temp(288.0) + + print(kv) + + assert kv == 0.0006 + + +def test_kvis_at_temp_single_range(empty_oil): + """ + test using the default value for the decay constant + + hand calculated, based on decay constant of 2100K + value at 273C: 2.0213282964723807e-05 + value at 300C: 1.0115129451432752e-05 + """ + oil = empty_oil + + oil.kvis = [1.32e-05] + oil.kvis_ref_temps = [289.01] + oil.kvis_weathering = [0.0] + + kvis = oil.kvis_at_temp([273, 300]) + + print(kvis) + + assert np.allclose(kvis, [2.021328e-05, 1.011512e-05], rtol=1e-4) + + +def test_kvis_at_temp_two(empty_oil): + oil = empty_oil + + oil.kvis = [0.0006, 8.3e-05] + oil.kvis_ref_temps = [288.0, 293.0] + oil.kvis_weathering = [0.0, 0.0] + + assert np.allclose(oil.kvis_at_temp(288.0), 0.0006, rtol=1e-8) + assert np.allclose(oil.kvis_at_temp(293.0), 8.3e-05, rtol=1e-8) + + +def test_kvis_at_temp_six(empty_oil): + """ + Test the least squared fit to six points + + data from HOOPS BLEND, ExxonMobil + """ + oil = empty_oil + + oil.kvis = np.array([19.6, 13.2, 9.85, 9.39, 6.99, 6.62]) * 1e-6 + oil.kvis_ref_temps = np.array([4.85, 15.85, 24.85, 26.85, 37.85, 39.85]) + 273.16 + oil.kvis_weathering = [0.0] * len(oil.kvis) + + # assert oil.kvis_at_temp(288.0) == 0.0006 + # assert oil.kvis_at_temp(293.0) == 8.3e-05 + + kvis = oil.kvis_at_temp(oil.kvis_ref_temps) + + print(oil.kvis) + print(kvis) + + assert np.allclose(kvis, oil.kvis, rtol=5e-2) + + +