Getting Started

This guide will help you get started with the Tracker Component Library.

Installation

Requirements

  • Python 3.10 or later

  • NumPy >= 1.24

  • SciPy >= 1.10

  • Numba >= 0.57

  • h5py >= 3.8

  • loguru >= 0.7

  • msgspec >= 0.18

  • rich >= 13

Install from PyPI

pip install nrl-tracker

Install from Source

git clone https://github.com/nedonatelli/TCL.git
cd TCL
pip install -e .

Optional Dependencies

Install optional features as needed:

# For astronomy features (ephemerides, celestial mechanics)
pip install nrl-tracker[astronomy]

# For geodesy features (coordinate transforms)
pip install nrl-tracker[geodesy]

# For terrain data (GEBCO, Earth2014 via NetCDF)
pip install nrl-tracker[terrain]

# For visualization (Plotly)
pip install nrl-tracker[visualization]

# For signal processing (wavelets)
pip install nrl-tracker[signal]

# For polars DataFrame accessors on track histories and metrics
pip install nrl-tracker[dataframe]

# For AIS/NMEA transponder decoding
pip install nrl-tracker[ais]

# For ASDF archival export/import
pip install nrl-tracker[asdf]

# For GPU acceleration (NVIDIA CUDA)
pip install nrl-tracker[gpu]

# For GPU acceleration (Apple Silicon M1/M2/M3)
pip install nrl-tracker[gpu-apple]

# Every user-facing extra except the GPU ones
pip install nrl-tracker[all]

Basic Concepts

State Representation

States are represented as 1D NumPy arrays. For kinematic tracking, common state vectors include:

  • Constant velocity (2D): [x, vx, y, vy]

  • Constant acceleration (2D): [x, vx, ax, y, vy, ay]

  • Singer model (2D): [x, vx, ax, y, vy, ay] (acceleration is correlated)

Covariance matrices are represented as 2D NumPy arrays of shape (n, n).

Motion Models

The library provides state transition matrices (F) and process noise covariance matrices (Q) for various motion models:

from pytcl.dynamic_models import (
    f_constant_velocity,
    f_constant_acceleration,
    f_singer,
    q_constant_velocity,
    q_constant_acceleration,
    q_singer,
)

# Constant velocity model
F_cv = f_constant_velocity(T=1.0, num_dims=2)
Q_cv = q_constant_velocity(T=1.0, sigma_a=1.0, num_dims=2)

# Singer maneuvering model
F_singer = f_singer(T=1.0, tau=10.0, num_dims=2)
Q_singer = q_singer(T=1.0, tau=10.0, sigma_m=1.0, num_dims=2)

Filters

The library provides several filtering algorithms:

Linear Kalman Filter - For linear dynamics and measurements:

import numpy as np

from pytcl.dynamic_estimation import kf_predict, kf_update

x = np.array([0.0, 1.0, 0.0, 1.0])  # [x, vx, y, vy]
P = np.eye(4)
H = np.array([[1.0, 0.0, 0.0, 0.0],
              [0.0, 0.0, 1.0, 0.0]])  # measure position only
R = 0.5 * np.eye(2)
z = np.array([1.1, 0.9])

pred = kf_predict(x, P, F_cv, Q_cv)
upd = kf_update(pred.x, pred.P, z, H, R)

Extended Kalman Filter - For nonlinear dynamics/measurements:

from pytcl.dynamic_estimation import ekf_predict, ekf_update

def f_func(x):
    return F_cv @ x

def h_func(x):
    # Range and bearing from the origin
    return np.array([np.hypot(x[0], x[2]), np.arctan2(x[2], x[0])])

def H_jacobian(x):
    r = np.hypot(x[0], x[2])
    return np.array([
        [x[0] / r, 0.0, x[2] / r, 0.0],
        [-x[2] / r**2, 0.0, x[0] / r**2, 0.0],
    ])

R_polar = np.diag([0.1, 0.01])
z_polar = np.array([1.5, 0.8])

# F and H are the Jacobian matrices evaluated at the current state
F = F_cv  # Jacobian of f (linear dynamics, so constant)
pred = ekf_predict(upd.x, upd.P, f_func, F, Q_cv)
upd = ekf_update(pred.x, pred.P, z_polar, h_func, H_jacobian(pred.x), R_polar)

Unscented Kalman Filter - For highly nonlinear systems:

from pytcl.dynamic_estimation import ukf_predict, ukf_update

pred = ukf_predict(upd.x, upd.P, f_func, Q_cv)
upd = ukf_update(pred.x, pred.P, z_polar, h_func, R_polar)

Particle Filter - For non-Gaussian distributions:

from pytcl.dynamic_estimation import (
    initialize_particles,
    bootstrap_pf_step,
)

# Q_sample is a callable that samples process noise
def Q_sample(n_particles, rng=None):
    if rng is None:
        rng = np.random.default_rng()
    return rng.multivariate_normal(np.zeros(4), Q_cv, size=n_particles)

state = initialize_particles(x, P, N=1000)
state = bootstrap_pf_step(
    state.particles, state.weights, z_polar, f_func, h_func, Q_sample, R_polar
)

Constrained Extended Kalman Filter - For state constraints (e.g., bounded positions):

from pytcl.dynamic_estimation.kalman import (
    constrained_ekf_predict,
    constrained_ekf_update,
    ConstraintFunction,
)

# Define constraints: 0 <= x[0] <= 100 (position within bounds)
def constraint_lower(x):
    return np.array([-x[0]])  # g(x) <= 0 means x[0] >= 0

def constraint_upper(x):
    return np.array([x[0] - 100.0])  # g(x) <= 0 means x[0] <= 100

constraints = [
    ConstraintFunction(constraint_lower),
    ConstraintFunction(constraint_upper),
]

# The predict step is unconstrained; constraints apply at the update
pred = constrained_ekf_predict(upd.x, upd.P, f_func, F_cv, Q_cv)
upd = constrained_ekf_update(
    pred.x,
    pred.P,
    z_polar,
    h_func,
    H_jacobian(pred.x),
    R_polar,
    constraints=constraints,
)

Rao-Blackwellized Particle Filter - Hybrid linear/nonlinear filtering:

from pytcl.dynamic_estimation import RBPFFilter

# Partition the state: nonlinear part 'y' is handled by particles,
# linear part 'x' is handled by a Kalman filter per particle
rbpf = RBPFFilter(max_particles=500)
rbpf.initialize(
    y0=np.array([0.0]),  # Nonlinear state
    x0=np.array([1.0]),  # Linear state
    P0=np.eye(1),
    num_particles=500,
)

def g_nl(y):  # Nonlinear transition: y[k+1] = g(y[k])
    return y + 0.1 * np.sin(y)

def f_lin(x, y):  # Linear transition: x[k+1] = f(x[k], y[k])
    return x

def h_meas(x, y):  # Measurement combines both parts
    return y + x

rbpf.predict(g=g_nl, Qy=0.01 * np.eye(1),
             f=f_lin, F=np.eye(1), Qx=0.01 * np.eye(1))
rbpf.update(z=np.array([1.2]), h=h_meas, H=np.eye(1), R=0.1 * np.eye(1))
y_est, x_est, P_est = rbpf.estimate()

Coordinate Systems

Convert between coordinate systems:

from pytcl.coordinate_systems import (
    cart2sphere,
    sphere2cart,
    geodetic2ecef,
    ecef2geodetic,
)

# Cartesian to spherical (tracking convention: range, azimuth, elevation)
r, az, el = cart2sphere(np.array([100.0, 200.0, 50.0]), system_type="az-el")

# Geodetic to ECEF (angles in radians); returns an ECEF [x, y, z] vector
ecef = geodetic2ecef(lat=np.deg2rad(40.0), lon=np.deg2rad(-75.0), alt=100.0)

Atmospheric Models

Get atmospheric density for satellite drag calculations:

import numpy as np
from pytcl.atmosphere import simplified_thermosphere

# Simplified thermosphere model with solar/geomagnetic activity
output = simplified_thermosphere(
    latitude=np.deg2rad(45.0),
    longitude=np.deg2rad(-75.0),
    altitude=400e3,          # meters
    year=2024,
    day_of_year=100,
    seconds_in_day=12 * 3600.0,
    f107=150.0,   # 10.7 cm solar flux (SFU)
    f107a=130.0,  # 81-day average
    ap=15.0,      # Planetary magnetic index
)
print(f"Density: {output.density:.3e} kg/m^3")

# Composition is available on the same result
print(f"Atomic oxygen: {output.o_density:.3e} m^-3")
print(f"Temperature: {output.temperature:.1f} K")