Troubleshooting Guide

Overview

This guide covers common issues, errors, and their solutions. See Library Architecture for module structure and API Navigation Guide for function discovery.

Installation Issues

ImportError: No module named ‘pytcl’

Cause: pytcl not installed or wrong Python environment

Solution:

# Check if installed
python -c "import pytcl; print(pytcl.__version__)"

# If not installed, install from PyPI
pip install nrl-tracker

# Or install from source
git clone https://github.com/nedonatelli/TCL.git
cd TCL
pip install -e .

Using Wrong Python Version

Cause: Project requires Python 3.10+, but you’re using an older version

Solution:

# Check your Python version
python --version

# If too old, create a virtual environment with Python 3.10+
python3.11 -m venv venv
source venv/bin/activate
pip install nrl-tracker

GPU Backend Issues

ImportError: No module named ‘cupy’

Cause: CuPy not installed (GPU acceleration is optional)

Solution:

# CuPy is optional - you can use CPU version
# Or install CuPy for GPU acceleration
pip install cupy-cuda12x  # Replace 12x with your CUDA version

# Check if GPU backend works
python -c "from pytcl.gpu import to_gpu; print('GPU backend OK')"

See GPU Acceleration Guide for detailed GPU setup.

Import and Usage Issues

ImportError: cannot import name ‘function_name’

Cause: Incorrect import path or function doesn’t exist

Solution:

# Wrong: there is no top-level pytcl.kalman module
# from pytcl.kalman import kf_predict     # ModuleNotFoundError

# Correct: kalman lives under dynamic_estimation
from pytcl.dynamic_estimation.kalman import kf_predict

# Or use interactive discovery
from pytcl.dynamic_estimation import kalman
print([x for x in dir(kalman) if not x.startswith('_')])

ValueError: cannot reshape array of size 2 into shape (3,1)

Cause: cart2sphere operates on 3-D points; a 2-element point cannot be interpreted as (x, y, z). Python lists are fine – inputs go through np.asarray – it is the number of components that matters.

Solution:

import numpy as np
from pytcl.coordinate_systems.conversions import cart2sphere

# Wrong: 2-D point
# r, az, el = cart2sphere([1.0, 0.0])
# ValueError: cannot reshape array of size 2 into shape (3,1)

# Correct: 3-D point (use z = 0 for planar problems).
# Lists and arrays are both accepted.
r, az, el = cart2sphere([1.0, 0.0, 0.0])
r, az, el = cart2sphere(np.array([1.0, 0.0, 0.0]))

AttributeError: module ‘pytcl’ has no attribute ‘xyz’

Cause: Function not re-exported at top level

Solution:

# Use full import path
from pytcl.dynamic_estimation.kalman import kf_predict

# Instead of trying
# from pytcl import kf_predict    # ImportError: not re-exported

Check available functions:

from pytcl import dynamic_estimation
print(dir(dynamic_estimation.kalman))

Array Shape and Dimension Issues

ValueError: Shapes (3,) and (4,4) not aligned

Cause: Dimension mismatch in matrix operations

Solution: Ensure shapes are compatible

import numpy as np
from pytcl.dynamic_estimation.kalman import kf_predict

# State vector must match F rows
F = np.eye(4)      # 4×4 motion model
Q = np.eye(4) * 0.1
x = np.array([0, 1, 0, 1])  # Must be length 4
P = np.eye(4)      # Must be 4×4

# ✅ Correct dimensions
x_pred, P_pred = kf_predict(x, P, F, Q)

Check dimensions:

def check_shapes(x, P, F, Q, H, R):
    print(f"x shape: {x.shape}")
    print(f"P shape: {P.shape}")
    print(f"F shape: {F.shape}")
    print(f"Q shape: {Q.shape}")
    print(f"H shape: {H.shape}")
    print(f"R shape: {R.shape}")

# Rules:
# x.shape = (n,)
# P.shape = (n, n)
# F.shape = (n, n)
# Q.shape = (n, n)
# H.shape = (m, n)
# R.shape = (m, m)

RuntimeError: Expected 2-D array, got 1-D array instead

Cause: Function expects 2D array but got 1D (or vice versa)

Solution:

import numpy as np

# ❌ Wrong: 1D array
x = np.array([1, 2, 3])
x_2d = x.reshape(-1, 1)  # Convert to column vector

# ✅ Correct: Use as-is if function expects 1D
# Check function documentation
from pytcl.mathematical_functions.special_functions import marcum_q
a = 2.0
b = 3.0
q = marcum_q(a, b)  # Scalar inputs OK

Filter and Estimation Issues

Filter Divergence: Covariance Grows Over Time

Symptoms: P increases instead of decreases, filter loses track

Causes and Solutions:

  1. Q too small (process noise underestimated)

    # ❌ Q too small
    Q = np.eye(4) * 0.001  # Too small!
    
    # ✅ Reasonable Q
    Q = np.eye(4) * 0.1  # Depends on system
    

    See Kalman Filter Tuning Guide for proper Q estimation.

  2. R too large (measurement noise overestimated)

    # ❌ R too large
    R = np.eye(2) * 1000  # Measurements are ignored
    
    # ✅ Match sensor accuracy
    R = np.eye(2) * 0.1
    
  3. Motion model wrong (F matrix incorrect)

    from pytcl.dynamic_models import f_constant_velocity
    
    # Verify motion model matches system
    T = 0.1  # time step
    F = f_constant_velocity(T, num_dims=2)
    # If system does accelerate, use:
    # from pytcl.dynamic_models import f_constant_acceleration
    # F = f_constant_acceleration(T, num_dims=2)
    
  4. Measurement outliers (sensor gives bad data)

    # Add outlier rejection (H, x_pred from your filter; z the measurement)
    H = np.array([[1.0, 0.0, 0.0, 0.0],
                  [0.0, 1.0, 0.0, 0.0]])
    x_pred = np.array([0.4, 0.5, 1.0, 0.0])
    z = np.array([0.5, 0.4])
    gate_threshold = 3.0
    
    innovation = z - H @ x_pred
    
    if np.linalg.norm(innovation) > gate_threshold:
        # Skip this measurement or downweight it
        pass
    

Diagnostic Code:

import numpy as np

def diagnose_divergence(x_sequence, P_sequence):
    """Check if filter is diverging"""
    trace_P = [np.trace(P) for P in P_sequence]
    det_P = [np.linalg.det(P) for P in P_sequence]

    print("Trace of P (should decrease): ", trace_P)
    print("Det of P (should decrease): ", det_P)

    # Check if increasing
    if trace_P[-1] > trace_P[0]:
        print("⚠ Filter diverging: P trace increased")

See Kalman Filter Tuning Guide for detailed diagnostics.

Filter Lags Behind Measurements

Symptom: Filter predictions are always 1-2 steps behind actual motion

Causes:

  1. Q too large (filter doesn’t trust model)

    # ❌ Q too large
    Q = np.eye(4) * 10  # Filter doubts the model
    
    # ✅ Reduce Q
    Q = np.eye(4) * 0.1
    
  2. Motion model too simple

    from pytcl.dynamic_models import (
        f_constant_acceleration,
        f_constant_velocity,
    )
    
    # If system has acceleration, need better model
    # Constant velocity model:
    F = f_constant_velocity(T, num_dims=2)
    
    # Better: constant acceleration
    F = f_constant_acceleration(T, num_dims=2)
    
  3. Measurement delay

    Ensure your time stamps are accurate:

    # Check if measurements lag (times/measurements from your data feed)
    times = [0.0, 0.1, 0.2]
    measurements = [np.zeros(2)] * 3
    current_time = 0.35
    expected_delay = 0.1
    
    for i, (t, z) in enumerate(zip(times, measurements)):
        delay = current_time - t
        if delay > expected_delay:
            print(f"Measurement {i} delayed by {delay}s")
    

Filter Too Jerky: Follows Noise

Symptom: Filter output wiggles even when target not moving

Cause: Q too small (filter over-trusts measurements)

Solution:

# ❌ Q too small: filter tries to match every measurement
Q = np.eye(4) * 0.001

# ✅ Increase Q to smooth
Q = np.eye(4) * 0.5

Data Association Issues

ValueError: cost matrix is infeasible

Cause: Gating replaced every candidate pairing in some row or column with np.inf, so no complete assignment exists. relaxation_assignment_nd raises rather than returning a partial assignment:

from scipy.spatial.distance import cdist
from pytcl.assignment_algorithms import relaxation_assignment_nd

# Check gating distance
positions = np.array([[0, 0], [1, 1]])
measurements = np.array([[10, 10], [20, 20]])  # Far away!

cost = cdist(positions, measurements)
gate = 5.0  # Maximum distance

# Mark out-of-range
cost[cost > gate] = np.inf

try:
    result = relaxation_assignment_nd(cost)
except ValueError as e:
    print(e)
# cost matrix is infeasible

Fix:

# 1. Gate with a large finite penalty instead of np.inf, so the solver
#    can still return an assignment; reject penalty-cost pairs afterwards
cost = cdist(positions, measurements)
cost[cost > gate] = 1e6
result = relaxation_assignment_nd(cost)
print(result.assignments)
# [[0 0]
#  [1 1]]
# Both pairings carry the 1e6 penalty cost: treat them as unassigned.

# 2. Or increase the gate threshold so real pairings survive
gate = 30.0  # More permissive

# 3. Or check if measurements are in right coordinate frame
# Ensure measurements converted to same frame as state

Multiple incorrect assignments

Cause: Cost matrix has poor gradation between good/bad matches

Solution:

from scipy.spatial.distance import cdist
from pytcl.assignment_algorithms import relaxation_assignment_nd

# Standard Euclidean
cost = cdist(positions, measurements)

# Better: Mahalanobis (weighted by covariance). cdist estimates the
# covariance from the data unless VI is supplied; with only a handful
# of points that estimate is singular, so pass VI explicitly.
S = np.eye(2) * 0.5  # innovation covariance from your filter
VI = np.linalg.inv(S)
cost = cdist(positions, measurements, metric='mahalanobis', VI=VI)

result = relaxation_assignment_nd(cost)

Coordinate Conversion Issues

“Result is NaN or Inf”

Cause: NaN or Inf in the inputs – typically an upstream computation failed. The conversions themselves are total functions: cart2sphere maps the origin to (0, 0, pi/2) by convention, and negative coordinates are perfectly valid points.

Solution: Trace the NaN back to its source and validate inputs before converting.

from pytcl.coordinate_systems.conversions import cart2sphere
import numpy as np

# These are all valid -- no NaN produced:
print(cart2sphere(np.array([0.0, 0.0, 0.0])))
# (0.0, 0.0, 1.5707963267948966)
print(cart2sphere(np.array([-1.0, 0.0, 0.0])))
# (1.0, 3.141592653589793, 1.5707963267948966)

# NaN out means NaN in:
r, az, el = cart2sphere(np.array([np.nan, 0.0, 0.0]))
print(r)
# nan

# Guard at the boundary where data enters your pipeline
point = np.array([1.0, 2.0, 3.0])
assert np.isfinite(point).all(), "invalid point from upstream"

Check input validity (ranges shown for the 'az-el' convention):

def is_valid_spherical(spherical_coords):
    r, az, el = spherical_coords

    if r < 0:
        print("Range must be positive")
        return False

    if not (-np.pi <= az <= np.pi):
        print("Azimuth must be in [-pi, pi]")
        return False

    if not (-np.pi/2 <= el <= np.pi/2):
        print("Elevation must be in [-pi/2, pi/2]")
        return False

    return True

Angle Wrapping Issues

Problem: Angles greater than 2π or negative angles

Solution:

import numpy as np

# Wrap angle to [-π, π]
def wrap_angle(angle):
    return np.arctan2(np.sin(angle), np.cos(angle))

# Test
large_angle = 3 * np.pi
wrapped = wrap_angle(large_angle)
print(f"{large_angle:.2f} -> {wrapped:.2f}")
# 9.42 -> 3.14

Performance Issues

“Program is too slow” or “Using too much memory”

Solution: Profile first to find bottleneck

import cProfile
import pstats

import numpy as np
from pytcl.dynamic_estimation.kalman import kf_predict, kf_update

def my_tracking_loop(x, P, F, Q, H, R, measurements):
    for z in measurements:
        x, P = kf_predict(x, P, F, Q)
        # kf_update returns KalmanUpdate(x, P, y, S, K, likelihood),
        # so unpack by attribute rather than tuple-unpacking
        upd = kf_update(x, P, z, H, R)
        x, P = upd.x, upd.P
    return x, P

x = np.zeros(4)
P = np.eye(4)
F = np.eye(4)
Q = np.eye(4) * 0.01
H = np.eye(2, 4)
R = np.eye(2) * 0.1
measurements = np.random.randn(1000, 2)

profiler = cProfile.Profile()
profiler.enable()
my_tracking_loop(x, P, F, Q, H, R, measurements)
profiler.disable()

stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10)  # Top 10 functions

See Performance Optimization Guide for detailed optimization techniques.

“Processing measurements too slowly”

Causes and Solutions:

  1. Too many array allocations

    n_targets, n_measurements = 4, 6
    
    def compute_distances(z):
        return np.zeros((n_targets, n_measurements))  # your cost function
    
    # Slow: allocate in loop
    for z in measurements:
        cost = np.zeros((n_targets, n_measurements))  # Allocate each time
    
    # Fast: allocate once
    cost = np.zeros((n_targets, n_measurements))
    for z in measurements:
        cost[:] = compute_distances(z)  # Reuse
    
  2. Not vectorizing operations

    # Slow: Python loop over rows
    norms = np.array([np.linalg.norm(z) for z in measurements])
    
    # Fast: one vectorized call over the whole batch
    norms = np.linalg.norm(measurements, axis=1)
    
  3. Using GPU backend without GPU

    # If no GPU, CPU version is faster (no transfer overhead)
    from pytcl.gpu import is_cupy_available, is_gpu_available
    
    # Check what is actually available before moving arrays
    if is_gpu_available():
        from pytcl.gpu import to_gpu
        data_gpu = to_gpu(measurements)
    else:
        print("GPU not available, using CPU")
        # Use CPU version instead
    
    # is_cupy_available() distinguishes CUDA (CuPy) from the
    # Apple Silicon MLX backend
    print(f"CUDA available: {is_cupy_available()}")
    

See Performance Optimization Guide and GPU Acceleration Guide.

“Memory usage keeps growing”

Cause: Memory leaks or unbounded data accumulation

Solution:

import tracemalloc

tracemalloc.start()

# Run your code (here: a loop that accumulates arrays)
history = []
for i in range(1000):
    history.append(np.zeros(100))

current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.1f} MB")
print(f"Peak: {peak / 1024 / 1024:.1f} MB")
tracemalloc.stop()

Fix:

# Don't accumulate history indefinitely
class Tracker:
    def __init__(self, max_history=100):
        self.max_history = max_history
        self.history = []

    def update(self, measurement):
        self.history.append(measurement)

        # Keep only recent history
        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history:]

Numerical Issues

Covariance Matrix Not Positive Definite

Symptom: Eigenvalues have negative or very small values

Cause: Numerical drift, poor initialization, or invalid update

import numpy as np

def check_positive_definite(P):
    eigs = np.linalg.eigvalsh(P)
    min_eig = np.min(eigs)

    if min_eig < 0:
        print(f"⚠ P not positive definite: min eigenvalue = {min_eig}")
        return False

    if min_eig < 1e-10:
        print(f"⚠ P nearly singular: min eigenvalue = {min_eig}")
        return False

    return True

# Check P after initialization
check_positive_definite(P)

Solution:

import numpy as np
from scipy.linalg import cholesky

# Fix: Enforce positive-definiteness
def fix_covariance(P):
    try:
        # Try Cholesky decomposition
        L = cholesky(P, lower=True)
        return L @ L.T  # Rebuild
    except np.linalg.LinAlgError:
        # If that fails, add small regularization
        epsilon = 1e-8
        P_fixed = P + epsilon * np.eye(len(P))
        return P_fixed

P = fix_covariance(P)

“Numerical underflow/overflow”

Cause: Very small or very large numbers in calculations

Solution:

import numpy as np

# Use higher precision
x = np.array([1e-300, 2e-300], dtype=np.float64)  # Not np.float32

# Or use log-space for very small numbers
probability = 1e-300
log_prob = np.log(probability)  # log_prob = -690.7 is representable,
                                # but probability**2 would underflow to 0

Debugging Tools

Print Debug Information:

import numpy as np
from pytcl.dynamic_estimation.kalman import kf_predict, kf_update

def debug_filter(x, P, z, F, Q, H, R):
    print(f"State before: {x}")
    print(f"Cov trace before: {np.trace(P):.4f}")

    x_p, P_p = kf_predict(x, P, F, Q)
    print(f"State after predict: {x_p}")
    print(f"Cov trace after predict: {np.trace(P_p):.4f}")

    innovation = z - H @ x_p
    print(f"Innovation: {innovation}")

    # kf_update returns the 6-field NamedTuple
    # KalmanUpdate(x, P, y, S, K, likelihood)
    upd = kf_update(x_p, P_p, z, H, R)
    print(f"State after update: {upd.x}")
    print(f"Cov trace after update: {np.trace(upd.P):.4f}")

    return upd.x, upd.P

Use Assertions:

import numpy as np

def track_measurement(x, P, z):
    # Assertions catch bugs early
    assert isinstance(x, np.ndarray), "x must be array"
    assert x.ndim == 1, "x must be 1D"
    assert len(x) == 4, "x must be 4D state"
    assert np.isfinite(x).all(), "x contains NaN or Inf"
    assert np.isfinite(P).all(), "P contains NaN or Inf"

    # ... rest of function

Use Logging:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def update_filter(x, P, z, F, Q, H, R):
    logger.debug(f"State: {x}")
    logger.debug(f"Measurement: {z}")

    x_pred, P_pred = kf_predict(x, P, F, Q)
    logger.debug(f"After predict: {x_pred}")

    upd = kf_update(x_pred, P_pred, z, H, R)
    logger.debug(f"After update: {upd.x}")

    return upd.x, upd.P

Getting Help

If you still can’t find the answer:

  1. Check the documentation at https://nedonatelli.github.io/TCL/

  2. Review examples in examples/ folder

  3. Check similar issues on GitHub: https://github.com/nedonatelli/TCL/issues

  4. Create an issue describing: - What you’re trying to do - Error message (full traceback) - Minimal reproducible example - Your environment (Python version, pytcl version)

Minimal Reproducible Example:

# Minimal code that shows the problem
import numpy as np
from pytcl.dynamic_estimation.kalman import kf_predict

# Minimum setup to reproduce
x = np.array([0.0, 1.0, 0.0, 1.0])
P = np.eye(4) * 0.1
F = np.eye(4)
Q = np.eye(4) * 0.01

# This fails with: [error message]
x_pred, P_pred = kf_predict(x, P, F, Q)

See Also