Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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`)
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
42 changes: 30 additions & 12 deletions bayesian_filters/gh/gh_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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].
Expand All @@ -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:
Expand All @@ -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]
Expand Down
12 changes: 11 additions & 1 deletion bayesian_filters/hinfinity/hinfinity_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions bayesian_filters/kalman/EKF.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 34 additions & 20 deletions bayesian_filters/kalman/fixed_lag_smoother.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
9 changes: 6 additions & 3 deletions bayesian_filters/kalman/kalman_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)):
Expand Down
4 changes: 2 additions & 2 deletions bayesian_filters/kalman/mmae.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion bayesian_filters/kalman/tests/test_enkf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 0 additions & 2 deletions bayesian_filters/kalman/tests/test_mmae.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,6 @@ def test_MMAE2():
plt.plot(xs)
plt.plot(pos[:, 0])

return bank


if __name__ == "__main__":
DO_PLOT = True
Expand Down
1 change: 0 additions & 1 deletion bayesian_filters/kalman/tests/test_ukf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
3 changes: 3 additions & 0 deletions bayesian_filters/stats/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading
Loading