Core Module

Core utilities and constants for the Tracker Component Library.

This module provides foundational functionality used throughout the library: - Physical and mathematical constants - Input validation utilities - Array manipulation helpers compatible with MATLAB conventions - Custom exception hierarchy for consistent error handling - Optional dependency management - Module maturity classification system

Constants

Physical and mathematical constants used throughout the Tracker Component Library.

This module provides standardized values for physical constants, with references to their sources. Constants are provided as module-level variables for convenience and as a PhysicalConstants class for documentation and grouping.

References

pytcl.core.constants.SPEED_OF_LIGHT: Final[float] = 299792458.0

Speed of light in vacuum [m/s]

pytcl.core.constants.GRAVITATIONAL_CONSTANT: Final[float] = 6.6743e-11

Newtonian gravitational constant [m^3 kg^-1 s^-2]

pytcl.core.constants.PLANCK_CONSTANT: Final[float] = 6.62607015e-34

Planck constant [J s]

pytcl.core.constants.BOLTZMANN_CONSTANT: Final[float] = 1.380649e-23

Boltzmann constant [J K^-1]

pytcl.core.constants.STEFAN_BOLTZMANN_CONSTANT: Final[float] = 5.670374419e-08

Stefan-Boltzmann constant [W m^-2 K^-4]

pytcl.core.constants.ELEMENTARY_CHARGE: Final[float] = 1.602176634e-19

Elementary charge [C]

pytcl.core.constants.AVOGADRO_CONSTANT: Final[float] = 6.02214076e+23

Avogadro constant [mol^-1]

pytcl.core.constants.UNIVERSAL_GAS_CONSTANT: Final[float] = 8.314462618

Universal gas constant [J mol^-1 K^-1]

pytcl.core.constants.STANDARD_ATMOSPHERE: Final[float] = 101325.0

Standard atmosphere pressure [Pa]

pytcl.core.constants.ABSOLUTE_ZERO_CELSIUS: Final[float] = -273.15

Absolute zero [K] (definition)

pytcl.core.constants.EARTH_SEMI_MAJOR_AXIS: Final[float] = 6378137.0

Semi-major axis (equatorial radius) [m]

pytcl.core.constants.EARTH_SEMI_MINOR_AXIS: Final[float] = 6356752.314245

Semi-minor axis (polar radius) [m]

pytcl.core.constants.EARTH_FLATTENING: Final[float] = 0.0033528106647474805

Flattening factor (dimensionless)

pytcl.core.constants.EARTH_ECCENTRICITY_SQ: Final[float] = 0.0066943799901413165

First eccentricity squared

pytcl.core.constants.EARTH_ECCENTRICITY: Final[float] = 0.08181919084262149

First eccentricity

pytcl.core.constants.EARTH_ECCENTRICITY_PRIME_SQ: Final[float] = 0.006739496742276434

Second eccentricity squared

pytcl.core.constants.EARTH_ROTATION_RATE: Final[float] = 7.292115e-05

Earth rotation rate [rad/s] (IERS Conventions 2010)

pytcl.core.constants.EARTH_GM: Final[float] = 398600441800000.0

Earth’s gravitational parameter GM [m^3/s^2] (WGS84)

pytcl.core.constants.EARTH_GM_EGM2008: Final[float] = 398600441500000.0

Earth’s gravitational parameter GM [m^3/s^2] (EGM2008, includes atmosphere)

pytcl.core.constants.EARTH_MEAN_ANGULAR_VELOCITY: Final[float] = 7.2921151467e-05

Mean angular velocity of Earth [rad/s]

pytcl.core.constants.EARTH_MEAN_RADIUS: Final[float] = 6371000.0

Nominal mean Earth radius [m] (IUGG)

pytcl.core.constants.STANDARD_GRAVITY: Final[float] = 9.80665

Standard gravitational acceleration at sea level [m/s^2]

pytcl.core.constants.SECONDS_PER_DAY: Final[float] = 86400.0

Seconds per day

pytcl.core.constants.SECONDS_PER_JULIAN_CENTURY: Final[float] = 3155760000.0

Seconds per Julian century

pytcl.core.constants.DAYS_PER_JULIAN_YEAR: Final[float] = 365.25

Days per Julian year

pytcl.core.constants.DAYS_PER_JULIAN_CENTURY: Final[float] = 36525.0

Days per Julian century

pytcl.core.constants.J2000_EPOCH_JD: Final[float] = 2451545.0

J2000.0 epoch as Julian Date

pytcl.core.constants.MJD_OFFSET: Final[float] = 2400000.5

Modified Julian Date offset (JD - MJD)

pytcl.core.constants.PI: Final[float] = 3.141592653589793

Pi

pytcl.core.constants.TWO_PI: Final[float] = 6.283185307179586

2*Pi

pytcl.core.constants.HALF_PI: Final[float] = 1.5707963267948966

Pi/2

pytcl.core.constants.DEG_TO_RAD: Final[float] = 0.017453292519943295

Degrees to radians conversion factor

pytcl.core.constants.RAD_TO_DEG: Final[float] = 57.29577951308232

Radians to degrees conversion factor

pytcl.core.constants.ARCSEC_TO_RAD: Final[float] = 4.84813681109536e-06

Arcseconds to radians conversion factor

pytcl.core.constants.RAD_TO_ARCSEC: Final[float] = 206264.80624709636

Radians to arcseconds conversion factor

class pytcl.core.constants.EllipsoidParameters(a, f, GM, omega, name='Custom')[source]

Bases: object

Parameters defining a reference ellipsoid.

Variables:
  • a (float) – Semi-major axis (equatorial radius) [m]

  • f (float) – Flattening factor (dimensionless)

  • GM (float) – Gravitational parameter [m^3/s^2]

  • omega (float) – Angular rotation rate [rad/s]

  • name (str) – Name of the ellipsoid

  • Properties

  • ----------

  • b (float) – Semi-minor axis (polar radius) [m]

  • e2 (float) – First eccentricity squared

  • e (float) – First eccentricity

  • ep2 (float) – Second eccentricity squared

a: float
f: float
GM: float
omega: float
name: str = 'Custom'
property b: float

Semi-minor axis [m].

property e2: float

First eccentricity squared.

property e: float

First eccentricity.

property ep2: float

Second eccentricity squared.

property ep: float

Second eccentricity.

__init__(a, f, GM, omega, name='Custom')
class pytcl.core.constants.PhysicalConstants(c=299792458.0, G=6.6743e-11, h=6.62607015e-34, k_B=1.380649e-23, sigma=5.670374419e-08, e=1.602176634e-19, N_A=6.02214076e+23, R=8.314462618, g_0=9.80665)[source]

Bases: object

Container for fundamental physical constants.

This class groups physical constants for convenient access and documentation. All values follow CODATA 2018 recommendations.

Examples

>>> from pytcl.core.constants import PhysicalConstants
>>> pc = PhysicalConstants()
>>> print(f"Speed of light: {pc.c} m/s")
Speed of light: 299792458.0 m/s
c: float = 299792458.0

Speed of light in vacuum [m/s]

G: float = 6.6743e-11

Gravitational constant [m^3 kg^-1 s^-2]

h: float = 6.62607015e-34

Planck constant [J s]

k_B: float = 1.380649e-23

Boltzmann constant [J K^-1]

sigma: float = 5.670374419e-08

Stefan-Boltzmann constant [W m^-2 K^-4]

e: float = 1.602176634e-19

Elementary charge [C]

N_A: float = 6.02214076e+23

Avogadro constant [mol^-1]

R: float = 8.314462618

Universal gas constant [J mol^-1 K^-1]

g_0: float = 9.80665

Standard gravity [m/s^2]

__init__(c=299792458.0, G=6.6743e-11, h=6.62607015e-34, k_B=1.380649e-23, sigma=5.670374419e-08, e=1.602176634e-19, N_A=6.02214076e+23, R=8.314462618, g_0=9.80665)
pytcl.core.constants.WGS84: Final[EllipsoidParameters] = EllipsoidParameters(a=6378137.0, f=0.0033528106647474805, GM=398600441800000.0, omega=7.292115e-05, name='WGS84')

WGS84 ellipsoid parameters

pytcl.core.constants.GRS80: Final[EllipsoidParameters] = EllipsoidParameters(a=6378137.0, f=0.003352810681182319, GM=398600500000000.0, omega=7.292115e-05, name='GRS80')

GRS80 ellipsoid parameters

pytcl.core.constants.CLARKE1866: Final[EllipsoidParameters] = EllipsoidParameters(a=6378206.4, f=0.0033900753039276207, GM=398600500000000.0, omega=7.292115e-05, name='Clarke1866')

Clarke 1866 ellipsoid (NAD27)

pytcl.core.constants.SPHERE_EARTH: Final[EllipsoidParameters] = EllipsoidParameters(a=6371000.0, f=0.0, GM=398600441800000.0, omega=7.292115e-05, name='Sphere')

Sphere with Earth mean radius

pytcl.core.constants.ASTRONOMICAL_UNIT: Final[float] = 149597870700.0

Astronomical Unit [m] (IAU 2012)

pytcl.core.constants.SUN_GM: Final[float] = 1.32712440018e+20

Sun gravitational parameter [m^3/s^2]

pytcl.core.constants.MOON_GM: Final[float] = 4902800118000.0

Moon gravitational parameter [m^3/s^2] (DE430/GRAIL, 4902.800118 km^3/s^2)

Must stay consistent with EARTH_GM / EARTH_MOON_MASS_RATIO. The previous value, 4.9028695e12, disagreed with that quotient and with DE430 by 1.4e-5 relative, and with the value pytcl.gravity.tides was independently using.

pytcl.core.constants.EARTH_MOON_MASS_RATIO: Final[float] = 81.30056

Earth-Moon mass ratio

pytcl.core.constants.c = 299792458.0

Alias for SPEED_OF_LIGHT

pytcl.core.constants.G = 6.6743e-11

Alias for GRAVITATIONAL_CONSTANT

Array Utilities

Array utility functions for the Tracker Component Library.

This module provides array manipulation functions that mirror MATLAB behavior, making it easier to port algorithms while maintaining Pythonic interfaces.

pytcl.core.array_utils.wrap_to_pi(angle)[source]

Wrap angles to the interval [-π, π).

This is equivalent to MATLAB’s wrapToPi function.

Parameters:

angle (array_like) – Angle(s) in radians.

Returns:

Angle(s) wrapped to [-π, π).

Return type:

NDArray

Examples

>>> wrap_to_pi(3 * np.pi)
-3.141592653589793
>>> wrap_to_pi([-4, -3, -2, -1, 0, 1, 2, 3, 4])
array([ 2.28318531, -3.        , -2.        , -1.        ,  0.        ,
        1.        ,  2.        ,  3.        , -2.28318531])
pytcl.core.array_utils.wrap_to_2pi(angle)[source]

Wrap angles to the interval [0, 2π).

This is equivalent to MATLAB’s wrapTo2Pi function.

Parameters:

angle (array_like) – Angle(s) in radians.

Returns:

Angle(s) wrapped to [0, 2π).

Return type:

NDArray

Examples

>>> wrap_to_2pi(-np.pi/2)
4.71238898038469
>>> wrap_to_2pi(3 * np.pi)
3.141592653589793
pytcl.core.array_utils.wrap_to_range(value, low, high)[source]

Wrap values to a specified interval [low, high).

Parameters:
  • value (array_like) – Value(s) to wrap.

  • low (float) – Lower bound of the interval (inclusive).

  • high (float) – Upper bound of the interval (exclusive).

Returns:

Value(s) wrapped to [low, high).

Return type:

NDArray

Examples

>>> wrap_to_range(370, 0, 360)
10.0
>>> wrap_to_range(-10, 0, 360)
350.0
pytcl.core.array_utils.wrap_to_pm180(angle)[source]

Wrap angles in degrees to the interval [-180, 180).

Parameters:

angle (array_like) – Angle(s) in degrees.

Returns:

Angle(s) wrapped to [-180, 180) degrees.

Return type:

NDArray

Examples

>>> wrap_to_pm180(270)
-90.0
pytcl.core.array_utils.wrap_to_360(angle)[source]

Wrap angles in degrees to the interval [0, 360).

Parameters:

angle (array_like) – Angle(s) in degrees.

Returns:

Angle(s) wrapped to [0, 360) degrees.

Return type:

NDArray

Examples

>>> wrap_to_360(-90)
270.0
pytcl.core.array_utils.column_vector(arr)[source]

Convert an array-like to a column vector (n, 1).

Parameters:

arr (array_like) – Input array.

Returns:

Column vector with shape (n, 1).

Return type:

NDArray

Examples

>>> column_vector([1, 2, 3])
array([[1],
       [2],
       [3]])
>>> column_vector([[1, 2, 3]])
array([[1],
       [2],
       [3]])
pytcl.core.array_utils.row_vector(arr)[source]

Convert an array-like to a row vector (1, n).

Parameters:

arr (array_like) – Input array.

Returns:

Row vector with shape (1, n).

Return type:

NDArray

Examples

>>> row_vector([1, 2, 3])
array([[1, 2, 3]])
>>> row_vector([[1], [2], [3]])
array([[1, 2, 3]])
pytcl.core.array_utils.vec(arr, order='F')[source]

Vectorize a matrix (stack columns or rows into a single column).

This mirrors MATLAB’s vec operator which stacks columns.

Parameters:
  • arr (array_like) – Input matrix.

  • order ({'F', 'C'}, optional) – ‘F’ (default): Stack columns (MATLAB-style, column-major). ‘C’: Stack rows (row-major).

Returns:

Column vector with shape (m*n, 1).

Return type:

NDArray

Examples

>>> A = np.array([[1, 2], [3, 4]])
>>> vec(A)  # Stack columns: [1, 3, 2, 4]
array([[1],
       [3],
       [2],
       [4]])
>>> vec(A, order='C')  # Stack rows: [1, 2, 3, 4]
array([[1],
       [2],
       [3],
       [4]])
pytcl.core.array_utils.unvec(v, shape, order='F')[source]

Reshape a vector back into a matrix.

Inverse of the vec operation.

Parameters:
  • v (array_like) – Input vector.

  • shape (tuple of int) – Target shape (rows, cols).

  • order ({'F', 'C'}, optional) – ‘F’ (default): Unstack to columns (MATLAB-style). ‘C’: Unstack to rows.

Returns:

Matrix with specified shape.

Return type:

NDArray

Examples

>>> import numpy as np
>>> from pytcl.core.array_utils import unvec
>>> v = np.array([1, 2, 3, 4, 5, 6])
>>> M = unvec(v, (2, 3))
>>> M
array([[1, 3, 5],
       [2, 4, 6]])
pytcl.core.array_utils.block_diag(*arrays)[source]

Create a block diagonal matrix from provided arrays.

Equivalent to MATLAB’s blkdiag function.

Parameters:

*arrays (array_like) – Input arrays to place on the diagonal.

Returns:

Block diagonal matrix.

Return type:

NDArray

Examples

>>> A = np.array([[1, 2], [3, 4]])
>>> B = np.array([[5]])
>>> block_diag(A, B)
array([[1, 2, 0],
       [3, 4, 0],
       [0, 0, 5]])
pytcl.core.array_utils.skew_symmetric(v)[source]

Create a 3x3 skew-symmetric matrix from a 3D vector.

The skew-symmetric matrix [v]× satisfies: [v]× @ u = v × u (cross product).

Parameters:

v (array_like) – 3-element vector.

Returns:

3x3 skew-symmetric matrix.

Return type:

NDArray

Examples

>>> v = [1, 2, 3]
>>> S = skew_symmetric(v)
>>> S
array([[ 0., -3.,  2.],
       [ 3.,  0., -1.],
       [-2.,  1.,  0.]])
>>> u = [4, 5, 6]
>>> np.allclose(S @ u, np.cross(v, u))
True
pytcl.core.array_utils.unskew(S)[source]

Extract the vector from a 3x3 skew-symmetric matrix.

Inverse of skew_symmetric.

Parameters:

S (array_like) – 3x3 skew-symmetric matrix.

Returns:

3-element vector.

Return type:

NDArray

Examples

>>> import numpy as np
>>> from pytcl.core.array_utils import unskew, skew_symmetric
>>> v = np.array([1, 2, 3])
>>> S = skew_symmetric(v)
>>> v_recovered = unskew(S)
>>> np.allclose(v, v_recovered)
True
pytcl.core.array_utils.normalize_vector(v, axis=None, return_norm=False)[source]

Normalize vector(s) to unit length.

Parameters:
  • v (array_like) – Vector(s) to normalize.

  • axis (int, optional) – Axis along which to compute norms. If None, normalize the flattened array.

  • return_norm (bool, optional) – If True, also return the original norm(s). Default is False.

Returns:

  • v_normalized (NDArray) – Unit vector(s).

  • norm (float or NDArray, optional) – Original norm(s), only returned if return_norm=True. A scalar float when v is 1-D and axis is None, otherwise an array.

Return type:

ndarray[tuple[Any, …], dtype[floating[Any]]] | tuple[ndarray[tuple[Any, …], dtype[floating[Any]]], float | ndarray[tuple[Any, …], dtype[floating[Any]]]]

Examples

>>> normalize_vector([3, 4])
array([0.6, 0.8])
>>> v_unit, norm = normalize_vector([3, 4], return_norm=True)
>>> norm
5.0
pytcl.core.array_utils.outer_product(a, b)[source]

Compute the outer product of two vectors.

Parameters:
  • a (array_like) – First vector (m,).

  • b (array_like) – Second vector (n,).

Returns:

Outer product matrix (m, n).

Return type:

NDArray

Examples

>>> outer_product([1, 2], [3, 4, 5])
array([[ 3,  4,  5],
       [ 6,  8, 10]])
pytcl.core.array_utils.repmat(arr, m, n)[source]

Replicate and tile an array.

Equivalent to MATLAB’s repmat function.

Parameters:
  • arr (array_like) – Input array.

  • m (int) – Number of times to replicate along rows.

  • n (int) – Number of times to replicate along columns.

Returns:

Tiled array.

Return type:

NDArray

Examples

>>> repmat([1, 2], 2, 3)
array([[1, 2, 1, 2, 1, 2],
       [1, 2, 1, 2, 1, 2]])
pytcl.core.array_utils.meshgrid_ij(*xi, indexing='ij')[source]

Create coordinate matrices from coordinate vectors.

Wrapper around np.meshgrid with ‘ij’ indexing as default (MATLAB-style).

Parameters:
  • *xi (array_like) – 1-D arrays representing coordinates.

  • indexing ({'ij', 'xy'}, optional) – Cartesian (‘xy’, default numpy) or matrix (‘ij’, MATLAB-style) indexing. Default is ‘ij’.

Returns:

Coordinate matrices.

Return type:

tuple of NDArray

Examples

>>> import numpy as np
>>> from pytcl.core.array_utils import meshgrid_ij
>>> x = np.array([1, 2, 3])
>>> y = np.array([4, 5])
>>> X, Y = meshgrid_ij(x, y)
>>> X
array([[1, 1],
       [2, 2],
       [3, 3]])
>>> Y
array([[4, 5],
       [4, 5],
       [4, 5]])
pytcl.core.array_utils.make_readonly(*arrays)[source]

Mark arrays read-only so they can be shared between callers safely.

Use this on anything handed out from behind a cache. A cached loader gives every caller the same object, so a writable array means one caller can silently change what every other holder sees. Marking it read-only turns that into an immediate ValueError at the assignment instead of a wrong answer discovered much later, or not at all.

Two cached DEM loaders had exactly this defect: two callers loading the same region received the same grid, and one write changed an elevation from 1287 m to -99999 m for the other, with no exception raised (gh-51).

A caller that needs to modify the data copies it first, which costs nothing for the majority who only read. Copying inside the loader instead would be correct but would duplicate a potentially large array on every call, which is most of what a cache exists to avoid.

Parameters:

*arrays (ndarray) – Arrays to mark. Modified in place; nothing is returned, to make it obvious at the call site that this is not a copy.

Examples

>>> import numpy as np
>>> shared = np.array([1.0, 2.0, 3.0])
>>> make_readonly(shared)
>>> shared.flags.writeable
False

A caller that needs to modify it works on a copy:

>>> working = shared.copy()
>>> working[0] = 99.0
>>> float(shared[0])
1.0
pytcl.core.array_utils.is_positive_definite(A, tol=1e-10)[source]

Check if a matrix is positive definite.

Every eigenvalue must be strictly positive. A singular matrix is positive semi-definite, not positive definite – use is_positive_semidefinite() for that.

Parameters:
  • A (array_like) – Square matrix to check.

  • tol (float, optional) – Relative tolerance on the eigenvalues, scaled by the largest magnitude eigenvalue. Default is 1e-10.

Returns:

True if the matrix is symmetric and every eigenvalue is positive.

Return type:

bool

Examples

>>> A = np.array([[4, 2], [2, 5]])
>>> is_positive_definite(A)
True

A singular matrix is not positive definite:

>>> is_positive_definite(np.diag([1.0, 0.0]))
False

Notes

This previously tested eigenvalues > -tol * max|lambda|, which admits zero and small negative eigenvalues, so diag(1, 0) returned True. The name promises a stronger guarantee than that test provided.

pytcl.core.array_utils.is_positive_semidefinite(A, tol=1e-10)[source]

Check if a matrix is positive semidefinite.

Every eigenvalue must be non-negative to within a relative tolerance. This is the right check for a covariance, which may legitimately be singular – a perfectly known state component gives a zero eigenvalue.

Parameters:
  • A (array_like) – Square matrix to check.

  • tol (float, optional) – Relative tolerance on the eigenvalues, scaled by the largest magnitude eigenvalue. Default is 1e-10.

Returns:

True if the matrix is symmetric and has no negative eigenvalue.

Return type:

bool

Examples

>>> is_positive_semidefinite(np.diag([1.0, 0.0]))
True
>>> is_positive_semidefinite(np.diag([1.0, -1.0]))
False
pytcl.core.array_utils.nearest_positive_definite(A)[source]

Find the nearest positive definite matrix.

Uses the method from Higham (1988) “Computing a Nearest Symmetric Positive Semidefinite Matrix”.

Parameters:

A (array_like) – Input matrix.

Returns:

Nearest positive definite matrix.

Return type:

NDArray

Examples

>>> import numpy as np
>>> from pytcl.core.array_utils import nearest_positive_definite
>>> A = np.array([[1, -2], [-2, 1]])  # Not PD
>>> A_pd = nearest_positive_definite(A)
>>> bool(np.min(np.linalg.eigvalsh(A_pd)) > -1e-10)
True
pytcl.core.array_utils.safe_cholesky(A, max_attempts=10)[source]

Compute Cholesky decomposition with fallback for near-singular matrices.

If standard Cholesky fails, attempts to find nearest positive definite matrix.

Parameters:
  • A (array_like) – Positive definite matrix.

  • max_attempts (int, optional) – Maximum regularization attempts. Default is 10.

Returns:

Lower triangular Cholesky factor L such that A = L @ L.T

Return type:

NDArray

Raises:

np.linalg.LinAlgError – If Cholesky decomposition fails after all attempts.

Examples

>>> import numpy as np
>>> from pytcl.core.array_utils import safe_cholesky
>>> A = np.array([[4, 2], [2, 3]])  # PD matrix
>>> L = safe_cholesky(A)
>>> np.allclose(L @ L.T, A)
True

Validation

Input validation utilities for the Tracker Component Library.

This module provides decorators and functions for validating input arrays, ensuring consistent behavior across the library and providing helpful error messages when inputs don’t meet requirements.

pytcl.core.validation.validate_array(arr, name='array', *, dtype=None, ndim=None, shape=None, min_ndim=None, max_ndim=None, finite=False, non_negative=False, positive=False, allow_empty=True)[source]

Validate and convert an array-like input to a NumPy array.

Parameters:
  • arr (array_like) – Input to validate and convert.

  • name (str, optional) – Name of the parameter (for error messages). Default is “array”.

  • dtype (type or np.dtype, optional) – If provided, ensure the array has this dtype (or can be safely cast).

  • ndim (int or tuple of int, optional) – If provided, ensure the array has exactly this number of dimensions. Can be a tuple to allow multiple valid dimensionalities.

  • shape (tuple, optional) – If provided, validate the shape. Use None for dimensions that can be any size. Example: (3, None) requires first dimension to be 3, second can be any size.

  • min_ndim (int, optional) – Minimum number of dimensions required.

  • max_ndim (int, optional) – Maximum number of dimensions allowed.

  • finite (bool, optional) – If True, ensure all elements are finite (no inf or nan). Default is False.

  • non_negative (bool, optional) – If True, ensure all elements are >= 0. Default is False.

  • positive (bool, optional) – If True, ensure all elements are > 0. Default is False.

  • allow_empty (bool, optional) – If False, raise an error for empty arrays. Default is True.

Returns:

Validated NumPy array.

Return type:

NDArray

Raises:

ValidationError – If the input fails any validation check.

Examples

>>> validate_array([1, 2, 3], "position", ndim=1, finite=True)
array([1, 2, 3])
>>> validate_array([[1, 2], [3, 4]], "matrix", shape=(2, 2))
array([[1, 2],
       [3, 4]])
pytcl.core.validation.ensure_2d(arr, name='array', axis='auto')[source]

Ensure an array is 2D, promoting 1D arrays as needed.

Parameters:
  • arr (array_like) – Input array.

  • name (str, optional) – Name of the parameter (for error messages).

  • axis ({'row', 'column', 'auto'}, optional) – How to promote 1D arrays: - ‘row’: Make 1D array a row vector (1, n) - ‘column’: Make 1D array a column vector (n, 1) - ‘auto’: Preserve as-is for 2D, use ‘column’ for 1D

Returns:

2D array.

Return type:

NDArray

Examples

>>> ensure_2d([1, 2, 3], axis='column')
array([[1],
       [2],
       [3]])
>>> ensure_2d([1, 2, 3], axis='row')
array([[1, 2, 3]])
pytcl.core.validation.ensure_column_vector(arr, name='vector')[source]

Ensure input is a column vector (n, 1).

Parameters:
  • arr (array_like) – Input array, must be 1D or a column vector.

  • name (str, optional) – Name of the parameter (for error messages).

Returns:

Column vector with shape (n, 1).

Return type:

NDArray

Examples

>>> ensure_column_vector([1, 2, 3])
array([[1],
       [2],
       [3]])
pytcl.core.validation.ensure_row_vector(arr, name='vector')[source]

Ensure input is a row vector (1, n).

Parameters:
  • arr (array_like) – Input array, must be 1D or a row vector.

  • name (str, optional) – Name of the parameter (for error messages).

Returns:

Row vector with shape (1, n).

Return type:

NDArray

Examples

>>> ensure_row_vector([1, 2, 3])
array([[1, 2, 3]])
pytcl.core.validation.ensure_square_matrix(arr, name='matrix')[source]

Ensure input is a square matrix.

Parameters:
  • arr (array_like) – Input array.

  • name (str, optional) – Name of the parameter (for error messages).

Returns:

Square matrix.

Return type:

NDArray

Raises:

ValidationError – If input is not a 2D square array.

pytcl.core.validation.ensure_symmetric(arr, name='matrix', rtol=1e-10, atol=1e-10)[source]

Ensure input is a symmetric matrix.

Parameters:
  • arr (array_like) – Input array.

  • name (str, optional) – Name of the parameter (for error messages).

  • rtol (float, optional) – Relative tolerance for symmetry check. Default is 1e-10.

  • atol (float, optional) – Absolute tolerance for symmetry check. Default is 1e-10.

Returns:

Symmetric matrix (symmetrized if nearly symmetric).

Return type:

NDArray

Raises:

ValidationError – If input is not symmetric within tolerance.

pytcl.core.validation.ensure_positive_definite(arr, name='matrix', rtol=1e-10)[source]

Ensure input is a positive definite matrix.

Every eigenvalue must be strictly positive. A singular matrix is positive semi-definite, not positive definite – use ensure_positive_semidefinite() for that, which is usually the right check for a covariance, since a perfectly known state component gives a zero eigenvalue.

Parameters:
  • arr (array_like) – Input array.

  • name (str, optional) – Name of the parameter (for error messages).

  • rtol (float, optional) – Relative tolerance on the eigenvalues, scaled by the largest magnitude eigenvalue. Default is 1e-10.

Returns:

Positive definite matrix.

Return type:

NDArray

Raises:

ValidationError – If input is not symmetric, or has any eigenvalue that is not strictly positive.

Notes

This previously tested min(eigenvalues) >= -rtol * max|lambda|. That threshold is negative, so it admitted zero and small negative eigenvalues and accepted diag(1, 0) – the semidefinite test, under a name and a docstring promising definiteness. pytcl.core.is_positive_definite() carried the identical defect and was corrected earlier; this is that fix applied to the validating counterpart, which the earlier pass missed.

pytcl.core.validation.ensure_positive_semidefinite(arr, name='matrix', rtol=1e-10)[source]

Ensure input is a positive semidefinite matrix.

Every eigenvalue must be non-negative to within a relative tolerance. This is the right check for a covariance, which may legitimately be singular – a perfectly known state component gives a zero eigenvalue. For the stricter check use ensure_positive_definite().

Parameters:
  • arr (array_like) – Input array.

  • name (str, optional) – Name of the parameter (for error messages).

  • rtol (float, optional) – Relative tolerance on the eigenvalues, scaled by the largest magnitude eigenvalue. Default is 1e-10.

Returns:

Positive semidefinite matrix.

Return type:

NDArray

Raises:

ValidationError – If input is not symmetric, or has a negative eigenvalue beyond tolerance.

Examples

>>> import numpy as np
>>> from pytcl.core.validation import ensure_positive_semidefinite
>>> _ = ensure_positive_semidefinite(np.diag([1.0, 0.0]))
pytcl.core.validation.validate_same_shape(*arrays, names=None)[source]

Validate that all input arrays have the same shape.

Parameters:
  • *arrays (array_like) – Arrays to compare.

  • names (sequence of str, optional) – Names for error messages. If not provided, uses “array_0”, “array_1”, etc.

Raises:

ValidationError – If arrays have different shapes.

pytcl.core.validation.validated_array_input(param_name, *, dtype=None, ndim=None, shape=None, finite=False)[source]

Decorator factory for validating a specific array parameter.

Parameters:
  • param_name (str) – Name of the parameter to validate.

  • dtype (type or np.dtype, optional) – Required dtype.

  • ndim (int or tuple of int, optional) – Required number of dimensions.

  • shape (tuple, optional) – Required shape (None for any size in a dimension).

  • finite (bool, optional) – If True, require all finite values.

Returns:

Decorator that validates the specified parameter.

Return type:

Callable

Examples

>>> @validated_array_input("x", ndim=1, finite=True)
... def my_func(x, y=1):
...     return np.sum(x) + y
class pytcl.core.validation.ArraySpec(*, dtype=None, ndim=None, shape=None, min_ndim=None, max_ndim=None, finite=False, non_negative=False, positive=False, allow_empty=True, square=False, symmetric=False, positive_definite=False)[source]

Bases: object

Specification for array validation in @validate_inputs decorator.

Parameters:
  • dtype (type or np.dtype, optional) – Required dtype.

  • ndim (int or tuple of int, optional) – Required dimensionality.

  • shape (tuple, optional) – Required shape (None for any size).

  • min_ndim (int, optional) – Minimum dimensions required.

  • max_ndim (int, optional) – Maximum dimensions allowed.

  • finite (bool, optional) – Require all finite values.

  • non_negative (bool, optional) – Require all values >= 0.

  • positive (bool, optional) – Require all values > 0.

  • allow_empty (bool, optional) – Allow empty arrays. Default True.

  • square (bool, optional) – Require square matrix.

  • symmetric (bool, optional) – Require symmetric matrix.

  • positive_definite (bool, optional) – Require positive definite matrix.

Examples

>>> spec = ArraySpec(ndim=2, finite=True, square=True)
>>> @validate_inputs(matrix=spec)
... def process_matrix(matrix):
...     return np.linalg.inv(matrix)
__init__(*, dtype=None, ndim=None, shape=None, min_ndim=None, max_ndim=None, finite=False, non_negative=False, positive=False, allow_empty=True, square=False, symmetric=False, positive_definite=False)[source]
validate(arr, name)[source]

Validate an array against this specification.

class pytcl.core.validation.ScalarSpec(*, dtype=None, min_value=None, max_value=None, finite=False, positive=False, non_negative=False)[source]

Bases: object

Specification for scalar validation in @validate_inputs decorator.

Parameters:
  • dtype (type, optional) – Required type (int, float, etc.).

  • min_value (float, optional) – Minimum allowed value (inclusive).

  • max_value (float, optional) – Maximum allowed value (inclusive).

  • finite (bool, optional) – Require finite value.

  • positive (bool, optional) – Require value > 0.

  • non_negative (bool, optional) – Require value >= 0.

Examples

>>> spec = ScalarSpec(dtype=int, min_value=1, max_value=10)
>>> @validate_inputs(k=spec)
... def get_k_nearest(k, data):
...     return data[:k]
__init__(*, dtype=None, min_value=None, max_value=None, finite=False, positive=False, non_negative=False)[source]
validate(value, name)[source]

Validate a scalar value against this specification.

pytcl.core.validation.validate_inputs(**param_specs)[source]

Decorator for validating multiple function parameters.

This decorator enables declarative input validation using specification objects (ArraySpec, ScalarSpec) or dictionaries of validation options.

Parameters:

**param_specs (ArraySpec | ScalarSpec | dict) – Keyword arguments mapping parameter names to validation specs. Each spec can be: - ArraySpec: For array validation - ScalarSpec: For scalar validation - dict: Options passed to ArraySpec (for convenience)

Returns:

Decorated function with input validation.

Return type:

Callable

Examples

>>> @validate_inputs(
...     x=ArraySpec(ndim=2, finite=True),
...     P=ArraySpec(ndim=2, positive_definite=True),
...     k=ScalarSpec(dtype=int, min_value=1),
... )
... def kalman_update(x, P, z, H, R, k=1):
...     # x and P are guaranteed valid here
...     pass

Using dict shorthand:

>>> @validate_inputs(
...     state={"ndim": 1, "finite": True},
...     covariance={"ndim": 2, "positive_definite": True},
... )
... def predict(state, covariance, dt):
...     pass

Notes

Validation happens in the order parameters are defined in the decorator. If any validation fails, a ValidationError is raised with a descriptive message identifying the parameter and the constraint violated.

See also

ArraySpec

Specification class for array validation.

ScalarSpec

Specification class for scalar validation.

validate_array

Lower-level array validation function.

pytcl.core.validation.check_compatible_shapes(*shapes, names=None, dimension=None)[source]

Check that array shapes are compatible for operations.

Parameters:
  • *shapes (tuple of int) – Shapes to check for compatibility.

  • names (sequence of str, optional) – Names for error messages.

  • dimension (int, optional) – If provided, only check compatibility along this dimension.

Raises:

ValidationError – If shapes are not compatible.

Examples

Incompatible sizes along a specific dimension raise:

>>> check_compatible_shapes((3, 4), (4, 5), names=["A", "B"], dimension=0)
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ValidationError: Arrays have incompatible sizes along dimension 0: A=3, B=4

Without a dimension constraint this passes (inner dimensions are compatible for matrix multiply):

>>> check_compatible_shapes((3, 4), (4, 5), names=["A", "B"])

Exceptions

Custom exception hierarchy for the Tracker Component Library.

This module provides a structured exception hierarchy for consistent error handling across the library. Custom exceptions enable more specific error catching, better error messages, and improved debugging.

Exception Hierarchy

TCLError (base)
├── ValidationError (input validation failures)
│   ├── DimensionError (array shape/dimension mismatches)
│   ├── ParameterError (invalid parameter values)
│   └── RangeError (out-of-range values)
├── ComputationError (numerical computation failures)
│   ├── ConvergenceError (iterative algorithm non-convergence)
│   ├── NumericalError (numerical stability issues)
│   └── SingularMatrixError (singular/non-invertible matrix)
├── StateError (object state violations)
│   ├── UninitializedError (object not initialized)
│   └── EmptyContainerError (container has no elements)
├── ConfigurationError (configuration/setup issues)
│   ├── MethodError (invalid method/algorithm selection)
│   └── DependencyError (missing optional dependency)
└── DataError (data format/structure issues)
    ├── FormatError (invalid data format)
    └── ParseError (data parsing failures)

Examples

Catching specific exception types:

>>> from pytcl.core.exceptions import ConvergenceError, ParameterError
>>> try:
...     result = solve_kepler(M=1.5, e=1.5)  # Invalid eccentricity
... except ParameterError as e:
...     print(f"Invalid parameter: {e}")

Catching all TCL errors:

>>> from pytcl.core.exceptions import TCLError
>>> try:
...     result = compute_orbit(...)
... except TCLError as e:
...     print(f"TCL error: {e}")
exception pytcl.core.exceptions.TCLError(message, details=None)[source]

Bases: Exception

Base exception for all Tracker Component Library errors.

All custom exceptions in the library inherit from this class, allowing users to catch all TCL-specific errors with a single except clause.

Parameters:
  • message (str) – Human-readable error message.

  • details (dict, optional) – Additional context about the error (parameter values, etc.).

Examples

>>> raise TCLError("Something went wrong", details={"value": 42})
Traceback (most recent call last):
    ...
pytcl.core.exceptions.TCLError: Something went wrong (value=42)
__init__(message, details=None)[source]
exception pytcl.core.exceptions.ValidationError(message, parameter=None, expected=None, actual=None, **kwargs)[source]

Bases: TCLError, ValueError

Base exception for input validation failures.

Raised when function inputs fail validation checks. This extends both TCLError (for TCL-specific catching) and ValueError (for compatibility with code expecting standard Python exceptions).

Parameters:
  • message (str) – Description of the validation failure.

  • parameter (str, optional) – Name of the parameter that failed validation.

  • expected (str, optional) – Description of what was expected.

  • actual (Any, optional) – The actual value that was provided.

Examples

>>> raise ValidationError(
...     "Invalid matrix dimensions",
...     parameter="P",
...     expected="3x3 matrix",
...     actual="2x4 array"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ValidationError: Invalid matrix dimensions (parameter=P, expected=3x3 matrix, actual=2x4 array)
__init__(message, parameter=None, expected=None, actual=None, **kwargs)[source]
exception pytcl.core.exceptions.DimensionError(message, expected_shape=None, actual_shape=None, parameter=None, **kwargs)[source]

Bases: ValidationError

Exception for array dimension and shape mismatches.

Raised when array dimensions or shapes don’t match requirements or aren’t compatible with each other.

Parameters:
  • message (str) – Description of the dimension mismatch.

  • expected_shape (tuple, optional) – Expected shape.

  • actual_shape (tuple, optional) – Actual shape.

  • parameter (str, optional) – Name of the parameter.

Examples

>>> raise DimensionError(
...     "Covariance matrix must be 3x3",
...     expected_shape=(3, 3),
...     actual_shape=(2, 4),
...     parameter="P"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.DimensionError: Covariance matrix must be 3x3 (parameter=P, expected_shape=(3, 3), actual_shape=(2, 4))
__init__(message, expected_shape=None, actual_shape=None, parameter=None, **kwargs)[source]
exception pytcl.core.exceptions.ParameterError(message, parameter=None, value=None, constraint=None, **kwargs)[source]

Bases: ValidationError

Exception for invalid parameter values.

Raised when a parameter value violates constraints (type, value range, allowed values, etc.).

Parameters:
  • message (str) – Description of the invalid parameter.

  • parameter (str, optional) – Name of the invalid parameter.

  • value (Any, optional) – The invalid value.

  • constraint (str, optional) – Description of the constraint that was violated.

Examples

>>> raise ParameterError(
...     "Variance must be positive",
...     parameter="variance",
...     value=-1.0,
...     constraint="must be > 0"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ParameterError: Variance must be positive (parameter=variance, value=-1.0, constraint=must be > 0)
__init__(message, parameter=None, value=None, constraint=None, **kwargs)[source]
exception pytcl.core.exceptions.RangeError(message, parameter=None, value=None, min_value=None, max_value=None, **kwargs)[source]

Bases: ValidationError

Exception for out-of-range values.

Raised when a numeric value falls outside an allowed range.

Parameters:
  • message (str) – Description of the range violation.

  • parameter (str, optional) – Name of the parameter.

  • value (float, optional) – The out-of-range value.

  • min_value (float, optional) – Minimum allowed value.

  • max_value (float, optional) – Maximum allowed value.

Examples

>>> raise RangeError(
...     "Eccentricity must be in [0, 1) for elliptic orbits",
...     parameter="e",
...     value=1.5,
...     min_value=0.0,
...     max_value=1.0
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.RangeError: Eccentricity must be in [0, 1) for elliptic orbits (parameter=e, value=1.5, min=0.0, max=1.0)
__init__(message, parameter=None, value=None, min_value=None, max_value=None, **kwargs)[source]
exception pytcl.core.exceptions.ComputationError(message, algorithm=None, **kwargs)[source]

Bases: TCLError, RuntimeError

Base exception for numerical computation failures.

Raised when a numerical algorithm fails to produce a valid result. This extends RuntimeError for compatibility with code that catches standard computation errors.

Parameters:
  • message (str) – Description of the computation failure.

  • algorithm (str, optional) – Name of the algorithm that failed.

Examples

>>> raise ComputationError(
...     "Failed to compute eigenvalues",
...     algorithm="numpy.linalg.eig"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ComputationError: Failed to compute eigenvalues (algorithm=numpy.linalg.eig)
__init__(message, algorithm=None, **kwargs)[source]
exception pytcl.core.exceptions.ConvergenceError(message, algorithm=None, iterations=None, max_iterations=None, residual=None, tolerance=None, **kwargs)[source]

Bases: ComputationError

Exception for iterative algorithm convergence failures.

Raised when an iterative algorithm fails to converge within the maximum number of iterations.

Parameters:
  • message (str) – Description of the convergence failure.

  • algorithm (str, optional) – Name of the algorithm.

  • iterations (int, optional) – Number of iterations performed.

  • max_iterations (int, optional) – Maximum iterations allowed.

  • residual (float, optional) – Final residual or error value.

  • tolerance (float, optional) – Convergence tolerance.

Examples

>>> raise ConvergenceError(
...     "Kepler's equation did not converge",
...     algorithm="Newton-Raphson",
...     iterations=100,
...     max_iterations=100,
...     residual=1e-5,
...     tolerance=1e-12
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ConvergenceError: Kepler's equation did not converge (algorithm=Newton-Raphson, iterations=100, max_iterations=100, residual=1e-05, tolerance=1e-12)
__init__(message, algorithm=None, iterations=None, max_iterations=None, residual=None, tolerance=None, **kwargs)[source]
exception pytcl.core.exceptions.NumericalError(message, operation=None, condition_number=None, **kwargs)[source]

Bases: ComputationError

Exception for numerical stability issues.

Raised when numerical operations fail due to precision issues, overflow, underflow, or ill-conditioned computations.

Parameters:
  • message (str) – Description of the numerical issue.

  • operation (str, optional) – The operation that failed.

  • condition_number (float, optional) – Matrix condition number (if applicable).

Examples

>>> raise NumericalError(
...     "Matrix is ill-conditioned",
...     operation="matrix inversion",
...     condition_number=1e16
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.NumericalError: Matrix is ill-conditioned (operation=matrix inversion, condition_number=1e+16)
__init__(message, operation=None, condition_number=None, **kwargs)[source]
exception pytcl.core.exceptions.SingularMatrixError(message, matrix_name=None, determinant=None, **kwargs)[source]

Bases: ComputationError

Exception for singular or non-invertible matrix operations.

Raised when a matrix operation requires an invertible matrix but the matrix is singular or nearly singular.

Parameters:
  • message (str) – Description of the singularity issue.

  • matrix_name (str, optional) – Name of the singular matrix.

  • determinant (float, optional) – Determinant value (near zero).

Examples

>>> raise SingularMatrixError(
...     "Covariance matrix is singular",
...     matrix_name="P",
...     determinant=1e-20
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.SingularMatrixError: Covariance matrix is singular (matrix=P, determinant=1e-20)
__init__(message, matrix_name=None, determinant=None, **kwargs)[source]
exception pytcl.core.exceptions.StateError(message, object_type=None, current_state=None, required_state=None, **kwargs)[source]

Bases: TCLError

Base exception for object state violations.

Raised when an operation is attempted on an object that is not in a valid state for that operation.

Parameters:
  • message (str) – Description of the state violation.

  • object_type (str, optional) – Type of the object.

  • current_state (str, optional) – Description of the current state.

  • required_state (str, optional) – Description of the required state.

Examples

>>> raise StateError(
...     "Cannot update without prediction",
...     object_type="KalmanFilter",
...     current_state="uninitialized",
...     required_state="predicted"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.StateError: Cannot update without prediction (object_type=KalmanFilter, current_state=uninitialized, required_state=predicted)
__init__(message, object_type=None, current_state=None, required_state=None, **kwargs)[source]
exception pytcl.core.exceptions.UninitializedError(message, object_type=None, required_initialization=None, **kwargs)[source]

Bases: StateError

Exception for uninitialized object access.

Raised when an operation requires an initialized object but the object hasn’t been properly initialized.

Parameters:
  • message (str) – Description of the issue.

  • object_type (str, optional) – Type of the uninitialized object.

  • required_initialization (str, optional) – What initialization is required.

Examples

>>> raise UninitializedError(
...     "Tracker not initialized",
...     object_type="SingleTargetTracker",
...     required_initialization="call initialize() first"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.UninitializedError: Tracker not initialized (object_type=SingleTargetTracker, current_state=uninitialized, required_state=call initialize() first)
__init__(message, object_type=None, required_initialization=None, **kwargs)[source]
exception pytcl.core.exceptions.EmptyContainerError(message, container_type=None, operation=None, **kwargs)[source]

Bases: StateError

Exception for empty container operations.

Raised when an operation requires a non-empty container but the container has no elements.

Parameters:
  • message (str) – Description of the issue.

  • container_type (str, optional) – Type of the empty container.

  • operation (str, optional) – The operation that requires non-empty container.

Examples

>>> raise EmptyContainerError(
...     "Cannot query empty RTree",
...     container_type="RTree",
...     operation="nearest neighbor query"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.EmptyContainerError: Cannot query empty RTree (object_type=RTree, current_state=empty, operation=nearest neighbor query)
__init__(message, container_type=None, operation=None, **kwargs)[source]
exception pytcl.core.exceptions.ConfigurationError(message, details=None)[source]

Bases: TCLError

Base exception for configuration and setup issues.

Raised when there are problems with algorithm configuration, method selection, or dependency availability.

Parameters:

message (str) – Description of the configuration issue.

Examples

>>> raise ConfigurationError("Invalid filter configuration")
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ConfigurationError: Invalid filter configuration
exception pytcl.core.exceptions.MethodError(message, method=None, valid_methods=None, **kwargs)[source]

Bases: ConfigurationError, ValueError

Exception for invalid method or algorithm selection.

Raised when an unknown or unsupported method/algorithm is specified.

Parameters:
  • message (str) – Description of the issue.

  • method (str, optional) – The invalid method name.

  • valid_methods (sequence of str, optional) – List of valid method names.

Examples

>>> raise MethodError(
...     "Unknown assignment method",
...     method="invalid_method",
...     valid_methods=["hungarian", "auction", "greedy"]
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.MethodError: Unknown assignment method (method=invalid_method, valid_methods=['hungarian', 'auction', 'greedy'])
__init__(message, method=None, valid_methods=None, **kwargs)[source]
exception pytcl.core.exceptions.DependencyError(message, package=None, feature=None, install_command=None, **kwargs)[source]

Bases: ConfigurationError, ImportError

Exception for missing optional dependencies.

Raised when an optional dependency is required but not installed.

Parameters:
  • message (str) – Description of the missing dependency.

  • package (str, optional) – Name of the missing package.

  • feature (str, optional) – Feature that requires the dependency.

  • install_command (str, optional) – Command to install the dependency.

Examples

>>> raise DependencyError(
...     "plotly is required for interactive plotting",
...     package="plotly",
...     feature="3D visualization",
...     install_command="pip install plotly"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.DependencyError: plotly is required for interactive plotting (package=plotly, feature=3D visualization, install=pip install plotly)
__init__(message, package=None, feature=None, install_command=None, **kwargs)[source]
exception pytcl.core.exceptions.DataError(message, details=None)[source]

Bases: TCLError

Base exception for data format and structure issues.

Raised when input data has format or structural problems.

Parameters:

message (str) – Description of the data issue.

Examples

>>> raise DataError("Invalid input data format")
Traceback (most recent call last):
    ...
pytcl.core.exceptions.DataError: Invalid input data format
exception pytcl.core.exceptions.FormatError(message, expected_format=None, actual_format=None, **kwargs)[source]

Bases: DataError, ValueError

Exception for invalid data format.

Raised when data doesn’t conform to the expected format.

Parameters:
  • message (str) – Description of the format issue.

  • expected_format (str, optional) – Description of the expected format.

  • actual_format (str, optional) – Description of the actual format.

Examples

>>> raise FormatError(
...     "Invalid TLE format",
...     expected_format="69 characters per line",
...     actual_format="line 1 has 65 characters"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.FormatError: Invalid TLE format (expected=69 characters per line, actual=line 1 has 65 characters)
__init__(message, expected_format=None, actual_format=None, **kwargs)[source]
exception pytcl.core.exceptions.ParseError(message, data_type=None, position=None, reason=None, **kwargs)[source]

Bases: DataError, ValueError

Exception for data parsing failures.

Raised when data cannot be parsed or interpreted.

Parameters:
  • message (str) – Description of the parsing failure.

  • data_type (str, optional) – Type of data being parsed.

  • position (int or tuple, optional) – Position where parsing failed.

  • reason (str, optional) – Reason for the parsing failure.

Examples

>>> raise ParseError(
...     "Failed to parse TLE checksum",
...     data_type="TLE",
...     position=68,
...     reason="invalid checksum digit"
... )
Traceback (most recent call last):
    ...
pytcl.core.exceptions.ParseError: Failed to parse TLE checksum (data_type=TLE, position=68, reason=invalid checksum digit)
__init__(message, data_type=None, position=None, reason=None, **kwargs)[source]

Maturity

Module maturity classification system for the Tracker Component Library.

This module provides a standardized way to indicate the production-readiness and stability of different modules within pyTCL. The maturity levels help users understand which APIs are stable and which may change.

Maturity Levels

STABLE (3)

Production-ready. Thoroughly tested, well-documented, and API is frozen. Breaking changes only in major version bumps.

MATURE (2)

Ready for production use. Good test coverage and documentation. Minor API adjustments possible in minor versions.

EXPERIMENTAL (1)

Functional but may change. Limited testing or documentation. API may change in any release.

DEPRECATED (0)

Scheduled for removal. Use the recommended replacement.

Examples

Check the maturity level of a module:

>>> from pytcl.core.maturity import get_maturity, MaturityLevel
>>> level = get_maturity("pytcl.dynamic_estimation.kalman.linear")
>>> level == MaturityLevel.STABLE
True

List all stable modules:

>>> from pytcl.core.maturity import get_modules_by_maturity, MaturityLevel
>>> stable_modules = get_modules_by_maturity(MaturityLevel.STABLE)

Notes

How modules are classified. The levels are a promise about breakage, so they are assigned from evidence rather than impression:

  • STABLE is never assigned automatically. Freezing an API is a release commitment, not something a coverage number implies; promoting a module here is a deliberate act.

  • MATURE requires at least 90% line coverage, no behaviour change in the current release, and a code path CI can actually execute.

  • EXPERIMENTAL covers everything else: below that coverage bar, changed behaviour this release, or – for pytcl.gpu – a CuPy branch that no CI runner can reach, so the reported coverage reflects only the MLX half.

Two invariants are enforced by tests/unit/test_maturity_comprehensive.py: every registered path must import, and every module must be registered. The first exists because 32 of 78 entries once named modules from a pre-2.0 layout, and since lookup is by exact path they silently reported EXPERIMENTAL instead of their recorded level. The second exists because whole subsystems shipped – io, diagnostics, transponders, gpu – without ever being classified.

See also

pytcl.core.optional_deps

Optional dependency management.

class pytcl.core.maturity.MaturityLevel(value)[source]

Bases: IntEnum

Maturity level classification for modules.

Variables:
  • DEPRECATED (int) – Level 0. Scheduled for removal.

  • EXPERIMENTAL (int) – Level 1. Functional but unstable API.

  • MATURE (int) – Level 2. Production-ready with possible minor changes.

  • STABLE (int) – Level 3. Production-ready with frozen API.

DEPRECATED = 0
EXPERIMENTAL = 1
MATURE = 2
STABLE = 3
pytcl.core.maturity.get_maturity(module_path)[source]

Get the maturity level of a module.

Parameters:

module_path (str) – Module path relative to pytcl (e.g., “dynamic_estimation.kalman.linear”) or full path (e.g., “pytcl.dynamic_estimation.kalman.linear”).

Returns:

The module’s maturity level. Returns EXPERIMENTAL if not classified.

Return type:

MaturityLevel

Examples

>>> get_maturity("dynamic_estimation.kalman.linear")
<MaturityLevel.STABLE: 3>
>>> get_maturity("pytcl.core.constants")
<MaturityLevel.STABLE: 3>
pytcl.core.maturity.get_modules_by_maturity(level)[source]

Get all modules at a specific maturity level.

Parameters:

level (MaturityLevel) – The maturity level to filter by.

Returns:

Module paths at the specified maturity level.

Return type:

list of str

Examples

>>> stable = get_modules_by_maturity(MaturityLevel.STABLE)
>>> "core.constants" in stable
True
pytcl.core.maturity.get_maturity_summary()[source]

Get a summary count of modules at each maturity level.

Returns:

Mapping from MaturityLevel to count of modules.

Return type:

dict

Examples

>>> summary = get_maturity_summary()
>>> summary[MaturityLevel.STABLE] > 0
True
pytcl.core.maturity.is_stable(module_path)[source]

Check if a module is stable (production-ready with frozen API).

Parameters:

module_path (str) – Module path to check.

Returns:

True if the module is stable.

Return type:

bool

Examples

>>> is_stable("dynamic_estimation.kalman.linear")
True
>>> is_stable("terrain.dem")
False
pytcl.core.maturity.is_production_ready(module_path)[source]

Check if a module is production-ready (STABLE or MATURE).

Parameters:

module_path (str) – Module path to check.

Returns:

True if the module is STABLE or MATURE.

Return type:

bool

Examples

>>> is_production_ready("dynamic_estimation.kalman.linear")
True
>>> is_production_ready("dynamic_estimation.imm")
True
>>> is_production_ready("terrain.dem")
False
pytcl.core.maturity.format_maturity_badge(level)[source]

Get a formatted badge string for a maturity level.

Parameters:

level (MaturityLevel) – The maturity level.

Returns:

A badge string suitable for documentation.

Return type:

str

Examples

>>> format_maturity_badge(MaturityLevel.STABLE)
'|stable|'

Optional Deps

Optional dependencies management for the Tracker Component Library.

This module provides a unified system for handling optional dependencies, including lazy imports, availability checks, and helpful error messages.

The system supports: - Lazy imports that only load modules when accessed - Availability flags for conditional code paths - Decorators for functions requiring optional dependencies - Helpful error messages with installation instructions

Examples

Check if a dependency is available:

>>> from pytcl.core.optional_deps import is_available
>>> if is_available("plotly"):
...     import plotly.graph_objects as go

Use a decorator to require a dependency:

>>> from pytcl.core.optional_deps import requires
>>> @requires("plotly", extra="visualization")
... def create_3d_plot(data):
...     import plotly.graph_objects as go
...     return go.Figure(data)

Import with a helpful error on failure:

>>> from pytcl.core.optional_deps import import_optional
>>> go = import_optional("plotly.graph_objects", package="plotly", extra="visualization")
pytcl.core.optional_deps.is_available(package)[source]

Check if an optional package is available.

Parameters:

package (str) – Name of the package to check (e.g., “plotly”, “pywt”).

Returns:

True if the package is installed and can be imported.

Return type:

bool

Examples

>>> from pytcl.core.optional_deps import is_available
>>> if is_available("plotly"):
...     from plotly import graph_objects as go
...     # use plotly
... else:
...     print("Plotly not available")

Notes

Results are cached for performance. Use _clear_cache() if you need to re-check availability (e.g., after installing a package).

pytcl.core.optional_deps.import_optional(module_name, *, package=None, extra=None, feature=None)[source]

Import an optional module with a helpful error message on failure.

Parameters:
  • module_name (str) – Full module path to import (e.g., “plotly.graph_objects”).

  • package (str, optional) – Package name for error message. If not provided, extracted from module_name.

  • extra (str, optional) – Name of the pytcl extra that provides this dependency (e.g., “visualization”, “astronomy”).

  • feature (str, optional) – Description of the feature requiring this dependency.

Returns:

module – The imported module.

Return type:

ModuleType

Raises:

DependencyError – If the module cannot be imported.

Examples

>>> go = import_optional(
...     "plotly.graph_objects",
...     package="plotly",
...     extra="visualization",
...     feature="3D plotting"
... )
pytcl.core.optional_deps.requires(*packages, extra=None, feature=None)[source]

Decorator to mark a function as requiring optional dependencies.

When the decorated function is called, it checks if the required packages are available. If not, it raises a DependencyError with a helpful message.

Parameters:
  • *packages (str) – One or more package names required by the function.

  • extra (str, optional) – Name of the pytcl extra that provides these dependencies.

  • feature (str, optional) – Description of the feature provided by the function.

Returns:

decorator – Decorator that wraps the function with dependency checking.

Return type:

callable

Examples

>>> from pytcl.core.optional_deps import requires
>>>
>>> @requires("plotly", extra="visualization")
... def create_plot(data):
...     import plotly.graph_objects as go
...     return go.Figure(data)
>>>
>>> # This will raise DependencyError if plotly is not installed
>>> fig = create_plot([1, 2, 3])

Multiple packages:

>>> @requires("astropy", "jplephem", extra="astronomy")
... def compute_ephemeris(body, time):
...     from astropy.time import Time
...     import jplephem
...     # ...

Notes

The decorator checks availability at call time, not at definition time. This allows the module to be imported even if the optional dependencies are not installed.

pytcl.core.optional_deps.check_dependencies(*packages, extra=None)[source]

Check that all required packages are available.

Parameters:
  • *packages (str) – Package names to check.

  • extra (str, optional) – pytcl extra for installation hint.

Raises:

DependencyError – If any package is not available.

Examples

>>> from pytcl.core.optional_deps import check_dependencies
>>> check_dependencies("plotly", extra="visualization")
>>> # Raises DependencyError if plotly is not installed
class pytcl.core.optional_deps.LazyModule(module_name, *, package=None, extra=None, feature=None)[source]

Bases: object

A lazy module loader that imports the module on first access.

This allows optional dependencies to be “imported” at module level without triggering an import error until they’re actually used.

Parameters:
  • module_name (str) – Full module path to import.

  • package (str, optional) – Package name for error messages.

  • extra (str, optional) – pytcl extra that provides this dependency.

  • feature (str, optional) – Feature description for error messages.

Examples

>>> from pytcl.core.optional_deps import LazyModule
>>> go = LazyModule("plotly.graph_objects", package="plotly")
>>> # No import yet...
>>> fig = go.Figure()  # Import happens here
__init__(module_name, *, package=None, extra=None, feature=None)[source]
__getattr__(name)[source]

Delegate attribute access to the loaded module.

__dir__()[source]

Return module attributes for tab completion.

Paths

Shared data directory utilities for pytcl.

All modules that load external data files (terrain, magnetism, gravity) use this common utility for locating the data directory.

pytcl.core.paths.get_data_dir()[source]

Get the pytcl data directory for external data files.

The data directory is located at ~/.pytcl/data/ by default. Can be overridden by setting the PYTCL_DATA_DIR environment variable.

Returns:

Path to the data directory.

Return type:

Path

pytcl.core.paths.ensure_data_dir()[source]

Ensure the data directory exists and return its path.