From 409308f41659877d7857ea203032fe4536760e45 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 12:57:00 +0000 Subject: [PATCH 01/10] feat: add zuban type checker with initial all-files-ignored configuration - Add zuban>=0.1.0 as dev dependency in pyproject.toml - Configure mypy (used by zuban) to ignore all files initially - Allow untyped definitions to start gradual type annotation adoption - This sets up the foundation for incrementally adding type annotations to modules one at a time, with zuban validating as we go - Configuration enables: allow_untyped_defs, allow_incomplete_defs, ignore_missing_imports, allow_untyped_globals - Files will be enabled in [mypy] section as they receive type annotations --- pyproject.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6b6e950..2e3090d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ Documentation = "https://georgepearse.github.io/bayesian_filters" dev = [ "pytest>=8.4.0", "pytest-cov>=7.0.0", + "zuban>=0.1.0", ] docs = [ "mkdocs>=1.6.0", @@ -91,3 +92,27 @@ select = ["E", "F", "W"] [tool.ruff.lint.per-file-ignores] # 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 From dbb1b3b394a237962c3cdb4a05241480f7fcb8b1 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 12:57:29 +0000 Subject: [PATCH 02/10] docs: add comprehensive type annotations and zuban configuration guide - Document the gradual type annotation adoption strategy - Explain how to enable modules one at a time for type checking - Provide type annotation guidelines and examples - Show how to increase strictness as coverage improves - Include common patterns and resources for type annotations --- TYPE_ANNOTATIONS.md | 303 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 TYPE_ANNOTATIONS.md diff --git a/TYPE_ANNOTATIONS.md b/TYPE_ANNOTATIONS.md new file mode 100644 index 0000000..3fd60f0 --- /dev/null +++ b/TYPE_ANNOTATIONS.md @@ -0,0 +1,303 @@ +# Type Annotations and Zuban Configuration + +This document describes how to gradually add type annotations to the bayesian-filters codebase using Zuban as the type checker. + +## Overview + +We're adopting type annotations incrementally to improve code quality and catch bugs early. The strategy is: + +1. **Start with all files ignored** - Zuban won't check anything initially +2. **Enable modules one at a time** - As a module gets type annotations, enable it in the mypy config +3. **Gradually increase strictness** - Start permissive, tighten rules as coverage improves + +## Current Status + +All files in `bayesian_filters/` are currently ignored by Zuban. The configuration allows untyped definitions and functions without type annotations. + +### Configuration (in `pyproject.toml`) + +```toml +[tool.mypy] +# Ignore all files by default +ignore_patterns = ["^bayesian_filters/.*"] + +# Permissive settings (will tighten as we add annotations) +allow_untyped_defs = true +allow_incomplete_defs = true +ignore_missing_imports = true +allow_untyped_globals = true +warn_unused_ignores = false +``` + +## How to Enable Type Checking for a Module + +### Step 1: Add Type Annotations + +Add type hints to function signatures and variable declarations in your module. For example: + +```python +def kinematic_state_transition( + dim: int, + order: int, + dt: float, +) -> np.ndarray: + """Generate kinematic state transition matrix.""" + ... +``` + +### Step 2: Update pyproject.toml + +Remove the ignore pattern for your module in the `[tool.mypy]` section: + +```toml +[tool.mypy] +# Before +ignore_patterns = ["^bayesian_filters/.*"] + +# After - module is now checked! +ignore_patterns = [ + "^bayesian_filters/.*", + # Add exceptions when ready: + "!^bayesian_filters/common/kinematic.py", # Now type-checked +] +``` + +Or more simply, just remove it from the ignore list if all other files are excluded. + +### Step 3: Run Zuban Locally + +Check your module for type errors: + +```bash +# Check a specific file +uv run zuban check bayesian_filters/common/kinematic.py + +# Check the whole project +uv run zuban check . +``` + +### Step 4: Fix Type Errors + +Address any type checking errors that Zuban reports. Use `# type: ignore` comments sparingly for legitimate cases. + +### Step 5: Commit + +Update the configuration and commit your typed module. + +## Zuban Commands + +### Check for type errors + +```bash +# Check entire project +uv run zuban check . + +# Check specific file +uv run zuban check bayesian_filters/kalman/kalman_filter.py + +# Check with specific configuration +uv run zuban check --config-file pyproject.toml . +``` + +### Interactive mode (LSP server) + +For IDE integration: + +```bash +uv run zuban server +``` + +### Mypy-compatible mode + +```bash +uv run zuban mypy ... +``` + +## Type Annotation Guidelines + +### Import typing utilities + +```python +from typing import Any, Callable, Optional, Tuple, Union +import numpy as np +from numpy.typing import NDArray +``` + +### Array types + +Use numpy's typing module for array annotations: + +```python +def filter_step( + z: NDArray[np.float64], + H: NDArray[np.float64], + R: NDArray[np.float64], +) -> NDArray[np.float64]: + """Process measurement update.""" + ... +``` + +### Optional types + +```python +def __init__( + self, + dim_x: int, + dim_z: int, + cov: Optional[NDArray] = None, +) -> None: + ... +``` + +### Union types + +```python +def process( + value: Union[int, float], + state: Union[np.ndarray, list[float]], +) -> float: + ... +``` + +### Callbacks + +```python +from typing import Callable + +def integrate( + f: Callable[[float, NDArray], NDArray], + y0: NDArray, + t: NDArray, +) -> NDArray: + """Integrate differential equation.""" + ... +``` + +## Gradual Strictness + +As we increase type annotation coverage, we can make the mypy configuration stricter: + +### Phase 1: Current (Very Permissive) +- ✅ Allow untyped function definitions +- ✅ Allow incomplete type hints +- ✅ Allow missing imports +- ✅ Allow untyped globals + +### Phase 2: Moderate (When ~50% of code typed) +```toml +allow_untyped_defs = false # Require type hints on all functions +allow_incomplete_defs = true # Still allow some flexibility +warn_unused_ignores = true # Start cleaning up `# type: ignore` +``` + +### Phase 3: Strict (When ~90% of code typed) +```toml +allow_incomplete_defs = false # Require complete type coverage +disallow_any_generics = true # Use specific types, not `Any` +warn_return_any = true # Flag functions returning `Any` +``` + +### Phase 4: Maximum (Full coverage) +```toml +strict = true # Enable all strict checks +``` + +## Example: Typing a Module + +Here's an example of taking `bayesian_filters/common/kinematic.py` from untyped to typed: + +1. **Before**: No type annotations +```python +def Q_discrete_white_noise(dim, dt, var, block_size=1, order_by_dim=True): + if dim < 1: + raise ValueError('dim must be >= 1') + ... +``` + +2. **After**: With type annotations +```python +def Q_discrete_white_noise( + dim: int, + dt: float, + var: float, + block_size: int = 1, + order_by_dim: bool = True, +) -> NDArray[np.float64]: + """Generate discrete-time process noise covariance matrix. + + Parameters + ---------- + dim : int + Dimension of state vector + dt : float + Time step + var : float + Noise variance + block_size : int, optional + Size of state vector blocks, by default 1 + order_by_dim : bool, optional + If True, orders by dimension; if False, by derivative order + + Returns + ------- + Q : ndarray + Process noise covariance matrix of shape (dim*block_size, dim*block_size) + """ + if dim < 1: + raise ValueError('dim must be >= 1') + ... +``` + +3. **Enable in pyproject.toml**: +```toml +[tool.mypy] +ignore_patterns = [ + "^bayesian_filters/.*", + "!^bayesian_filters/common/kinematic.py", # Now checked! +] +``` + +4. **Run Zuban**: +```bash +uv run zuban check bayesian_filters/common/kinematic.py +``` + +5. **Fix any errors and commit** + +## Modules Enabled for Type Checking + +Currently enabled (checked by Zuban): +- (None - all files ignored initially) + +## Common Type Checking Patterns + +### Skip type checking for a line + +```python +result = some_untyped_function() # type: ignore +``` + +### Skip type checking for a function + +```python +def legacy_function(): # type: ignore + # Type errors here are ignored + return untyped_result * something_else +``` + +### Use Any when unavoidable + +```python +from typing import Any + +def flexible_function(value: Any) -> Any: + """Function that works with any type.""" + return value +``` + +## Resources + +- [Zuban Documentation](https://github.com/fruits-lab/Rust-Python-Type-Checker) +- [Python typing module](https://docs.python.org/3/library/typing.html) +- [Numpy typing](https://numpy.org/doc/stable/reference/typing.html) +- [PEP 484 - Type Hints](https://www.python.org/dev/peps/pep-0484/) From 64c3fc57f796330c7883401e5774cd4911f5939d Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 13:21:09 +0000 Subject: [PATCH 03/10] docs: add scratch_files directory and update agent documentation guidelines - Create scratch_files/ directory for agent-generated documentation - Update AGENTS.md with clear guidance that agents should write documentation to scratch_files/ instead of the repository root - Keep root directory clean with only essential docs (README.md, AGENTS.md, etc) - Allow agents to freely create capitalized markdown files in scratch_files/ - Include examples of appropriate documentation locations --- AGENTS.md | 33 +++++++++++++++++++++++++++++++++ scratch_files/.gitkeep | 0 2 files changed, 33 insertions(+) create mode 100644 scratch_files/.gitkeep diff --git a/AGENTS.md b/AGENTS.md index 61c5f56..e851393 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/scratch_files/.gitkeep b/scratch_files/.gitkeep new file mode 100644 index 0000000..e69de29 From 867159e10e61518eecc5d4fabb6870bee1730392 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 13:22:42 +0000 Subject: [PATCH 04/10] refactor: move capitalized documentation files to scratch_files/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the following documentation files to scratch_files/ to keep the repository root clean with only essential documentation: - PUBLISHING.md → scratch_files/PUBLISHING.md - PYPI_SETUP.md → scratch_files/PYPI_SETUP.md - TYPE_ANNOTATIONS.md → scratch_files/TYPE_ANNOTATIONS.md Keep in root: README.md, AGENTS.md, CLAUDE.md (essential project docs) Agent-generated and operational docs now live in scratch_files/ --- PUBLISHING.md => scratch_files/PUBLISHING.md | 0 PYPI_SETUP.md => scratch_files/PYPI_SETUP.md | 0 TYPE_ANNOTATIONS.md => scratch_files/TYPE_ANNOTATIONS.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename PUBLISHING.md => scratch_files/PUBLISHING.md (100%) rename PYPI_SETUP.md => scratch_files/PYPI_SETUP.md (100%) rename TYPE_ANNOTATIONS.md => scratch_files/TYPE_ANNOTATIONS.md (100%) 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/TYPE_ANNOTATIONS.md b/scratch_files/TYPE_ANNOTATIONS.md similarity index 100% rename from TYPE_ANNOTATIONS.md rename to scratch_files/TYPE_ANNOTATIONS.md From 700678856deb3690a18e38e63a36d984f55cc070 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 18:02:57 +0000 Subject: [PATCH 05/10] fix: resolve type annotation issues in kalman filters and stats - Fix batch_filter to handle None Bs parameter when us is provided - Fix MMAE __init__ to use default arrays instead of None for x and P - Add type narrowing assertion in plot_covariance_ellipse for ellipse parameter - Initialize fx, hx, and H attributes in ExtendedKalmanFilter class These changes resolve type checking errors identified by zuban type checker. --- bayesian_filters/kalman/EKF.py | 6 ++++++ bayesian_filters/kalman/kalman_filter.py | 2 ++ bayesian_filters/kalman/mmae.py | 4 ++-- bayesian_filters/stats/stats.py | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) 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/kalman_filter.py b/bayesian_filters/kalman/kalman_filter.py index 09e1798..6e2bbe1 100644 --- a/bayesian_filters/kalman/kalman_filter.py +++ b/bayesian_filters/kalman/kalman_filter.py @@ -1743,6 +1743,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/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") From ea4f6064e013631d7105443f509fbe4b186db620 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 18:05:21 +0000 Subject: [PATCH 06/10] refactor: swap zuban for ty and achieve full type compliance - Replace zuban with ty type checker in pyproject.toml - Update mypy configuration section to ty configuration with proper src/env settings - Add comprehensive type annotations to achieve full ty type compliance - gh_filter.py: Add type hints and k parameter validation for order 2 filters - hinfinity_filter.py: Add SaverProtocol and proper type annotations for batch_filter - fixed_lag_smoother.py: Add type hints and N parameter validation for smooth() - kalman_filter.py: Fix Bs parameter handling and F array conversion in predict() All 21 initial type diagnostics now resolved. ty check reports: "All checks passed!" --- bayesian_filters/gh/gh_filter.py | 42 ++++++++++----- .../hinfinity/hinfinity_filter.py | 12 ++++- bayesian_filters/kalman/fixed_lag_smoother.py | 54 ++++++++++++------- bayesian_filters/kalman/kalman_filter.py | 10 ++-- pyproject.toml | 41 ++++++-------- 5 files changed, 99 insertions(+), 60 deletions(-) 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/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 6e2bbe1..06a9464 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,12 @@ 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) + # Ensure F is a proper numpy array for dot products + if isscalar(F): + F = np.atleast_2d(np.asarray(F, dtype=float)) + else: + F = np.asarray(F) + x = dot(F, x) + dot(B, u) P = (alpha * alpha) * dot(dot(F, P), F.T) + Q diff --git a/pyproject.toml b/pyproject.toml index f3ab315..9a3dec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "pytest-benchmark>=4.0.0", "pytest-xdist>=3.5.0", "pytest-mpl>=0.17.0", - "zuban>=0.1.0", + "ty>=0.1.0", ] docs = [ "mkdocs>=1.6.0", @@ -134,26 +134,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" From 2df223ee39bb8dc64ac0b216919b7990c9ccad4b Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 18:17:32 +0000 Subject: [PATCH 07/10] fix: restore scalar F broadcasting in predict() - use simple asarray instead of atleast_2d Replace the problematic 4-line atleast_2d() conversion that broke broadcasting when scalar F=1 was converted to shape (1,1) matrix. The original behavior using 0-D arrays allows NumPy dot products to correctly broadcast scalars across any dimensionality of x. Changed from: if isscalar(F): F = np.atleast_2d(np.asarray(F, dtype=float)) else: F = np.asarray(F) To: F = np.asarray(F, dtype=float) This single-line approach: - Preserves 0-D scalar arrays for correct broadcasting - Maintains type compliance (F is ndarray) - Simplifies code (1 line vs 4 lines) - Fixes regression in test_functions (scalar F with multi-dim x) Fixes #P0-scalar-F-regression --- bayesian_filters/kalman/kalman_filter.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bayesian_filters/kalman/kalman_filter.py b/bayesian_filters/kalman/kalman_filter.py index 06a9464..04fe0e7 100644 --- a/bayesian_filters/kalman/kalman_filter.py +++ b/bayesian_filters/kalman/kalman_filter.py @@ -1599,11 +1599,8 @@ def predict(x, P, F=1, Q=0, u=0, B=1, alpha: float = 1.0): Prior covariance matrix """ - # Ensure F is a proper numpy array for dot products - if isscalar(F): - F = np.atleast_2d(np.asarray(F, dtype=float)) - else: - F = np.asarray(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 From 4da981df46196032eff60529e5d507bc4c0dcd09 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 18:23:09 +0000 Subject: [PATCH 08/10] refactor: remove fork metadata while preserving educational references Remove GitHub fork metadata to present this as an independent project: - Remove 'fork' language from README, docs/index.md, and AGENTS.md - Remove 'About This Fork' section from docs/index.md - Remove 'Original Project' URL from pyproject.toml - Change AGENTS.md language from 'fork' to 'independent project' - Delete import-upstream-*.py scripts Preserve educational/historical context: - Keep references to original FilterPy library in docs - Keep book references (Kalman and Bayesian Filters in Python) - Keep links to original repository in resource sections - Maintain critical guidance in AGENTS.md about not submitting PRs upstream --- AGENTS.md | 10 +++++----- README.md | 12 +++--------- docs/changelog.md | 9 +-------- docs/index.md | 11 ----------- pyproject.toml | 1 - 5 files changed, 9 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e851393..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 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/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 9a3dec1..6513bab 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 = [ From 6fac69217edc0c0aa71b7f61779c4d0636c631c1 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 19:04:30 +0000 Subject: [PATCH 09/10] fix: remove test return values to resolve PytestReturnNotNoneWarning Remove unnecessary return statements from test functions: - test_enkf.py::test_1d_const_vel: Remove 'return f' - test_mmae.py::test_MMAE2: Remove 'return bank' - test_ukf.py::test_linear_rts: Remove 'return ukf' Test functions should return None, not test objects. This resolves all 4 PytestReturnNotNoneWarning warnings. All 231 tests now pass cleanly with only the hypothesis pytest plugin warning remaining (unrelated). --- bayesian_filters/kalman/tests/test_enkf.py | 1 - bayesian_filters/kalman/tests/test_mmae.py | 2 -- bayesian_filters/kalman/tests/test_ukf.py | 1 - 3 files changed, 4 deletions(-) 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(): From 46ca02b90db074d084dc2d08aba6723f8e705150 Mon Sep 17 00:00:00 2001 From: George Pearse Date: Sat, 25 Oct 2025 19:14:39 +0000 Subject: [PATCH 10/10] fix: update ty dependency to match available version 0.0.1a24 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6513bab..1bc0f7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dev = [ "pytest-benchmark>=4.0.0", "pytest-xdist>=3.5.0", "pytest-mpl>=0.17.0", - "ty>=0.1.0", + "ty>=0.0.1a24", ] docs = [ "mkdocs>=1.6.0",