compensates for delta, even if not perfect
[notebooks.git] / fmda_kf_local.ipynb
blob829e2b4810037f770560f29f3b88479a8af88bee
2   "nbformat": 4,
3   "nbformat_minor": 0,
4   "metadata": {
5     "colab": {
6       "name": "fmda_kf_local.ipynb",
7       "provenance": [],
8       "collapsed_sections": []
9     },
10     "kernelspec": {
11       "display_name": "Python 3",
12       "language": "python",
13       "name": "python3"
14     },
15     "language_info": {
16       "codemirror_mode": {
17         "name": "ipython",
18         "version": 3
19       },
20       "file_extension": ".py",
21       "mimetype": "text/x-python",
22       "name": "python",
23       "nbconvert_exporter": "python",
24       "pygments_lexer": "ipython3",
25       "version": "3.8.5"
26     }
27   },
28   "cells": [
29     {
30       "cell_type": "markdown",
31       "metadata": {
32         "id": "khOTxJsYc91W"
33       },
34       "source": [
35         "# Kalman Filtering for Fuel Moisture\n",
36         "## Jan Mandel, University of Colorado Denver\n",
37         "### Instructor for MATH 4779 - Math Clinic\n",
38         "### December 2021"
39       ]
40     },
41     {
42       "cell_type": "markdown",
43       "metadata": {
44         "id": "sXaqfI-EdCEk"
45       },
46       "source": [
47         "## Abstract"
48       ]
49     },
50     {
51       "cell_type": "markdown",
52       "metadata": {
53         "id": "ZbtSiYm4dF7B"
54       },
55       "source": [
56         "''Fuel moisture is an important factor of the spread of wildland fires. Some weather stations have fuel moisture sensors and data are available online. We review a simple model of fuel moisture from atmospheric conditions, and show how to adjust the model using the weather station data."
57       ]
58     },
59     {
60       "cell_type": "markdown",
61       "metadata": {
62         "id": "eZ6dfHlZ63j1"
63       },
64       "source": [
65         "## Table of contents"
66       ]
67     },
68     {
69       "cell_type": "markdown",
70       "metadata": {
71         "id": "WHIgN2uZ689b"
72       },
73       "source": [
74         "1 Introduction\n",
75         "\n",
76         "2 Background\n",
77         "\n",
78         "2.1 Imports\n",
79         "\n",
80         "2.2 Kalman filter\n",
81         "\n",
82         "2.2.1 Overview\n",
83         "\n",
84         "2.2.2 Formulation\n",
85         "\n",
86         "2.2.3 A Kalman filter tester\n",
87         "\n",
88         "2.3 Fuel moisture model\n",
89         "\n",
90         "2.3.1 A simple time lag model\n",
91         "\n",
92         "2.3.1 Fuel moisture model with drying equilibrium, wetting equilibrium, and rain\n",
93         "\n",
94         "3 Methods\n",
95         "\n",
96         "3.1 Kalman filter demonstration on the simple model\n",
97         "\n",
98         "3.1.1 Creating synthetic data\n",
99         "\n",
100         "3.1.2 Running the Kalman filter\n",
101         "\n",
102         "3.2 Acquisition and preprocessing of real data\n",
103         "\n",
104         "3.2.1 Acquisition of fuel moisture observations\n",
105         "\n",
106         "3.2.2 Acquisition of weather data\n",
107         "\n",
108         "3.2.3 Preprocessing and visualization of the weather data\n",
109         "\n",
110         "4 Results\n",
111         "\n",
112         "4.1 Kalman filter with fuel moisture observations, followed by forecasting\n",
113         "\n",
114         "4.2 Model with an augmented state\n",
115         "\n",
116         "4.3 Kalman filter on the augmented model\n",
117         "\n",
118         "4.4 A comment on the information flow in the Kalman filter and in neural networks\n",
119         "\n",
120         "5. Conclusion\n",
121         "\n",
122         "Contributions of Authors\n",
123         "\n",
124         "Acknowledgements\n",
125         "\n",
126         "References\n",
127         "\n",
128         "\n",
129         "\n",
130         "\n",
131         "\n",
132         "\n",
133         "\n"
134       ]
135     },
136     {
137       "cell_type": "markdown",
138       "metadata": {
139         "id": "ZFafUPCTO1N1"
140       },
141       "source": [
142         "## 1 Introduction"
143       ]
144     },
145     {
146       "cell_type": "markdown",
147       "metadata": {
148         "id": "4_RcdWybPFks"
149       },
150       "source": [
151         "The Kalman filter is at the foundation of many technologies in daily use, from GPS to weather forecasting. No model is completely accurate. Think space navigation: the movement of a Apollo 13 between the moon and the earth, subject to gravitational forces and propulsion, with the position ascertained by visual measurements. No matter how accurate the model of spacecraft motion is, the measurements are always burdened with noise. The idea of Kalman filter is to evolve a quantification of the of the state (here, positin and velocity of the spacecraft) in the form of a covariance matrix, and, using an estimate of the uncertainty of the data, adjust the state to split the difference every time measurements are taken. \n",
152         "\n",
153         "Here, we use the Kalman filter to estimate the evolution of fuel (dead wood) moisture content from a simple theoretical model, adjusting the state of the model hourly for measurements from fuel moisture a sensor in a wood stick exposed to the elements. This is needed for forecasting of wildfire progress; for this purpose, we also want to have the filter adjust the model from the data, so that it gives more accurate data for future when we only have hourly weather forecast but no actual data - because the future has not happened yet. "
154       ]
155     },
156     {
157       "cell_type": "markdown",
158       "metadata": {
159         "id": "M2kbwDPBTB7A"
160       },
161       "source": [
162         "## 2 Background"
163       ]
164     },
165     {
166       "cell_type": "markdown",
167       "metadata": {
168         "id": "ar1BbXac49hO"
169       },
170       "source": [
171         "In this section, we take care of preliminaries: we install some packages we need, and then proceed with the Kalman filter."
172       ]
173     },
174     {
175       "cell_type": "markdown",
176       "metadata": {
177         "id": "_5F5CuRqc91X"
178       },
179       "source": [
180         "### 2.1 Imports"
181       ]
182     },
183     {
184       "cell_type": "markdown",
185       "metadata": {
186         "id": "K6sWUMf0c91Y"
187       },
188       "source": [
189         "We may need the pygrib package to read weather data, but pygrib requires current numpy while google colab is using an old numpy version for compatibility with tensorflow. We will upgrade numpy and restart the runtime then the notebook will need to be run again. If numpy is current, we just download and import packages we need."
190       ]
191     },
192     {
193       "cell_type": "markdown",
194       "metadata": {
195         "id": "X9rvlymMZdJg"
196       },
197       "source": [
198         "### 2.2 Kalman filter"
199       ]
200     },
201     {
202       "cell_type": "markdown",
203       "metadata": {
204         "id": "x5E2UE3F5gf2"
205       },
206       "source": [
207         "#### 2.2.1 Overview"
208       ]
209     },
210     {
211       "cell_type": "markdown",
212       "metadata": {
213         "id": "NPgTHlCLAlA-"
214       },
215       "source": [
216         "The Kalman filter provides an estimate $u$ of the time evolution of some unknown process, called \"nature\" or \"truth\". We do not know with certainty what the nature is, but we can observe it at regular intervals (steps) with some error. In each step, model $F$ advances the model state $u$ in time, $ u \\leftarrow F(u)$, and attempts to reconcile the state with an observation $d$ of the true state, so $u \\approx d$. The filter modifies the model state $u$ to balance the uncertainty in the model and the data (this is called *analysis*) and the cycle continues. For that purpose, the filter evolves also an estimate of the uncertainly of the model.\n",
217         "\n",
218         "More generally, instead of $u \\approx d$, only a part of the state is observed, and $Hu \\approx d$ where $H$ is a matrix, or observation function. Basically, $Hu$ is what the data would be if the model was completely accurate. \n",
219         "\n",
220         "See Kalman (1960) for the original publication, Kalnay (2003) for a gentle introduction, and the [Wikipedia article](https://en.wikipedia.org/wiki/Extended_Kalman_filter)."
221       ]
222     },
223     {
224       "cell_type": "markdown",
225       "metadata": {
226         "id": "y6j34L5s5pEL"
227       },
228       "source": [
229         "#### 2.2.2 Formulation\n",
230         "\n",
231         "---\n",
232         "\n"
233       ]
234     },
235     {
236       "cell_type": "markdown",
237       "metadata": {
238         "id": "b3GZW5vP5_o8"
239       },
240       "source": [
241         "We present the Kalman filter in perhaps the most used form, as extended to nonlinear models.\n",
242         " Consider a discrete time model of some natural\n",
243         "process. At time step $k$, the model has state $u_{k}\\in\\mathbb{R}^{n}$, which\n",
244         "can be approximated from the previous step $u_{k-1}$ by applying the model\n",
245         "$\\mathcal{M}$ to get a forecast $u_{k}^{f}=\\mathcal{M}\\left(  u_{k-1}\\right)\n",
246         "$. We model uncertainty in the model itself by adding normally distributed\n",
247         "noise with mean zero and covariance $Q$ to the uncertainty of $u_{k}^{f}$. We\n",
248         "also need to estimate now the uncertainty in the previous state $u_{k-1}$\n",
249         "propagates to the uncertainty of the forecast $u_{k}^{f}$. So, assume that the\n",
250         "model is differentiable and quantify the uncertainty of the state by a\n",
251         "covariance matrix. That is,  assume that at step $k-1$, the state has\n",
252         "(approximately) normal distribution with mean $u_{k-1}$ and covariance\n",
253         "$P_{k-1}$. Using the Taylor expansion of order $1$ of the model operator at\n",
254         "$u_{k-1}$, $\\mathcal{M}\\left(  u\\right)  \\approx\\mathcal{M}\\left(\n",
255         "u_{k-1}\\right)  +\\mathcal{M}^{\\prime}\\left(  u_{k-1}\\right)  \\left(\n",
256         "u-u_{k-1}\\right)  $, where $\\mathcal{M}^{\\prime}\\left(  u_{k-1}\\right)  $ is\n",
257         "the Jacobian matrix of $\\mathcal{M}$ at $u_{k-1}$. It can be shown that the\n",
258         "forecast has then (approximately)\\ normal distribution with mean and\n",
259         "covariance\n",
260         "$$\n",
261         "u_{k}^{f}=\\mathcal{M}\\left(  u_{k-1}\\right)  ,\\ P_{k}^{f}=\\mathcal{M}\\left(\n",
262         "u_{k-1}\\right)  P_{k-1}\\mathcal{M}^{\\prime}\\left(  u_{k-1}\\right)  +Q\n",
263         "$$\n",
264         "At time $k$, we also have an observation $d_{k}\\approx Hu_{k}$, where $H$ is a\n",
265         "given observation operator, and we want to find $u_{k}$ so that both\n",
266         "$$\n",
267         "u_{k}\\approx u_{k}^{f}\\text{ and }d_{k}\\approx Hu_{k}.\n",
268         "$$\n",
269         "We quantify the uncertainly of the error of observation $d_{k}$ by a covariance\n",
270         "matrix $R$: assume that the observation error has normal probability\n",
271         "distribution with a known covariance $R$. Then, the likelihood of state $u$ is\n",
272         "proportional to $e^{-\\left\\Vert d_{k}-Hu\\right\\Vert _{R^{-1}}^{2}/2}$, where\n",
273         "we used the notation for the norm $\\left\\Vert v\\right\\Vert _{A}%\n",
274         "=\\left(v^{\\top}Av\\right)^{1/2}$ induced by a positive definite matrix $A$. Similarly, we quantify the\n",
275         "uncertainty of the state by a covariance matrix $P_{k}$. That is, the forecast\n",
276         "state has (approximately) normal distribution with mean $u_{k}^{f}$  and covariance\n",
277         "$P_{k}^{f}$. From the Bayes theorem of statistics, the probability distribution\n",
278         "of the state after taking the data into account has density\n",
279         "$$\n",
280         "p_{k}\\left(  u\\right) \\propto e^\\frac{-\\left\\Vert d_{k}\n",
281         "-Hu\\right\\Vert_{R^{-1}}^{2}}{2}e^\\frac{-\\left\\Vert u-u_{k}^{f}\\right\\Vert _{\n",
282         "{P_{k}^f}^{-1}  }^{2}}{2}%\n",
283         "$$\n",
284         "where $\\propto$ means proportional.\n",
285         "Note that the probability density at $u$ is maximal when $\\left\\Vert\n",
286         "d_{k}-Hu\\right\\Vert _{R^{-1}}^{2}+\\left\\Vert u-u_{k}\\right\\Vert _{{P_{k}^{f}}^{-1}}^{2}$\n",
287         " is minimal, which quantifies the statement that $d_{k}\\approx\n",
288         "Hu_{k}$ and $u\\approx u_{k}^{f}$.  By a direct computation completing the\n",
289         "square and using the Sherman-Morrison-Woodbury formula, \n",
290         "$$p_{k}\\left(\n",
291         "\t\tu\n",
292         "\t   \\right) \\propto \n",
293         "e^{-\\frac{\n",
294         "\t\\left\\Vert u-u_{k\n",
295         "\t         }\n",
296         "\t\\right\\Vert_\n",
297         "\t\t{P_{k\n",
298         "\t\t      }^{-1}\n",
299         "\t\t}^{2}\n",
300         "\t}\n",
301         "\t{2}},\n",
302         "$$ \n",
303         "which is the density of the normal distribution with the mean\n",
304         "$$\n",
305         "u_{k}^{f}=u_{k}^{f}+K_{k}(d-Hu_{k}^{f}),\\ \\text{where }K_{k}=P_{k}%\n",
306         "^{f}H^{\\mathrm{T}}(HP_{k}^{f}H^{\\mathrm{T}}+R)^{-1}%\n",
307         "$$\n",
308         "and covariance\n",
309         "$$\n",
310         "P_{k}=\\left(  \\left(  P_{k}^{f}\\right)  ^{-1}+H^{\\mathrm{T}}R^{-1}H\\right)\n",
311         "^{-1}=(I-KH)P_{k}^{f}.\n",
312         "$$\n",
313         "\n",
314         "These are the equations of the extended Kalman filter. The original Kalman (1960) filter was\n",
315         "formulated for a linear process. The extension to the\n",
316         "nonlinear case made broad array of applications possible, including the Apollo spacecraft naviation (McGee and Schmidt, 1966),  and is\n",
317         "still a de-facto standard in navigation and GPS.\n"
318       ]
319     },
320     {
321       "cell_type": "code",
322       "metadata": {
323         "id": "-bvUtJ_OLwQA"
324       },
325       "source": [
326         "import numpy as np\n",
327         "def ext_kf(u,P,F,Q=0,d=None,H=None,R=None):\n",
328         "  \"\"\"\n",
329         "  One step of the extended Kalman filter. \n",
330         "  If there is no data, only advance in time.\n",
331         "  :param u:   the state vector, shape n\n",
332         "  :param P:   the state covariance, shape (n,n)\n",
333         "  :param F:   the model function, args vector u, returns F(u) and Jacobian J(u)\n",
334         "  :param Q:   the process model noise covariance, shape (n,n)\n",
335         "  :param d:   data vector, shape (m). If none, only advance in time\n",
336         "  :param H:   observation matrix, shape (m,n)\n",
337         "  :param R:   data error covariance, shape (n,n)\n",
338         "  :return ua: the analysis state vector, shape (n)\n",
339         "  :return Pa: the analysis covariance matrix, shape (n,n)\n",
340         "  \"\"\"\n",
341         "  def d2(a):\n",
342         "    return np.atleast_2d(a) # convert to at least 2d array\n",
343         "\n",
344         "  def d1(a):\n",
345         "    return np.atleast_1d(a) # convert to at least 1d array\n",
346         "\n",
347         "  # forecast\n",
348         "  uf, J  = F(u)          # advance the model state in time and get the Jacobian\n",
349         "  uf = d1(uf)            # if scalar, make state a 1D array\n",
350         "  J = d2(J)              # if scalar, make jacobian a 2D array\n",
351         "  P = d2(P)              # if scalar, make Jacobian as 2D array\n",
352         "  Pf  = d2(J.T @ P) @ J + Q  # advance the state covariance Pf = J' * P * J + Q\n",
353         "  # analysis\n",
354         "  if d is None or not d.size :  # no data, no analysis\n",
355         "    return uf, Pf\n",
356         "  # K = P H' * inverse(H * P * H' + R) = (inverse(H * P * H' + R)*(H P))'\n",
357         "  H = d2(H)\n",
358         "  HP  = d2(H @ P)            # precompute a part used twice  \n",
359         "  K   = d2(np.linalg.solve( d2(HP @ H.T) + R, HP)).T  # Kalman gain\n",
360         "  # print('H',H)\n",
361         "  # print('K',K)\n",
362         "  res = d1(H @ d1(uf) - d)          # res = H*uf - d\n",
363         "  ua = uf - K @ res # analysis mean uf - K*res\n",
364         "  Pa = Pf - K @ d2(H @ P)        # analysis covariance\n",
365         "  return ua, d2(Pa)\n"
366       ],
367       "execution_count": null,
368       "outputs": []
369     },
370     {
371       "cell_type": "markdown",
372       "metadata": {
373         "id": "Uvsbbv2XZ2Hd"
374       },
375       "source": [
376         "#### 2.2.3 A Kalman filter tester"
377       ]
378     },
379     {
380       "cell_type": "markdown",
381       "metadata": {
382         "id": "gcmGBqPOU1e5"
383       },
384       "source": [
385         "It is a very good idea to make write a simple tester for every piece of code. How else would we know it actually works, and that something basic did not get broken inadvertently, perhaps as a side effect of changing something else? A simple tester may save a great deal of time trying to debug cryptic errors later. And, what better place for a tester that right after the code it is testing so that it gets run every time?"
386       ]
387     },
388     {
389       "cell_type": "code",
390       "metadata": {
391         "id": "OsOqvQk6ZXZV"
392       },
393       "source": [
394         "# a basic ext_kf test\n",
395         "import numpy as np\n",
396         "u = [1,\n",
397         "     2]\n",
398         "P = [[2 , -1],\n",
399         "    [-1 , 2]]\n",
400         "A = [ [1 ,2],\n",
401         "      [3 ,4]]\n",
402         "u = np.array(u)      \n",
403         "Q = np.array([[1,0],[0,1]])\n",
404         "A = np.array(A)\n",
405         "def fun(u):\n",
406         "  return A @ u, A\n",
407         "F = lambda u: fun(u)\n",
408         "H = [[1, 0],\n",
409         "     [0, 1]]\n",
410         "d = [2,\n",
411         "    3]\n",
412         "R = [[2, 0],\n",
413         "    [0, 2]]\n",
414         "H = np.array(H)      \n",
415         "d = np.array(d)\n",
416         "R = np.array(R)\n",
417         "ua,Pa = ext_kf(u,P,F,Q)\n",
418         "print('ua=',ua)\n",
419         "print('Pa=',Pa)\n",
420         "ua,Pa = ext_kf(u,P,F,Q,d,H,R)\n",
421         "print('ua=',ua)\n",
422         "print('Pa=',Pa)\n"
423       ],
424       "execution_count": null,
425       "outputs": []
426     },
427     {
428       "cell_type": "markdown",
429       "metadata": {
430         "id": "A9ZpmNcdRpmp"
431       },
432       "source": [
433         "### 2.3  Fuel moisture models\n",
434         "\n",
435         "\n"
436       ]
437     },
438     {
439       "cell_type": "markdown",
440       "metadata": {
441         "id": "eZL8gN7ISGVh"
442       },
443       "source": [
444         "#### 2.3.1 A simple fuel moisture model"
445       ]
446     },
447     {
448       "cell_type": "markdown",
449       "metadata": {
450         "id": "1XvOC4kYSQgH"
451       },
452       "source": [
453         "First consider a simplified fuel moisture model without considering the effect of rain.\n",
454         "The evolution of fuel moisture content $m(t)$ is modeled by the time-lag differential equation on interval $\\left[\n",
455         "t_{0},t_{1}\\right]  $,\n",
456         "$$\n",
457         "\\frac{dm}{dt}=\\frac{E-m(t)}{T},\\quad m(t_{0})=m_{0}.\n",
458         "$$\n",
459         "where the initial fuel moisture content $m_{0}=m\\left(  t_{0}\\right)  $ is the\n",
460         "input, and $m_{1}=m(t_{1})$ is the output. Tnus, $m_1=F(m_0)$. The parameters of the model are the\n",
461         "fuel moisture equilibrium $E$, assumed to be constant over the interval $\\left[\n",
462         "t_{0},t_{1}\\right]  $, NS the characteristic decay time $T$. \n",
463         "\n",
464         "We can build the general model later by calling this simple model with different\n",
465         "equilibria and time constants (drying, wetting, rain).\n",
466         "\n",
467         "Since $E$ is constant in time, the solution can be found\n",
468         "analytically,\n",
469         "$$\n",
470         "m\\left(  t\\right)  =E+\\left(  m_{0}-E\\right)  e^{-t/T}%\n",
471         "$$\n",
472         "For convenience, we use $T_{1}=1/T$ instead of $T$, and the model becomes\n",
473         "$$\n",
474         "m_{1}=E+\\left(  m_{0}-E\\right)  e^{-\\left(  t_{1}-t_{0}\\right)  T_{1}}%\n",
475         "$$\n",
476         "In the extended Kalman filter, we will need the partial derivatives of $m_{1}$\n",
477         "with respect to the input and the parameters. Compute\n",
478         "$$\n",
479         "\\frac{dm_{1}}{d_{m0}}=e^{-\\left(  t_{1}-t_{0}\\right)  T_{1}}\n",
480         "$$\n",
481         "$$\n",
482         "\\frac{dm_{1}}{dE}=1-e^{-\\left(  t_{1}-t_{0}\\right)  T_{1}}\n",
483         "$$\n",
484         "$$\n",
485         "\\frac{dm_{1}}{dT_{1}}=-\\left(  m_{0}-E\\right)  \\left(  t_{1}-t_{0}\\right)\n",
486         "e^{-\\left(  t_{1}-t_{0}\\right)  T_{1}}\n",
487         "$$\n",
488         "At the moment, we need only ${dm_{1}}/{dm_{0}}$ but we put in the code all partials for possible use in future.\n"
489       ]
490     },
491     {
492       "cell_type": "code",
493       "metadata": {
494         "id": "wuVIAGLiSeR8"
495       },
496       "source": [
497         "import numpy as np\n",
498         "def model_decay(m0,E,partials=0,T1=0.1,tlen=1):  \n",
499         "  # Arguments: \n",
500         "  #   m0          fuel moisture content at start dimensionless, unit (1)\n",
501         "  #   E           fuel moisture eqilibrium (1)\n",
502         "  #   partials=0: return m1 = fuel moisture contents after time tlen (1)\n",
503         "  #           =1: return m1, dm0/dm0 \n",
504         "  #           =2: return m1, dm1/dm0, dm1/dE\n",
505         "  #           =3: return m1, dm1/dm0, dm1/dE dm1/dT1   \n",
506         "  #   T1          1/T, where T is the time constant approaching the equilibrium\n",
507         "  #               default 0.1/hour\n",
508         "  #   tlen        the time interval length, default 1 hour\n",
509         "\n",
510         "  exp_t = np.exp(-tlen*T1)                  # compute this subexpression only once\n",
511         "  m1 = E + (m0 - E)*exp_t                   # the solution at end\n",
512         "  if partials==0:\n",
513         "    return m1\n",
514         "  dm1_dm0 = exp_t\n",
515         "  if partials==1:\n",
516         "    return m1, dm1_dm0          # return value and Jacobian\n",
517         "  dm1_dE = 1 - exp_t      \n",
518         "  if partials==2:\n",
519         "     return m1, dm1_dm0, dm1_dE \n",
520         "  dm1_dT1 = -(m0 - E)*tlen*exp_t            # partial derivative dm1 / dT1\n",
521         "  if partials==3:\n",
522         "    return m1, dm1_dm0, dm1_dE, dm1_dT1       # return value and all partial derivatives wrt m1 and parameters\n",
523         "  raise('Bad arg partials')\n",
524         "  "
525       ],
526       "execution_count": null,
527       "outputs": []
528     },
529     {
530       "cell_type": "markdown",
531       "metadata": {
532         "id": "dOARZlj-RUCi"
533       },
534       "source": [
535         "#### 2.3.2 Fuel moisture model with drying equilibrium, wetting equilibrium, and rain"
536       ]
537     },
538     {
539       "cell_type": "markdown",
540       "metadata": {
541         "id": "AJp6FTpTSx5B"
542       },
543       "source": [
544         "Here is a little more realistic fuel moisture model from Mandel et al. (2004). A rain-wetting lag time $t_{\\mathrm{r}}$ is reached for heavy rain only\n",
545         "asymptotically, when the rain intensity $r$ (mm/h) is\n",
546         "large:\n",
547         "$$\n",
548         "\\frac{\\mathrm{d}m}{\\mathrm{d}t}=\\frac{S-m}{t_{\\mathrm{r}}}\\left(1-\\exp\\left(-\\frac{r-r_0}{r_{\\mathrm{s}}}\n",
549         "\\right)  \\right),\\ \\text{if}\\ r>r_0, \n",
550         "$$\n",
551         "where $r_0$ is the threshold rain intensity below which no perceptible\n",
552         "wetting occurs, and $r_{\\mathrm{s}}$ is the saturation rain\n",
553         "intensity. At the saturation rain intensity, $1-1/e\\approx 0.63$ of\n",
554         "the maximal rain-wetting rate is achieved. For 10h fuel, the model takes $S=250\\,{\\%}$,\n",
555         "$t_{\\mathrm{r}}=14$h, $r_0=0.05$mm/h and\n",
556         "$r_{\\mathrm{s}}=8$mm/h. "
557       ]
558     },
559     {
560       "cell_type": "code",
561       "metadata": {
562         "id": "ITsKE0psRblG"
563       },
564       "source": [
565         "### Define model function with drying, wetting, and rain equilibria\n",
566         "\n",
567         "# Parameters\n",
568         "r0 = 0.05                                   # threshold rainfall [mm/h]\n",
569         "rs = 8.0                                    # saturation rain intensity [mm/h]\n",
570         "Tr = 14.0                                   # time constant for rain wetting model [h]\n",
571         "S = 250                                     # saturation intensity [dimensionless]\n",
572         "T = 10.0                                    # time constant for wetting/drying\n",
573         "\n",
574         "def model_moisture(m0,Eqd,Eqw,r,t,partials=0,T=10.0,tlen=1.0):\n",
575         "    # arguments:\n",
576         "    # m0         starting fuel moistureb (%s\n",
577         "    # Eqd        drying equilibrium      (%) \n",
578         "    # Eqw        wetting equilibrium     (%)\n",
579         "    # r          rain intensity          (mm/h)\n",
580         "    # t          time\n",
581         "    # partials = 0, 1, 2\n",
582         "    # returns: same as model_decay\n",
583         "    #   if partials==0: m1 = fuel moisture contents after time 1 hour\n",
584         "    #              ==1: m1, dm1/dm0 \n",
585         "    #              ==2: m1, dm1/dm0, dm1/dE  \n",
586         "    \n",
587         "    if r > r0:\n",
588         "        # print('raining')\n",
589         "        E = S\n",
590         "        T1 =  (1.0 - np.exp(- (r - r0) / rs)) / Tr\n",
591         "    elif m0 <= Eqw: \n",
592         "        # print('wetting')\n",
593         "        E=Eqw\n",
594         "        T1 = 1.0/T\n",
595         "    elif m0 >= Eqd:\n",
596         "        # print('drying')\n",
597         "        E=Eqd\n",
598         "        T1 = 1.0/T\n",
599         "    else: # no change'\n",
600         "        E = m0\n",
601         "        T1=0.0\n",
602         "    exp_t = np.exp(-tlen*T1)\n",
603         "    m1 = E + (m0 - E)*exp_t  \n",
604         "    dm1_dm0 = exp_t\n",
605         "    dm1_dE = 1 - exp_t\n",
606         "    #if t>=933 and t < 940:\n",
607         "    #  print('t,Eqw,Eqd,r,T1,E,m0,m1,dm1_dm0,dm1_dE',\n",
608         "    #        t,Eqw,Eqd,r,T1,E,m0,m1,dm1_dm0,dm1_dE)   \n",
609         "    if partials==0: \n",
610         "        return m1\n",
611         "    if partials==1:\n",
612         "        return m1, dm1_dm0\n",
613         "    if partials==2:\n",
614         "        return m1, dm1_dm0, dm1_dE\n",
615         "    raise('bad partials')"
616       ],
617       "execution_count": null,
618       "outputs": []
619     },
620     {
621       "cell_type": "markdown",
622       "metadata": {
623         "id": "JDLU3B_jV42l"
624       },
625       "source": [
626         "## 3. Methods"
627       ]
628     },
629     {
630       "cell_type": "markdown",
631       "metadata": {
632         "id": "hLPJT3FcA2a7"
633       },
634       "source": [
635         "### 3.1 Kalman filter demonstration on the simple model"
636       ]
637     },
638     {
639       "cell_type": "markdown",
640       "metadata": {
641         "id": "kIA3X8vluFdd"
642       },
643       "source": [
644         "We demonstrate the Kalman filter for this model on a simple artificial example. The model is solving the differential equation for one hour. The equilibrium $E$ is constant during the hour, but it changes over the day so that it is higher at night and lower during the day, with a 24-hour period.  First, we create the \"truth\" by choosing the equilibrium $E$ and solving the differential aquation every hour, with a small additive noise. The synthetic data is obtained as the values of the \"truth\", with random noise to simulate observation error."
645       ]
646     },
647     {
648       "cell_type": "markdown",
649       "metadata": {
650         "id": "bBv10PTiChhm"
651       },
652       "source": [
653         "#### 3.1.1 Creating synthetic data"
654       ]
655     },
656     {
657       "cell_type": "code",
658       "metadata": {
659         "id": "-_pz-wXnCMnP"
660       },
661       "source": [
662         "import numpy as np, random\n",
663         "days = 20       \n",
664         "hours = days*24\n",
665         "h2 = int(hours/2)\n",
666         "hour = range(hours)\n",
667         "day = np.array(range(hours))/24.\n",
668         "\n",
669         "# artificial equilibrium data\n",
670         "E = np.power(np.sin(np.pi*day),4) # diurnal curve\n",
671         "E = 0.05+0.25*E\n",
672         "# FMC free run\n",
673         "m_f = np.zeros(hours)\n",
674         "m_f[0] = 0.1         # initial FMC\n",
675         "for t in range(hours-1):\n",
676         "  m_f[t+1] = max(0.,model_decay(m_f[t],E[t])  + random.gauss(0,0.005) )\n",
677         "data = m_f + np.random.normal(loc=0,scale=0.02,size=hours)    \n",
678         "\n",
679         "%matplotlib inline\n",
680         "import matplotlib.pyplot as plt \n",
681         "# fig1, ax1 = plt.subplots()\n",
682         "\n",
683         "plt.figure(figsize=(16,4))\n",
684         "plt.plot(hour,E,linestyle='--',c='r',label='Equilibrium')\n",
685         "plt.plot(hour,m_f,linestyle='-',c='k',label='10-h fuel truth')\n",
686         "plt.scatter(hour[:h2],data[:h2],c='b',label='10-h fuel data')\n",
687         "plt.title('Kalman filter example on artificial data')\n",
688         "plt.xlabel('Time (hours)')\n",
689         "plt.ylabel('Fuel moisture content (%)')\n",
690         "plt.legend()\n",
691         " "
692       ],
693       "execution_count": null,
694       "outputs": []
695     },
696     {
697       "cell_type": "code",
698       "metadata": {
699         "id": "ECsvw2KJ7tAX"
700       },
701       "source": [
702         ""
703       ],
704       "execution_count": null,
705       "outputs": []
706     },
707     {
708       "cell_type": "markdown",
709       "metadata": {
710         "id": "z-3WLAEpD2yJ"
711       },
712       "source": [
713         "#### 3.1.2 Running the Kalman filter"
714       ]
715     },
716     {
717       "cell_type": "markdown",
718       "metadata": {
719         "id": "T4g-RrrYAlBD"
720       },
721       "source": [
722         "We have used the same code for model and for the truth, and run the Kalman filter for 10 days. The graph below shows that the model state was remarkably close to the truth, even if the model is fed only noisy observations. This is because the dynamics of the model and of the truth are the same. After 10 days, we let the model continue without any new data to simulate forecasting the future, and the agreement with the truth was still very good."
723       ]
724     },
725     {
726       "cell_type": "code",
727       "metadata": {
728         "id": "_-CjONZkD18n"
729       },
730       "source": [
731         "import numpy as np\n",
732         "import matplotlib.pyplot as plt \n",
733         "\n",
734         "def kf_example(DeltaE):\n",
735         "  h2 = int(hours/2)\n",
736         "  m = np.zeros(hours)\n",
737         "  m[0]=0.1             # background state  \n",
738         "  P = np.zeros(hours)\n",
739         "  P[0] = 0.03 # background state variance\n",
740         "  Q = np.array([0.02]) # process noise variance\n",
741         "  H = np.array([1.])   # all observed\n",
742         "  R = np.array([0.02]) # data variance\n",
743         "\n",
744         "  for t in range(h2):\n",
745         "    # use lambda construction to pass additional arguments to the model \n",
746         "    m[t+1],P[t+1] = ext_kf(m[t],P[t],lambda u: model_decay(u,E[t]+DeltaE,partials=1),Q,\n",
747         "                    d=data[t],H=H,R=R)\n",
748         "  for t in range(h2,hours - 1):\n",
749         "    m[t+1],P[t+1] = ext_kf(m[t],P[t],lambda u: model_decay(u,E[t]+DeltaE,partials=1))\n",
750         "  \n",
751         "  %matplotlib inline\n",
752         "  plt.figure(figsize=(16,4))\n",
753         "  plt.plot(hour,E,linestyle='--',c='r',label='Equilibrium')\n",
754         "  # print(len(hour),len(m_f))\n",
755         "  plt.plot(hour,m_f,linestyle='-',c='b',label='10-h fuel truth')\n",
756         "  plt.scatter(hour[:h2],data[:h2],c='b',label='10-h fuel data')\n",
757         "  plt.plot(hour[:h2],m[:h2],linestyle='-',c='k',label='filtered')\n",
758         "  plt.plot(hour[h2:hours],m[h2:hours],linestyle='-',c='r',label='forecast')\n",
759         "  plt.title('Kalman filtering and forecast on artificial data')\n",
760         "  plt.xlabel('Time (hours)') \n",
761         "  plt.ylabel('Fuel moisture content (%)')\n",
762         "  plt.legend()\n",
763         " \n",
764         "  return E, P"
765       ],
766       "execution_count": null,
767       "outputs": []
768     },
769     {
770       "cell_type": "code",
771       "metadata": {
772         "id": "d0EFhTPZAlBD",
773         "scrolled": true
774       },
775       "source": [
776         "DeltaE = 0.0          # bias\n",
777         "E, P = kf_example(DeltaE)"
778       ],
779       "execution_count": null,
780       "outputs": []
781     },
782     {
783       "cell_type": "markdown",
784       "metadata": {
785         "id": "vqyB2Yz3uCsD"
786       },
787       "source": [
788         "We have recovered the fuel moisture from data with random noise - we **filtered** the noise out. "
789       ]
790     },
791     {
792       "cell_type": "markdown",
793       "metadata": {
794         "id": "Dl7pBZ9B3Nox"
795       },
796       "source": [
797         "Let's have a look at the evolution of the filter's estimate of the variance $P$. A common problem with the Kalman filter is when the variance converges to zero over time, then, since the filter trusts the model too much, it ignores the observations. Of course, once we switch to forecasting mode, the variance is not of interest. We could keep evolving the variance to bridge over periods when there are no observations, but not in this simplified version."
798       ]
799     },
800     {
801       "cell_type": "code",
802       "metadata": {
803         "id": "wRJgbmGLc91g"
804       },
805       "source": [
806         "%matplotlib inline\n",
807         "plt.figure(figsize=(16,4))\n",
808         "plt.plot(P,linestyle='-',c='b',label='Estimated state variance P')\n",
809         "plt.title('Kalman filtering and forecast on artificial data')\n",
810         "plt.xlabel('Time (hours)') \n",
811         "plt.ylabel('Estimated variance of fuel moisture (%^2)')\n",
812         "plt.legend()"
813       ],
814       "execution_count": null,
815       "outputs": []
816     },
817     {
818       "cell_type": "markdown",
819       "metadata": {
820         "id": "Ccr-uKbmAlBE"
821       },
822       "source": [
823         "Now what if the model is wrong - different from nature? That is always so in reality. Now suppose that the model and the truth are not the same. That is always the case in reality.  Consider a simple case when the model thinks that the equilibrium $E$ is too high."
824       ]
825     },
826     {
827       "cell_type": "code",
828       "metadata": {
829         "id": "spMdGW8oAlBE"
830       },
831       "source": [
832         "DeltaE = -0.05\n",
833         "E, P = kf_example(DeltaE)  "
834       ],
835       "execution_count": null,
836       "outputs": []
837     },
838     {
839       "cell_type": "markdown",
840       "metadata": {
841         "id": "DQeF7J8T4j2i"
842       },
843       "source": [
844         "We have found a good estimate of the state $m$, while data is available. Also, the estimated state variance $P$ converges with time - we have *learned* the variance that balances the noise. But for forecasting fuel moisture, we need to continue the fuel moisture model into the future, and we can't have any measurements from future. We only have the equilibrium from weather forecast. And the forecast and the truth disagree - as soon as there is no data to attract the simulation, the model is doing its own thing."
845       ]
846     },
847     {
848       "cell_type": "markdown",
849       "metadata": {
850         "id": "6uXVJj9koGF2"
851       },
852       "source": [
853         "### 3.2 Acquisition and preprocessing of real data"
854       ]
855     },
856     {
857       "cell_type": "markdown",
858       "metadata": {
859         "id": "q3BpOBuzc91i"
860       },
861       "source": [
862         "Data assimilation for fuel moisture from Remote Automated Weather Stations (RAWS) was developed in Vejmelka et al. (2016). First, they use regression from all RAWS in a given area to extend the data spatially from RAWS to a grid in the whole area, then they run the extended Kalman filter at each grid node. Here, we are interested in a simplified problem: estimate future fuel moisture at a single RAWS location from weather data.  "
863       ]
864     },
865     {
866       "cell_type": "markdown",
867       "metadata": {
868         "id": "c8Y6bL1Yc91i"
869       },
870       "source": [
871         "#### 3.2.1 Acquisition of fuel moisture observations"
872       ]
873     },
874     {
875       "cell_type": "markdown",
876       "metadata": {
877         "id": "0CuXyWBFc91i"
878       },
879       "source": [
880         "We try to load the data from a saved file first. If that fails, retrieve the fuel moisture data from sensors on weather stations in the Mesowest network. Get all stations with fuel moisture data in a spatial box within one hour, then pick one station and retrieve the whole time series."
881       ]
882     },
883     {
884       "cell_type": "code",
885       "metadata": {
886         "id": "LFrlbbMmc91i"
887       },
888       "source": [
889         "import json\n",
890         "jfile = 'raws.json'; vars='fuel_moisture'\n",
891         "#jfile = 'raws2.json'; vars='fuel_moisture,precip_accum_one_hour'\n",
892         "def json_w(j,f):\n",
893         "  print('writing json file',f)\n",
894         "  json.dump(j,open(f,'w'),indent=4)\n",
895         "try:\n",
896         "    #! wget --no-clobber http://math.ucdenver.edu/~jmandel/data/math4779f21/raws.json\n",
897         "    j = json.load(open(jfile,'r'))\n",
898         "    print('loaded from ',jfile)\n",
899         "    # Take the first station in the boulding box that has data between time_start and time_s2.\n",
900         "    # Then retrieve data for that station between time_start and time_end\n",
901         "    time_start = j['time_start']      # start of data time series\n",
902         "    # time_s2    = j['time_s2']         # end of segment to read coordinates\n",
903         "    time_end  = j['time_end']         # end of data time series\n",
904         "    meso_ts  = j['meso_ts']           # get meso observations time series\n",
905         "    obs_lon =   j['obs_lon']          # where we retrieved observations\n",
906         "    obs_lat =   j['obs_lat']\n",
907         "except:\n",
908         "    print(\"can't read\",jfile,', creating')\n",
909         "    # set up bounds\n",
910         "    time_start = \"201806010800\"  # June 1 2018 08:00 in format yyyymmddHHMM\n",
911         "    time_s2    = \"201806010900\"  # June 1 2018 09:00 in format yyyymmddHHMM \n",
912         "    time_end   = \"201907200900\"  # June 20 2018 09:00 in format yyyymmddHHMM \n",
913         "    #time_start=  \"201810230100\"\n",
914         "    #time_s2=  \"201810230300\"\n",
915         "    #time_end  =  \"201806022300\"\n",
916         "    !pip install MesoPy\n",
917         "    from MesoPy import Meso\n",
918         "    bounding_box = \"-115, 38, -110, 40\"  # min longtitude, latitude\n",
919         "    meso_token=\"b40cb52cbdef43ef81329b84e8fd874f\"       # you should get your own if you do more of this\n",
920         "    m = Meso(meso_token)# create a Meso object\n",
921         "    print('reading MesoWest fuel moisture data')\n",
922         "    json_w(m.variables(),'variables.json')\n",
923         "    meso_obss = m.timeseries(time_start, time_s2, bbox=bounding_box, \n",
924         "                             showemptystations = '0', vars=vars)   # ask the object for data\n",
925         "    json_w(meso_obss,'meso_obss.json')                        \n",
926         "    # pick one station and retrieve the whole time series.\n",
927         "    station=meso_obss['STATION'][0]\n",
928         "    json_w(station,'station.json')\n",
929         "    lon,lat = (float(station['LONGITUDE']),float(station['LATITUDE']))\n",
930         "    print(station['NAME'],'station',station['STID'],'at',lon,lat)\n",
931         "    e = 0.01   # tolerance\n",
932         "    bb = '%s, %s, %s, %s' % (lon - e, lat - e, lon + e, lat + e)\n",
933         "    print('bounding box',bb)\n",
934         "    meso_ts = m.timeseries(time_start, time_end, bbox=bb, showemptystations = '0', vars=vars)   # ask the object for data\n",
935         "    json_w(meso_ts,'meso_ts.json')                        \n",
936         "    obs_lon, obs_lat = (lon, lat)   # remember station coordinates for later\n",
937         "    j={'time_start':time_start,'time_s2':time_s2,'time_end':time_end,\n",
938         "       'meso_ts':meso_ts,'obs_lon':obs_lon,'obs_lat':obs_lat}\n",
939         "    json_w(j,jfile)\n",
940         "    print('done')"
941       ],
942       "execution_count": null,
943       "outputs": []
944     },
945     {
946       "cell_type": "code",
947       "metadata": {
948         "id": "3bXopS3btyz0",
949         "scrolled": true
950       },
951       "source": [
952         "# process the data retrieved for this station\n",
953         "# print(json.dumps(meso_ts['STATION'][0], indent=4))\n",
954         "from datetime import datetime, timedelta, time\n",
955         "import numpy as np\n",
956         "import matplotlib.pyplot as plt\n",
957         "import pytz\n",
958         "station = meso_ts['STATION'][0]\n",
959         "time_str  = station['OBSERVATIONS']['date_time']\n",
960         "obs_time = [datetime.strptime(t, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=pytz.UTC) for t in time_str]\n",
961         "start_time = obs_time[0].replace(minute=0)     # remember obs_time and start_time for later\n",
962         "end_time = obs_time[-1]\n",
963         "obs_data = np.array(station['OBSERVATIONS'][\"fuel_moisture_set_1\"])\n",
964         "# display the data retrieved\n",
965         "#for o_time,o_data in zip (obs_time,obs_data):\n",
966         "#    print(o_time,o_data)\n",
967         "%matplotlib inline\n",
968         "plt.figure(figsize=(16,4))\n",
969         "plt.plot(obs_data,linestyle='-',c='k',label='10-h fuel data')\n",
970         "plt.title(station['STID'] + ' 10 h fuel moisture data')\n",
971         "plt.xlabel('Time (hours)') \n",
972         "plt.ylabel('Fuel moisture content (%)')\n",
973         "plt.legend()\n",
974         " "
975       ],
976       "execution_count": null,
977       "outputs": []
978     },
979     {
980       "cell_type": "markdown",
981       "metadata": {
982         "id": "pY4hPeATK9wZ"
983       },
984       "source": [
985         "#### 3.2.2 Acquisition of weather data"
986       ]
987     },
988     {
989       "cell_type": "markdown",
990       "metadata": {
991         "id": "xhyjXqxVN6B2"
992       },
993       "source": [
994         "Our weather data are results from atmospheric models, with assimilated observations from weather stations, satellites, radars, etc. The models can be run in reanalysis mode (for the past, with data for the period modeled)  or in forecast mode (for the future, with only past data assimilated - because future data are not here yet). We use the Real-Time Mesoscale Analysis ([RTMA](https://www.nco.ncep.noaa.gov/pmb/products/rtma/)) interpolated to the RAWS location. RTMA is a real-time product, posted hourly, and available only for few days in the past. We have our own collection of selected RAWS data over past few years, obtained as a side effect of running the fuel moisture modeling software [WRFXPY](https://github.com/openwfm/wrfxpy).\n",
995         "\n",
996         "First try to read the data already extracted for this RAWS and staged for download."
997       ]
998     },
999     {
1000       "cell_type": "code",
1001       "metadata": {
1002         "id": "WlqJRP8Vc91o"
1003       },
1004       "source": [
1005         "import json\n",
1006         "jfile = 'rtma.json'\n",
1007         "try:\n",
1008         "    ! wget --no-clobber http://math.ucdenver.edu/~jmandel/data/math4779f21/rtma.json\n",
1009         "    j = json.load(open(jfile,'r'))\n",
1010         "    print('loaded from ',jfile)\n",
1011         "    if j['obs_lat']!=obs_lat or j['obs_lon']!=obs_lon:\n",
1012         "      print('lon lat doesnot agree, need to load original RTMA files')\n",
1013         "      read_rtma=True\n",
1014         "    else:\n",
1015         "      read_rtma=False\n",
1016         "except:\n",
1017         "    print(\"can't read\",jfile,', creating')\n",
1018         "    read_rtma=True\n",
1019         "\n",
1020         "print('')"
1021       ],
1022       "execution_count": null,
1023       "outputs": []
1024     },
1025     {
1026       "cell_type": "markdown",
1027       "source": [
1028         "####<font color=red>Note: If read_rtma==True, the notebook will say it crashed when run the first time. This is because it needs to install different version of some python packages and restart runtime. Simply run it again.</fonr>"
1029       ],
1030       "metadata": {
1031         "id": "2iBNHQg5hPxB"
1032       }
1033     },
1034     {
1035       "cell_type": "code",
1036       "metadata": {
1037         "id": "mxZABVDxt0gd"
1038       },
1039       "source": [
1040         "# Set up environment to read RTMA gribs\n",
1041         "# we will need current numpy for pygrib - needed on Colab, tensorflow is using numpy 1.19\\\n",
1042         "if read_rtma:\n",
1043         "  import subprocess,os\n",
1044         "  def load_rtma(path,file,reload=0):\n",
1045         "    url='http://math.ucdenver.edu/~jmandel/rtma/' + path \n",
1046         "    if os.path.exists(file):\n",
1047         "      if reload:\n",
1048         "        print(file + ' already exists, removing')\n",
1049         "        os.remove(file)\n",
1050         "      else:\n",
1051         "        print(file + ' already exists, exiting')\n",
1052         "        # add checking size here\n",
1053         "        return 0\n",
1054         "    try:\n",
1055         "      ret = subprocess.check_output(['wget','--no-clobber','--output-document='+ file, url,],stderr=subprocess.STDOUT).decode() # execute command from python strings\n",
1056         "      if os.path.exists(file):\n",
1057         "        print('loaded ' + url + ' as ' + file)\n",
1058         "        return 0\n",
1059         "      else: \n",
1060         "        print('file transfer completed, but the file is missing? ' + url)  \n",
1061         "      return 1\n",
1062         "    except:\n",
1063         "      print('file transfer failed: ' + url)\n",
1064         "      return 2\n"
1065       ],
1066       "execution_count": null,
1067       "outputs": []
1068     },
1069     {
1070       "cell_type": "markdown",
1071       "metadata": {
1072         "id": "dQ-uJI2sy6I3"
1073       },
1074       "source": [
1075         "Create a function to transfer RTMA files in GRIB2 format from the stash. The function returns zero if the file transfer succeeded. If the file is not available, it returns a nonzero value. Note: if needed, maybe in future add more sophisticated checks, check the return code of wget and if the file size is correct."
1076       ]
1077     },
1078     {
1079       "cell_type": "markdown",
1080       "metadata": {
1081         "id": "THI6gElyHOOc"
1082       },
1083       "source": [
1084         "Next, functions to get the files, open as grib, and interpolate to the station coordinates"
1085       ]
1086     },
1087     {
1088       "cell_type": "code",
1089       "metadata": {
1090         "id": "PL3gxK67AlBI"
1091       },
1092       "source": [
1093         "if read_rtma:\n",
1094         "  def rtma_grib(t,var):\n",
1095         "    tpath = '%4i%02i%02i/%02i' % (t.year, t.month, t.day, t.hour)  # remote path on server\n",
1096         "    tstr  = '%4i%02i%02i%02i_' % (t.year, t.month, t.day, t.hour)  # time string for local path\n",
1097         "    gribfile = os.path.join('data',tstr + var + '.grib')\n",
1098         "    remote = tpath + '/' + var + '.grib'\n",
1099         "    if load_rtma(remote,gribfile):\n",
1100         "        print('cannot load remote file',remote,'as',gribfile)\n",
1101         "        return []\n",
1102         "    else:\n",
1103         "        try:\n",
1104         "            gf=GribFile(gribfile)\n",
1105         "            v = np.array(gf[1].values())\n",
1106         "        except:\n",
1107         "            print('cannot read grib file',gribfile)\n",
1108         "            return []\n",
1109         "        print('loaded ',gribfile,' containing array shape ',v.shape)\n",
1110         "        return gf[1]   # grib message\n"
1111       ],
1112       "execution_count": null,
1113       "outputs": []
1114     },
1115     {
1116       "cell_type": "code",
1117       "metadata": {
1118         "id": "ccp10kurAlBI"
1119       },
1120       "source": [
1121         "from scipy.interpolate import LinearNDInterpolator, interpn\n",
1122         "from scipy.optimize import root\n",
1123         "def interp_to_lat_lon_slow(lats,lons,v,lat,lon): \n",
1124         "    # on mesh with coordinates lats and lons interpolate v to given lat lon\n",
1125         "    interp=LinearNDInterpolator(list(zip(lats.flatten(),lons.flatten())),v.flatten())\n",
1126         "    return interp(lat,lon)\n",
1127         "def interp_to_lat_lon(lats,lons,v,lat,lon):\n",
1128         "    # on mesh with coordinates lats and lons interpolate v to given lat lon\n",
1129         "    points=(np.array(range(lats.shape[0]),float),np.array(range(lats.shape[1]),float))  # uniform mesh\n",
1130         "    def res(ij):  # interpolation of lons lats on the uniform mesh, to noninteger coordinates   \n",
1131         "       return np.hstack((interpn(points,lats,ij)-lat, interpn(points,lons,ij)-lon))\n",
1132         "    # solve for xi,xj such that lats(xi,xj)=lat lons(xi,xj)=lon, then interpolate to (xi, xj) on uniform grid \n",
1133         "    result = root(res,(0,0)) # solve res(ij) = 0\n",
1134         "    if not result.success:\n",
1135         "        print(result.message)\n",
1136         "        exit(1)\n",
1137         "    return interpn(points,v,result.x) \n"
1138       ],
1139       "execution_count": null,
1140       "outputs": []
1141     },
1142     {
1143       "cell_type": "markdown",
1144       "metadata": {
1145         "id": "jvnpq6S5AlBI"
1146       },
1147       "source": [
1148         "The interpolation function needs to  be tested."
1149       ]
1150     },
1151     {
1152       "cell_type": "code",
1153       "metadata": {
1154         "id": "NVMJBYI7AlBI"
1155       },
1156       "source": [
1157         "def interp_to_lat_lon_test(lats,lons):\n",
1158         "    print('testing interp_to_lat_lon')\n",
1159         "    vx, vy = np.meshgrid(range(lats.shape[0]),range(lats.shape[1]),indexing='ij')\n",
1160         "    i, j = (1,2)\n",
1161         "    lat,lon = ((lats[i,j]+lats[i+1,j+1])/2,(lons[i,j]+lons[i+1,j+1])/2)\n",
1162         "    vi = interp_to_lat_lon(lats,lons,vx,lat,lon)\n",
1163         "    vj = interp_to_lat_lon(lats,lons,vy,lat,lon)\n",
1164         "    print(vi,vj,'should be about',i+0.5,j+0.5)\n",
1165         "    test_slow = 0\n",
1166         "    if test_slow:\n",
1167         "        print('Testing against the standard slow method scipy.interpolate.LinearNDInterpolator. Please wait...')\n",
1168         "        vi_slow = interp_to_lat_lon_slow(lats,lons,vx,lat,lon)\n",
1169         "        print(vi_slow)\n",
1170         "        vj_slow = interp_to_lat_lon_slow(lats,lons,vy,lat,lon)\n",
1171         "        print(vj_slow)\n",
1172         "        \n",
1173         "#gf = rtma_grib(start_time,'temp')      #  read the first grib file and use it to test interpolation\n",
1174         "#lats, lons = gf.latlons()\n",
1175         "#interp_to_lat_lon_test(lats,lons)\n"
1176       ],
1177       "execution_count": null,
1178       "outputs": []
1179     },
1180     {
1181       "cell_type": "code",
1182       "metadata": {
1183         "id": "vt-Mk8fIc91m"
1184       },
1185       "source": [
1186         "#%debug\n"
1187       ],
1188       "execution_count": null,
1189       "outputs": []
1190     },
1191     {
1192       "cell_type": "markdown",
1193       "metadata": {
1194         "id": "LQbWB_3GAlBI"
1195       },
1196       "source": [
1197         "Now we are ready for a function to read the RTMA files and interpolate to the station coordinates"
1198       ]
1199     },
1200     {
1201       "cell_type": "code",
1202       "metadata": {
1203         "id": "b3JJH3XPAlBI"
1204       },
1205       "source": [
1206         "if read_rtma:\n",
1207         "  import pandas as pd, json\n",
1208         "  def read_interp_rtma(varnames,times,lat,lon):\n",
1209         "    # read RTMA from start_time to end_time and interpolate to obs_lat obs_lon\n",
1210         "    ntimes = len(times)\n",
1211         "    time_str = 'time_str'\n",
1212         "    j={time_str:times.strftime('%Y-%m-%d %H:%M').tolist()}\n",
1213         "    for varname in varnames:\n",
1214         "        j[varname]=np.full(ntimes,np.nan)  # initialize array of nans as list\n",
1215         "    n=0\n",
1216         "    for t in times:\n",
1217         "        tim=t.strftime('%Y-%m-%d %H:%M')\n",
1218         "        should_be = j[time_str][n]\n",
1219         "        if tim != should_be:\n",
1220         "            print('n=',n,'time',tim,'expected',should_be)\n",
1221         "            raise 'Invalid time' \n",
1222         "        for varname in varnames:\n",
1223         "            gf = rtma_grib(t,varname)   # read and create grib object, download if needed\n",
1224         "            if gf:\n",
1225         "                lats,lons = gf.latlons()    # coordinates\n",
1226         "                v = gf.values()\n",
1227         "                vi=interp_to_lat_lon(lats,lons,v,lat,lon) # append to array\n",
1228         "                print(varname,'at',t,'interpolated to',lat,lon,' value ',vi)\n",
1229         "                j[varname][n] = vi\n",
1230         "            else:\n",
1231         "                print(varname,'at',t,' could not be loaded')\n",
1232         "        n = n+1\n",
1233         "    return j"
1234       ],
1235       "execution_count": null,
1236       "outputs": []
1237     },
1238     {
1239       "cell_type": "code",
1240       "source": [
1241         "if read_rtma:\n",
1242         "    times = pd.date_range(start=time_start,end=time_end,freq='1H')\n",
1243         "    varnames=['temp','td','precipa']\n",
1244         "    j =    read_interp_rtma(varnames,times,obs_lat,obs_lon)      # temperature\n",
1245         "    for varname in varnames:\n",
1246         "        j[varname]=j[varname].tolist() \n",
1247         "    j['obs_lat']=obs_lat\n",
1248         "    j['obs_lon']=obs_lon\n",
1249         "    json.dump(j,open('rtma.json','w'),indent=4)\n",
1250         "    print('done')"
1251       ],
1252       "metadata": {
1253         "id": "OY1oTYKlfd17"
1254       },
1255       "execution_count": null,
1256       "outputs": []
1257     },
1258     {
1259       "cell_type": "code",
1260       "metadata": {
1261         "id": "bMpYIZT6c91o"
1262       },
1263       "source": [
1264         "# %debug\n"
1265       ],
1266       "execution_count": null,
1267       "outputs": []
1268     },
1269     {
1270       "cell_type": "markdown",
1271       "metadata": {
1272         "id": "KVXBjGA0CiXr"
1273       },
1274       "source": [
1275         "#### 3.2.3 Preprocessing and visualization of the weather data"
1276       ]
1277     },
1278     {
1279       "cell_type": "code",
1280       "metadata": {
1281         "id": "fNA3Vbo1c91o"
1282       },
1283       "source": [
1284         "rtma = j\n",
1285         "td = np.array(rtma['td'])\n",
1286         "t2 = np.array(rtma['temp'])\n",
1287         "rain=np.array(rtma['precipa'])\n",
1288         "# compute relative humidity\n",
1289         "rh = 100*np.exp(17.625*243.04*(td - t2) / (243.04 + t2 - 273.15) / (243.0 + td - 273.15))\n",
1290         "Ed = 0.924*rh**0.679 + 0.000499*np.exp(0.1*rh) + 0.18*(21.1 + 273.15 - t2)*(1 - np.exp(-0.115*rh))\n",
1291         "Ew = 0.618*rh**0.753 + 0.000454*np.exp(0.1*rh) + 0.18*(21.1 + 273.15 - t2)*(1 - np.exp(-0.115*rh))"
1292       ],
1293       "execution_count": null,
1294       "outputs": []
1295     },
1296     {
1297       "cell_type": "code",
1298       "metadata": {
1299         "id": "tZIK59bJAlBJ"
1300       },
1301       "source": [
1302         "%matplotlib inline\n",
1303         "plt.figure(figsize=(16,4))\n",
1304         "plt.plot(t2,linestyle='-',c='k',label='Temperature')\n",
1305         "plt.title(station['STID'] + ' Temperature')\n",
1306         "plt.xlabel('Time (hours)') \n",
1307         "plt.ylabel('Temperature (K)')\n",
1308         "plt.legend()"
1309       ],
1310       "execution_count": null,
1311       "outputs": []
1312     },
1313     {
1314       "cell_type": "code",
1315       "metadata": {
1316         "id": "LbyqcuXYAlBJ"
1317       },
1318       "source": [
1319         "%matplotlib inline\n",
1320         "plt.figure(figsize=(16,4))\n",
1321         "plt.plot(td,linestyle='-',c='k',label='Dew point')\n",
1322         "plt.title(station['STID'] + ' Dew point (K)')\n",
1323         "plt.xlabel('Time (hours)') \n",
1324         "plt.ylabel('Dew point (K)')\n",
1325         "plt.legend()"
1326       ],
1327       "execution_count": null,
1328       "outputs": []
1329     },
1330     {
1331       "cell_type": "code",
1332       "metadata": {
1333         "id": "dfoOK2kSc91p"
1334       },
1335       "source": [
1336         "%matplotlib inline\n",
1337         "plt.figure(figsize=(16,4))\n",
1338         "plt.plot(rh,linestyle='-',c='k',label='Dew point')\n",
1339         "plt.title(station['STID'] + ' relative humidity')\n",
1340         "plt.xlabel('Time (hours)') \n",
1341         "plt.ylabel('Relative humidity (%)')\n",
1342         "plt.legend()"
1343       ],
1344       "execution_count": null,
1345       "outputs": []
1346     },
1347     {
1348       "cell_type": "code",
1349       "metadata": {
1350         "id": "MWTJ5b2kc91p"
1351       },
1352       "source": [
1353         "%matplotlib inline\n",
1354         "plt.figure(figsize=(16,4))\n",
1355         "plt.plot(Ed,linestyle='-',c='r',label='drying equilibrium')\n",
1356         "plt.plot(Ew,linestyle=':',c='b',label='wetting equilibrium')\n",
1357         "plt.title(station['STID'] + ' drying and wetting equilibria')\n",
1358         "plt.xlabel('Time (hours)') \n",
1359         "plt.ylabel('Fuel moisture contents (%)')\n",
1360         "plt.legend()"
1361       ],
1362       "execution_count": null,
1363       "outputs": []
1364     },
1365     {
1366       "cell_type": "markdown",
1367       "metadata": {
1368         "id": "jY3_eeBRc91p"
1369       },
1370       "source": [
1371         " "
1372       ]
1373     },
1374     {
1375       "cell_type": "code",
1376       "metadata": {
1377         "id": "PQKSRvRSAlBJ"
1378       },
1379       "source": [
1380         "%matplotlib inline\n",
1381         "plt.figure(figsize=(16,4))\n",
1382         "plt.plot(rain,linestyle='-',c='k',label='Precipitation')\n",
1383         "plt.title(station['STID'] + ' Precipitation' )\n",
1384         "plt.xlabel('Time (hours)') \n",
1385         "plt.ylabel('Precipitation (mm/hour)')\n",
1386         "plt.legend()"
1387       ],
1388       "execution_count": null,
1389       "outputs": []
1390     },
1391     {
1392       "cell_type": "code",
1393       "metadata": {
1394         "id": "Dwbt4UXfro5x"
1395       },
1396       "source": [
1397         "print(rain[1900:2000])"
1398       ],
1399       "execution_count": null,
1400       "outputs": []
1401     },
1402     {
1403       "cell_type": "markdown",
1404       "metadata": {
1405         "id": "_yRu_7WvHc6P"
1406       },
1407       "source": [
1408         "Precipitation from RTMA is in kg/m${}^2$. 1m water depth over 1m${}^2$ is 1m${}^3$ with mass 1000 kg thus 1 kg/m${}^2$ is the same as 1 mm of precipitation. RTMA values are accumulations over 1 h so these are values in mm/h. So 9999 mm/h = 10m/h makes no sense. Replace anything over 1m/h by nan and try again."
1409       ]
1410     },
1411     {
1412       "cell_type": "code",
1413       "metadata": {
1414         "id": "XPYO_Iuvc91q"
1415       },
1416       "source": [
1417         "rain[rain > 1000] = np.NaN"
1418       ],
1419       "execution_count": null,
1420       "outputs": []
1421     },
1422     {
1423       "cell_type": "code",
1424       "metadata": {
1425         "scrolled": true,
1426         "id": "GYWTfbBBc91q"
1427       },
1428       "source": [
1429         "%matplotlib inline\n",
1430         "plt.figure(figsize=(16,4))\n",
1431         "plt.plot(rain,linestyle='-',c='k',label='Precipitation')\n",
1432         "plt.title(station['STID'] + ' Precipitation' )\n",
1433         "plt.xlabel('Time (hours)') \n",
1434         "plt.ylabel('Precipitation (mm/hour)')\n",
1435         "plt.legend()"
1436       ],
1437       "execution_count": null,
1438       "outputs": []
1439     },
1440     {
1441       "cell_type": "markdown",
1442       "metadata": {
1443         "id": "Q_L0R2Njc91q"
1444       },
1445       "source": [
1446         "Fix some missing data, then we can use the data for up to 1942 hours until a biger gap."
1447       ]
1448     },
1449     {
1450       "cell_type": "code",
1451       "metadata": {
1452         "id": "_tkU7UJic91q"
1453       },
1454       "source": [
1455         "# fix isolated nans\n",
1456         "def fixnan(a,n):\n",
1457         "    for c in range(n):\n",
1458         "        for i in np.where(np.isnan(a)):\n",
1459         "            a[i]=0.5*(a[i-1]+a[i+1])\n",
1460         "        if not any(np.isnan(a)):\n",
1461         "            break\n",
1462         "    return a\n",
1463         "\n",
1464         "rain=fixnan(rain,2)\n",
1465         "t2=fixnan(t2,2)\n",
1466         "rh=fixnan(rh,2)\n",
1467         "obs_data=fixnan(obs_data,2)\n",
1468         "Ed=fixnan(Ed,2)\n",
1469         "Ew=fixnan(Ew,2)\n",
1470         "\n",
1471         "print(np.where(np.isnan(rain)))\n",
1472         "print(np.where(np.isnan(t2)))\n",
1473         "print(np.where(np.isnan(rh)))\n",
1474         "print(np.where(np.isnan(obs_data)))"
1475       ],
1476       "execution_count": null,
1477       "outputs": []
1478     },
1479     {
1480       "cell_type": "markdown",
1481       "metadata": {
1482         "id": "XqQYnyI9DIy1"
1483       },
1484       "source": [
1485         "## 4 Results"
1486       ]
1487     },
1488     {
1489       "cell_type": "markdown",
1490       "metadata": {
1491         "id": "2tIC_Tqnc91r"
1492       },
1493       "source": [
1494         "### 4.1 Kalman filter with fuel moisture observations, followed by forecasting\n",
1495         "We run the model first with Kalman filter for 150 hours. The observations are the RAWS data\n",
1496         "After 150 hours, we run in forecast mode - the RAWS data are no longer used, and we run the model from the weather data without the Kalman filter. The weather data are taken to be RTMA interpolated to one RAWS location.\n",
1497         "In a real forecasting application, the model would be run from weather forecast rather than data."
1498       ]
1499     },
1500     {
1501       "cell_type": "code",
1502       "metadata": {
1503         "id": "aXnSQM7wc91r"
1504       },
1505       "source": [
1506         "# run KF on an initial data seqment\n",
1507         "import numpy as np\n",
1508         "import matplotlib.pyplot as plt \n",
1509         "\n",
1510         "hours=1200 # total simulation\n",
1511         "h2 = 150\n",
1512         "m = np.zeros(hours) # preallocate\n",
1513         "m[0]= obs_data[0]             # initial state  \n",
1514         "P = np.zeros(hours)\n",
1515         "P[0] = 1e-3 # background state variance\n",
1516         "H = np.array([1.])   # all oQ = np.array([0.02]) # process noise variancebserved\n",
1517         "Q = np.array([1e-3]) # process noise variance\n",
1518         "R = np.array([1e-3]) # data variance\n",
1519         "for t in range(hours-1):\n",
1520         "    # using lambda construction to pass additional arguments to the model \n",
1521         "    if t < h2 and not np.isnan(obs_data[t]) and not np.isnan(Ew[t]) and not np.isnan(rain[t]): # advance model and run KF\n",
1522         "        m[t+1],P[t+1] = ext_kf(m[t],P[t],lambda u: model_moisture(u,Ed[t],Ew[t],rain[t],t,partials=1),Q,\n",
1523         "                    d=obs_data[t],H=H,R=R)\n",
1524         "    else:  # just advance to next hour, no process noise\n",
1525         "        m[t+1],P[t+1] = ext_kf(m[t],P[t],lambda u: model_moisture(u,Ed[t],Ew[t],rain[t],t,partials=1),Q*0.0)"
1526       ],
1527       "execution_count": null,
1528       "outputs": []
1529     },
1530     {
1531       "cell_type": "code",
1532       "metadata": {
1533         "scrolled": true,
1534         "id": "peMi-OF3c91r"
1535       },
1536       "source": [
1537         "%matplotlib inline\n",
1538         "plt.figure(figsize=(16,4))\n",
1539         "plt.plot(Ed[:hours],linestyle='--',c='r',label='Drying Equilibrium')\n",
1540         "plt.plot(Ew[:hours],linestyle='--',c='b',label='Wetting Equilibrium')\n",
1541         "plt.plot(obs_data[:hours],linestyle=':',c='k',label='RAWS data')\n",
1542         "plt.plot(m[:h2],linestyle='-',c='k',label='filtered')\n",
1543         "plt.plot(range(h2,hours),m[h2:hours],linestyle='-',c='r',label='forecast')\n",
1544         "plt.title('Kalman filtering and forecast with real data')\n",
1545         "plt.xlabel('Time (hours)') \n",
1546         "plt.ylabel('Fuel moisture content (%)')\n",
1547         "plt.legend()"
1548       ],
1549       "execution_count": null,
1550       "outputs": []
1551     },
1552     {
1553       "cell_type": "markdown",
1554       "metadata": {
1555         "id": "3TnwXYcLc91r"
1556       },
1557       "source": [
1558         "Clearly, there is a problem - the forecast fuel moisture is too high. We need to assimilate also some parameters of the model, not just its output state. "
1559       ]
1560     },
1561     {
1562       "cell_type": "markdown",
1563       "metadata": {
1564         "id": "-WMWCDz4DX45"
1565       },
1566       "source": [
1567         "#### 4.2 Model with an augmented state"
1568       ]
1569     },
1570     {
1571       "cell_type": "markdown",
1572       "metadata": {
1573         "id": "jivOYEhiXMi5"
1574       },
1575       "source": [
1576         "In reality, the equilibrium moisture $E$ computed from atmospheric conditions\n",
1577         "generally does not agree with the data. We want to add a correction $\\Delta\n",
1578         "E$ to $E$ constant in time, and identify the new parameter $\\Delta E$ from data. \n",
1579         "Because the Kalman filter identifies state, add the parameter to the state.\n",
1580         "Define augmented state $u=\\left[\n",
1581         "\\begin{array}\n",
1582         "[c]{c}\n",
1583         "m\\\\\n",
1584         "\\Delta E\n",
1585         "\\end{array}\n",
1586         "\\right]  .$ Since $\\Delta E$ is constant in time, it satisfies the\n",
1587         "differential equation $\\frac{d\\Delta E}{dt}=0.$ So, we want to estimate the\n",
1588         "state $u$ governed by the\n",
1589         "$$\n",
1590         "\\frac{d}{dt}\\left[\n",
1591         "\\begin{array}\n",
1592         "[c]{c}\n",
1593         "m\\\\\n",
1594         "\\Delta E\n",
1595         "\\end{array}\n",
1596         "\\right]  =\\left[\n",
1597         "\\begin{array}\n",
1598         "[c]{c}\n",
1599         "\\frac{E+\\Delta E-m(t)}{T}\\\\\n",
1600         "0\n",
1601         "\\end{array}\n",
1602         "\\right]  ,\n",
1603         "$$\n",
1604         "which we write as $\\frac{du}{dt}=F(u),$ where\n",
1605         "$$\n",
1606         "F(u)=\\left[\n",
1607         "\\begin{array}\n",
1608         "[c]{c}\n",
1609         "F_{1}\\left(  u\\right)  \\\\\n",
1610         "F_{2}\\left(  u\\right)\n",
1611         "\\end{array}\n",
1612         "\\right]  =F\\left(  \\left[\n",
1613         "\\begin{array}\n",
1614         "[c]{c}\n",
1615         "m\\\\\n",
1616         "\\Delta E\n",
1617         "\\end{array}\n",
1618         "\\right]  \\right)  =\\left[\n",
1619         "\\begin{array}\n",
1620         "[c]{c}\n",
1621         "\\left(  E+\\Delta E-m(t)\\right)  T_{1}\\\\\n",
1622         "0\n",
1623         "\\end{array}\n",
1624         "\\right]  ,\\quad T_{1}=\\frac{1}{T}.\n",
1625         "$$\n",
1626         "The Jacobian of $F$ is\n",
1627         "$$\n",
1628         "\\left[\n",
1629         "\\begin{array}\n",
1630         "[c]{cc}\n",
1631         "\\frac{\\partial F_{1}}{\\partial u_{1}} & \\frac{\\partial F_{1}}{\\partial u_{2}\n",
1632         "}\\\\\n",
1633         "\\frac{\\partial F_{2}}{\\partial u_{1}} & \\frac{\\partial F_{2}}{\\partial u_{2}}\n",
1634         "\\end{array}\n",
1635         "\\right]  =\\left[\n",
1636         "\\begin{array}\n",
1637         "[c]{cc}\n",
1638         "\\frac{\\partial m_{1}}{\\partial m_{0}} & \\frac{\\partial m_{1}}{\\partial E}\\\\\n",
1639         "\\frac{\\partial\\Delta E}{\\partial m_{0}} & \\frac{\\partial\\Delta E}\n",
1640         "{\\partial\\Delta E}\n",
1641         "\\end{array}\n",
1642         "\\right]  =\\left[\n",
1643         "\\begin{array}\n",
1644         "[c]{cc}\n",
1645         "\\frac{\\partial m_{1}}{\\partial m_{0}} & \\frac{\\partial m_{1}}{\\partial E}\\\\\n",
1646         "0 & 1\n",
1647         "\\end{array}\n",
1648         "\\right]\n",
1649         "$$\n",
1650         "Here is a function that implements the augmented model $F$. The input is\n",
1651         "$u_{0}$. The output is $u_{1}$ and the Jacobian $du_{1}/du_{0}$."
1652       ]
1653     },
1654     {
1655       "cell_type": "markdown",
1656       "metadata": {
1657         "id": "MJ1C_1Omc91s"
1658       },
1659       "source": [
1660         "\n",
1661         "Define augmented model function with drying, wetting, and rain equilibria"
1662       ]
1663     },
1664     {
1665       "cell_type": "code",
1666       "metadata": {
1667         "id": "GHtAaGp9WSHT"
1668       },
1669       "source": [
1670         "def model_augmented(u0,Ed,Ew,r,t):\n",
1671         "    # state u is the vector [m,dE] with dE correction to equilibria Ed and Ew at t\n",
1672         "    # \n",
1673         "    m0, Ec = u0  # decompose state u0\n",
1674         "    # reuse model_moisture(m0,Eqd,Eqw,r,partials=0):\n",
1675         "    # arguments:\n",
1676         "    # m0         starting fuel moistureb (1)\n",
1677         "    # Ed         drying equilibrium      (1) \n",
1678         "    # Ew         wetting equilibrium     (1)\n",
1679         "    # r          rain intensity          (mm/h)\n",
1680         "    # partials = 0, 1, 2\n",
1681         "    # returns: same as model_decay\n",
1682         "    #   if partials==0: m1 = fuel moisture contents after time 1 hour\n",
1683         "    #              ==1: m1, dm0/dm0 \n",
1684         "    #              ==2: m1, dm1/dm0, dm1/dE \n",
1685         "    m1, dm1_dm0, dm1_dE  = model_moisture(m0,Ed + Ec, Ew + Ec, r, t, partials=2)\n",
1686         "    u1 = np.array([m1,Ec])   # dE is just copied\n",
1687         "    J =  np.array([[dm1_dm0, dm1_dE],\n",
1688         "                   [0.     ,     1.]])\n",
1689         "    return u1, J"
1690       ],
1691       "execution_count": null,
1692       "outputs": []
1693     },
1694     {
1695       "cell_type": "markdown",
1696       "metadata": {
1697         "id": "8SuVNg8TsW4d"
1698       },
1699       "source": [
1700         "### 4.3 Kalman filter on the augmented model"
1701       ]
1702     },
1703     {
1704       "cell_type": "code",
1705       "metadata": {
1706         "id": "1No3g6HyAEh_"
1707       },
1708       "source": [
1709         "u = np.zeros((2,hours))\n",
1710         "u[:,0]=[0.1,0.1]       # initialize,background state  \n",
1711         "P = np.zeros((2,2,hours))\n",
1712         "P[:,:,0] = np.array([[1e-3, 0.],\n",
1713         "                    [0.,  1e-3]]) # background state covariance\n",
1714         "Q = np.array([[1e-3, 0.],\n",
1715         "              [0,  1e-3]]) # process noise covariance\n",
1716         "H = np.array([[1., 0.]])  # first component observed\n",
1717         "R = np.array([1e-3]) # data variance\n",
1718         "\n",
1719         "# ext_kf(u,P,F,Q=0,d=None,H=None,R=None) returns ua, Pa\n",
1720         "\n",
1721         "# print('initial u=',u,'P=',P)\n",
1722         "# print('Q=',Q,'H=',H,'R=',R)\n",
1723         "for t in range(1,h2):\n",
1724         "    # use lambda construction to pass additional arguments to the model \n",
1725         "    u[:,t],P[:,:,t] = ext_kf(u[:,t-1],P[:,:,t-1],\n",
1726         "                                 lambda uu: model_augmented(uu,Ed[t],Ew[t],rain[t],t),\n",
1727         "                                 Q,obs_data[t],H=H,R=R)\n",
1728         "    # print('time',t,'data',obs_data[t],'filtered',u[0,t],'Ec',u[1,t])\n",
1729         "for t in range(h2,hours):\n",
1730         "    u[:,t],P[:,:,t] = ext_kf(u[:,t-1],P[:,:,t-1],\n",
1731         "                                 lambda uu: model_augmented(uu,Ed[t],Ew[t],rain[t],t),\n",
1732         "                                 Q*0.0)\n",
1733         "    # print('time',t,'data',obs_data[t],'forecast',u[0,t],'Ec',u[1,t])\n",
1734         "m,Ec = u  # extract from state"
1735       ],
1736       "execution_count": null,
1737       "outputs": []
1738     },
1739     {
1740       "cell_type": "markdown",
1741       "source": [
1742         "Plot the result:\n"
1743       ],
1744       "metadata": {
1745         "id": "FYAbWNCfk2wD"
1746       }
1747     },
1748     {
1749       "cell_type": "code",
1750       "source": [
1751         "def plot_moisture(hmin,hmax):\n",
1752         "  print('training from 0 to',h2,'plot from',hmin,'to',hmax)\n",
1753         "  plt.figure(figsize=(16,4))\n",
1754         "  plt.plot(range(hmin,hmax),Ed[hmin:hmax],linestyle='--',c='r',label='Drying Equilibrium (%)')\n",
1755         "  plt.plot(range(hmin,hmax),Ew[hmin:hmax],linestyle='--',c='b',label='Wetting Equilibrium (%)')\n",
1756         "  plt.plot(range(hmin,hmax),Ec[hmin:hmax],linestyle='--',c='g',label='Equilibrium Correction (%)')\n",
1757         "  plt.plot(range(hmin,hmax),obs_data[hmin:hmax],linestyle=':',c='k',label='RAWS data (%)')\n",
1758         "  plt.plot(range(hmin,hmax),rain[hmin:hmax],linestyle='-',c='b',label='RTMA rain (mm/h)')\n",
1759         "  if hmin>=h2:\n",
1760         "    plt.plot(m[hmin:h2],linestyle='-',c='k',label='Filtered')\n",
1761         "  h1 = np.maximum(hmin,h2)\n",
1762         "  plt.plot(range(h1,hmax),m[h1:hmax],linestyle='-',c='r',label='Forecast (%)')\n",
1763         "  plt.title('Kalman filtering and forecast with augmented state, real data. Training 0:%i hmax' % h2)\n",
1764         "  plt.xlabel('Time (hours)') \n",
1765         "  plt.ylabel('Fuel moisture content (%)')\n",
1766         "  plt.legend()"
1767       ],
1768       "metadata": {
1769         "id": "hlkby_oTlB_f"
1770       },
1771       "execution_count": null,
1772       "outputs": []
1773     },
1774     {
1775       "cell_type": "code",
1776       "source": [
1777         "plot_moisture(0,hours)"
1778       ],
1779       "metadata": {
1780         "id": "q-h5omKgnow2"
1781       },
1782       "execution_count": null,
1783       "outputs": []
1784     },
1785     {
1786       "cell_type": "markdown",
1787       "source": [
1788         "A detailed view of transition from training to forecast:"
1789       ],
1790       "metadata": {
1791         "id": "0w0YtHtqnza5"
1792       }
1793     },
1794     {
1795       "cell_type": "code",
1796       "metadata": {
1797         "id": "B7sXGUotc91s"
1798       },
1799       "source": [
1800         "plot_moisture(0,300)\n",
1801         "\n"
1802       ],
1803       "execution_count": null,
1804       "outputs": []
1805     },
1806     {
1807       "cell_type": "markdown",
1808       "metadata": {
1809         "id": "7W03QTo3c91t"
1810       },
1811       "source": [
1812         "Filtering by extended Kalman filter using RAWS data until 150 hours, then forecasting mode - running the model from interpolated RTMA only. For the first 60 hours the forecast is good, the equilibium correction made the model quite close to data. But then the big spike in equilibrium moisture around 230 hours attracted the solution, and it took a while for it to get back. The spike in the RAWS measurement is there but much smaller. The model becomes inaccurate during periods when the fuel moisture equilibrium is large.\n",
1813         "\n",
1814         "Possible reasons include: 1. There was something in the data we do not know about - maybe it rained but RTMA did not tell us. Try comparing with data from the RAWS itself? 2. The model is too simple, assumes the whole depth of the wood stick is wetting and drying at the same time. Perhaps the moisture got stored in the inside layers of the measurement stick. Try a two-layer model as in van der Kamp (2017) and make the state larger? "
1815       ]
1816     },
1817     {
1818       "cell_type": "markdown",
1819       "source": [
1820         "A detailed vieew of rain episode:"
1821       ],
1822       "metadata": {
1823         "id": "owEI4EtTo7Ek"
1824       }
1825     },
1826     {
1827       "cell_type": "code",
1828       "source": [
1829         "plot_moisture(900,1100)"
1830       ],
1831       "metadata": {
1832         "id": "C_hoDjgtpMEJ"
1833       },
1834       "execution_count": null,
1835       "outputs": []
1836     },
1837     {
1838       "cell_type": "markdown",
1839       "source": [
1840         "It seems there is some rain that the model does not know about."
1841       ],
1842       "metadata": {
1843         "id": "DRraWhwdpSkV"
1844       }
1845     },
1846     {
1847       "cell_type": "markdown",
1848       "metadata": {
1849         "id": "gVQxv9Blc91t"
1850       },
1851       "source": [
1852         "### 4.4 A comment on the information flow in the Kalman filter and in neural networks"
1853       ]
1854     },
1855     {
1856       "cell_type": "markdown",
1857       "metadata": {
1858         "id": "CZmR4HPAc91t"
1859       },
1860       "source": [
1861         "At each time step $k$,\n",
1862         "\n",
1863         "* input state $u_{k-1}$ size $n$ and its covariance matrix $P_{k-1}$ size $n \\times n$.\n",
1864         "* the model is applied to external data $e_k$ and the input $u_{k-1},P_{k-1}$ produce the forecast $u_k^f$ and its covariance $P^f_k$\n",
1865         "* the new state $u_k$ is found by minimizing $|| u^f_k - u_k||^2_{P^f_k} + ||H u_k - d_k||^2_{R}$   \n",
1866         "* the new state covariance is $P_k = ( (P^f_k)^{-1} + H^\\top R^{-1} H)^{-1}$.\n",
1867         "\n",
1868         "Here, the state consists of \n",
1869         "* the fuel moisture and the adjustment to the equilibrium, dimension 2\n",
1870         "* the covariance matrix of vector of dimension 2, which is symmetric $2 \\times 2$ matrix, given by 3 numbers because it is symmetric\n",
1871         "Thus, the dimension of the state is 2 + 3 = 5. The first component of the state, the fuel moisture, is the quantity of interest, the rest are auxiliary.\n",
1872         "\n",
1873         "\n",
1874         "This can be understood as:\n",
1875         "\n",
1876         "* a mapping $M$ of the 5-dimensional hidden and external data state to a new hidden state:\n",
1877         "$$M:(u_{k-1},P_{k-1},e_k) \\mapsto (u_{k},P_{k})$$\n",
1878         "* taking the output (the quantity of interest) as the first component of the hiddent state\n",
1879         "* feeding the hiddent state back to the mapping $M$ for the next step $k+1$\n",
1880         "* training consists of fitting the hidden state to minimize a loss function\n",
1881         "$$\\ell(u_{k},P_{k},d_k,R_k) \\to \\min$$\n",
1882         "\n",
1883         "Note that in the augmented Kalman filter above, the mapping $M$ is fixed and it has a one component of the hidden state as a parameter. To get a better fit, we could increase the number of parameters, e.g., by modeling the moisture in multiple layers, as in van der Kamp et al. (2017) two-layer model.\n",
1884         "\n",
1885         "A recurrent neural network (RNN) has a similar information flow but it can be more flexible and look for the best model automatically, i.e., build the model from data. \n"
1886       ]
1887     },
1888     {
1889       "cell_type": "markdown",
1890       "metadata": {
1891         "id": "_g_OTEg6ePb9"
1892       },
1893       "source": [
1894         "## 5. Conclusion"
1895       ]
1896     },
1897     {
1898       "cell_type": "markdown",
1899       "metadata": {
1900         "id": "aNxw7xI3FqFt"
1901       },
1902       "source": [
1903         "We have shown how to combine a model and data for improved forecasting of fuel moisture from weather forecast using the Kalman filter. Augmenting the filter state by a model parameter and joint estimation of augmented state resulted in an improvement of the forecast."
1904       ]
1905     },
1906     {
1907       "cell_type": "markdown",
1908       "metadata": {
1909         "id": "IWpmDwUPGElR"
1910       },
1911       "source": [
1912         "## Contributions of authors "
1913       ]
1914     },
1915     {
1916       "cell_type": "markdown",
1917       "metadata": {
1918         "id": "jujW1VFgGOCn"
1919       },
1920       "source": [
1921         "Not applicable."
1922       ]
1923     },
1924     {
1925       "cell_type": "markdown",
1926       "metadata": {
1927         "id": "HWslw7HmGZmP"
1928       },
1929       "source": [
1930         "## Acknowledgements"
1931       ]
1932     },
1933     {
1934       "cell_type": "markdown",
1935       "metadata": {
1936         "id": "xubqDAV2GjkZ"
1937       },
1938       "source": [
1939         "This Math Clinic was sponsored by the team of investigators of the NASA grant no. 80NSSC19K1091 *Coupled Interactive Forecasting of Weather, Fire Behavior, and Smoke Impact for Improved Wildland Fire Decision Making* under the NASA ROSES18 Disasters program. The author would like to thank Brian Zhang from the Math Clinic class for bringing the reference van der Kamp et al. (2017) to his attention."
1940       ]
1941     },
1942     {
1943       "cell_type": "markdown",
1944       "metadata": {
1945         "id": "ZsNZxOv7c91t"
1946       },
1947       "source": [
1948         "## References"
1949       ]
1950     },
1951     {
1952       "cell_type": "markdown",
1953       "metadata": {
1954         "id": "vFY-iS1Wc91t"
1955       },
1956       "source": [
1957         "J. Mandel, S. Amram, J. D. Beezley, G. Kelman, A. K. Kochanski, V. Y. Kondratenko, B. H. Lynn, B. Regev, and M. Vejmelka. *Recent advances and applications of WRF-SFIRE.* Natural Hazards and Earth System Science, 14(10):2829–2845, 2014. [doi:10.5194/nhessd-2-1759-2014](https://doi.org/10.5194/nhessd-2-1759-2014)\n",
1958         "\n",
1959         "R. E. Kalman. *A new approach to linear filtering and prediction problems.* Transactions of the ASME – Journal of Basic Engineering, Series D, 82:35–45, 1960. [doi:10.1115/1.3662552](https://doi.org/10.1115/1.3662552)\n",
1960         "\n",
1961         "E. Kalnay. *Atmospheric Modeling, Data Assimilation and Predictability.* Cambridge University Press, 2003. [doi:10.1017/CBO9780511802270](https://doi.org/10.1017/CBO9780511802270)\n",
1962         "\n",
1963         "D. W. van der Kamp, R. D. Moore, and I. G. McKendry. *A model for simulating the moisture content of standardized fuel sticks of various sizes.* Agricultural and Forest Meteorology, 236:123–134, 2017. [doi:10.1016/j.agrformet.2017.01.013](https://doi.org/10.1016/j.agrformet.2017.01.013)\n",
1964         "\n",
1965         "S. F. Schmidt. *Application of state-space methods to navigation problems.* volume 3 of Advances in Control Systems, C. T.  Leondes, ed., pages 293–340. Elsevier, 1966. [doi:10.1016/B978-1-4831-6716-9.50011-4](https://doi.org/10.1016/B978-1-4831-6716-9.50011-4)\n",
1966         "\n",
1967         "M. Vejmelka, A. K. Kochanski, and J. Mandel. *Data assimilation of dead fuel moisture observations from remote automatic weather stations.* International Journal of Wildland Fire, 25:558– 568, 2016. [doi:10.1071/WF14085](https://doi.org/10.1071/WF14085)\n"
1968       ]
1969     }
1970   ]