{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Particle Filters: Sequential Monte Carlo Methods\n", "\n", "This notebook covers particle filtering techniques for nonlinear, non-Gaussian state estimation. We explore:\n", "\n", "1. **Bootstrap Particle Filter** - The fundamental SIR algorithm\n", "2. **Resampling Strategies** - Multinomial, systematic, residual, stratified\n", "3. **Degeneracy and Effective Sample Size** - Diagnosing filter health\n", "4. **Effect of Particle Count** - Accuracy vs computation trade-offs\n", "\n", "## Prerequisites\n", "\n", "```bash\n", "pip install nrl-tracker plotly numpy\n", "```" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ PyTCL particle filter modules and Plotly imported successfully\n" ] } ], "source": [ "import numpy as np\n", "import plotly.graph_objects as go\n", "from plotly.subplots import make_subplots\n", "import time\n", "\n", "from pytcl.dynamic_estimation.particle_filters import (\n", " bootstrap_pf_predict, bootstrap_pf_update, bootstrap_pf_step,\n", " resample_multinomial, resample_systematic, resample_residual,\n", " effective_sample_size, particle_mean, particle_covariance,\n", " initialize_particles, gaussian_likelihood,\n", ")\n", "\n", "print(\"✓ PyTCL particle filter modules and Plotly imported successfully\")\n", "\n", "np.random.seed(42)\n", "\n", "# Plotly dark theme template\n", "dark_template = go.layout.Template()\n", "dark_template.layout = go.Layout(\n", " paper_bgcolor='#0d1117',\n", " plot_bgcolor='#0d1117',\n", " font=dict(color='#e6edf3'),\n", " xaxis=dict(gridcolor='#30363d', zerolinecolor='#30363d'),\n", " yaxis=dict(gridcolor='#30363d', zerolinecolor='#30363d'),\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Bootstrap Particle Filter\n", "\n", "The bootstrap particle filter (Sequential Importance Resampling) represents the posterior distribution using a set of weighted samples:\n", "\n", "$$p(x_k | z_{1:k}) \\approx \\sum_{i=1}^{N} w_k^{(i)} \\delta(x - x_k^{(i)})$$\n", "\n", "### Algorithm:\n", "1. **Prediction**: Propagate particles through dynamics\n", "2. **Update**: Compute likelihood weights\n", "3. **Resample**: Eliminate low-weight particles\n", "\n", "### Example: Highly Nonlinear System" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "State range: [-24.8, 19.4]\n" ] } ], "source": [ "# Nonlinear state-space model\n", "def f_nonlinear(x, k):\n", " \"\"\"Highly nonlinear dynamics.\"\"\"\n", " return x / 2 + 25 * x / (1 + x**2) + 8 * np.cos(1.2 * k)\n", "\n", "def h_nonlinear(x):\n", " \"\"\"Nonlinear measurement.\"\"\"\n", " return x**2 / 20\n", "\n", "# Noise parameters\n", "Q = 10.0 # Process noise variance\n", "R = 1.0 # Measurement noise variance\n", "\n", "# Generate true trajectory and measurements\n", "n_steps = 100\n", "true_states = [0.0]\n", "measurements = []\n", "\n", "for k in range(n_steps):\n", " # Propagate state\n", " x_new = f_nonlinear(true_states[-1], k) + np.random.normal(0, np.sqrt(Q))\n", " true_states.append(x_new)\n", " \n", " # Generate measurement\n", " z = h_nonlinear(x_new) + np.random.normal(0, np.sqrt(R))\n", " measurements.append(z)\n", "\n", "true_states = np.array(true_states)\n", "measurements = np.array(measurements)\n", "\n", "print(f\"State range: [{true_states.min():.1f}, {true_states.max():.1f}]\")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initialized 500 particles\n", "Initial ESS: 500.0\n" ] } ], "source": [ "# Particle filter parameters\n", "N_particles = 500\n", "\n", "# Initialize particles from prior\n", "particles = np.random.normal(0, np.sqrt(10), N_particles) # Initial uncertainty\n", "weights = np.ones(N_particles) / N_particles\n", "\n", "# Storage for results\n", "pf_estimates = [np.average(particles, weights=weights)]\n", "pf_variances = [np.average((particles - pf_estimates[0])**2, weights=weights)]\n", "ess_history = [effective_sample_size(weights)]\n", "\n", "print(f\"Initialized {N_particles} particles\")\n", "print(f\"Initial ESS: {ess_history[0]:.1f}\")" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Minimum ESS: 2.0\n", "Resampling events: 78\n" ] } ], "source": [ "# Run particle filter\n", "ESS_threshold = N_particles / 2 # Resample when ESS drops below this\n", "rng = np.random.default_rng(42)\n", "\n", "for k, z in enumerate(measurements):\n", " # Prediction: propagate particles\n", " particles_pred = np.array([f_nonlinear(p, k) for p in particles])\n", " particles_pred += np.random.normal(0, np.sqrt(Q), N_particles)\n", " \n", " # Update: compute likelihood weights\n", " z_pred = np.array([h_nonlinear(p) for p in particles_pred])\n", " likelihoods = np.exp(-0.5 * (z - z_pred)**2 / R)\n", " weights = weights * likelihoods\n", " weights = weights / np.sum(weights) # Normalize\n", " \n", " # Compute ESS\n", " ess = effective_sample_size(weights)\n", " ess_history.append(ess)\n", " \n", " # Resample if needed\n", " if ess < ESS_threshold:\n", " particles = resample_systematic(particles_pred, weights, rng)\n", " weights = np.ones(N_particles) / N_particles\n", " else:\n", " particles = particles_pred\n", " \n", " # Store estimates\n", " mean = np.average(particles, weights=weights)\n", " var = np.average((particles - mean)**2, weights=weights)\n", " pf_estimates.append(mean)\n", " pf_variances.append(var)\n", "\n", "pf_estimates = np.array(pf_estimates)\n", "pf_variances = np.array(pf_variances)\n", "\n", "print(f\"Minimum ESS: {min(ess_history):.1f}\")\n", "print(f\"Resampling events: {sum(1 for e in ess_history if e < ESS_threshold)}\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "hoverinfo": "skip", "line": { "width": 0 }, "mode": "lines", "showlegend": false, "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x", "y": { "bdata": "xcY3UYeEGUCX+WM/DWg0QKcLw7+EhjRAWu+6EuEEGEBgNPQVQQE1QJcgNq1mOC1AzmbZ+tsTLUB+RquR6IIsQCum45zZMhhArQri6ZsKN8C6u2eH/SouwMp62FoEugZArKUz1sl7M0C81ypSy5UjQL4zEmjk8hlA05MaGxfVIECib45iGJcRQNOPLmrsMRBApzTbNgIoKECnMmBaZPIKQPdmsuMJpyFAvvF6e4fRGkC61EHJCRITQBLLHMgU+j9A/9z56DtvMMCFq8OKAbQqwFcVEyAw9wrA7cBUr4kWGUB8PzWEMVkhQBb/WF5qaR7ArV6nvyldLsAQcJcRYsMjwAD/y75CvA9AnWZePLLoE0D6IhWMNVkbQJmJivprAQxAtZKRywtUI0BKNWZ9GNc5QIJgW9oJYTJA3xedN413KkCFoKriQggLQDSlpmmbgCBAWL+ZtRCZ+z9MCpBfdCwnQL+D7zqDMDpALj/kXAKdGkAli6Au7F8wQICUMJEQqS1AzefPgk5vDkCl07fovRwaQCANideZ2/2/JrJB1vqnM8CSf0dmSC8mwPl6UzXEgxNAUKAxSkPEC0CUG2GRfdIgQE7CGbLDDTjAxjgJZBJFMMBcoZSYcNXwP5qPdy4pMQ1Ajic81h6HMkDJRUI0WMYlwCW+fs9iyBLANsYlA6UQBUA46oLgWfMnQAy3Q0HrGyNAWUuhVVe+McC/DKm3caszwGbtId03MyfAxklCKAwTEkC0FD+3P2UNQMaf47gf5hvAhnFSoJq4M8BjofLDp0QjwLDDEnycHRBA2geKEXl/C0B0OQYHMpQHwDhNJlqUUDLAVgpkSYn0I8BVnKf93IkSQHV11w0IxTZAdTp8G9gNJUDaB5ql0n8VQOoksJO67SxARW/Jv7NNMkAmNYSgUKgwQICja9R4AyNA5pKtkcd0FEB45xkeuI38vxAwRXV6Vf6/3fWkogLYKUD9AAl4DDkiQEgdb1aIthxAnMpLTpcLMsD+G1MSM6QYwFallUSLjA9AJFESey3iJEAqVFmk+TUOQMVI+azP6ivAXOVi3gv2J8DgM9+u/54JQA==", "dtype": "f8" }, "yaxis": "y" }, { "fill": "tonexty", "fillcolor": "rgba(0, 212, 255, 0.3)", "line": { "width": 0 }, "mode": "lines", "name": "±2σ", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x", "y": { "bdata": "DW/ys8RkGcAFtdxQW4AtwIDfZvmFbhNAsMrKEy/SCMD0BEZHUtY0wO1jBL00dCzApBOzAS+xJkC22w301w0mQJ7O8FymXQLAbaXh0/8vOMCDfkpitKoxwFPjOZfkZhjAZJQTzN/oMUBdyEMg2uIRQD6P0XSZ2R/AVnN1wCXOOsBtLFp3jDQtwOAcj9MvEw/AOXLeyp6vJ8CvPgW4du8LwBaz4XQDDBfAqxOG9rsZIsBAFLFfFgIWwCyNe3qabyvAjSMWjuGUMsBEFmYiaAgwwNFZVRW7RyPAAc6J44hqFMC0I0lMCiQswKIzHM5MDTTAqQda/JWQMcDcxKP8zqMqwN4CYjDo2BPAcrq61ZEqJMCaZnUyAOY2wEa+PFt2fhPAKE2npXwlOMDwlrzHfvf0v9MXlThpvxLAHrbKNuE+EcB7LahAi2gMwEgX69DMMzvAwsLFM0gtF8B228tSgTg2wLmMtc1nlTfAev/FH5IzEcA0Gp9bjmAYwJ3WmXOcIRPAw8ZCSl+eDcAaV5j56yUiwOjF7rJkQjfAGNlqWXJjNcAk1SByF7crwCGeUYFcEBnAdBN//dquG8BQuXE3XMUbwCQHu9CunDnAfsYkKgWLMsB+j7U4gxwgwCNx2Fvv2RHA6oyXG/paN8C52wbBEJErwMJRfB4EnCPAZlUt/ekpJsAsZJjq3N4kwDQ9Hn28OCTAxVGEWEn9M8CZdid16ZQ1wAI4LA85xCzAzPVt1rLCFMCSD+nGNzMZwCrbrLiGHTTAFr/zk1XXNcCPKnBJdwUqwP4VxgoraRLAb88M1DB1HsAIkm1kuoA5wDb9l/y6eDTAiK/JQwnJKsClN+EHpBIfwITnr122lBXAv/6sQeZWBsBSzGL3SdUUwE6cV9dvmSHAovbKQPUDKUD0DUXd72csQIteiVOZXxBAMkoMV+HvEsCiz4eL/kM3wNaZMCOZOCfAvoZHALzKF8AF0/kdxWgiwG6vDrnHxhHArOBUAMUlNMChIGVFuXclwKDT9YRLVQ7A6C2UMmR1McD67nWYt3sywABSVef5cTDA6tvmWhNzLcC47VNRiugRwA==", "dtype": "f8" }, "yaxis": "y" }, { "line": { "color": "#00ff88", "width": 2 }, "mode": "lines", "name": "True state", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x", "y": { "bdata": "AAAAAAAAAAD9AEoXOSQjQJEnujz1oShAo0v+i7eP+D9moIKuzQokQA6blirHyBpAe3+nm5puKkDDNV3f3z0sQFir/fS/Lum/jQRm15ufN8B3F4AdT0sxwHyul5yww/Q/LPdpJYZlM0Cub2sOJLwcQOhQEmZ7WRLA5Ocj9HopKsBUqH5mQj4UwCquAqdQ28c/Sme0jddwHUBOYQyVxOnRP+QmM5wdwAjAZhTOL4ETCcAIJrDELwrzv0buZpCXYi7ARjtch7zwMMBnjvTUWowvwIzqcYqz0xzApI5RWM1U9b8ue53eAOkTwIp2Q3VBJi3AAIvyGhNtL8BI8PI70/UnwEyrLPHFJRLAVrKrhKOP8j90g5GZ2D4kQP6pUd+ACeU/oUhniZt+I0D8o76aFfIpQE6hQcxGuB1ArH8/FhquG0AT3U8qDuLSv2BbGXNmfynAgPFwBxSEzD/yUMPzw5MlQPd5DHAl6yhAAHzfKOYEl79Nlrq1DJobwFy/pAY+0x3A9A1UTQhm1r+QXsft5n4IwAvo4llA2ivApjrQIh3YNMCFAeHXmWEqwMZNdXqWlvu/ADBIeD9zej+ZvRjxA+8AwI2OMKtoxzjA6ddkRPPPL8Cvw14yb1gQwOQqWVvgH+u/eu8OtNIaJMBbxQ5wX3MpwLwJP+m/JiHA6qNfjTEp77++ha8y5I0gwOQkcOCJ0RXAy2+383btMsD/EQJpk3U0wNSeV0V9JSjAsfkz6cDFBMDRs+KqxOoMwBpXI4rY5ynAGn1cbKPqNMBugz+zBY8lwP6vPVmagRDAoBwcP4uT6L/Z1gbzsoEuwAiLHLe2XzPAi9qfSjR4KMAdWz+NfocQQAnrlfBmpiVA7ec2pwZqCUCzB1hyqjgSQB2Gwj2tPBlAJkLeacPKLEA47vqXmbkvQOCnLaTeAB5AUp7dIafh1b+GdNfQBNkpwHN25NfbMxbAAPLhYXIomT8b/vHAV28WQMPGnS1orADAHS9Vyf24MsA+NuXF3zchwPLTWRbNlgHANiH5Z0HoG8Cgwwc+aMIkwFPJtxKg2S3Ax5L2S2MuK8CscHZJyQX8vw==", "dtype": "f8" }, "yaxis": "y" }, { "line": { "color": "#00d4ff", "dash": "dash", "width": 1.5 }, "mode": "lines", "name": "PF estimate", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x", "y": { "bdata": "FLhXRZ3Cjz9TfNZbfp8GQIfDHD4mYilAAxSrEZM39z/9tRdXZ3e1PyqVNwY+hsg/OT1GfoXiKUAakdxCYEgpQLl91twMCP4/Ddjh3k2dN8AwLv+SGWAwwN1Lm9PEE/q/CJ0j0VSyMkDru0xiOIccQARu/TLUmue/bSnoMppjIsCc9BJGAGkUwFYs4AyQCrU/TJsw/9oYvj8cgaG0S6Kvv681BqUghPg/Lmsi4+DD8r8p/HmzZIDXv3wE34pHQiJARgCIuw6CMcAH7MfnaGItwCcfWh2HBRrAsssrLwOw4j9vyCeQsZUFwGdzsmWnpyvAgNsWbpVfMMB2mh2HmDMnwPIa4Ic21t+/Rg4Xb3FsBMDbHXDPsg8gwOXl3XcB9+W/mge9f+32HMDba+qQoIcoQBs1bBhfYhtA0Lw3nBzYEUBlz9jfhQS2v67EFxx/8yLA7FJfBgRHAMCfrAdGjkQVwDW4z2nb2PQ/aH88euDS8j8wiXEvkY8UQDGpY1dCGBRAXCGkEecdqj8gtfEUNF70v7pWZ1AeICnAn0XWl7aFNMBbKjTsL/MowJ6M+C9hMua/l4bMsHKZ+79i90Gte37nP7lkakE51TjAov8WxwtoMcCm9kVLqgMMwK9K5STWCtq/cpVtFW1PA8DBkKR6tKsowNSwO4Y1AB3A2ONjvMDlEMBcMFSv56PoP41iqL0TzdG/j84SV9DdMsCsQWiWLaA0wLQSJ3a4+ynAK2BdcTV91b9vCpPWLwH1vxvD5aYOFyvAThgjGvjHNMD5ZbGGD6UmwGySmnV0XNK/gstHS3S1AMA3WU6lQHMswDclX6unZDPA79yWRsleJ8CgNnMUjhH5v5R7a3baXyFAi/UhFj3wDkD/cOfGFVG1PzcRsXiVqAZAluouYK7PLkAgvCaPSNwuQMVSMH5FMxtARosUqmNOyD8abmkN2gwpwNg/2XFIAxvA/GQCRUnlC0BdBGn4Utynv7LbwDqB3/U/pFVQJ64YM8BQV0dn6eQgwFcb/fn7c7M/WRUs1DURDMDpiNXH8GkdwGP20b1hZy7Ao+CknI+0KsAhT5HnKWTkvw==", "dtype": "f8" }, "yaxis": "y" }, { "line": { "color": "#ff4757", "width": 1.5 }, "mode": "lines", "name": "|Error|", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x2", "y": { "bdata": "FLhXRZ3Cjz/Qw6gAs/gaQMB+UywgBtg/AHozpUeCtT/6cNTf3t8jQGXeZDqVBBpAQEgorKOC0T9IJQXk/Kv3P7Kpqmu2TwVAAABkIcRvgj/gKB1QsWbtPyx9Gbi6awdAgETLiCpm5j+A4VkP1nWqP09GZb9BzA5A3PnuBIMXD0AAJCbK716lP/4vJUERrLo/3aS3IXT4HEBykaALDt7VP94gW/cWgRJAnr15fCFj/z/8TaMvLVTqP2H5oo1vUjhAAKB4hUYq4j8AE2Vpj0/xPyhbvmhjceY/fXTn786s/j/tLRMtUDwCQDAyEPmg6ec/AMCyE3wh5T9AuqqaVkfYP52prohiKBBAcedsMUO0DUCo0IC0RScyQPLHlytBgPU/N+aiJAn9MEAQgkOdUKfmP5hhq549r+I/uIUP9PqrA0B0UrNk2cHKP8haBlydLwpABGLWRkUPAkChk2OLBRswQPCC0gIKUCZAWP3fEvQu8z++D5byzhQoQEY0BC/A9ShAIJKIL8Wp2T8ACJ3GmZ/8P4iK3EsQ0fU/wEF9vqKZ1D+gcs26nubmP3cHeeJlffA/x85E8OWz+z9yO2ncos4GQABYrHMsoas/2DpJTiIB+D/gQt5l0LTiPxkLzZHqNNw/OxRn3e6NHkBAk0atXvXYP5CKCTEpNfU/tt5vFTUBCkDEyKStIhgiQLuelaS4tBRAAHhCSTlNrz+A1hezFk3FPwA+9wyzY+0/rE0IOxoWAkCaLpm/LGoCQBDAJsxh8+I/AGayHKlVwT+wKB43nWDhP64NyCOmtw5AtIgB9yIh9T8Q7cNtknPwPwC8aArRw5M/wNmPQLCW4T/FKFwS4ssWQNS9qegxGgFAeDasu9kY5j/vaTwbZuMRQAP70wLF0AtAgEOFslcn8D8AQ4YaIarbP9io6i/JbOY/+vFze2wE4T+AzcBtWIXZP5Ql02eyPfM/GKE+YPiyC0Ak0OJmEJ8WQJw0/soonAtAwKHJfhfs1z+Au3enl73EP828KfZsMgJAEy3G+0y/C0Cu/HNovzUIQACiRWM1uNE/AIls1Ot0zj8cya1VtNPxPw==", "dtype": "f8" }, "yaxis": "y2" }, { "line": { "color": "#a855f7", "width": 1.5 }, "mode": "lines", "name": "ESS", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "xaxis": "x3", "y": [ 500, 34.66700159943149, 6.063222776799575, 69.10161187752433, 392.95589024149507, 18.85501389860079, 263.04433959790873, 35.23467316387982, 159.43357151430993, 299.2296913007884, 2.046629607026214, 110.4727582942472, 379.3013730270607, 16.766073980737225, 252.6975459360286, 219.48656334701812, 90.6460329876705, 120.52061190016089, 347.198629166067, 111.15650539032345, 194.90900957048987, 172.5984416703741, 126.85398933252038, 277.48856889965043, 34.099871420909736, 19.166641727026065, 117.522380182327, 260.27870619545234, 245.19807626090432, 171.83713963948787, 79.10598669145423, 105.30399639966747, 167.35065739029022, 445.05876493717557, 227.71420728316807, 153.18355970346684, 56.72712879909994, 101.39051028428435, 24.471770604527112, 24.022640492642008, 313.3359960533777, 191.29525937564503, 56.39685173842977, 239.8896337798658, 30.362786927397398, 40.98910761795796, 255.3149463797072, 47.70028774103464, 242.46606060047017, 54.421807142296515, 201.42544881249103, 78.41679178263283, 38.06431176578815, 173.68172439154844, 467.4194767846972, 201.27929622090696, 84.67976007098144, 4.7989190857140205, 104.77010950094775, 410.2208071875959, 260.31772279442305, 81.40148567892123, 64.97156177100449, 53.005378353051526, 278.89183566751996, 89.41697949982316, 224.2121239031672, 30.02314085776909, 65.5516726763482, 88.06380844111806, 454.37851294619674, 108.27799781308507, 79.49274607476903, 33.21658326878187, 196.82526156295802, 435.44591999776907, 174.13269133838193, 67.8653021673019, 75.8391257216388, 174.77877312367224, 442.65154150161544, 23.458698542957503, 305.1514570202997, 244.1726958402356, 158.30653688369588, 57.263782618589524, 117.1159549365067, 147.79876450187507, 475.3847193820495, 44.676764231609624, 251.22054528442075, 60.92886112218478, 133.1868522915348, 207.20566790964267, 25.43699059470767, 198.19333700984072, 380.3755940201802, 92.30313815725603, 153.3080298216202, 136.84675149227664, 113.75243626518386, 374.416485032615 ], "yaxis": "y3" }, { "line": { "color": "#ff4757", "dash": "dash" }, "mode": "lines", "name": "Threshold (250)", "type": "scatter", "x": [ 0, 100 ], "xaxis": "x3", "y": [ 250, 250 ], "yaxis": "y3" } ], "layout": { "annotations": [ { "font": { "size": 16 }, "showarrow": false, "text": "Particle Filter State Estimation", "x": 0.5, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" }, { "font": { "size": 16 }, "showarrow": false, "text": "Absolute Error (RMSE: 5.136)", "x": 0.5, "xanchor": "center", "xref": "paper", "y": 0.6333333333333333, "yanchor": "bottom", "yref": "paper" }, { "font": { "size": 16 }, "showarrow": false, "text": "Effective Sample Size", "x": 0.5, "xanchor": "center", "xref": "paper", "y": 0.26666666666666666, "yanchor": "bottom", "yref": "paper" } ], "height": 700, "legend": { "x": 1.02, "y": 0.5 }, "showlegend": true, "template": { "layout": { "font": { "color": "#e6edf3" }, "paper_bgcolor": "#0d1117", "plot_bgcolor": "#0d1117", "xaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" }, "yaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" } } }, "xaxis": { "anchor": "y", "domain": [ 0, 1 ] }, "xaxis2": { "anchor": "y2", "domain": [ 0, 1 ] }, "xaxis3": { "anchor": "y3", "domain": [ 0, 1 ], "title": { "text": "Time step" } }, "yaxis": { "anchor": "x", "domain": [ 0.7333333333333334, 1 ], "title": { "text": "State" } }, "yaxis2": { "anchor": "x2", "domain": [ 0.3666666666666667, 0.6333333333333333 ], "title": { "text": "|Error|" } }, "yaxis3": { "anchor": "x3", "domain": [ 0, 0.26666666666666666 ], "title": { "text": "ESS" } } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Visualization\n", "fig = make_subplots(\n", " rows=3, cols=1,\n", " subplot_titles=('Particle Filter State Estimation', \n", " f'Absolute Error (RMSE: {np.sqrt(np.mean((pf_estimates - true_states)**2)):.3f})',\n", " 'Effective Sample Size'),\n", " vertical_spacing=0.1\n", ")\n", "\n", "time = np.arange(len(true_states))\n", "\n", "# State estimation\n", "fig.add_trace(\n", " go.Scatter(x=time, y=pf_estimates + 2*np.sqrt(pf_variances), mode='lines',\n", " line=dict(width=0), showlegend=False, hoverinfo='skip'),\n", " row=1, col=1\n", ")\n", "fig.add_trace(\n", " go.Scatter(x=time, y=pf_estimates - 2*np.sqrt(pf_variances), mode='lines',\n", " fill='tonexty', fillcolor='rgba(0, 212, 255, 0.3)',\n", " line=dict(width=0), name='±2σ'),\n", " row=1, col=1\n", ")\n", "fig.add_trace(\n", " go.Scatter(x=time, y=true_states, mode='lines',\n", " name='True state', line=dict(color='#00ff88', width=2)),\n", " row=1, col=1\n", ")\n", "fig.add_trace(\n", " go.Scatter(x=time, y=pf_estimates, mode='lines',\n", " name='PF estimate', line=dict(color='#00d4ff', width=1.5, dash='dash')),\n", " row=1, col=1\n", ")\n", "\n", "# Estimation error\n", "error = np.abs(pf_estimates - true_states)\n", "fig.add_trace(\n", " go.Scatter(x=time, y=error, mode='lines',\n", " name='|Error|', line=dict(color='#ff4757', width=1.5)),\n", " row=2, col=1\n", ")\n", "\n", "# Effective sample size\n", "fig.add_trace(\n", " go.Scatter(x=time, y=ess_history, mode='lines',\n", " name='ESS', line=dict(color='#a855f7', width=1.5)),\n", " row=3, col=1\n", ")\n", "fig.add_trace(\n", " go.Scatter(x=[time[0], time[-1]], y=[ESS_threshold, ESS_threshold], mode='lines',\n", " name=f'Threshold ({ESS_threshold:.0f})', line=dict(color='#ff4757', dash='dash')),\n", " row=3, col=1\n", ")\n", "\n", "fig.update_layout(\n", " template=dark_template,\n", " height=700,\n", " showlegend=True,\n", " legend=dict(x=1.02, y=0.5)\n", ")\n", "fig.update_yaxes(title_text='State', row=1, col=1)\n", "fig.update_yaxes(title_text='|Error|', row=2, col=1)\n", "fig.update_yaxes(title_text='ESS', row=3, col=1)\n", "fig.update_xaxes(title_text='Time step', row=3, col=1)\n", "\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Resampling Strategies Comparison\n", "\n", "Different resampling methods have different variance and computation properties:\n", "\n", "$$\n", "\\begin{array}{l|l|l|l}\n", "\\text{Method} & \\text{Variance} & \\text{Computation} & \\text{Notes} \\\\\n", "\\hline\n", "\\text{Multinomial} & \\text{High} & O(N \\log N) & \\text{Simple, but high variance} \\\\\n", "\\text{Systematic} & \\text{Low} & O(N) & \\text{Single random number, best balance} \\\\\n", "\\text{Residual} & \\text{Very Low} & O(N) & \\text{Deterministic + random hybrid} \\\\\n", "\\end{array}\n", "$$\n", "\n", "### Key Properties\n", "\n", "- **Multinomial**: Most straightforward approach but suffers from high variance\n", "- **Systematic**: Uses a single random number for all particles; preserves good diversity with low variance\n", "- **Residual**: Combines deterministic copying of high-weight particles with random resampling of the remainder\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Multinomial : RMSE = 4.7336\n", "Systematic : RMSE = 4.9128\n", "Residual : RMSE = 4.7257\n" ] } ], "source": [ "# Compare resampling methods\n", "def run_pf_with_resampler(resampler_func, name):\n", " \"\"\"Run particle filter with specified resampling method.\"\"\"\n", " np.random.seed(42)\n", " n_pf = 500\n", " particles = np.random.normal(0, np.sqrt(10), (n_pf, 1)) # Shape (N, 1) for resampler\n", " weights = np.ones(n_pf) / n_pf\n", " estimates = [np.average(particles, axis=0, weights=weights)[0]]\n", " \n", " rng = np.random.default_rng(42)\n", " for k, z in enumerate(measurements):\n", " # Predict\n", " particles_pred = np.array([[f_nonlinear(p[0], k)] for p in particles])\n", " particles_pred += np.random.normal(0, np.sqrt(Q), (n_pf, 1))\n", " \n", " # Update weights\n", " z_pred = np.array([h_nonlinear(p[0]) for p in particles_pred])\n", " likelihoods = np.exp(-0.5 * (z - z_pred)**2 / R)\n", " weights = weights * likelihoods\n", " weights = weights / np.sum(weights)\n", " \n", " # Resample\n", " ess = effective_sample_size(weights)\n", " if ess < n_pf / 2:\n", " particles = resampler_func(particles_pred, weights, rng)\n", " weights = np.ones(n_pf) / n_pf\n", " else:\n", " particles = particles_pred\n", " \n", " estimates.append(np.average(particles, axis=0, weights=weights)[0])\n", " \n", " rmse = np.sqrt(np.mean((np.array(estimates) - true_states)**2))\n", " return np.array(estimates), rmse\n", "\n", "# Run with each resampler\n", "resamplers = [\n", " (resample_multinomial, 'Multinomial'),\n", " (resample_systematic, 'Systematic'),\n", " (resample_residual, 'Residual'),\n", "]\n", "\n", "results = {}\n", "for resampler_func, name in resamplers:\n", " est, rmse = run_pf_with_resampler(resampler_func, name)\n", " results[name] = {'estimates': est, 'rmse': rmse}\n", " print(f\"{name:12s}: RMSE = {rmse:.4f}\")" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "line": { "color": "white", "width": 2 }, "mode": "lines", "name": "True", "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "y": { "bdata": "AAAAAAAAAAD9AEoXOSQjQJEnujz1oShAo0v+i7eP+D9moIKuzQokQA6blirHyBpAe3+nm5puKkDDNV3f3z0sQFir/fS/Lum/jQRm15ufN8B3F4AdT0sxwHyul5yww/Q/LPdpJYZlM0Cub2sOJLwcQOhQEmZ7WRLA5Ocj9HopKsBUqH5mQj4UwCquAqdQ28c/Sme0jddwHUBOYQyVxOnRP+QmM5wdwAjAZhTOL4ETCcAIJrDELwrzv0buZpCXYi7ARjtch7zwMMBnjvTUWowvwIzqcYqz0xzApI5RWM1U9b8ue53eAOkTwIp2Q3VBJi3AAIvyGhNtL8BI8PI70/UnwEyrLPHFJRLAVrKrhKOP8j90g5GZ2D4kQP6pUd+ACeU/oUhniZt+I0D8o76aFfIpQE6hQcxGuB1ArH8/FhquG0AT3U8qDuLSv2BbGXNmfynAgPFwBxSEzD/yUMPzw5MlQPd5DHAl6yhAAHzfKOYEl79Nlrq1DJobwFy/pAY+0x3A9A1UTQhm1r+QXsft5n4IwAvo4llA2ivApjrQIh3YNMCFAeHXmWEqwMZNdXqWlvu/ADBIeD9zej+ZvRjxA+8AwI2OMKtoxzjA6ddkRPPPL8Cvw14yb1gQwOQqWVvgH+u/eu8OtNIaJMBbxQ5wX3MpwLwJP+m/JiHA6qNfjTEp77++ha8y5I0gwOQkcOCJ0RXAy2+383btMsD/EQJpk3U0wNSeV0V9JSjAsfkz6cDFBMDRs+KqxOoMwBpXI4rY5ynAGn1cbKPqNMBugz+zBY8lwP6vPVmagRDAoBwcP4uT6L/Z1gbzsoEuwAiLHLe2XzPAi9qfSjR4KMAdWz+NfocQQAnrlfBmpiVA7ec2pwZqCUCzB1hyqjgSQB2Gwj2tPBlAJkLeacPKLEA47vqXmbkvQOCnLaTeAB5AUp7dIafh1b+GdNfQBNkpwHN25NfbMxbAAPLhYXIomT8b/vHAV28WQMPGnS1orADAHS9Vyf24MsA+NuXF3zchwPLTWRbNlgHANiH5Z0HoG8Cgwwc+aMIkwFPJtxKg2S3Ax5L2S2MuK8CscHZJyQX8vw==", "dtype": "f8" } }, { "line": { "color": "#00d4ff", "dash": "dash", "width": 1.5 }, "mode": "lines", "name": "Multinomial (RMSE=4.734)", "opacity": 0.8, "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "y": { "bdata": "b4oDwIEklj8Wjp2sLusMQJW4qtQ5xipAv4MEgYD1+D9g10YtREgCwLmS+9pqaPu/RuJoChNtKUDgaIN3S14pQJqOdsorqPs/vniH0JTsN8DguytiwlAwwFSdBINms/m/gIM9gfl3MkBY7Y7sMIscQIGrCNmxjem/8p/8IviYIMCKrZoA6BASwG8MQRIEML0/ucCN62OF07+zk9xOUwyzv/zpTIzC4P4/dH7+zc123b+sXb1YLfW9P22XJRmoQhhAgqKE55Q9McCYiqT8DXktwJ5XFdHwwxnAMAJ+pVhm4D8fUKDbrrEIwD7N9/iHXizAypgP5jpMMMCaMmEiNWwnwCaRub0tsNq/xv4q0oE6CsAUU4yo8pUgwHHbrgEx17e/E1dc3cWB9r8oCi/xLfQrQIAl15kiWSJA/pg1Db55GECgE7eY/RC9vyl7W2VGPyTAjgS2j3/E+7/EcTsNHkcCwFFcFxG+yhlAE+LMXT4l9j8WLioQOg4TQAi9Q35zVBJAgMfNI3bI4z9MNjeBnUz4vx0ttKaHvSrAF/oV4oaBNMCZeD2tkucowLte2yTtu+e/9+xCDA6k+L+e9mT0/QrgP22M+BJgcznAA8+BB+dKMcCd3xSb+SALwHvWfPH9bde/41yyUMWh9b/ZIjFzDvcnwNSrXb4EOB3Asin2sV4bEsB8x6zo2UJvP0SKQ29QAde/OHKFL7nnMsCbTmTkA6M0wO3C1lCV+SnAB9ThvZOO2L+MszaoYyEAwJM/q6yJeSrAvf8/2ZLTNMDBhdp8M+EmwLIy1XUCa8u//uIKESrI/r+Zb4d6E6orwOUGdgeVfDPAVCDL5p88J8C4Cb8Rm5j2v48Qjd6XNh1AelhzJlWlDkB5/jj00dXbv59B+vK77AdAyk1DqL/1LkAwoVDiULYuQOBfTIl+GxpA6nRZSWUuuT+1ILaHb4oqwFfQMSC4dRvAG/wQlBCVBUC0URf7kXPSv2cRM/SWNfk/XBpiAskiM8AEdLVl/O8gwHGjdDnTRb4/m2UXn2ILEcACj51NYd8gwJpeMiq3Ri7A6dmebo7+KsBy5NDykjHqvw==", "dtype": "f8" } }, { "line": { "color": "#ff4757", "dash": "dash", "width": 1.5 }, "mode": "lines", "name": "Systematic (RMSE=4.913)", "opacity": 0.8, "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "y": { "bdata": "b4oDwIEklj9LupfzL+wKQLm9NF8eNCpAHwFniopA+D/r27SEOe0HwIJmneGumf+/FdKmC0f2KUCduOvP5VApQKt5lLjmffs/D48Ln0pvOMCS6fAbb1wwwH+PSN9Hkvm/gMhEXM6fMkDBwl+pd5QcQN+ydayX/fG/RAVUD101I8CpKLusZo4VwCBYo2URVso/LtO4tpxC0r9C9u3pxY+HvwUCvFtRwwRAj/xHHMEK6L8IH8901EGCP5zyN0Q0ISNAqOaLbSJZMcCfbdIMHGwtwFin3m6ayhnAhOdywiTX4T/B/YLeCHgHwNBZpUV4NyzAwhzPn3tZMMBBAUPmGIQnwDJnRrDH+du//T8KTUXMCcCdb96Tg2oiwHgeQJmdwtK/jmb5TCr/D8A878fdlQosQD070vPcWCJAQp48X+14GEAAV8wf+mjFv8dE9B22wSPAv90Ed8jf+78f5RmwlM8DQFrnPrBjbyJAs1bYovAK9T9hz4pGSiISQKMv8tQMgBBA+n2GuIXQ4T9IQgePLxPzv18o+Uxq6SrAZJRE3slmNMBqlX+dpuQowHwx+PT12ue/EZM5LEbP978gzVrj17vlP/m5fnk1rTjACJHb/x9QMcDmwRz3fS8LwP8gu8aQotK/sbCLS5AE9L8Hk6rozMMnwG1ojtVs/RzA6UCeMrizEcAniPEOLkjVP1bFwolihfG/Kop/G6TiMsDF57jGz6I0wLZ53WMnEirAP/4z+Pk817+bZaCw4nr4v6Du1IXv2CrAYDXRpHrZNMAhe6Sn/8wmwO9Re/oH4Ma/rJZmOgMA/7+eIjtL5hktwLUpOL8cczPAVw8V7YJoJ8C12zzwU/n2v7ZSBjxImhVAOSUqWFHsCECQHEqaJfDXvy4I5I/CWwFAJFoRio/ZLkC4Vdji5cMuQNdwKH+KYhpAiyQnoP/xtj/awI37H/oowNT7r5RWqBrAVXJup53lBUDtTjNcK5nfvy+oVTuEI/g/PH8Cm8UXM8D6bqzhT/ggwIAUPhdZ274/P+1qSXTOCMB29d+YthodwKHUfveJOy7AN7g+zcPqKsB0kDCIgmvqvw==", "dtype": "f8" } }, { "line": { "color": "#00ff88", "dash": "dash", "width": 1.5 }, "mode": "lines", "name": "Residual (RMSE=4.726)", "opacity": 0.8, "type": "scatter", "x": { "bdata": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2Q=", "dtype": "i1" }, "y": { "bdata": "b4oDwIEklj/6wXE5KwwMQBJluT9asilAfztOs1qJ+D+XTXVZpKUKwMRfrETfXAXACbLf8whTKUBQJqqIDXwpQMbKWbpGx/s/QyBCSynuN8DAFHP5pVUwwPo4XhYCzPm/Qba35nSZMkDbF0EG6IscQMbC2XsOoO+/lRlJnQskJMArs7+dWKoUwK66R+3BDMc/9ccb9Flbqj9hRIyfty+mvyW1wyeyAAFAZ4UDtBhP0b8EhFppoU3Ov3yrQdwleR1AXrSff493McDN+2N8BWMtwPKFwr1lyhnA9m4xZpc94j8NpSWl+1EIwPibWAmwrCvA0X7iEjdMMMAlZQulAGgnwKA+uWvSUNu/Qfb9VLKhBsBw0WurejUiwD2OIpoRytS/BSlhy0AVCMC1QpgEUBQsQA9NE3lOISJAEcVjdnttGEC5JzS0YZHAvyicqnyLLCPA+wpasRfK/L/wLPPRZe4NQHmEr2XLGSVAfx3Z6Sb18z8Gn1m+jJEPQLbNfh0uzQ5ABLkHNVwg3D+3eNUNV7kKwLQ05EvhNyvAYDWyTi+CNMDtBRv6eOcowD+7XLoy4ee/5XQTbdYR/L+D0RPxZO/nP+xk9y5DtDjAJZ9qbvlKMcDCRa98OjoLwLK9jiYsvs+/jUPqtd0p8b/3aRyD/donwC3rvf9KWx3APasjrRsHEsBwuvmpkHLdPw7ESBO8ieu/oljSXLHQMsA32YIvZJk0wNDVESkYIyrATyMCtroT2L+GVEF7Mzr7vyWK9g3FoSvAzfyNcG3XNMBD7dCbit8mwObirxpej8i/aKgeobah/7/nSwlci1MswFoBXVFBbTPAhf+WIwVfJ8CyiSaDmVP3vwonaCEtRxZA6p7cGCJZCkAQhKQYRwLSv8QiktTnQA9ApO+9mrkNL0CmmmBSOcEuQFlFWlj2bxpAQGUvrha8tz+ZJHCaGZgpwLJqeX1QXRvAcQgLMKR+BkAksazejmrTv/+2nkopJfs/WNAK+oDtMsAutUImgfkgwHzYWAZP3cA/jRlhpSFHCsA5Dn0ddiQdwKtAsH2SQy7Aw41nQSjXKsDuKgHB7oHpvw==", "dtype": "f8" } } ], "layout": { "height": 450, "template": { "layout": { "font": { "color": "#e6edf3" }, "paper_bgcolor": "#0d1117", "plot_bgcolor": "#0d1117", "xaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" }, "yaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" } } }, "title": { "text": "Resampling Method Comparison" }, "xaxis": { "range": [ 0, 30 ], "title": { "text": "Time step" } }, "yaxis": { "title": { "text": "State" } } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Visualize differences (use subset for clarity)\n", "fig = go.Figure()\n", "\n", "fig.add_trace(\n", " go.Scatter(x=time, y=true_states, mode='lines',\n", " name='True', line=dict(color='white', width=2))\n", ")\n", "\n", "colors = ['#00d4ff', '#ff4757', '#00ff88', '#ffb800']\n", "for (name, data), color in zip(results.items(), colors):\n", " fig.add_trace(\n", " go.Scatter(x=time, y=data['estimates'], mode='lines',\n", " name=f\"{name} (RMSE={data['rmse']:.3f})\",\n", " line=dict(color=color, width=1.5, dash='dash'), opacity=0.8)\n", " )\n", "\n", "fig.update_layout(\n", " template=dark_template,\n", " title='Resampling Method Comparison',\n", " xaxis_title='Time step',\n", " yaxis_title='State',\n", " xaxis=dict(range=[0, 30]), # Zoom in for detail\n", " height=450\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Particle Degeneracy and Weight Distribution\n", "\n", "A key challenge in particle filtering is **degeneracy**: after a few iterations, most particles have negligible weight. The Effective Sample Size (ESS) measures this:\n", "\n", "$$\\text{ESS} = \\frac{1}{\\sum_{i=1}^{N} (w^{(i)})^2}$$\n", "\n", "When ESS drops significantly below N, resampling is needed." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "line": { "color": "#00d4ff", "width": 2 }, "mode": "lines", "name": "ESS", "type": "scatter", "x": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50 ], "xaxis": "x", "y": [ 500, 40.18341304492362, 4.8444668498288745, 6.785546935407156, 1.810129634864611, 1.183345575043732, 1.089270178392704, 1.0296848785753572, 1.0120325789948312, 1.0000000001540534, 1, 1, 1, 1, 1.0000001108444643, 1.000000234628451, 1, 1.0024373929544288, 1.0066882702630007, 1.0104942997713595, 1.0000323860529063, 1.0000000087551049, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.0000000000320046, 1, 1, 1, 1, 1, 1, 1, 1, 1.000000000002116, 1, 1, 1, 1, 1.0000002076186227, 1.0000000000000528, 1.0000000000000502, 1.0000000096075559 ], "yaxis": "y" }, { "line": { "color": "#ff4757", "dash": "dash" }, "mode": "lines", "name": "Complete degeneracy", "type": "scatter", "x": [ 0, 51 ], "xaxis": "x", "y": [ 1, 1 ], "yaxis": "y" }, { "line": { "color": "#00ff88", "width": 2 }, "mode": "lines", "name": "Entropy", "type": "scatter", "x": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49 ], "xaxis": "x2", "y": [ 3.9086432853686963, 2.0407303093964346, 2.2717155815896257, 0.9066752330071263, 0.3592019386300119, 0.2307855638664038, 0.0914500471471363, 0.04083676994105771, 1.8717303602741935e-9, 6.349117697344183e-21, 6.36158616061997e-21, 1.8423661703578757e-15, 2.506839291971025e-16, 9.814326324761067e-7, 0.0000019894624862023425, 2.1979177899312663e-16, 0.009385433930604805, 0.02233862969176669, 0.032638294086398394, 0.0001948141422316931, 8.863132418730827e-8, 2.717410661001536e-56, 1.2160276079709342e-47, 4.793960146504969e-38, 4.695787632757658e-29, 9.72867425772962e-30, 1.1737664854486273e-30, 3.54633918505318e-42, 9.043408598141883e-48, 5.168624657028428e-40, 2.4489273296305775e-40, 2.1799558998201843e-40, 4.1378949098513104e-10, 1.4489168067441526e-15, 1.3793107313482625e-22, 2.8591961161991853e-21, 2.950730520156368e-39, 1.140623460347231e-27, 6.349525245406764e-37, 6.205961349998618e-48, 1.811582559148427e-38, 3.023103129235055e-11, 2.8541134484487226e-32, 1.7111924407575256e-34, 1.5311625159083313e-34, 1.1092837448016121e-32, 0.0000017883240403295226, 8.540230548299206e-13, 8.107980839755868e-13, 9.68146832424854e-8 ], "yaxis": "y2" } ], "layout": { "annotations": [ { "font": { "size": 16 }, "showarrow": false, "text": "ESS Collapse Without Resampling", "x": 0.225, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" }, { "font": { "size": 16 }, "showarrow": false, "text": "Weight Distribution Entropy", "x": 0.775, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" } ], "height": 350, "showlegend": true, "template": { "layout": { "font": { "color": "#e6edf3" }, "paper_bgcolor": "#0d1117", "plot_bgcolor": "#0d1117", "xaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" }, "yaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" } } }, "xaxis": { "anchor": "y", "domain": [ 0, 0.45 ], "title": { "text": "Time step" } }, "xaxis2": { "anchor": "y2", "domain": [ 0.55, 1 ], "title": { "text": "Time step" } }, "yaxis": { "anchor": "x", "domain": [ 0, 1 ], "title": { "text": "ESS (log scale)" }, "type": "log" }, "yaxis2": { "anchor": "x2", "domain": [ 0, 1 ], "title": { "text": "Weight Entropy" } } } } }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "ESS at step 0: 500.0\n", "ESS at step 10: 1.00\n", "ESS at step 50: 1.00\n" ] } ], "source": [ "# Demonstrate weight degeneracy without resampling\n", "np.random.seed(123)\n", "particles = np.random.normal(0, np.sqrt(10), N_particles)\n", "weights = np.ones(N_particles) / N_particles\n", "\n", "ess_no_resample = [effective_sample_size(weights)]\n", "weight_entropy = []\n", "\n", "for k, z in enumerate(measurements[:50]): # First 50 steps\n", " # Predict\n", " particles = np.array([f_nonlinear(p, k) for p in particles])\n", " particles += np.random.normal(0, np.sqrt(Q), N_particles)\n", " \n", " # Update WITHOUT resampling\n", " z_pred = np.array([h_nonlinear(p) for p in particles])\n", " likelihoods = np.exp(-0.5 * (z - z_pred)**2 / R)\n", " weights = weights * likelihoods\n", " weights = weights / np.sum(weights)\n", " \n", " ess_no_resample.append(effective_sample_size(weights))\n", " \n", " # Entropy of weights (measure of spread)\n", " w_nonzero = weights[weights > 1e-300]\n", " entropy = -np.sum(w_nonzero * np.log(w_nonzero))\n", " weight_entropy.append(entropy)\n", "\n", "fig = make_subplots(rows=1, cols=2, \n", " subplot_titles=('ESS Collapse Without Resampling', 'Weight Distribution Entropy'))\n", "\n", "fig.add_trace(\n", " go.Scatter(x=list(range(len(ess_no_resample))), y=ess_no_resample, mode='lines',\n", " name='ESS', line=dict(color='#00d4ff', width=2)),\n", " row=1, col=1\n", ")\n", "fig.add_trace(\n", " go.Scatter(x=[0, len(ess_no_resample)], y=[1, 1], mode='lines',\n", " name='Complete degeneracy', line=dict(color='#ff4757', dash='dash')),\n", " row=1, col=1\n", ")\n", "\n", "fig.add_trace(\n", " go.Scatter(x=list(range(len(weight_entropy))), y=weight_entropy, mode='lines',\n", " name='Entropy', line=dict(color='#00ff88', width=2)),\n", " row=1, col=2\n", ")\n", "\n", "fig.update_layout(\n", " template=dark_template,\n", " height=350,\n", " showlegend=True\n", ")\n", "fig.update_yaxes(type='log', title_text='ESS (log scale)', row=1, col=1)\n", "fig.update_yaxes(title_text='Weight Entropy', row=1, col=2)\n", "fig.update_xaxes(title_text='Time step', row=1, col=1)\n", "fig.update_xaxes(title_text='Time step', row=1, col=2)\n", "\n", "fig.show()\n", "\n", "print(f\"ESS at step 0: {ess_no_resample[0]:.1f}\")\n", "print(f\"ESS at step 10: {ess_no_resample[10]:.2f}\")\n", "print(f\"ESS at step 50: {ess_no_resample[50]:.2f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Effect of Number of Particles\n", "\n", "More particles generally lead to better estimation, but with diminishing returns. The computational cost scales linearly with particle count." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "N= 50: RMSE=8.2356, Time=0.012s\n", "N= 100: RMSE=5.2342, Time=0.015s\n", "N= 200: RMSE=5.0700, Time=0.023s\n", "N= 500: RMSE=4.9373, Time=0.048s\n", "N= 1000: RMSE=4.8166, Time=0.094s\n", "N= 2000: RMSE=4.7384, Time=0.187s\n" ] } ], "source": [ "import time\n", "\n", "particle_counts = [50, 100, 200, 500, 1000, 2000]\n", "rmse_vs_particles = []\n", "time_vs_particles = []\n", "\n", "for N in particle_counts:\n", " np.random.seed(42)\n", " particles = np.random.normal(0, np.sqrt(10), (N, 1))\n", " weights = np.ones(N) / N\n", " estimates = []\n", " \n", " start = time.time()\n", " rng = np.random.default_rng(42)\n", " for k, z in enumerate(measurements):\n", " particles_pred = np.array([[f_nonlinear(p[0], k)] for p in particles])\n", " particles_pred += np.random.normal(0, np.sqrt(Q), (N, 1))\n", " \n", " z_pred = np.array([h_nonlinear(p[0]) for p in particles_pred])\n", " likelihoods = np.exp(-0.5 * (z - z_pred)**2 / R)\n", " weights = weights * likelihoods\n", " weights = weights / np.sum(weights)\n", " \n", " if effective_sample_size(weights) < N/2:\n", " particles = resample_systematic(particles_pred, weights, rng)\n", " weights = np.ones(N) / N\n", " else:\n", " particles = particles_pred\n", " \n", " estimates.append(np.average(particles, axis=0, weights=weights)[0])\n", " \n", " elapsed = time.time() - start\n", " rmse = np.sqrt(np.mean((np.array(estimates) - true_states[1:])**2))\n", " rmse_vs_particles.append(rmse)\n", " time_vs_particles.append(elapsed)\n", " print(f\"N={N:5d}: RMSE={rmse:.4f}, Time={elapsed:.3f}s\")" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "line": { "color": "#00d4ff", "width": 2 }, "marker": { "size": 8 }, "mode": "lines+markers", "name": "RMSE", "type": "scatter", "x": [ 50, 100, 200, 500, 1000, 2000 ], "xaxis": "x", "y": [ 8.235605642549363, 5.2342117813747615, 5.070011831780623, 4.937283865308533, 4.8165776103534075, 4.7384238476677805 ], "yaxis": "y" }, { "line": { "color": "#ff4757", "width": 2 }, "marker": { "size": 8 }, "mode": "lines+markers", "name": "Time", "type": "scatter", "x": [ 50, 100, 200, 500, 1000, 2000 ], "xaxis": "x2", "y": [ 0.011801719665527344, 0.015117168426513672, 0.023251771926879883, 0.04829287528991699, 0.0943002700805664, 0.18743085861206055 ], "yaxis": "y2" } ], "layout": { "annotations": [ { "font": { "size": 16 }, "showarrow": false, "text": "Accuracy vs Particle Count", "x": 0.225, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" }, { "font": { "size": 16 }, "showarrow": false, "text": "Computation Time vs Particle Count", "x": 0.775, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" } ], "height": 350, "showlegend": false, "template": { "layout": { "font": { "color": "#e6edf3" }, "paper_bgcolor": "#0d1117", "plot_bgcolor": "#0d1117", "xaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" }, "yaxis": { "gridcolor": "#30363d", "zerolinecolor": "#30363d" } } }, "xaxis": { "anchor": "y", "domain": [ 0, 0.45 ], "title": { "text": "Number of Particles" }, "type": "log" }, "xaxis2": { "anchor": "y2", "domain": [ 0.55, 1 ], "title": { "text": "Number of Particles" }, "type": "log" }, "yaxis": { "anchor": "x", "domain": [ 0, 1 ], "title": { "text": "RMSE" } }, "yaxis2": { "anchor": "x2", "domain": [ 0, 1 ], "title": { "text": "Computation Time (s)" }, "type": "log" } } } }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = make_subplots(rows=1, cols=2, \n", " subplot_titles=('Accuracy vs Particle Count', 'Computation Time vs Particle Count'))\n", "\n", "fig.add_trace(\n", " go.Scatter(x=particle_counts, y=rmse_vs_particles, mode='lines+markers',\n", " name='RMSE', line=dict(color='#00d4ff', width=2),\n", " marker=dict(size=8)),\n", " row=1, col=1\n", ")\n", "\n", "fig.add_trace(\n", " go.Scatter(x=particle_counts, y=time_vs_particles, mode='lines+markers',\n", " name='Time', line=dict(color='#ff4757', width=2),\n", " marker=dict(size=8)),\n", " row=1, col=2\n", ")\n", "\n", "fig.update_layout(\n", " template=dark_template,\n", " height=350,\n", " showlegend=False\n", ")\n", "fig.update_xaxes(type='log', title_text='Number of Particles', row=1, col=1)\n", "fig.update_xaxes(type='log', title_text='Number of Particles', row=1, col=2)\n", "fig.update_yaxes(title_text='RMSE', row=1, col=1)\n", "fig.update_yaxes(type='log', title_text='Computation Time (s)', row=1, col=2)\n", "\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "Key takeaways:\n", "\n", "1. **Particle filters** handle arbitrary nonlinear and non-Gaussian systems\n", "2. **Resampling** is critical to prevent weight degeneracy\n", "3. **Systematic resampling** offers the best variance-computation trade-off\n", "4. **ESS monitoring** helps detect filter health issues\n", "5. **More particles** improve accuracy but with diminishing returns\n", "\n", "## Exercises\n", "\n", "### Exercise 1: Adaptive Resampling Threshold\n", "Modify the filter to adjust the ESS threshold dynamically based on recent history. Track how it affects total resampling events.\n", "\n", "### Exercise 2: Outlier Robustness\n", "Add outlier measurements to the stream and observe:\n", "- How filter estimates diverge when outliers appear\n", "- Recovery time after outlier disappears\n", "- Compare with Kalman filters on same data\n", "\n", "### Exercise 3: Multi-Modal Distributions\n", "Implement a mixture model as the proposal distribution to better capture multimodal posterior distributions. Measure improvements in likelihood.\n", "\n", "### Exercise 4: GPU Acceleration\n", "Modify the particle propagation loop to use CuPy (if GPU available) for batch operations on large particle sets (10,000+).\n", "\n", "### Exercise 5: Gaussian Process Proposal\n", "Replace the simple dynamics propagation with a learned Gaussian process model for the proposal distribution.\n", "\n", "## Advanced Topics\n", "\n", "### Approximate Bayesian Computation (ABC)\n", "When the likelihood is intractable, ABC methods use summary statistics:\n", "- Define acceptable tolerance distance\n", "- Run forward simulations\n", "- Keep particles where simulated data ≈ observed data\n", "\n", "### Sequential Monte Carlo Samplers (SMCS)\n", "Alternative to time-series filtering; sample from evolving posterior distributions:\n", "- Temperature annealing: $p_t(x) \\propto p(x)^{t/T}$\n", "- Tempering resample particles according to target likelihood changes\n", "\n", "### Rao-Blackwellized Particle Filter (RBPF)\n", "Marginalize linear subspace to reduce particle requirement:\n", "- Split state: $\\mathbf{x} = [\\mathbf{x}_{nl}, \\mathbf{x}_l]$\n", "- Use Kalman filter for $\\mathbf{x}_{l} | \\mathbf{x}_{nl}, \\mathbf{z}_{1:t}$\n", "- Only resample $\\mathbf{x}_{nl}$\n", "\n", "## Recommended Learning Path\n", "\n", "```\n", "1. Week 1: Bootstrap Particle Filter Fundamentals\n", " → Read: Arulampalam et al. tutorial (§2-3)\n", " → Code: Implement multinomial resampling from scratch\n", " → Exercise 1: Adaptive threshold tuning\n", "\n", "2. Week 2: Resampling & Degeneracy\n", " → Read: Persson et al. on particle degeneracy\n", " → Code: Compare all resampling methods\n", " → Exercise 3: Multi-modal distributions\n", "\n", "3. Week 3: Particle Filter Applications\n", " → Read: Doucet & Johansen handbook chapter\n", " → Code: Non-Gaussian navigation scenario\n", " → Exercise 2: Outlier handling\n", "\n", "4. Week 4: Advanced Topics & GPU\n", " → Read: RBPF and ABC literature\n", " → Code: GPU-accelerated version\n", " → Exercise 4: Performance benchmarking\n", "```\n", "\n", "## References\n", "\n", "### Core References\n", "1. **Arulampalam, M. S., et al.** (2002). \"A Tutorial on Particle Filters for Online Nonlinear/Non-Gaussian Bayesian Tracking.\" *IEEE Transactions on Signal Processing*, 50(2), 174-188.\n", " - **Best for**: Foundational tutorial with excellent derivations\n", "\n", "2. **Doucet, A., & Johansen, A. M.** (2009). \"A Tutorial on Particle Filtering and Smoothing: Fifteen Years Later.\" *Handbook of Nonlinear Filtering*, 12(656-704).\n", " - **Best for**: Comprehensive modern reference with applications\n", "\n", "3. **Ristic, B., Arulampalam, S., & Gordon, N.** (2004). *Beyond the Kalman Filter: Particle Filters for Tracking Applications*. Artech House.\n", " - **Best for**: Practical implementation guidance\n", "\n", "### Advanced Methods\n", "4. **Persson, A., Mehta, N., & Hendeby, G.** (2020). \"Detecting Particle Filter Divergence.\" *IEEE Conference on Decision and Control*.\n", " - **Coverage**: Divergence detection, N_eff monitoring\n", "\n", "5. **Andrieu, C., Doucet, A., & Holenstein, R.** (2010). \"Particle Markov chain Monte Carlo methods.\" *Journal of the Royal Statistical Society*, 72(3), 269-342.\n", " - **Best for**: PMCMC methods extending particle filters\n", "\n", "### PyTCL Documentation\n", "- **Particle Filter Module**: `pytcl.dynamic_estimation.particle_filters`\n", "- **GPU Acceleration**: `pytcl.gpu.particle_filter` (CuPy backend)\n", "- **Example Gallery**: See `examples/particle_filters.py` for applications" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" } }, "nbformat": 4, "nbformat_minor": 4 }