Assignment Algorithms

Algorithms for data association and assignment problems in multi-target tracking.

Assignment algorithms for data association in target tracking.

This module provides: - 2D assignment algorithms (Hungarian, Auction) - K-best 2D assignment (Murty’s algorithm) - 3D assignment algorithms (Lagrangian relaxation, Auction) - Gating methods (ellipsoidal, rectangular) - Data association algorithms (GNN, JPDA)

pytcl.assignment_algorithms.hungarian(cost_matrix, maximize=False)[source]

Solve 2D assignment using Hungarian (Kuhn-Munkres) algorithm.

The Hungarian algorithm finds the optimal assignment that minimizes (or maximizes) the total cost.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n, m). Can be rectangular.

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

  • row_ind (ndarray) – Row indices of optimal assignment.

  • col_ind (ndarray) – Column indices of optimal assignment.

  • total_cost (float) – Total cost of the assignment.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], ndarray[tuple[Any, …], dtype[int64]], float]

Examples

>>> cost = np.array([[4, 1, 3], [2, 0, 5], [3, 2, 2]])
>>> row_ind, col_ind, total_cost = hungarian(cost)
>>> total_cost
5.0

Notes

Time complexity is O(n^3) for an n x n matrix.

References

pytcl.assignment_algorithms.auction(cost_matrix, epsilon=None, max_iter=1000, maximize=False)[source]

Solve 2D assignment using the Auction algorithm.

The auction algorithm is an iterative method that can be faster than Hungarian for sparse problems and is naturally parallelizable.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n, m).

  • epsilon (float, optional) – Price increment. If None, uses 1/(n+1) where n is matrix size.

  • max_iter (int, optional) – Maximum iterations (default: 1000).

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

  • row_ind (ndarray) – Row indices of optimal assignment.

  • col_ind (ndarray) – Column indices of optimal assignment.

  • total_cost (float) – Total cost of the assignment.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], ndarray[tuple[Any, …], dtype[int64]], float]

Examples

>>> cost = np.array([[4, 1, 3], [2, 0, 5], [3, 2, 2]])
>>> row_ind, col_ind, total_cost = auction(cost)

Notes

The auction algorithm treats rows as “bidders” and columns as “objects”. Each iteration, unassigned bidders bid for their most desirable objects, and objects are assigned to the highest bidder.

References

pytcl.assignment_algorithms.linear_sum_assignment(cost_matrix, maximize=False)[source]

Solve the linear sum assignment problem (wrapper around scipy).

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n, m).

  • maximize (bool, optional) – If True, solve maximization problem instead of minimization.

Returns:

  • row_ind (ndarray) – Row indices of optimal assignment.

  • col_ind (ndarray) – Column indices of optimal assignment.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], ndarray[tuple[Any, …], dtype[int64]]]

Examples

>>> cost = np.array([[4, 1, 3], [2, 0, 5], [3, 2, 2]])
>>> row_ind, col_ind = linear_sum_assignment(cost)
>>> row_ind
array([0, 1, 2])
>>> col_ind
array([1, 0, 2])
>>> cost[row_ind, col_ind].sum()
5

Notes

This is a thin wrapper around scipy.optimize.linear_sum_assignment for API consistency.

pytcl.assignment_algorithms.assign2d(cost_matrix, cost_of_non_assignment=inf, maximize=False)[source]

Solve 2D assignment with cost of non-assignment.

This extends the basic assignment problem to allow tracks and measurements to remain unassigned at a specified cost.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • cost_of_non_assignment (float, optional) – Cost for leaving a track or measurement unassigned. If inf, all must be assigned (default: inf).

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

result – Named tuple containing: - row_indices: Indices of assigned rows (tracks) - col_indices: Indices of assigned columns (measurements) - cost: Total assignment cost - unassigned_rows: Indices of unassigned rows - unassigned_cols: Indices of unassigned columns

Return type:

AssignmentResult

Examples

>>> cost = np.array([[1, 10], [10, 1], [5, 5]])
>>> result = assign2d(cost, cost_of_non_assignment=3)
>>> result.row_indices
array([0, 1])
>>> result.col_indices
array([0, 1])
>>> result.unassigned_rows
array([2])

Notes

The algorithm augments the cost matrix with dummy rows and columns representing non-assignment options, then solves the augmented problem.

class pytcl.assignment_algorithms.AssignmentResult(row_indices, col_indices, cost, unassigned_rows, unassigned_cols)[source]

Bases: NamedTuple

Result of a 2D assignment problem.

row_indices

Indices of assigned rows.

Type:

ndarray

col_indices

Indices of assigned columns (col_indices[i] is assigned to row_indices[i]).

Type:

ndarray

cost

Total assignment cost.

Type:

float

unassigned_rows

Indices of unassigned rows.

Type:

ndarray

unassigned_cols

Indices of unassigned columns.

Type:

ndarray

row_indices: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

col_indices: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 1

cost: float

Alias for field number 2

unassigned_rows: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 3

unassigned_cols: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 4

class pytcl.assignment_algorithms.KBestResult(assignments, costs, n_found)[source]

Bases: NamedTuple

Result of k-best assignment problem.

assignments

List of k best assignments, sorted by cost (ascending for minimization).

Type:

List[AssignmentResult]

costs

Array of costs for each assignment.

Type:

ndarray

n_found

Number of assignments found (may be less than k if fewer exist).

Type:

int

assignments: List[AssignmentResult]

Alias for field number 0

costs: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 1

n_found: int

Alias for field number 2

pytcl.assignment_algorithms.murty(cost_matrix, k, cost_of_non_assignment=inf, maximize=False)[source]

Find k-best assignments using Murty’s algorithm.

Murty’s algorithm systematically partitions the solution space to enumerate assignments in order of increasing cost. It is widely used in Multiple Hypothesis Tracking (MHT) for hypothesis generation.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • k (int) – Number of best assignments to find.

  • cost_of_non_assignment (float, optional) – Cost for leaving a track or measurement unassigned. If inf, all must be assigned (default: inf).

  • maximize (bool, optional) – If True, find k-best in descending order (default: False).

Returns:

result – Named tuple containing: - assignments: List of k best AssignmentResult objects - costs: Array of costs for each assignment - n_found: Number of assignments actually found

Return type:

KBestResult

Examples

>>> import numpy as np
>>> cost = np.array([[10, 5, 13], [3, 15, 8], [12, 7, 9]])
>>> result = murty(cost, k=3)
>>> result.n_found
3
>>> result.costs  # Three lowest-cost assignments
array([15., 17., 18.])

Notes

The algorithm has time complexity O(k * n^3) for an n x n matrix, as each partition requires solving a constrained assignment problem.

For large k, consider using lazy evaluation techniques or early termination based on cost thresholds.

References

pytcl.assignment_algorithms.kbest_assign2d(cost_matrix, k, cost_of_non_assignment=inf, maximize=False, cost_threshold=None)[source]

Find k-best assignments with optional cost threshold.

This is an enhanced version of Murty’s algorithm that supports early termination when costs exceed a threshold.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • k (int) – Maximum number of assignments to find.

  • cost_of_non_assignment (float, optional) – Cost for leaving a track or measurement unassigned.

  • maximize (bool, optional) – If True, find k-best in descending order.

  • cost_threshold (float, optional) – Stop when assignment cost exceeds this threshold (for minimization) or falls below (for maximization). If None, no threshold.

Returns:

result – Named tuple containing assignments, costs, and count.

Return type:

KBestResult

Examples

>>> cost = np.array([[10, 5, 13], [3, 15, 8], [12, 7, 9]])
>>> result = kbest_assign2d(cost, k=10, cost_threshold=20)
>>> result.n_found  # Only assignments with cost <= 20
4

Notes

The cost threshold enables efficient pruning for MHT where hypotheses with very low probability (high cost) can be discarded.

pytcl.assignment_algorithms.ranked_assignments(cost_matrix, max_assignments=100, cost_threshold=None, maximize=False)[source]

Enumerate assignments in ranked order (best to worst).

This is a convenience function for MHT hypothesis generation that provides sensible defaults.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • max_assignments (int, optional) – Maximum number of assignments to enumerate (default: 100).

  • cost_threshold (float, optional) – Stop when cost exceeds threshold (minimization) or falls below (maximization).

  • maximize (bool, optional) – If True, rank in descending order of cost.

Returns:

result – Ranked assignments.

Return type:

KBestResult

Examples

>>> cost = np.array([[10, 5], [3, 15]])
>>> result = ranked_assignments(cost, max_assignments=5)
>>> len(result.assignments)
2
>>> result.costs
array([12., 20.])

Notes

This function assumes all tracks must be assigned to measurements (no non-assignment option). For track-oriented MHT with missed detections, use kbest_assign2d with an appropriate cost_of_non_assignment.

class pytcl.assignment_algorithms.Assignment3DResult(tuples, cost, converged, n_iterations, gap)[source]

Bases: NamedTuple

Result of a 3D assignment problem.

tuples

Array of shape (n_assignments, 3) containing assigned index tuples. Each row is (i, j, k) representing an assignment.

Type:

ndarray

cost

Total assignment cost.

Type:

float

converged

Whether the algorithm converged (for iterative methods).

Type:

bool

n_iterations

Number of iterations used (for iterative methods).

Type:

int

gap

Optimality gap (upper_bound - lower_bound) for relaxation methods.

Type:

float

tuples: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

cost: float

Alias for field number 1

converged: bool

Alias for field number 2

n_iterations: int

Alias for field number 3

gap: float

Alias for field number 4

pytcl.assignment_algorithms.assign3d(cost_tensor, method='lagrangian', maximize=False, **kwargs)[source]

Solve 3D assignment problem.

This is the main entry point for 3D assignment, providing access to multiple algorithms through a unified interface.

Parameters:
  • cost_tensor (array_like) – Cost tensor of shape (n1, n2, n3).

  • method (str, optional) – Algorithm to use: - “lagrangian”: Lagrangian relaxation (default) - “auction”: Auction-based algorithm - “greedy”: Fast greedy heuristic - “decompose”: Sequential 2D decomposition

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

  • **kwargs (Any) – Additional arguments passed to the specific method.

Returns:

result – Assignment result.

Return type:

Assignment3DResult

Examples

>>> import numpy as np
>>> np.random.seed(42)
>>> cost = np.random.rand(5, 5, 5)
>>> result = assign3d(cost, method="lagrangian")
>>> result.tuples.shape
(5, 3)

See also

assign3d_lagrangian

Lagrangian relaxation method

assign3d_auction

Auction-based method

greedy_3d

Greedy heuristic

decompose_to_2d

Sequential decomposition

Notes

The 3D assignment problem is NP-hard, so all methods provide approximate solutions. The Lagrangian relaxation method generally provides the best quality with reasonable computation time.

pytcl.assignment_algorithms.assign3d_lagrangian(cost_tensor, max_iter=100, tol=1e-06, step_size=1.0, maximize=False)[source]

Solve 3D assignment using Lagrangian relaxation.

This algorithm relaxes the 3D constraints and solves a sequence of 2D assignment problems. The Lagrange multipliers are updated using subgradient optimization to improve the bound.

Parameters:
  • cost_tensor (array_like) – Cost tensor of shape (n1, n2, n3).

  • max_iter (int, optional) – Maximum number of iterations (default: 100).

  • tol (float, optional) – Convergence tolerance for gap (default: 1e-6).

  • step_size (float, optional) – Initial step size for subgradient update (default: 1.0).

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

result – Assignment result with optimality gap information.

Return type:

Assignment3DResult

Examples

>>> import numpy as np
>>> np.random.seed(42)
>>> cost = np.random.rand(5, 5, 5)
>>> result = assign3d_lagrangian(cost, max_iter=50)
>>> result.converged
True

Notes

The Lagrangian relaxation dualizes the constraint that each index in the third dimension appears at most once. The resulting problem decomposes into n3 independent 2D assignment problems.

The gap provides a measure of solution quality: gap = 0 indicates an optimal solution.

References

pytcl.assignment_algorithms.assign3d_auction(cost_tensor, epsilon=None, max_iter=1000, maximize=False)[source]

Solve 3D assignment using an auction-based algorithm.

This extends the 2D auction algorithm to 3D by treating the problem as a tripartite matching with iterative bidding.

Parameters:
  • cost_tensor (array_like) – Cost tensor of shape (n1, n2, n3).

  • epsilon (float, optional) – Price increment. If None, uses adaptive epsilon.

  • max_iter (int, optional) – Maximum iterations (default: 1000).

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

result – Assignment result.

Return type:

Assignment3DResult

Examples

>>> import numpy as np
>>> cost = np.random.rand(4, 4, 4)
>>> result = assign3d_auction(cost)
>>> len(result.tuples) <= 4
True

Notes

The auction algorithm for 3D assignment alternates between phases: 1. Bidding phase: unassigned elements bid for their preferred matches 2. Assignment phase: matches are updated based on bids

This is a heuristic extension of the 2D auction algorithm and may not find the global optimum.

References

pytcl.assignment_algorithms.greedy_3d(cost_tensor, maximize=False)[source]

Solve 3D assignment using a greedy algorithm.

This algorithm iteratively selects the lowest-cost unassigned tuple until no more valid assignments can be made. It provides a fast but suboptimal solution.

Parameters:
  • cost_tensor (array_like) – Cost tensor of shape (n1, n2, n3).

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

result – Assignment result with tuples, cost, and metadata.

Return type:

Assignment3DResult

Examples

>>> import numpy as np
>>> cost = np.random.rand(3, 3, 3)
>>> result = greedy_3d(cost)
>>> result.tuples.shape
(3, 3)

Notes

Time complexity is O(n1 * n2 * n3 * min(n1, n2, n3)) in the worst case. The greedy solution provides a starting point for more sophisticated algorithms but is generally not optimal.

pytcl.assignment_algorithms.decompose_to_2d(cost_tensor, fixed_dimension=0, maximize=False)[source]

Solve 3D assignment by decomposing into sequential 2D problems.

This heuristic fixes one dimension and solves a sequence of 2D assignment problems. While not optimal, it provides a polynomial-time approximation.

Parameters:
  • cost_tensor (array_like) – Cost tensor of shape (n1, n2, n3).

  • fixed_dimension (int, optional) – Which dimension to iterate over (0, 1, or 2). Default: 0.

  • maximize (bool, optional) – If True, solve maximization problem (default: False).

Returns:

result – Assignment result.

Return type:

Assignment3DResult

Examples

>>> import numpy as np
>>> cost = np.random.rand(4, 4, 4)
>>> result = decompose_to_2d(cost, fixed_dimension=0)
>>> result.tuples.shape[0] <= 4
True

Notes

The algorithm iterates over the fixed dimension and solves a 2D assignment for each slice. Indices that have been assigned in previous slices are excluded from subsequent problems.

Time complexity is O(n * m^3) where n is the size of the fixed dimension and m is the maximum of the other two dimensions.

pytcl.assignment_algorithms.ellipsoidal_gate(innovation, innovation_covariance, gate_threshold)[source]

Test if a measurement passes an ellipsoidal gate.

The ellipsoidal gate defines a validation region based on the chi-squared distribution of the squared Mahalanobis distance.

Parameters:
  • innovation (array_like) – Innovation vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • gate_threshold (float) – Gate threshold (chi-squared value). Common values: - 9.21 for 99% probability with 2 measurements - 11.34 for 99% probability with 3 measurements - 16.27 for 99% probability with 4 measurements

Returns:

True if measurement passes the gate (is inside the ellipsoid).

Return type:

bool

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[2.0, 0.0], [0.0, 1.0]])
>>> ellipsoidal_gate(innovation, S, gate_threshold=9.21)
True

See also

chi2_gate_threshold

Compute threshold from probability.

pytcl.assignment_algorithms.rectangular_gate(innovation, innovation_covariance, num_sigmas=3.0)[source]

Test if a measurement passes a rectangular gate.

The rectangular gate defines a validation region as a hypercube based on the marginal standard deviations.

Parameters:
  • innovation (array_like) – Innovation vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • num_sigmas (float, optional) – Number of standard deviations for gate bounds (default: 3.0).

Returns:

True if measurement passes the gate.

Return type:

bool

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[4.0, 0.0], [0.0, 1.0]])
>>> rectangular_gate(innovation, S, num_sigmas=3.0)
True

Notes

Rectangular gating is computationally cheaper but less tight than ellipsoidal gating. It may pass more false measurements.

pytcl.assignment_algorithms.gate_measurements(predicted_measurement, innovation_covariance, measurements, gate_threshold, gate_type='ellipsoidal')[source]

Gate multiple measurements against a predicted track state.

Parameters:
  • predicted_measurement (array_like) – Predicted measurement of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • measurements (array_like) – Array of measurements of shape (n_meas, m).

  • gate_threshold (float) – Gate threshold. For ellipsoidal gates, this is the chi-squared value. For rectangular gates, this is the number of sigmas.

  • gate_type (str, optional) – Type of gate: “ellipsoidal” or “rectangular” (default: “ellipsoidal”).

Returns:

  • valid_indices (ndarray) – Indices of measurements that pass the gate.

  • distances (ndarray) – Squared Mahalanobis distances for valid measurements.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], ndarray[tuple[Any, …], dtype[float64]]]

Examples

>>> z_pred = np.array([0.0, 0.0])
>>> S = np.eye(2)
>>> measurements = np.array([[0.5, 0.5], [5.0, 5.0], [1.0, -1.0]])
>>> valid_idx, dists = gate_measurements(z_pred, S, measurements, 9.21)
>>> valid_idx
array([0, 2])

Notes

This function efficiently gates multiple measurements against a single track prediction, which is common in multi-target tracking.

pytcl.assignment_algorithms.mahalanobis_distance(innovation, innovation_covariance)[source]

Compute the squared Mahalanobis distance.

The Mahalanobis distance measures how many standard deviations a point is from the center of a distribution.

Parameters:
  • innovation (array_like) – Innovation (measurement residual) vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

Returns:

Squared Mahalanobis distance.

Return type:

float

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[2.0, 0.0], [0.0, 1.0]])
>>> d2 = mahalanobis_distance(innovation, S)
>>> d2
0.75

Notes

The squared Mahalanobis distance is defined as:

d^2 = (z - z_pred)^T @ S^{-1} @ (z - z_pred)

where S is the innovation covariance matrix.

pytcl.assignment_algorithms.chi2_gate_threshold(probability, num_dimensions)[source]

Compute chi-squared gate threshold for a given probability.

Parameters:
  • probability (float) – Gate probability (e.g., 0.99 for 99% of true measurements to pass).

  • num_dimensions (int) – Measurement dimension (degrees of freedom).

Returns:

Chi-squared threshold value.

Return type:

float

Examples

>>> chi2_gate_threshold(0.99, 2)  # 2D measurement, 99% probability
9.210340371976184
>>> chi2_gate_threshold(0.99, 3)  # 3D measurement, 99% probability
11.344866730144373
pytcl.assignment_algorithms.compute_gate_volume(innovation_covariance, gate_threshold)[source]

Compute the volume of an ellipsoidal gate.

Parameters:
  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • gate_threshold (float) – Chi-squared gate threshold.

Returns:

Volume of the ellipsoidal gate region.

Return type:

float

Notes

The gate volume is used in probabilistic data association methods to compute the clutter density.

For an m-dimensional ellipsoid with threshold gamma:

V = c_m * sqrt(det(S)) * gamma^(m/2)

where c_m is the volume of the unit hypersphere in m dimensions.

Examples

Compute gate volume for a 2D measurement with 99% gate probability:

>>> import numpy as np
>>> from scipy.stats import chi2
>>> S = np.array([[4.0, 0.0], [0.0, 1.0]])  # innovation covariance
>>> gate_prob = 0.99
>>> threshold = chi2.ppf(gate_prob, df=2)
>>> volume = compute_gate_volume(S, threshold)
>>> volume > 0
True

See also

ellipsoidal_gate

Test if measurement passes gate.

mahalanobis_distance

Compute distance used in gating.

pytcl.assignment_algorithms.gnn_association(cost_matrix, gate_threshold=inf, cost_of_non_assignment=None)[source]

Global Nearest Neighbor (GNN) data association.

Finds the globally optimal assignment of tracks to measurements that minimizes the total association cost.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements). cost_matrix[i, j] is the cost of assigning track i to measurement j.

  • gate_threshold (float, optional) – Maximum cost for valid assignment. Entries above this threshold are set to infinity before optimization.

  • cost_of_non_assignment (float, optional) – Cost for not assigning a track or measurement. If None, all tracks/measurements that can be assigned will be assigned.

Returns:

result – Association result with track and measurement assignments.

Return type:

AssociationResult

Examples

>>> cost = np.array([[1.0, 5.0, 2.0],
...                  [4.0, 2.0, 3.0]])
>>> result = gnn_association(cost, gate_threshold=10.0)
>>> result.track_to_measurement
array([0, 1])
>>> result.total_cost
3.0

Notes

GNN uses the Hungarian algorithm to find the globally optimal assignment. It is more computationally expensive than nearest neighbor but guarantees optimality.

The algorithm handles rectangular cost matrices (different numbers of tracks and measurements) and allows tracks/measurements to remain unassigned if cost_of_non_assignment is specified.

References

pytcl.assignment_algorithms.nearest_neighbor(cost_matrix, gate_threshold=inf)[source]

Simple nearest neighbor data association.

Each track is assigned to its closest measurement, but a measurement can only be assigned to one track. Greedy assignment, not globally optimal.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • gate_threshold (float, optional) – Maximum cost for valid assignment. Assignments with cost above this threshold are rejected.

Returns:

result – Association result with track and measurement assignments.

Return type:

AssociationResult

Notes

This is a greedy algorithm that processes tracks in order of their minimum cost. It does not guarantee globally optimal assignment. Use gnn_association for globally optimal assignment.

Examples

>>> cost = np.array([[1.0, 5.0], [4.0, 2.0]])
>>> result = nearest_neighbor(cost, gate_threshold=10.0)
>>> result.track_to_measurement
array([0, 1])
pytcl.assignment_algorithms.compute_association_cost(track_predictions, track_covariances, measurements, measurement_models=None)[source]

Compute cost matrix for track-to-measurement association.

Parameters:
  • track_predictions (array_like) – Predicted track states of shape (n_tracks, n_state).

  • track_covariances (array_like) – Track covariance matrices of shape (n_tracks, n_state, n_state).

  • measurements (array_like) – Measurements of shape (n_measurements, n_meas).

  • measurement_models (array_like, optional) – Measurement matrices of shape (n_tracks, n_meas, n_state) or (n_meas, n_state) if same for all tracks. If None, assumes direct measurement of first n_meas states.

Returns:

cost_matrix – Cost matrix of shape (n_tracks, n_measurements). cost_matrix[i, j] is the squared Mahalanobis distance from track i to measurement j.

Return type:

ndarray

Examples

>>> # 2 tracks, 3 measurements, 2D state [x, vx]
>>> predictions = np.array([[0.0, 1.0], [5.0, -1.0]])
>>> covariances = np.array([np.eye(2), np.eye(2)])
>>> measurements = np.array([[0.1], [4.9], [10.0]])
>>> H = np.array([[1.0, 0.0]])  # Measure position only
>>> costs = compute_association_cost(predictions, covariances, measurements, H)
pytcl.assignment_algorithms.gated_gnn_association(track_predictions, track_covariances, measurements, measurement_models=None, gate_probability=0.99, cost_of_non_assignment=None)[source]

GNN association with automatic gating.

Combines gating and GNN association in a single function for convenience.

Parameters:
  • track_predictions (array_like) – Predicted track states of shape (n_tracks, n_state).

  • track_covariances (array_like) – Track covariance matrices of shape (n_tracks, n_state, n_state).

  • measurements (array_like) – Measurements of shape (n_measurements, n_meas).

  • measurement_models (array_like, optional) – Measurement matrices. See compute_association_cost for details.

  • gate_probability (float, optional) – Probability for chi-squared gate threshold (default: 0.99).

  • cost_of_non_assignment (float, optional) – Cost for not assigning a track or measurement.

Returns:

result – Association result.

Return type:

AssociationResult

Examples

>>> predictions = np.array([[0.0, 1.0], [5.0, -1.0]])
>>> covariances = np.array([0.1 * np.eye(2), 0.1 * np.eye(2)])
>>> measurements = np.array([[0.1], [4.9]])
>>> H = np.array([[1.0, 0.0]])
>>> result = gated_gnn_association(
...     predictions, covariances, measurements, H,
...     gate_probability=0.99
... )
class pytcl.assignment_algorithms.AssociationResult(track_to_measurement, measurement_to_track, costs, total_cost)[source]

Bases: NamedTuple

Result of data association.

track_to_measurement

Array of shape (n_tracks,). track_to_measurement[i] gives the measurement index assigned to track i, or -1 if unassigned.

Type:

ndarray

measurement_to_track

Array of shape (n_measurements,). measurement_to_track[j] gives the track index assigned to measurement j, or -1 if unassigned.

Type:

ndarray

costs

Array of association costs (squared Mahalanobis distances) for each assigned pair.

Type:

ndarray

total_cost

Total assignment cost.

Type:

float

track_to_measurement: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

measurement_to_track: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 1

costs: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 2

total_cost: float

Alias for field number 3

class pytcl.assignment_algorithms.JPDAResult(association_probs, marginal_probs, likelihood_matrix, gated)[source]

Bases: NamedTuple

Result of JPDA algorithm.

association_probs

Association probability matrix of shape (n_tracks, n_measurements + 1). association_probs[i, j] is the probability that track i is associated with measurement j. The last column (j = n_measurements) represents the probability that track i has no measurement.

Type:

ndarray[Any]

marginal_probs

List of marginal association probabilities for each track. marginal_probs[i][j] = P(measurement j originated from track i).

Type:

list of ndarray

likelihood_matrix

Measurement likelihood matrix of shape (n_tracks, n_measurements).

Type:

ndarray[Any]

gated

Boolean matrix indicating which track-measurement pairs passed gating.

Type:

ndarray[Any]

association_probs: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

marginal_probs: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 1

likelihood_matrix: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

gated: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 3

class pytcl.assignment_algorithms.JPDAUpdate(states, covariances, association_probs, innovations)[source]

Bases: NamedTuple

Result of JPDA-based track update.

states

Updated state estimates for each track.

Type:

list of ndarray

covariances

Updated covariances for each track (includes spread of means).

Type:

list of ndarray

association_probs

Association probability matrix.

Type:

ndarray[Any]

innovations

Combined weighted innovations for each track.

Type:

list of ndarray

states: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 0

covariances: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 1

association_probs: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

innovations: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 3

pytcl.assignment_algorithms.jpda(track_states, track_covariances, measurements, H, R, detection_prob=0.9, clutter_density=1e-06, gate_probability=0.99)[source]

Compute JPDA association probabilities.

This is a convenience function that computes association probabilities without performing the state update.

Parameters:
  • track_states (list of array_like) – Predicted state estimates for each track.

  • track_covariances (list of array_like) – Predicted covariances for each track.

  • measurements (array_like) – Measurements, shape (n_meas, m).

  • H (array_like) – Measurement matrix, shape (m, n).

  • R (array_like) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection. Default 0.9.

  • clutter_density (float) – Spatial density of clutter. Default 1e-6.

  • gate_probability (float) – Probability for chi-squared gate. Default 0.99.

Returns:

result – Association probabilities and related information.

Return type:

JPDAResult

Examples

Compute association probabilities for 2 tracks and 3 measurements:

>>> import numpy as np
>>> # Two tracks with [x, vx] state
>>> states = [np.array([0.0, 1.0]), np.array([10.0, -0.5])]
>>> covariances = [np.eye(2) * 0.5, np.eye(2) * 0.5]
>>> # Three position measurements
>>> measurements = np.array([[0.1], [9.8], [5.0]])
>>> H = np.array([[1, 0]])  # measure position only
>>> R = np.array([[0.1]])
>>> result = jpda(states, covariances, measurements, H, R)
>>> result.association_probs.shape  # (2 tracks, 4 columns: 3 meas + miss)
(2, 4)
>>> # Track 0 should have high prob for measurement 0
>>> result.association_probs[0, 0] > 0.5
True

See also

jpda_update

JPDA with state update.

pytcl.assignment_algorithms.jpda_update(track_states, track_covariances, measurements, H, R, detection_prob=0.9, clutter_density=1e-06, gate_probability=0.99)[source]

Perform JPDA-based track update.

Parameters:
  • track_states (list of array_like) – Predicted state estimates for each track.

  • track_covariances (list of array_like) – Predicted covariances for each track.

  • measurements (array_like) – Measurements, shape (n_meas, m).

  • H (array_like) – Measurement matrix, shape (m, n).

  • R (array_like) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection (Pd). Default 0.9.

  • clutter_density (float) – Spatial density of clutter. Default 1e-6.

  • gate_probability (float) – Probability for chi-squared gate. Default 0.99.

Returns:

result – Updated states and covariances with association probabilities.

Return type:

JPDAUpdate

Examples

>>> import numpy as np
>>> # Two tracks, three measurements
>>> x1 = np.array([0., 1.])  # [position, velocity]
>>> x2 = np.array([5., -1.])
>>> P = np.eye(2) * 0.1
>>> measurements = np.array([[0.1], [5.2], [10.0]])
>>> H = np.array([[1., 0.]])  # Measure position
>>> R = np.array([[0.1]])
>>> result = jpda_update([x1, x2], [P, P], measurements, H, R)
>>> len(result.states)
2

Notes

The JPDA update consists of: 1. Compute measurement likelihoods and gating 2. Compute association probabilities 3. Compute combined innovations for each track 4. Update each track with weighted innovation 5. Compute covariance with spread of means term

References

pytcl.assignment_algorithms.jpda_probabilities(likelihood_matrix, gated, detection_prob=1.0, clutter_density=1e-06)[source]

Compute JPDA association probabilities.

Uses an efficient algorithm that avoids explicit enumeration of all joint hypotheses when possible.

Parameters:
  • likelihood_matrix (ndarray[Any]) – Likelihood values, shape (n_tracks, n_meas).

  • gated (ndarray[Any]) – Boolean gating matrix, shape (n_tracks, n_meas).

  • detection_prob (float) – Probability of detection (Pd).

  • clutter_density (float) – Spatial density of clutter (lambda_c).

Returns:

beta – Association probability matrix, shape (n_tracks, n_meas + 1). beta[i, j] = P(measurement j is from track i) for j < n_meas. beta[i, n_meas] = P(track i has no measurement).

Return type:

ndarray[Any]

Examples

>>> import numpy as np
>>> # Likelihood matrix: 2 tracks, 2 measurements
>>> # Track 0 has high likelihood for meas 0
>>> # Track 1 has high likelihood for meas 1
>>> likelihood = np.array([[0.9, 0.1],
...                        [0.1, 0.8]])
>>> gated = np.array([[True, True],
...                   [True, True]])
>>> beta = jpda_probabilities(likelihood, gated, detection_prob=0.9)
>>> beta.shape  # 2 tracks, 3 columns (2 meas + 1 miss)
(2, 3)
>>> # Track 0 most likely associated with measurement 0
>>> np.argmax(beta[0, :2])
0
pytcl.assignment_algorithms.compute_likelihood_matrix(track_states, track_covariances, measurements, H, R, detection_prob=1.0, gate_threshold=None)[source]

Compute likelihood matrix for all track-measurement pairs.

Parameters:
  • track_states (list of ndarray) – State estimates for each track.

  • track_covariances (list of ndarray) – Covariances for each track.

  • measurements (ndarray[Any]) – Measurements, shape (n_meas, m).

  • H (ndarray[Any]) – Measurement matrix, shape (m, n).

  • R (ndarray[Any]) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection.

  • gate_threshold (float, optional) – Mahalanobis distance threshold for gating.

Returns:

  • likelihood_matrix (ndarray[Any]) – Likelihood values, shape (n_tracks, n_meas).

  • gated (ndarray[Any]) – Boolean gating matrix, shape (n_tracks, n_meas).

Return type:

tuple[ndarray[tuple[Any, …], dtype[Any]], ndarray[tuple[Any, …], dtype[Any]]]

Examples

>>> import numpy as np
>>> # Two tracks, two measurements
>>> states = [np.array([0.0, 1.0]), np.array([5.0, 0.0])]
>>> covs = [np.eye(2) * 0.5, np.eye(2) * 0.5]
>>> measurements = np.array([[0.1], [5.2]])
>>> H = np.array([[1, 0]])
>>> R = np.array([[0.1]])
>>> L, gated = compute_likelihood_matrix(states, covs, measurements, H, R)
>>> L.shape
(2, 2)
>>> # Track 0 should have high likelihood for measurement 0
>>> L[0, 0] > L[0, 1]
True
class pytcl.assignment_algorithms.AssignmentNDResult(assignments, cost, converged, n_iterations, gap)[source]

Bases: NamedTuple

Result of an N-dimensional assignment problem.

assignments

Array of shape (n_assignments, n_dimensions) containing assigned index tuples. Each row is an n-tuple of indices.

Type:

ndarray

cost

Total assignment cost.

Type:

float

converged

Whether the algorithm converged (for iterative methods).

Type:

bool

n_iterations

Number of iterations used (for iterative methods).

Type:

int

gap

Optimality gap (upper_bound - lower_bound) for relaxation methods.

Type:

float

assignments: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

cost: float

Alias for field number 1

converged: bool

Alias for field number 2

n_iterations: int

Alias for field number 3

gap: float

Alias for field number 4

pytcl.assignment_algorithms.validate_cost_tensor(cost_tensor)[source]

Validate cost tensor and return dimensions.

Parameters:

cost_tensor (ndarray) – Cost tensor of arbitrary dimension.

Returns:

dims – Dimensions of the cost tensor.

Return type:

tuple

Raises:

ValueError – If tensor has fewer than 2 dimensions.

Examples

>>> import numpy as np
>>> cost = np.random.rand(3, 4, 5)
>>> dims = validate_cost_tensor(cost)
>>> dims
(3, 4, 5)
>>> # 1D tensor should raise error
>>> try:
...     validate_cost_tensor(np.array([1, 2, 3]))
... except ValueError:
...     print("Caught expected error")
Caught expected error
pytcl.assignment_algorithms.greedy_assignment_nd(cost_tensor, max_assignments=None)[source]

Greedy solver for N-dimensional assignment.

Selects minimum-cost tuples in order until no more valid assignments exist (no dimension index is repeated).

Parameters:
  • cost_tensor (ndarray) – Cost tensor of shape (n1, n2, …, nk).

  • max_assignments (int, optional) – Maximum number of assignments to find (default: min(dimensions)).

Returns:

Assignments, total cost, and algorithm info.

Return type:

AssignmentNDResult

Examples

>>> import numpy as np
>>> # 3D cost tensor: 3 measurements x 2 tracks x 2 hypotheses
>>> cost = np.array([
...     [[1.0, 5.0], [3.0, 2.0]],   # meas 0
...     [[4.0, 1.0], [2.0, 6.0]],   # meas 1
...     [[2.0, 3.0], [5.0, 1.0]],   # meas 2
... ])
>>> result = greedy_assignment_nd(cost)
>>> result.cost  # Total cost of greedy solution
4.0
>>> len(result.assignments)  # Number of assignments made
2

Notes

Greedy assignment is fast O(n log n) but not optimal. Used as heuristic or starting solution for optimization methods.

pytcl.assignment_algorithms.relaxation_assignment_nd(cost_tensor, max_iterations=100, tolerance=1e-06, verbose=False)[source]

Lagrangian relaxation solver for N-dimensional assignment.

Uses iterative subgradient optimization on Lagrange multipliers to tighten the lower bound and find good solutions.

Parameters:
  • cost_tensor (ndarray) – Cost tensor of shape (n1, n2, …, nk).

  • max_iterations (int, optional) – Maximum iterations (default 100).

  • tolerance (float, optional) – Convergence tolerance for gap (default 1e-6).

  • verbose (bool, optional) – Print iteration info (default False).

Returns:

Assignments, total cost, convergence info, and optimality gap.

Return type:

AssignmentNDResult

Examples

>>> import numpy as np
>>> # 3x3x3 assignment problem
>>> np.random.seed(42)
>>> cost = np.random.rand(3, 3, 3)
>>> result = relaxation_assignment_nd(cost, max_iterations=50)
>>> result.converged
True
>>> result.assignments.shape[1]  # 3D assignments
3

Notes

The relaxation approach: 1. Maintain Lagrange multipliers for each dimension 2. Solve relaxed problem (select best entries per tuple) 3. Update multipliers based on constraint violations 4. Iterate until convergence or gap tolerance met

This guarantees a lower bound on optimal cost and often finds near-optimal or optimal solutions.

pytcl.assignment_algorithms.auction_assignment_nd(cost_tensor, max_iterations=100, epsilon=0.01, verbose=False)[source]

Auction algorithm for N-dimensional assignment.

Inspired by the classical auction algorithm for 2D assignment, adapted to higher dimensions. Objects bid for assignments based on relative costs.

Parameters:
  • cost_tensor (ndarray) – Cost tensor of shape (n1, n2, …, nk).

  • max_iterations (int, optional) – Maximum iterations (default 100).

  • epsilon (float, optional) – Bid increment (default 0.01). Larger epsilon → fewer iterations, worse solution; smaller epsilon → more iterations, better solution.

  • verbose (bool, optional) – Print iteration info (default False).

Returns:

Assignments, total cost, convergence info, gap estimate.

Return type:

AssignmentNDResult

Examples

>>> import numpy as np
>>> # 4D assignment: sensors x measurements x tracks x hypotheses
>>> np.random.seed(123)
>>> cost = np.random.rand(2, 3, 3, 2) * 10
>>> result = auction_assignment_nd(cost, max_iterations=50, epsilon=0.1)
>>> len(result.assignments) > 0
True
>>> result.n_iterations <= 50
True

Notes

The algorithm maintains a “price” for each index and allows bidding (price adjustment) to maximize value. Converges to epsilon-optimal solution in finite iterations.

pytcl.assignment_algorithms.detect_dimension_conflicts(assignments, dims)[source]

Check if assignments violate dimension uniqueness.

For valid assignment, each index should appear at most once per dimension.

Parameters:
  • assignments (ndarray) – Array of shape (n_assignments, n_dimensions) with assignments.

  • dims (tuple) – Dimensions of the cost tensor.

Returns:

has_conflicts – True if any index appears more than once in any dimension.

Return type:

bool

Examples

>>> import numpy as np
>>> # Valid assignment: no index repeated in any dimension
>>> assignments = np.array([[0, 0], [1, 1]])
>>> detect_dimension_conflicts(assignments, (3, 3))
False
>>> # Invalid: index 0 used twice in first dimension
>>> assignments = np.array([[0, 0], [0, 1]])
>>> detect_dimension_conflicts(assignments, (3, 3))
True
class pytcl.assignment_algorithms.FlowStatus(value)[source]

Bases: Enum

Status of min-cost flow computation.

OPTIMAL = 0
UNBOUNDED = 1
INFEASIBLE = 2
TIMEOUT = 3
class pytcl.assignment_algorithms.MinCostFlowResult(flow, cost, status, iterations)[source]

Bases: NamedTuple

Result of min-cost flow computation.

flow

Flow values on each edge, shape (n_edges,).

Type:

ndarray

cost

Total flow cost.

Type:

float

status

Optimization status.

Type:

FlowStatus

iterations

Number of iterations used.

Type:

int

flow: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 0

cost: float

Alias for field number 1

status: FlowStatus

Alias for field number 2

iterations: int

Alias for field number 3

pytcl.assignment_algorithms.assignment_to_flow_network(cost_matrix)[source]

Convert 2D assignment problem to min-cost flow network.

Network structure: - Source node (0) supplies all workers - Worker nodes (1 to m) demand 1 unit each - Task nodes (m+1 to m+n) supply 1 unit each - Sink node (m+n+1) collects all completed tasks

Parameters:

cost_matrix (ndarray) – Cost matrix of shape (m, n) where cost[i,j] is cost of assigning worker i to task j.

Returns:

  • edges (list[FlowEdge]) – List of edges in the flow network.

  • supplies (ndarray) – Supply/demand at each node (shape n_nodes,). Positive = supply, negative = demand.

  • node_names (ndarray) – Names of nodes for reference.

Return type:

Tuple[list[FlowEdge], ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[Any]]]

Examples

>>> import numpy as np
>>> # 2 workers, 3 tasks
>>> cost = np.array([[1.0, 2.0, 3.0],
...                  [4.0, 5.0, 6.0]])
>>> edges, supplies, names = assignment_to_flow_network(cost)
>>> len(edges)  # source->workers + workers->tasks + tasks->sink
11
>>> supplies[0]  # source supplies 2 (num workers)
2.0
>>> names[0]
'source'
pytcl.assignment_algorithms.min_cost_flow_successive_shortest_paths(edges, supplies, max_iterations=1000)[source]

Solve min-cost flow using successive shortest paths with cost scaling.

Algorithm: 1. Initialize potentials using Bellman-Ford 2. While there is excess supply:

  • Find shortest path using reduced costs (Dijkstra with potentials)

  • Push unit flow along path

  • Update node potentials

  • Recompute shortest paths to maintain optimality

This is the standard min-cost flow algorithm that guarantees optimality and convergence. It uses Dijkstra’s algorithm with potentials, which maintains the dual feasibility (reduced cost property).

Parameters:
  • edges (list[FlowEdge]) – List of edges with capacities and costs.

  • supplies (ndarray) – Supply/demand at each node.

  • max_iterations (int, optional) – Maximum iterations (default 1000).

Returns:

Solution with flow values, cost, status, and iterations.

Return type:

MinCostFlowResult

Examples

>>> import numpy as np
>>> from pytcl.assignment_algorithms.network_flow import (
...     FlowEdge, assignment_to_flow_network
... )
>>> cost = np.array([[1.0, 5.0], [4.0, 2.0]])
>>> edges, supplies, _ = assignment_to_flow_network(cost)
>>> result = min_cost_flow_successive_shortest_paths(edges, supplies)
>>> result.status == FlowStatus.OPTIMAL
True
>>> result.cost  # Optimal is 1+2=3 (diagonal)
3.0

Notes

This implementation uses successive shortest paths with potentials. The algorithm is guaranteed to find the optimal solution for any feasible min-cost flow problem.

For rectangular assignment problems (m < n or m > n), all m units of flow must be satisfied. The algorithm ensures this by finding augmenting paths until all supply is routed.

pytcl.assignment_algorithms.min_cost_assignment_via_flow(cost_matrix, use_simplex=True)[source]

Solve 2D assignment problem via min-cost flow network.

Uses Dijkstra-optimized successive shortest paths (Phase 1B) by default. Falls back to Bellman-Ford if needed.

Parameters:
  • cost_matrix (ndarray) – Cost matrix of shape (m, n).

  • use_simplex (bool, optional) – Use Dijkstra-optimized algorithm (default True) or Bellman-Ford based successive shortest paths (False).

Returns:

  • assignment (ndarray) – Assignment array of shape (n_assignments, 2).

  • total_cost (float) – Total assignment cost.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], float]

Examples

>>> import numpy as np
>>> cost = np.array([[1.0, 5.0, 9.0],
...                  [3.0, 2.0, 8.0],
...                  [7.0, 6.0, 4.0]])
>>> assignment, total_cost = min_cost_assignment_via_flow(cost)
>>> total_cost  # Optimal assignment: (0,0), (1,1), (2,2) = 1+2+4 = 7
7.0
>>> len(assignment)
3

Notes

Phase 1B: Dijkstra-based optimization provides O(K*E log V) vs Bellman-Ford O(K*V*E), where K is number of shortest paths needed.

2D Assignment

Optimal assignment algorithms for bipartite matching problems.

Hungarian Algorithm

Gating

Validation region (gating) functions for measurement association.

Gating functions for data association in target tracking.

This module provides gating methods to determine which measurements fall within a validation region around predicted track states.

pytcl.assignment_algorithms.gating.mahalanobis_distance(innovation, innovation_covariance)[source]

Compute the squared Mahalanobis distance.

The Mahalanobis distance measures how many standard deviations a point is from the center of a distribution.

Parameters:
  • innovation (array_like) – Innovation (measurement residual) vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

Returns:

Squared Mahalanobis distance.

Return type:

float

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[2.0, 0.0], [0.0, 1.0]])
>>> d2 = mahalanobis_distance(innovation, S)
>>> d2
0.75

Notes

The squared Mahalanobis distance is defined as:

d^2 = (z - z_pred)^T @ S^{-1} @ (z - z_pred)

where S is the innovation covariance matrix.

pytcl.assignment_algorithms.gating.mahalanobis_batch(innovations, S_inv, output)[source]

Compute Mahalanobis distances for a batch of innovations.

JIT-compiled for performance. Computes squared Mahalanobis distances for multiple innovations against a single covariance matrix.

Parameters:
  • innovations (ndarray) – Innovations of shape (n_measurements, dim).

  • S_inv (ndarray) – Inverse of innovation covariance matrix of shape (dim, dim).

  • output (ndarray) – Output array of shape (n_measurements,) to store distances.

pytcl.assignment_algorithms.gating.ellipsoidal_gate(innovation, innovation_covariance, gate_threshold)[source]

Test if a measurement passes an ellipsoidal gate.

The ellipsoidal gate defines a validation region based on the chi-squared distribution of the squared Mahalanobis distance.

Parameters:
  • innovation (array_like) – Innovation vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • gate_threshold (float) – Gate threshold (chi-squared value). Common values: - 9.21 for 99% probability with 2 measurements - 11.34 for 99% probability with 3 measurements - 16.27 for 99% probability with 4 measurements

Returns:

True if measurement passes the gate (is inside the ellipsoid).

Return type:

bool

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[2.0, 0.0], [0.0, 1.0]])
>>> ellipsoidal_gate(innovation, S, gate_threshold=9.21)
True

See also

chi2_gate_threshold

Compute threshold from probability.

pytcl.assignment_algorithms.gating.rectangular_gate(innovation, innovation_covariance, num_sigmas=3.0)[source]

Test if a measurement passes a rectangular gate.

The rectangular gate defines a validation region as a hypercube based on the marginal standard deviations.

Parameters:
  • innovation (array_like) – Innovation vector of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • num_sigmas (float, optional) – Number of standard deviations for gate bounds (default: 3.0).

Returns:

True if measurement passes the gate.

Return type:

bool

Examples

>>> innovation = np.array([1.0, 0.5])
>>> S = np.array([[4.0, 0.0], [0.0, 1.0]])
>>> rectangular_gate(innovation, S, num_sigmas=3.0)
True

Notes

Rectangular gating is computationally cheaper but less tight than ellipsoidal gating. It may pass more false measurements.

pytcl.assignment_algorithms.gating.gate_measurements(predicted_measurement, innovation_covariance, measurements, gate_threshold, gate_type='ellipsoidal')[source]

Gate multiple measurements against a predicted track state.

Parameters:
  • predicted_measurement (array_like) – Predicted measurement of shape (m,).

  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • measurements (array_like) – Array of measurements of shape (n_meas, m).

  • gate_threshold (float) – Gate threshold. For ellipsoidal gates, this is the chi-squared value. For rectangular gates, this is the number of sigmas.

  • gate_type (str, optional) – Type of gate: “ellipsoidal” or “rectangular” (default: “ellipsoidal”).

Returns:

  • valid_indices (ndarray) – Indices of measurements that pass the gate.

  • distances (ndarray) – Squared Mahalanobis distances for valid measurements.

Return type:

Tuple[ndarray[tuple[Any, …], dtype[int64]], ndarray[tuple[Any, …], dtype[float64]]]

Examples

>>> z_pred = np.array([0.0, 0.0])
>>> S = np.eye(2)
>>> measurements = np.array([[0.5, 0.5], [5.0, 5.0], [1.0, -1.0]])
>>> valid_idx, dists = gate_measurements(z_pred, S, measurements, 9.21)
>>> valid_idx
array([0, 2])

Notes

This function efficiently gates multiple measurements against a single track prediction, which is common in multi-target tracking.

pytcl.assignment_algorithms.gating.chi2_gate_threshold(probability, num_dimensions)[source]

Compute chi-squared gate threshold for a given probability.

Parameters:
  • probability (float) – Gate probability (e.g., 0.99 for 99% of true measurements to pass).

  • num_dimensions (int) – Measurement dimension (degrees of freedom).

Returns:

Chi-squared threshold value.

Return type:

float

Examples

>>> chi2_gate_threshold(0.99, 2)  # 2D measurement, 99% probability
9.210340371976184
>>> chi2_gate_threshold(0.99, 3)  # 3D measurement, 99% probability
11.344866730144373
pytcl.assignment_algorithms.gating.compute_gate_volume(innovation_covariance, gate_threshold)[source]

Compute the volume of an ellipsoidal gate.

Parameters:
  • innovation_covariance (array_like) – Innovation covariance matrix of shape (m, m).

  • gate_threshold (float) – Chi-squared gate threshold.

Returns:

Volume of the ellipsoidal gate region.

Return type:

float

Notes

The gate volume is used in probabilistic data association methods to compute the clutter density.

For an m-dimensional ellipsoid with threshold gamma:

V = c_m * sqrt(det(S)) * gamma^(m/2)

where c_m is the volume of the unit hypersphere in m dimensions.

Examples

Compute gate volume for a 2D measurement with 99% gate probability:

>>> import numpy as np
>>> from scipy.stats import chi2
>>> S = np.array([[4.0, 0.0], [0.0, 1.0]])  # innovation covariance
>>> gate_prob = 0.99
>>> threshold = chi2.ppf(gate_prob, df=2)
>>> volume = compute_gate_volume(S, threshold)
>>> volume > 0
True

See also

ellipsoidal_gate

Test if measurement passes gate.

mahalanobis_distance

Compute distance used in gating.

Data Association

Track-to-measurement association algorithms.

Global Nearest Neighbor (GNN)

Data association algorithms for multi-target tracking.

This module provides algorithms for associating measurements to tracks, including Global Nearest Neighbor (GNN) and related methods.

class pytcl.assignment_algorithms.data_association.AssociationResult(track_to_measurement, measurement_to_track, costs, total_cost)[source]

Bases: NamedTuple

Result of data association.

track_to_measurement

Array of shape (n_tracks,). track_to_measurement[i] gives the measurement index assigned to track i, or -1 if unassigned.

Type:

ndarray

measurement_to_track

Array of shape (n_measurements,). measurement_to_track[j] gives the track index assigned to measurement j, or -1 if unassigned.

Type:

ndarray

costs

Array of association costs (squared Mahalanobis distances) for each assigned pair.

Type:

ndarray

total_cost

Total assignment cost.

Type:

float

track_to_measurement: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 0

measurement_to_track: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 1

costs: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 2

total_cost: float

Alias for field number 3

pytcl.assignment_algorithms.data_association.compute_association_cost(track_predictions, track_covariances, measurements, measurement_models=None)[source]

Compute cost matrix for track-to-measurement association.

Parameters:
  • track_predictions (array_like) – Predicted track states of shape (n_tracks, n_state).

  • track_covariances (array_like) – Track covariance matrices of shape (n_tracks, n_state, n_state).

  • measurements (array_like) – Measurements of shape (n_measurements, n_meas).

  • measurement_models (array_like, optional) – Measurement matrices of shape (n_tracks, n_meas, n_state) or (n_meas, n_state) if same for all tracks. If None, assumes direct measurement of first n_meas states.

Returns:

cost_matrix – Cost matrix of shape (n_tracks, n_measurements). cost_matrix[i, j] is the squared Mahalanobis distance from track i to measurement j.

Return type:

ndarray

Examples

>>> # 2 tracks, 3 measurements, 2D state [x, vx]
>>> predictions = np.array([[0.0, 1.0], [5.0, -1.0]])
>>> covariances = np.array([np.eye(2), np.eye(2)])
>>> measurements = np.array([[0.1], [4.9], [10.0]])
>>> H = np.array([[1.0, 0.0]])  # Measure position only
>>> costs = compute_association_cost(predictions, covariances, measurements, H)
pytcl.assignment_algorithms.data_association.nearest_neighbor(cost_matrix, gate_threshold=inf)[source]

Simple nearest neighbor data association.

Each track is assigned to its closest measurement, but a measurement can only be assigned to one track. Greedy assignment, not globally optimal.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements).

  • gate_threshold (float, optional) – Maximum cost for valid assignment. Assignments with cost above this threshold are rejected.

Returns:

result – Association result with track and measurement assignments.

Return type:

AssociationResult

Notes

This is a greedy algorithm that processes tracks in order of their minimum cost. It does not guarantee globally optimal assignment. Use gnn_association for globally optimal assignment.

Examples

>>> cost = np.array([[1.0, 5.0], [4.0, 2.0]])
>>> result = nearest_neighbor(cost, gate_threshold=10.0)
>>> result.track_to_measurement
array([0, 1])
pytcl.assignment_algorithms.data_association.gnn_association(cost_matrix, gate_threshold=inf, cost_of_non_assignment=None)[source]

Global Nearest Neighbor (GNN) data association.

Finds the globally optimal assignment of tracks to measurements that minimizes the total association cost.

Parameters:
  • cost_matrix (array_like) – Cost matrix of shape (n_tracks, n_measurements). cost_matrix[i, j] is the cost of assigning track i to measurement j.

  • gate_threshold (float, optional) – Maximum cost for valid assignment. Entries above this threshold are set to infinity before optimization.

  • cost_of_non_assignment (float, optional) – Cost for not assigning a track or measurement. If None, all tracks/measurements that can be assigned will be assigned.

Returns:

result – Association result with track and measurement assignments.

Return type:

AssociationResult

Examples

>>> cost = np.array([[1.0, 5.0, 2.0],
...                  [4.0, 2.0, 3.0]])
>>> result = gnn_association(cost, gate_threshold=10.0)
>>> result.track_to_measurement
array([0, 1])
>>> result.total_cost
3.0

Notes

GNN uses the Hungarian algorithm to find the globally optimal assignment. It is more computationally expensive than nearest neighbor but guarantees optimality.

The algorithm handles rectangular cost matrices (different numbers of tracks and measurements) and allows tracks/measurements to remain unassigned if cost_of_non_assignment is specified.

References

pytcl.assignment_algorithms.data_association.gated_gnn_association(track_predictions, track_covariances, measurements, measurement_models=None, gate_probability=0.99, cost_of_non_assignment=None)[source]

GNN association with automatic gating.

Combines gating and GNN association in a single function for convenience.

Parameters:
  • track_predictions (array_like) – Predicted track states of shape (n_tracks, n_state).

  • track_covariances (array_like) – Track covariance matrices of shape (n_tracks, n_state, n_state).

  • measurements (array_like) – Measurements of shape (n_measurements, n_meas).

  • measurement_models (array_like, optional) – Measurement matrices. See compute_association_cost for details.

  • gate_probability (float, optional) – Probability for chi-squared gate threshold (default: 0.99).

  • cost_of_non_assignment (float, optional) – Cost for not assigning a track or measurement.

Returns:

result – Association result.

Return type:

AssociationResult

Examples

>>> predictions = np.array([[0.0, 1.0], [5.0, -1.0]])
>>> covariances = np.array([0.1 * np.eye(2), 0.1 * np.eye(2)])
>>> measurements = np.array([[0.1], [4.9]])
>>> H = np.array([[1.0, 0.0]])
>>> result = gated_gnn_association(
...     predictions, covariances, measurements, H,
...     gate_probability=0.99
... )

Joint Probabilistic Data Association (JPDA)

The JPDA algorithm computes association probabilities for all feasible track-measurement pairings and updates each track with a weighted combination of innovations.

Joint Probabilistic Data Association (JPDA) algorithm.

JPDA computes association probabilities between tracks and measurements by considering all possible joint association hypotheses, then performs a probabilistically weighted update for each track.

This is more sophisticated than GNN which makes hard assignment decisions, as JPDA can handle measurement origin uncertainty in cluttered environments.

class pytcl.assignment_algorithms.jpda.JPDAResult(association_probs, marginal_probs, likelihood_matrix, gated)[source]

Bases: NamedTuple

Result of JPDA algorithm.

association_probs

Association probability matrix of shape (n_tracks, n_measurements + 1). association_probs[i, j] is the probability that track i is associated with measurement j. The last column (j = n_measurements) represents the probability that track i has no measurement.

Type:

ndarray[Any]

marginal_probs

List of marginal association probabilities for each track. marginal_probs[i][j] = P(measurement j originated from track i).

Type:

list of ndarray

likelihood_matrix

Measurement likelihood matrix of shape (n_tracks, n_measurements).

Type:

ndarray[Any]

gated

Boolean matrix indicating which track-measurement pairs passed gating.

Type:

ndarray[Any]

association_probs: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

marginal_probs: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 1

likelihood_matrix: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

gated: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 3

class pytcl.assignment_algorithms.jpda.JPDAUpdate(states, covariances, association_probs, innovations)[source]

Bases: NamedTuple

Result of JPDA-based track update.

states

Updated state estimates for each track.

Type:

list of ndarray

covariances

Updated covariances for each track (includes spread of means).

Type:

list of ndarray

association_probs

Association probability matrix.

Type:

ndarray[Any]

innovations

Combined weighted innovations for each track.

Type:

list of ndarray

states: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 0

covariances: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 1

association_probs: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

innovations: List[ndarray[tuple[Any, ...], dtype[floating]]]

Alias for field number 3

pytcl.assignment_algorithms.jpda.compute_measurement_likelihood(innovation, innovation_cov, detection_prob=1.0)[source]

Compute measurement likelihood for a track-measurement pair.

Parameters:
  • innovation (ndarray[Any]) – Measurement innovation (residual), shape (m,).

  • innovation_cov (ndarray[Any]) – Innovation covariance, shape (m, m).

  • detection_prob (float) – Probability of detection (Pd).

Returns:

likelihood – Measurement likelihood.

Return type:

float

pytcl.assignment_algorithms.jpda.compute_likelihood_matrix(track_states, track_covariances, measurements, H, R, detection_prob=1.0, gate_threshold=None)[source]

Compute likelihood matrix for all track-measurement pairs.

Parameters:
  • track_states (list of ndarray) – State estimates for each track.

  • track_covariances (list of ndarray) – Covariances for each track.

  • measurements (ndarray[Any]) – Measurements, shape (n_meas, m).

  • H (ndarray[Any]) – Measurement matrix, shape (m, n).

  • R (ndarray[Any]) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection.

  • gate_threshold (float, optional) – Mahalanobis distance threshold for gating.

Returns:

  • likelihood_matrix (ndarray[Any]) – Likelihood values, shape (n_tracks, n_meas).

  • gated (ndarray[Any]) – Boolean gating matrix, shape (n_tracks, n_meas).

Return type:

tuple[ndarray[tuple[Any, …], dtype[Any]], ndarray[tuple[Any, …], dtype[Any]]]

Examples

>>> import numpy as np
>>> # Two tracks, two measurements
>>> states = [np.array([0.0, 1.0]), np.array([5.0, 0.0])]
>>> covs = [np.eye(2) * 0.5, np.eye(2) * 0.5]
>>> measurements = np.array([[0.1], [5.2]])
>>> H = np.array([[1, 0]])
>>> R = np.array([[0.1]])
>>> L, gated = compute_likelihood_matrix(states, covs, measurements, H, R)
>>> L.shape
(2, 2)
>>> # Track 0 should have high likelihood for measurement 0
>>> L[0, 0] > L[0, 1]
True
pytcl.assignment_algorithms.jpda.jpda_probabilities(likelihood_matrix, gated, detection_prob=1.0, clutter_density=1e-06)[source]

Compute JPDA association probabilities.

Uses an efficient algorithm that avoids explicit enumeration of all joint hypotheses when possible.

Parameters:
  • likelihood_matrix (ndarray[Any]) – Likelihood values, shape (n_tracks, n_meas).

  • gated (ndarray[Any]) – Boolean gating matrix, shape (n_tracks, n_meas).

  • detection_prob (float) – Probability of detection (Pd).

  • clutter_density (float) – Spatial density of clutter (lambda_c).

Returns:

beta – Association probability matrix, shape (n_tracks, n_meas + 1). beta[i, j] = P(measurement j is from track i) for j < n_meas. beta[i, n_meas] = P(track i has no measurement).

Return type:

ndarray[Any]

Examples

>>> import numpy as np
>>> # Likelihood matrix: 2 tracks, 2 measurements
>>> # Track 0 has high likelihood for meas 0
>>> # Track 1 has high likelihood for meas 1
>>> likelihood = np.array([[0.9, 0.1],
...                        [0.1, 0.8]])
>>> gated = np.array([[True, True],
...                   [True, True]])
>>> beta = jpda_probabilities(likelihood, gated, detection_prob=0.9)
>>> beta.shape  # 2 tracks, 3 columns (2 meas + 1 miss)
(2, 3)
>>> # Track 0 most likely associated with measurement 0
>>> np.argmax(beta[0, :2])
0
pytcl.assignment_algorithms.jpda.jpda_update(track_states, track_covariances, measurements, H, R, detection_prob=0.9, clutter_density=1e-06, gate_probability=0.99)[source]

Perform JPDA-based track update.

Parameters:
  • track_states (list of array_like) – Predicted state estimates for each track.

  • track_covariances (list of array_like) – Predicted covariances for each track.

  • measurements (array_like) – Measurements, shape (n_meas, m).

  • H (array_like) – Measurement matrix, shape (m, n).

  • R (array_like) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection (Pd). Default 0.9.

  • clutter_density (float) – Spatial density of clutter. Default 1e-6.

  • gate_probability (float) – Probability for chi-squared gate. Default 0.99.

Returns:

result – Updated states and covariances with association probabilities.

Return type:

JPDAUpdate

Examples

>>> import numpy as np
>>> # Two tracks, three measurements
>>> x1 = np.array([0., 1.])  # [position, velocity]
>>> x2 = np.array([5., -1.])
>>> P = np.eye(2) * 0.1
>>> measurements = np.array([[0.1], [5.2], [10.0]])
>>> H = np.array([[1., 0.]])  # Measure position
>>> R = np.array([[0.1]])
>>> result = jpda_update([x1, x2], [P, P], measurements, H, R)
>>> len(result.states)
2

Notes

The JPDA update consists of: 1. Compute measurement likelihoods and gating 2. Compute association probabilities 3. Compute combined innovations for each track 4. Update each track with weighted innovation 5. Compute covariance with spread of means term

References

pytcl.assignment_algorithms.jpda.jpda(track_states, track_covariances, measurements, H, R, detection_prob=0.9, clutter_density=1e-06, gate_probability=0.99)[source]

Compute JPDA association probabilities.

This is a convenience function that computes association probabilities without performing the state update.

Parameters:
  • track_states (list of array_like) – Predicted state estimates for each track.

  • track_covariances (list of array_like) – Predicted covariances for each track.

  • measurements (array_like) – Measurements, shape (n_meas, m).

  • H (array_like) – Measurement matrix, shape (m, n).

  • R (array_like) – Measurement noise covariance, shape (m, m).

  • detection_prob (float) – Probability of detection. Default 0.9.

  • clutter_density (float) – Spatial density of clutter. Default 1e-6.

  • gate_probability (float) – Probability for chi-squared gate. Default 0.99.

Returns:

result – Association probabilities and related information.

Return type:

JPDAResult

Examples

Compute association probabilities for 2 tracks and 3 measurements:

>>> import numpy as np
>>> # Two tracks with [x, vx] state
>>> states = [np.array([0.0, 1.0]), np.array([10.0, -0.5])]
>>> covariances = [np.eye(2) * 0.5, np.eye(2) * 0.5]
>>> # Three position measurements
>>> measurements = np.array([[0.1], [9.8], [5.0]])
>>> H = np.array([[1, 0]])  # measure position only
>>> R = np.array([[0.1]])
>>> result = jpda(states, covariances, measurements, H, R)
>>> result.association_probs.shape  # (2 tracks, 4 columns: 3 meas + miss)
(2, 4)
>>> # Track 0 should have high prob for measurement 0
>>> result.association_probs[0, 0] > 0.5
True

See also

jpda_update

JPDA with state update.