Data Structures & Containers

This guide covers the containers the library actually provides: Track, TrackList, MeasurementSet, ClusterSet, and the spatial indices.

Note

Earlier revisions of this page documented a TrackSet class imported from tcl.tracking_containers, with attributes such as track.uid, track.position and track.gate_size. No such class, module or attributes exist. The container that fills that role is TrackList in pytcl.containers, described below. Every example on this page is executed by tests/contract/test_docs_data_structures.py.

Track

Track is an immutable snapshot of one target, produced by MultiTargetTracker. It is a NamedTuple, so it unpacks and compares like a tuple and cannot be mutated in place.

Field

Type

Meaning

id

int

Identifier, stable for the life of the track

state

ndarray

State vector, layout set by the model you gave the tracker

covariance

ndarray

State covariance, (state_dim, state_dim)

status

TrackStatus

TENTATIVE, CONFIRMED or DELETED

hits

int

Number of associated detections

misses

int

Consecutive scans without an association

time

float

Time of the most recent update

There is no position or velocity attribute: which elements of state are position depends on the transition matrix you supplied. With f_constant_velocity the layout is [x, vx, y, vy], so position is state[0] and state[2] — which is why TrackList.positions() takes the indices as an argument.

import numpy as np
from pytcl.dynamic_models import f_constant_velocity
from pytcl.trackers import MultiTargetTracker, TrackStatus

F = f_constant_velocity(1.0, num_dims=2)  # [x, vx, y, vy] layout
H = np.array([[1.0, 0.0, 0.0, 0.0],
              [0.0, 0.0, 1.0, 0.0]])

tracker = MultiTargetTracker(
    state_dim=4, meas_dim=2, F=F, H=H,
    Q=np.eye(4) * 0.01, R=np.eye(2) * 4.0, confirm_hits=2,
)

for k in range(6):
    tracker.process(
        [np.array([k * 2.0, k * 1.0]), np.array([50.0 + k, 60.0 - k * 0.5])],
        dt=1.0,
    )

track = tracker.tracks[0]
print(track.id, track.status is TrackStatus.CONFIRMED, track.hits)

TrackList

TrackList is an immutable collection of Track objects. Every operation returns a new list rather than modifying in place, so a filtered view can be passed around without any risk of aliasing.

from pytcl.containers import TrackList
from pytcl.trackers import TrackStatus

tracks = TrackList.from_tracker(tracker)

tracks.track_ids                 # [0, 1]
tracks.stats()                   # TrackListStats(n_tracks=2, n_confirmed=2, ...)

tracks.confirmed                 # property, not a call
tracks.tentative

tracks.states()                  # (n_tracks, state_dim)
tracks.covariances()             # (n_tracks, state_dim, state_dim)
tracks.positions(indices=(0, 2)) # (n_tracks, 2), defaults to [x, y] of [x, vx, y, vy]

tracks.get_by_id(0)
tracks.filter_by_status(TrackStatus.CONFIRMED)
tracks.filter_by_region([0.0, 0.0], radius=20.0)
tracks.filter_by_time(min_time=2.0)
tracks.filter_by_predicate(lambda t: t.hits >= 5)

confirmed and tentative are properties; track_ids is too. The filters and accessors are methods. add, remove and merge also return new lists.

MeasurementSet

MeasurementSet holds detections with their times, covariances and sensor of origin. Measurement is a NamedTuple with fields (value, time, covariance, sensor_id, id).

import numpy as np
from pytcl.containers import MeasurementSet

detections = MeasurementSet.from_arrays(
    values=np.array([[0.0, 0.0], [1.0, 2.0], [2.0, 4.0], [3.0, 6.0]]),
    times=np.array([0.0, 1.0, 2.0, 3.0]),
    covariances=np.stack([np.eye(2)] * 4),
)

detections.times          # property: array([0., 1., 2., 3.])
detections.time_range     # property: (0.0, 3.0)
detections.sensors        # property

detections.values()                        # (n, meas_dim)
detections.at_time(2.0)
detections.in_time_window(1.0, 2.0)
detections.in_region([1.0, 2.0], radius=3.0)
detections.nearest_to([2.0, 4.0], k=2)     # MeasurementQuery

A MeasurementSet iterates over its Measurement objects and supports len(). Because each measurement carries its own covariance, a set converted from polar detections feeds straight into the tracker’s per-detection covariance argument:

scan = detections.at_time(2.0)
tracker.process(
    [m.value for m in scan],
    dt=1.0,
    measurement_covariances=[m.covariance for m in scan],
)

See Library Architecture for why a single R is not sufficient for converted measurements.

ClusterSet

ClusterSet groups tracks that belong together — formations, or targets too close to resolve individually.

from pytcl.containers import ClusterSet

# method is a name -- 'dbscan' or 'kmeans' -- not a function
clusters = ClusterSet.from_tracks(tracks, method="dbscan", eps=30.0)
clusters.cluster_ids
clusters.all_stats()

Spatial Indices

Five index structures, all with the same query surface, for finding neighbors without a linear scan:

Class

Constructor

Suited to

KDTree

KDTree(data, leaf_size=10)

Low-dimensional Euclidean data

BallTree

BallTree(data)

Higher dimensions, still a metric space

RTree

RTree(max_entries=10)

Extended objects and bounding boxes

VPTree

VPTree(data, metric=None)

Arbitrary metrics

CoverTree

CoverTree(data, metric=None, base=2.0)

Arbitrary metrics, bounded intrinsic dimension

import numpy as np
from pytcl.containers import KDTree

points = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 4.0], [5.0, 5.0]])
index = KDTree(points)

index.query(np.array([1.1, 1.1]), k=2)      # k nearest
index.query_radius(np.array([0.0, 0.0]), r=2.0)

KDTree, VPTree and CoverTree take their data at construction. RTree is built incrementally with insert_point / insert_points, or in one go with RTree.from_points.

Persistence

Tracks are written and read with pytcl.io. The round trip is exact — states, covariances, timestamps and metadata all come back unchanged.

import numpy as np
from pytcl.io import TrackHDF5Storage

storage = TrackHDF5Storage("tracks.h5")
storage.open("w")
storage.store_track(
    "track_0",
    states=np.zeros((10, 4)),
    covariances=np.stack([np.eye(4)] * 10),
    timestamps=np.arange(10.0),
    metadata={"status": "confirmed"},
)
storage.close()

storage = TrackHDF5Storage("tracks.h5")
storage.open("r")
storage.list_tracks()                    # ['track_0']
record = storage.retrieve_track("track_0")
record["states"], record["covariances"], record["metadata"]
storage.close()

get_track_trajectory returns just the state history, optionally windowed by time.

See Also