diff --git a/AGENTS.md b/AGENTS.md index 61c5f56..108f8af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,16 +6,16 @@ **🚫 ABSOLUTELY NO PULL REQUESTS TO:** https://github.com/rlabbe/filterpy -This is a **STANDALONE FORK** for independent development. +This is an **INDEPENDENT PROJECT** for standalone development. - **ALL PRs MUST target:** https://github.com/GeorgePearse/bayesian_filters (THIS REPOSITORY ONLY) -- **NEVER** submit changes to the original filterpy repository -- **NEVER** attempt to upstream changes -- **NEVER** contribute to https://github.com/rlabbe/filterpy under any circumstances +- **NEVER** submit changes to external repositories +- **NEVER** attempt to contribute externally +- **Focus exclusively on:** https://github.com/GeorgePearse/bayesian_filters ### Why? -This is a personal/independent fork with its own roadmap, features, and direction. All development work, features, fixes, and improvements are **exclusively for this fork**. There is no intention to contribute back to the original project. +This is an independent project with its own roadmap, features, and direction. All development work, features, fixes, and improvements are **exclusively for this repository**. ### Pull Request Checklist @@ -24,3 +24,36 @@ Before creating ANY pull request, verify: - ✅ NOT targeting: `rlabbe/filterpy` (FORBIDDEN) - ✅ Base branch is set correctly for this fork - ✅ You are NOT attempting to contribute upstream + +## 📝 Agent Documentation Guidelines + +### Where to Write Documentation + +When agents need to create documentation files (design docs, analysis reports, guides, etc.), they should **write to the `scratch_files/` directory** instead of the repository root. + +**Example:** +- ❌ DON'T: Create `MY_ANALYSIS.md` in root +- ✅ DO: Create `scratch_files/MY_ANALYSIS.md` + +### Why? + +The repository root should contain only essential documentation files: +- `README.md` - Project overview +- `AGENTS.md` - Agent guidelines (this file) +- Other critical docs + +The `scratch_files/` directory is where agents can freely write: +- Analysis and investigation reports +- Design documents +- Planning notes +- Implementation guides +- Troubleshooting docs +- Architecture diagrams +- Any other supporting documentation + +### File Naming + +Agent-generated documentation in `scratch_files/` can use any naming convention: +- Capitalized markdown files are fine (e.g., `ANALYSIS.md`, `DESIGN.md`) +- Descriptive names are encouraged +- Dates/timestamps are helpful (e.g., `2025-10-25_investigation.md`) diff --git a/README.md b/README.md index a7281cd..e14869f 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,11 @@ For people new to Kalman filters, they're well explained here https://www.youtub [![PyPI Publishing](https://github.com/GeorgePearse/bayesian_filters/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/GeorgePearse/bayesian_filters/actions/workflows/publish-pypi.yml) [![Latest Release](https://img.shields.io/badge/latest%20release-v1.4.5-success)](https://pypi.org/project/bayesian-filters/1.4.5/) -> **Note**: This is a personal fork of the original FilterPy library (now renamed to Bayesian Filters). The original project can be found at https://github.com/rlabbe/filterpy -> -> Maintained by George Pearse, Lead MLE at [Visia](https://www.visia.ai/) +Maintained by George Pearse, Lead MLE at [Visia](https://www.visia.ai/) This library provides Kalman filtering and various related optimal and non-optimal filtering software written in Python. It contains Kalman filters, Extended Kalman filters, Unscented Kalman filters, Kalman smoothers, Least Squares filters, fading memory filters, g-h filters, discrete Bayes, and more. -This is code originally developed in conjunction with the book [Kalman and Bayesian Filter in Python](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/). +This is a comprehensive implementation of Kalman filters and related estimation algorithms in Python. All computations use NumPy and SciPy. @@ -93,8 +91,6 @@ The library is broken up into subdirectories: Each subdirectory contains Python files relating to that form of filter. The functions and methods contain comprehensive docstrings. -The book [Kalman and Bayesian Filters in Python](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python/) uses this library and is the best place to learn about Kalman filtering and/or this library. - ## Requirements This library requires: @@ -127,14 +123,12 @@ The original author uses three main reference texts: ### Online Resources -- **[Kalman and Bayesian Filters in Python](https://github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python)** - Free online book with Jupyter notebooks teaching Kalman filtering and Bayesian statistics. Written by the original FilterPy author, this is the companion book to this library and provides excellent intuitive explanations with interactive examples. - **[Kalman Filter Background](https://kalmanfilter.net/background.html)** - Comprehensive background and theory on Kalman filtering ## Tools and Projects Using Kalman Filters -### Repositories Using FilterPy/Bayesian Filters +### Repositories Using Bayesian Filters -- **[rlabbe/filterpy](https://github.com/rlabbe/filterpy)** - The original FilterPy library from which this fork is derived - **[Gavin-Furtado/Kalman-Filter-Simulator](https://github.com/Gavin-Furtado/Kalman-Filter-Simulator)** - Python project simulating sensor tracking with state estimation - **[sparshgarg23/object-detection-and-tracking](https://github.com/sparshgarg23/object-detection-and-tracking)** - Object detection pipeline with OpenCV and FilterPy-based Kalman filters - **[Norfair](https://github.com/tryolabs/norfair)** - Lightweight Python library for real-time multi-object tracking using Kalman filters diff --git a/bayesian_filters/gh/gh_filter.py b/bayesian_filters/gh/gh_filter.py index 8ec09d5..ba80e29 100644 --- a/bayesian_filters/gh/gh_filter.py +++ b/bayesian_filters/gh/gh_filter.py @@ -21,6 +21,8 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Optional + import numpy as np from numpy import dot from bayesian_filters.common import pretty_str @@ -105,28 +107,41 @@ class GHFilterOrder(object): """ - def __init__(self, x0, dt, order, g, h=None, k=None): + def __init__( + self, + x0: float | np.ndarray, + dt: float, + order: int, + g: float, + h: Optional[float] = None, + k: Optional[float] = None, + ) -> None: """Creates a g-h filter of order 0, 1, or 2.""" if order < 0 or order > 2: raise ValueError("order must be between 0 and 2") + if order == 2 and k is None: + raise ValueError("k parameter is required for order 2 filters") + if np.isscalar(x0): - self.x = np.zeros(order + 1) + self.x: np.ndarray = np.zeros(order + 1) self.x[0] = x0 else: - self.x = np.copy(x0.astype(float)) + self.x = np.copy(np.asarray(x0, dtype=float)) - self.dt = dt - self.order = order + self.dt: float = dt + self.order: int = order - self.g = g - self.h = h - self.k = k - self.y = np.zeros(len(self.x)) # residual - self.z = np.zeros(len(self.x)) # last measurement + self.g: float = g + self.h: Optional[float] = h + self.k: Optional[float] = k + self.y: np.ndarray = np.zeros(len(self.x)) # residual + self.z: np.ndarray = np.zeros(len(self.x)) # last measurement - def update(self, z, g=None, h=None, k=None): + def update( + self, z: float | np.ndarray, g: Optional[float] = None, h: Optional[float] = None, k: Optional[float] = None + ) -> None: """ Update the filter with measurement z. z must be the same type or treatable as the same type as self.x[0]. @@ -151,7 +166,7 @@ def update(self, z, g=None, h=None, k=None): self.x[0] = x + dxdt + g * self.y self.x[1] = dx + h * self.y / self.dt - self.z = z + self.z = np.asarray(z) else: # order == 2 if g is None: @@ -161,6 +176,9 @@ def update(self, z, g=None, h=None, k=None): if k is None: k = self.k + # At this point, k is guaranteed to be float due to __init__ validation + assert k is not None, "k must not be None for order 2 filter" + x = self.x[0] dx = self.x[1] ddx = self.x[2] diff --git a/bayesian_filters/hinfinity/hinfinity_filter.py b/bayesian_filters/hinfinity/hinfinity_filter.py index 7513161..6e11fb6 100644 --- a/bayesian_filters/hinfinity/hinfinity_filter.py +++ b/bayesian_filters/hinfinity/hinfinity_filter.py @@ -21,12 +21,22 @@ from __future__ import absolute_import, division import copy import warnings +from typing import Optional, Protocol + import numpy as np from numpy import dot, zeros, eye import scipy.linalg as linalg from bayesian_filters.common import pretty_str +class SaverProtocol(Protocol): + """Protocol for Saver objects to avoid circular imports.""" + + def save(self) -> None: + """Save the current state.""" + ... + + class HInfinityFilter(object): """ H-Infinity filter. You are responsible for setting the @@ -154,7 +164,7 @@ def predict(self, u=0): # x = Fx + Bu self.x = dot(self.F, self.x) + dot(self.B, u) - def batch_filter(self, Zs, update_first=False, saver=False): + def batch_filter(self, Zs, update_first: bool = False, saver: Optional[SaverProtocol] = None): """Batch processes a sequences of measurements. Parameters diff --git a/bayesian_filters/kalman/EKF.py b/bayesian_filters/kalman/EKF.py index 558eb5b..b785088 100644 --- a/bayesian_filters/kalman/EKF.py +++ b/bayesian_filters/kalman/EKF.py @@ -167,6 +167,12 @@ def __init__(self, dim_x, dim_z, dim_u=0): self.x_post = self.x.copy() self.P_post = self.P.copy() + # Optional function attributes for state transition and measurement + # These can be set after initialization if needed for specific applications + self.fx = None # state transition function + self.hx = None # measurement function + self.H = None # Jacobian function (can be set as attribute or passed to methods) + def predict_update(self, z, HJacobian, Hx, args=(), hx_args=(), u=0): """Performs the predict/update innovation of the extended Kalman filter. diff --git a/bayesian_filters/kalman/fixed_lag_smoother.py b/bayesian_filters/kalman/fixed_lag_smoother.py index 9dc416d..43dee67 100644 --- a/bayesian_filters/kalman/fixed_lag_smoother.py +++ b/bayesian_filters/kalman/fixed_lag_smoother.py @@ -17,6 +17,8 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from typing import Optional, List + import numpy as np from numpy import dot, zeros, eye from scipy.linalg import inv @@ -81,7 +83,7 @@ class FixedLagSmoother(object): """ - def __init__(self, dim_x, dim_z, N=None): + def __init__(self, dim_x: int, dim_z: int, N: Optional[int] = None) -> None: """Create a fixed lag Kalman filter smoother. You are responsible for setting the various state variables to reasonable values; the defaults below will not give you a functional filter. @@ -105,31 +107,32 @@ def __init__(self, dim_x, dim_z, N=None): using smooth_batch() function. Required if calling smooth() """ - self.dim_x = dim_x - self.dim_z = dim_z - self.N = N - - self.x = zeros((dim_x, 1)) # state - self.x_s = zeros((dim_x, 1)) # smoothed state - self.P = eye(dim_x) # uncertainty covariance - self.Q = eye(dim_x) # process uncertainty - self.F = eye(dim_x) # state transition matrix - self.H = eye(dim_z, dim_x) # Measurement function - self.R = eye(dim_z) # state uncertainty - self.K = zeros((dim_x, 1)) # kalman gain - self.y = zeros((dim_z, 1)) - self.B = 0.0 - self.S = zeros((dim_z, dim_z)) + self.dim_x: int = dim_x + self.dim_z: int = dim_z + self.N: Optional[int] = N + + self.x: np.ndarray = zeros((dim_x, 1)) # state + self.x_s: np.ndarray = zeros((dim_x, 1)) # smoothed state + self.P: np.ndarray = eye(dim_x) # uncertainty covariance + self.Q: np.ndarray = eye(dim_x) # process uncertainty + self.F: np.ndarray = eye(dim_x) # state transition matrix + self.H: np.ndarray = eye(dim_z, dim_x) # Measurement function + self.R: np.ndarray = eye(dim_z) # state uncertainty + self.K: np.ndarray = zeros((dim_x, 1)) # kalman gain + self.y: np.ndarray = zeros((dim_z, 1)) + self.B: float = 0.0 + self.S: np.ndarray = zeros((dim_z, dim_z)) # identity matrix. Do not alter this. - self._I = np.eye(dim_x) + self._I: np.ndarray = np.eye(dim_x) - self.count = 0 + self.count: int = 0 + self.xSmooth: List[np.ndarray] = [] if N is not None: self.xSmooth = [] - def smooth(self, z, u=None): + def smooth(self, z: np.ndarray | float, u: Optional[np.ndarray] = None) -> None: """Smooths the measurement using a fixed lag smoother. On return, self.xSmooth is populated with the N previous smoothed @@ -155,8 +158,19 @@ def smooth(self, z, u=None): u : ndarray, optional If provided, control input to the filter + + Raises + ------ + ValueError + If N was not provided in __init__ """ + if self.N is None: + raise ValueError( + "N must be provided in __init__ to use smooth() method. " + "Use smooth_batch() instead for batch processing without N." + ) + # take advantage of the fact that np.array are assigned by reference. H = self.H R = self.R @@ -165,7 +179,7 @@ def smooth(self, z, u=None): x = self.x Q = self.Q B = self.B - N = self.N + N: int = self.N # Now guaranteed to be int, not Optional[int] k = self.count diff --git a/bayesian_filters/kalman/kalman_filter.py b/bayesian_filters/kalman/kalman_filter.py index 09e1798..04fe0e7 100644 --- a/bayesian_filters/kalman/kalman_filter.py +++ b/bayesian_filters/kalman/kalman_filter.py @@ -1554,7 +1554,7 @@ def update_steadystate(x, z, K, H=None): return x + dot(K, y) -def predict(x, P, F=1, Q=0, u=0, B=1, alpha=1.0): +def predict(x, P, F=1, Q=0, u=0, B=1, alpha: float = 1.0): """ Predict next state (prior) using the Kalman filter state propagation equations. @@ -1599,8 +1599,9 @@ def predict(x, P, F=1, Q=0, u=0, B=1, alpha=1.0): Prior covariance matrix """ - if np.isscalar(F): - F = np.array(F) + # Convert F to array (keeps 0-D scalar arrays for proper broadcasting) + F = np.asarray(F, dtype=float) + x = dot(F, x) + dot(B, u) P = (alpha * alpha) * dot(dot(F, P), F.T) + Q @@ -1743,6 +1744,8 @@ def batch_filter(x, P, zs, Fs, Qs, Hs, Rs, Bs=None, us=None, update_first=False, if us is None: us = [0.0] * n Bs = [0.0] * n + elif Bs is None: + Bs = [0.0] * n if update_first: for i, (z, F, Q, H, R, B, u) in enumerate(zip(zs, Fs, Qs, Hs, Rs, Bs, us)): diff --git a/bayesian_filters/kalman/mmae.py b/bayesian_filters/kalman/mmae.py index 4ca7c53..8ab2c3f 100644 --- a/bayesian_filters/kalman/mmae.py +++ b/bayesian_filters/kalman/mmae.py @@ -126,8 +126,8 @@ def __init__(self, filters, p, dim_x, H=None): except AttributeError: self.z = 0 - self.x = None - self.P = None + self.x = np.zeros((dim_x, 1)) + self.P = np.eye(dim_x) # these will always be a copy of x,P after predict() is called self.x_prior = self.x.copy() diff --git a/bayesian_filters/kalman/tests/test_enkf.py b/bayesian_filters/kalman/tests/test_enkf.py index 4bbf078..8f19eaa 100644 --- a/bayesian_filters/kalman/tests/test_enkf.py +++ b/bayesian_filters/kalman/tests/test_enkf.py @@ -72,7 +72,6 @@ def fx(x, dt): plt.plot(results + ps, c="k", linestyle="--") plt.legend(loc="best") # print(ps) - return f def test_circle(): diff --git a/bayesian_filters/kalman/tests/test_mmae.py b/bayesian_filters/kalman/tests/test_mmae.py index 0b07ab0..ff48194 100644 --- a/bayesian_filters/kalman/tests/test_mmae.py +++ b/bayesian_filters/kalman/tests/test_mmae.py @@ -183,8 +183,6 @@ def test_MMAE2(): plt.plot(xs) plt.plot(pos[:, 0]) - return bank - if __name__ == "__main__": DO_PLOT = True diff --git a/bayesian_filters/kalman/tests/test_ukf.py b/bayesian_filters/kalman/tests/test_ukf.py index 36cd4c9..bb0d62c 100644 --- a/bayesian_filters/kalman/tests/test_ukf.py +++ b/bayesian_filters/kalman/tests/test_ukf.py @@ -959,7 +959,6 @@ def o_func(x): assert np.allclose(dx, 0, atol=1e-7) assert np.allclose(dxx, 0, atol=1e-6) - return ukf def _test_log_likelihood(): diff --git a/bayesian_filters/stats/stats.py b/bayesian_filters/stats/stats.py index 24b8813..923d370 100644 --- a/bayesian_filters/stats/stats.py +++ b/bayesian_filters/stats/stats.py @@ -1090,6 +1090,9 @@ def plot_covariance( if cov is not None: ellipse = covariance_ellipse(cov) + # Type narrowing: at this point, ellipse cannot be None due to validation above + assert ellipse is not None, "ellipse must be provided if cov is None" + if axis_equal: plt.axis("equal") diff --git a/docs/changelog.md b/docs/changelog.md index 36a1a05..0f542b9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -22,11 +22,4 @@ All notable changes to this project will be documented in this file. ## Legacy Changelog -For changes prior to the fork, see the original [changelog.txt](https://github.com/rlabbe/filterpy/blob/master/filterpy/changelog.txt) in the upstream repository. - -## Original FilterPy Releases - -The original FilterPy library by Roger R. Labbe Jr can be found at: -https://github.com/rlabbe/filterpy - -This fork maintains compatibility while adding modernizations and improvements. +For historical context, see the [original FilterPy changelog](https://github.com/rlabbe/filterpy/blob/master/filterpy/changelog.txt). diff --git a/docs/index.md b/docs/index.md index f1b2779..fbf854e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,17 +65,6 @@ The code is written to match equations from textbooks on a 1-to-1 basis, making All filters include comprehensive test suites and are used in production systems. -## About This Fork - -This is a fork of the original [FilterPy](https://github.com/rlabbe/filterpy) library by Roger Labbe. The main changes: - -- Renamed from `filterpy` to `bayesian_filters` for PyPI publication -- Modern packaging with `uv` and `pyproject.toml` -- Updated documentation with MkDocs Material theme -- Automated releases and GitHub Pages deployment - -Original project credit goes to Roger Labbe. - ## License MIT License - see [LICENSE](https://github.com/GeorgePearse/filterpy/blob/master/LICENSE) for details. diff --git a/pyproject.toml b/pyproject.toml index f3ab315..1bc0f7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ classifiers = [ Homepage = "https://github.com/GeorgePearse/bayesian_filters" Repository = "https://github.com/GeorgePearse/bayesian_filters" Documentation = "https://georgepearse.github.io/bayesian_filters" -"Original Project" = "https://github.com/rlabbe/filterpy" [project.optional-dependencies] dev = [ @@ -53,7 +52,7 @@ dev = [ "pytest-benchmark>=4.0.0", "pytest-xdist>=3.5.0", "pytest-mpl>=0.17.0", - "zuban>=0.1.0", + "ty>=0.0.1a24", ] docs = [ "mkdocs>=1.6.0", @@ -134,26 +133,19 @@ select = ["E", "F", "W"] # Allow undefined names in examples (they may be defined elsewhere) "**/examples/**" = ["F821"] -[tool.mypy] -# Type checking configuration for zuban (mypy-compatible) -# Zuban reads this [mypy] section for configuration -# Initially, all files are ignored to allow gradual adoption of type annotations -# As we add type annotations to modules, they will be enabled one at a time - -# Ignore all files by default - enable specific modules as they get typed -ignore_patterns = ["^bayesian_filters/.*"] - -# Allow untyped function definitions initially -allow_untyped_defs = true - -# Don't require type annotations on functions -allow_incomplete_defs = true - -# Allow imports without type stubs -ignore_missing_imports = true - -# Report unused type ignore comments (we can enable this later when strict) -warn_unused_ignores = false - -# Suppress errors for untyped globals -allow_untyped_globals = true +[tool.ty] +# Type checking configuration for ty +# Ty will check all Python files under the bayesian_filters package +# We'll configure ty to enforce type annotations for a gradual migration + +[tool.ty.src] +# Include all Python files in the bayesian_filters package +include = ["bayesian_filters/**/*.py"] +# Exclude test and example files for now (we'll enable them progressively) +exclude = ["bayesian_filters/**/tests/**", "bayesian_filters/examples/**"] +# Respect .gitignore and similar files +respect-ignore-files = true + +[tool.ty.environment] +# Python version we're targeting +python-version = "3.11" diff --git a/scratch_files/.gitkeep b/scratch_files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/PUBLISHING.md b/scratch_files/PUBLISHING.md similarity index 100% rename from PUBLISHING.md rename to scratch_files/PUBLISHING.md diff --git a/PYPI_SETUP.md b/scratch_files/PYPI_SETUP.md similarity index 100% rename from PYPI_SETUP.md rename to scratch_files/PYPI_SETUP.md diff --git a/TESTING.md b/scratch_files/TESTING.md similarity index 100% rename from TESTING.md rename to scratch_files/TESTING.md diff --git a/TESTING_PLAN.md b/scratch_files/TESTING_PLAN.md similarity index 100% rename from TESTING_PLAN.md rename to scratch_files/TESTING_PLAN.md diff --git a/TYPE_ANNOTATIONS.md b/scratch_files/TYPE_ANNOTATIONS.md similarity index 100% rename from TYPE_ANNOTATIONS.md rename to scratch_files/TYPE_ANNOTATIONS.md