Library Architecture ==================== Overview -------- ``pytcl`` is **145 modules** across **22 implemented packages**, exporting about 1046 public names. There is no framework object to inherit from: the library is a set of composable functions and small result types, so a tracker is something you assemble rather than something you subclass. Three placeholder packages — ``misc``, ``physical_values`` and ``scheduling`` — used to exist with no modules, mirroring the MATLAB directory layout. They were removed before 2.0.0: an importable-but-empty package implied support that does not exist, which is worse than the honest ``ImportError``. ``pytcl.transponders`` was one of them until v2.2.0, when AIS decoding (see :mod:`pytcl.transponders.ais`) gave it real content; if the remaining three are ported (see :doc:`matlab_parity_inventory`), they return the same way. Subsystem Map ------------- Packages grouped by the role they play. Arrows point from a consumer toward what it depends on; only the structurally significant edges are drawn. .. mermaid:: graph TD subgraph Estimation DE["dynamic_estimation
17 modules"] DM["dynamic_models
7 modules"] SE["static_estimation
4 modules"] end subgraph Association_and_Tracking AA["assignment_algorithms
9 modules"] TR["trackers
5 modules"] CL["clustering
4 modules"] CO["containers
8 modules"] end subgraph Geometry_and_Navigation CS["coordinate_systems
6 modules"] NA["navigation
5 modules"] AS["astronomical
9 modules"] end subgraph Environment_Models GR["gravity
5 modules"] MA["magnetism
3 modules"] AT["atmosphere
5 modules"] TE["terrain
3 modules"] end subgraph Foundation MF["mathematical_functions
26 modules"] CR["core
7 modules"] end subgraph Support IO["io
12 modules"] GP["gpu
7 modules"] PL["plotting
4 modules"] PE["performance_evaluation
2 modules"] end TR --> DE TR --> AA TR --> CO DE --> DM DE --> MF AA --> MF CL --> CO NA --> CS NA --> GR AS --> CS SE --> MF CS --> MF PE --> MF IO --> CO GP --> MF PL --> CO DM --> CR MF --> CR Tracking Pipeline ----------------- The order in which these subsystems are normally composed. Every stage is a function call over plain arrays and small result objects, so any one of them can be replaced without touching the others. .. mermaid:: flowchart LR M["Raw measurements
range, bearing, elevation"] --> C C["coordinate_systems
convert to a common frame"] --> G G["assignment_algorithms
gating"] --> A A["assignment_algorithms
data association"] --> F F["dynamic_estimation
predict and update"] --> TM TM["trackers
initiation, confirmation, deletion"] --> P P["io
persist tracks"] --> E E["performance_evaluation
NEES, NIS, OSPA"] DM["dynamic_models
F and Q"] -.-> F E -.->|tune| DM Per-detection Measurement Covariance ------------------------------------ ``SingleTargetTracker.update`` and ``MultiTargetTracker.process`` accept a covariance per detection, not just the fixed ``R`` given to the constructor: .. code-block:: python # each detection carries the covariance that actually applies to it tracks = tracker.process(detections, dt, measurement_covariances=covariances) This matters whenever the measurement error is not the same for every detection, and a converted polar detection is the common case. Its Cartesian covariance is ``J R_polar J.T``, which is anisotropic and grows with range: down-range spread stays at ``sigma_range`` while cross-range spread is ``r * sigma_bearing``. No single ``R`` describes that. Size it to the down-range term and the gate is too tight at long range -- true detections fall outside it and the tracker starts duplicate tracks. Size it to the cross-range term and cardinality is right, but the covariance is inflated and the filter under-reports its own accuracy. Supplying each detection's covariance avoids the choice; both the gate and the Kalman gain then use the covariance that applies. Estimator Families ------------------ What ``dynamic_estimation`` actually provides. These are exposed as ``*_predict`` / ``*_update`` function pairs rather than filter classes, which is why the examples below thread state through explicitly. .. mermaid:: graph LR KF["Linear Kalman
kf_predict / kf_update"] KF --> EKF["Extended
ekf_predict / ekf_update"] KF --> UKF["Unscented
ukf_predict / ukf_update"] KF --> SR["Square-root and UD
numerically robust"] KF --> CE["Constrained
ConstrainedEKF"] KF --> INF["Information and SRIF"] KF --> IMM["IMM
maneuver switching"] PF["Particle filters"] --> RBPF["Rao-Blackwellised"] GSF["Gaussian sum"] KF --> SM["Smoothers
RTS, fixed-lag, two-filter"] PF --> SM Package Reference ----------------- Counts are measured from the packages themselves rather than asserted; ``tests/test_docs_architecture.py`` fails if this table drifts. .. list-table:: :header-rows: 1 :widths: 26 10 10 54 * - Package - Modules - Public - Purpose * - ``mathematical_functions`` - 26 - 73 - Special functions, statistics, transforms, signal processing, geometry, polynomials * - ``dynamic_estimation`` - 17 - 88 - Kalman variants, particle filters, smoothers, information filters * - ``assignment_algorithms`` - 9 - 52 - 2-D, 3-D and N-D assignment, gating, JPDA, k-best, network flow * - ``astronomical`` - 9 - 149 - Orbital mechanics, SGP4/SDP4, TLEs, ephemerides, reference frames * - ``containers`` - 8 - 35 - Spatial indices (k-d tree, R-tree, VP-tree, cover tree) and track sets * - ``core`` - 7 - 72 - Constants, exceptions, validation, array helpers, data paths * - ``dynamic_models`` - 7 - 37 - Motion models and process-noise matrices, continuous and discrete * - ``gpu`` - 7 - 38 - CuPy and MLX backends for array-heavy routines * - ``io`` - 12 - 37 - Track and measurement persistence, including HDF5 * - ``coordinate_systems`` - 6 - 77 - Frame conversions, u-v direction cosines, rotations, Jacobians, map projections * - ``gravity`` - 5 - 58 - Spherical-harmonic gravity, EGM, solid Earth tides * - ``navigation`` - 5 - 101 - Geodesy, great circle and rhumb line, INS and INS/GNSS * - ``clustering`` - 4 - 28 - k-means, DBSCAN, hierarchical, Gaussian mixtures * - ``plotting`` - 4 - 30 - Track, uncertainty and coverage visualization * - ``trackers`` - 5 - 19 - End-to-end single- and multi-target trackers * - ``atmosphere`` - 5 - 60 - Simplified thermosphere, standard atmospheres, ionosphere, humidity, refractivity * - ``magnetism`` - 3 - 36 - WMM, WMMHR, IGRF, EMM * - ``static_estimation`` - 4 - 50 - Least squares, robust estimation, model selection, localization * - ``terrain`` - 3 - 30 - DEM handling, line of sight, horizon and viewshed * - ``performance_evaluation`` - 2 - 21 - NEES, NIS, OSPA, Cramer-Rao bounds * - ``diagnostics`` - 1 - 10 - Opt-in loguru logging, ASCII-safe rich progress bars and track tables * - ``transponders`` - 1 - 5 - AIS NMEA decoding, checksum validation, and position-report extraction (pyais) Composition Examples -------------------- Each of these runs as written; the same test that checks the table also checks that every import on this page resolves. Filtering a constant-velocity target ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import numpy as np from pytcl.dynamic_models import f_constant_velocity, q_constant_velocity from pytcl.dynamic_estimation import kf_predict, kf_update T = 0.1 F = f_constant_velocity(T, num_dims=2) Q = q_constant_velocity(T, sigma_a=0.1, num_dims=2) # f_constant_velocity is block diagonal -- one (position, velocity) pair # per spatial dimension -- so the state is [x, vx, y, vy], not # [x, y, vx, vy]. Getting this backwards silently measures velocity. x = np.array([0.0, 1.0, 0.0, 0.5]) # x, vx, y, vy P = np.eye(4) * 0.1 pred = kf_predict(x, P, F, Q) H = np.array([[1.0, 0.0, 0.0, 0.0], # observe x [0.0, 0.0, 1.0, 0.0]]) # observe y R = np.eye(2) * 0.05 upd = kf_update(pred.x, pred.P, np.array([0.1, 0.05]), H, R) Associating measurements to tracks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import numpy as np from pytcl.assignment_algorithms import assign2d # cost[i, j] is the cost of assigning track i to measurement j cost = np.array([[4.0, 1.0, 3.0], [2.0, 0.0, 5.0], [3.0, 2.0, 2.0]]) result = assign2d(cost) # result.col_indices[i] is the measurement assigned to track i Geodetic conversion and range ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import numpy as np from pytcl.navigation import geodetic_to_ecef, inverse_geodetic dc_lat, dc_lon = np.radians(38.9), np.radians(-77.0) ny_lat, ny_lon = np.radians(40.7), np.radians(-74.0) x, y, z = geodetic_to_ecef(dc_lat, dc_lon, 0.0) distance, azimuth, back_azimuth = inverse_geodetic(dc_lat, dc_lon, ny_lat, ny_lon) Optional Dependencies --------------------- A core install pulls only ``numpy``, ``scipy``, ``numba``, ``h5py``, ``loguru``, ``msgspec`` and ``rich``. Everything else is opt-in, so an import failing inside one of these packages usually means a missing extra rather than a bug. .. mermaid:: graph LR CORE["core install
numpy, scipy, numba, h5py,
loguru, msgspec, rich"] CORE --> AST["astronomy
astropy, jplephem"] CORE --> GEO["geodesy
pyproj, geographiclib"] CORE --> TER["terrain
netCDF4"] CORE --> VIS["visualization
plotly"] CORE --> SIG["signal
pywavelets"] CORE --> GPU["gpu, gpu-apple
cupy, mlx"] AST --> ALL["all
everything except gpu"] GEO --> ALL TER --> ALL VIS --> ALL SIG --> ALL Angles and Units ---------------- Every angle at an API boundary is in **radians** and every distance in **meters**, unless a function documents otherwise. Passing degrees where radians are expected is the most common way to get results that look plausible and are wrong: as a latitude in radians, ``-5`` is -286 degrees, which is past the poles. See Also -------- - :doc:`getting_started` - installation and first steps - :doc:`performance_optimization` - profiling and GPU acceleration - :doc:`examples/index` - runnable example scripts