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:
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.
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
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)
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:
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
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)
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:
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
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)
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:
Check the documentation at https://nedonatelli.github.io/TCL/
Review examples in
examples/folderCheck similar issues on GitHub: https://github.com/nedonatelli/TCL/issues
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
Kalman Filter Tuning Guide - Filter parameter tuning
Performance Optimization Guide - Performance profiling
GPU Acceleration Guide - GPU acceleration setup
API Navigation Guide - Function discovery
Examples:
examples/folder