{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Application Example: Gyroscope Allan Variance"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The signals of, e.g., angular rate sensors contain white noise along with colored noise resulting from a variety of sources. With the ``pyplnoise``module, this noise can be generated and added on top of the deterministic sensor errors in a sensor model. Such a sensor model can be used for example for the simulation of a Kalman filter accommodating correlated noise terms.\n",
    "\n",
    "In the code below it is outlined how you can use ``pyplnoise`` to approximate the Allan variance of a real yaw rate sensor. Familiarity with the IEEE Standard 952 is helpful."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import pyplnoise\n",
    "import allan\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The angular rate sensor is described by the quantities $N$ (angle random walk), $B$ (bias instability) and $K$ (rate random walk) from the IEEE Std 952. The values prescribed here approximate the properties of a Bosch SMI130 yaw rate sensor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 5e-3 # °/sqrt(s) = 0.3 °/sqrt(h)\n",
    "B = 7e-4 # °/s = 2.5 °/h\n",
    "K = 6e-6 # °/s/sqrt(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Define the sampling rate ``fs`` and initialize noise sources:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "fs = 10.\n",
    "npts = 70000000\n",
    "t = np.linspace(0., npts/fs, num=npts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "whnoise = pyplnoise.WhiteNoise(fs)\n",
    "rdnoise = pyplnoise.RedNoise(fs, 1e-8)\n",
    "pknoise = pyplnoise.PinkNoise(fs, 1e-6, fs/2.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now create an artifical sensor noise signal $w_{noise}$ from the individual noise sources denoted by $\\xi$. The noise is additive and the coefficients $N$, $B$ and $K$ from the IEEE Std 952 enter as follows:\n",
    "\n",
    "$$w_{noise} = N\\xi_{white} + \\frac{B}{\\sqrt{2\\pi}}\\xi_{pink} + \\frac{K}{2\\pi}\\xi_{red}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "w = N * whnoise.get_series(npts) + B / np.sqrt(2. * np.pi) * pknoise.get_series(npts) +\\\n",
    "K / (2. * np.pi) * rdnoise.get_series(npts)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now calculate the Allan variance. The module ``allan`` is not provided by ``pyplnoise``, but many similar codes can be found on pypi."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "tau, avar, adev, error_adev_pct = allan.allan_variance(w, dt=1./fs, n_clusters=8000,\n",
    "                                                       verbose=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# omit values with insufficient statistical quality\n",
    "idx_valid = error_adev_pct < 10\n",
    "avar_valid = avar[idx_valid]\n",
    "adev_valid = adev[idx_valid]\n",
    "tau_valid = tau[idx_valid]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's how the calculated Allan deviation looks like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 27964 characters]\n",
      "text/plain": [
       "<Figure size 720x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.rcParams.update({'font.size': 14})\n",
    "fig = plt.figure(figsize=(10,8))\n",
    "ax = fig.gca()\n",
    "ax.loglog(tau_valid, adev_valid*3600)\n",
    "ax.set_xlabel(\"Cluster length (s)\")\n",
    "ax.set_ylabel(\"Allan deviation (°/h)\")\n",
    "ax.grid(True, which='major')\n",
    "ax.grid(True, which='minor', axis='y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's extract the properties $N$, $B$ and $K$ automatically:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Extracted angle random walk: N = 0.005027 °/s/sqrt(Hz) = 0.30159 °/sqrt(h)\n",
      "Extracted bias instability: B = 0.0007754 °/s = 2.79154 °/h\n",
      "Extracted rate random walk: K = 6.792e-06 °/s^(3/2) = 1.46705 °/h^(3/2)\n"
     ]
    }
   ],
   "source": [
    "noise_density, a_n_fit, b_n_fit = allan.read_noise_density(adev_valid, tau_valid)\n",
    "print(\"Extracted angle random walk: N = {:.4g} °/s/sqrt(Hz) = {:.5f} °/sqrt(h)\"\n",
    "      .format(noise_density, noise_density*60))\n",
    "bias_instability = allan.read_bias_instability(adev_valid, tau_valid)\n",
    "print(\"Extracted bias instability: B = {:.4g} °/s = {:.5f} °/h\"\n",
    "      .format(bias_instability, bias_instability*3600))\n",
    "rate_random_walk, a_k_fit, b_k_fit = allan.read_rate_random_walk(adev_valid, tau_valid)\n",
    "print(\"Extracted rate random walk: K = {:.4g} °/s^(3/2) = {:.5f} °/h^(3/2)\"\n",
    "      .format(rate_random_walk, rate_random_walk*3600**(3/2)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you rather prefer to find these values manually, you can read off the value of the red line at $\\tau = 1$ to get $N$. The value of the green line at $\\tau = 3$ is equal to $K$, and multiplication of the global minimum by $\\sqrt{\\frac{\\pi}{2\\ln 2}}\\approx1/0.664$ yields $B$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 54376 characters]\n",
      "text/plain": [
       "<Figure size 864x720 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "fit_vals_n = np.power(10.0, a_n_fit * np.log10(tau_valid) + b_n_fit)\n",
    "fit_vals_k = np.power(10.0, a_k_fit * np.log10(tau_valid) + b_k_fit)\n",
    "fig = plt.figure(figsize=(12,10))\n",
    "ax = fig.gca()\n",
    "ax.loglog(tau_valid, adev_valid)\n",
    "ax.loglog(tau_valid, fit_vals_n, color='red')\n",
    "ax.loglog(tau_valid, fit_vals_k, color='green')\n",
    "ax.set_xlabel(\"Cluster length (s)\")\n",
    "ax.set_ylabel(\"Allan deviation (°/s)\")\n",
    "ax.grid(True, which='both')"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 2
}
