Tracking Containers

This example demonstrates track and measurement container data structures.

Overview

Efficient tracking systems require organized data structures:

  • TrackList: Collection of tracks with spatial queries

  • MeasurementSet: Organized measurement storage

  • Track state: Position, velocity, covariance, metadata

Key Concepts

  • Track ID management: Unique identifiers for each track

  • Temporal indexing: Accessing data by time step

  • Spatial queries: Finding tracks in a region

  • Track history: Storing past states for smoothing

Spatial Indexing: KD-trees enable efficient nearest-neighbor queries for track-to-measurement association.

Range Queries: R-trees support efficient rectangular range queries for gating operations.

Code Highlights

The example demonstrates:

  • Creating and populating TrackList containers

  • Adding tracks with state and covariance

  • Querying tracks by ID, time, or spatial region

  • Iterating over tracks for batch processing

Source Code

  1"""
  2Tracking Containers Example
  3===========================
  4
  5This example demonstrates the tracking container classes in PyTCL:
  6- TrackList: Collection of tracks with filtering and batch operations
  7- MeasurementSet: Time-indexed measurements with spatial queries
  8- ClusterSet: Track clustering for formation detection
  9
 10These containers provide efficient data management for multi-target tracking
 11applications with immutable design patterns and lazy spatial indexing.
 12"""
 13
 14import os
 15from pathlib import Path
 16
 17import numpy as np
 18import plotly.graph_objects as go
 19from plotly.subplots import make_subplots
 20
 21from pytcl.containers import (
 22    ClusterSet,
 23    MeasurementSet,
 24    TrackList,
 25)
 26from pytcl.containers.cluster_set import cluster_tracks_dbscan, cluster_tracks_kmeans
 27from pytcl.containers.measurement_set import Measurement
 28from pytcl.containers.track_list import Track, TrackStatus
 29
 30SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 31OUTPUT_DIR = Path(__file__).resolve().parent / "output"
 32
 33
 34def create_sample_tracks(n_tracks: int = 10, seed: int = 42) -> TrackList:
 35    """Create sample tracks for demonstration."""
 36    rng = np.random.default_rng(seed)
 37
 38    tracks = []
 39    for i in range(n_tracks):
 40        # State: [x, vx, y, vy] - 2D position and velocity
 41        x = rng.uniform(-100, 100)
 42        y = rng.uniform(-100, 100)
 43        vx = rng.uniform(-5, 5)
 44        vy = rng.uniform(-5, 5)
 45        state = np.array([x, vx, y, vy])
 46
 47        # Covariance matrix
 48        pos_var = rng.uniform(1, 5)
 49        vel_var = rng.uniform(0.1, 0.5)
 50        P = np.diag([pos_var, vel_var, pos_var, vel_var])
 51
 52        # Random status based on hits
 53        hits = rng.integers(1, 20)
 54        misses = rng.integers(0, 5)
 55        if hits >= 5:
 56            status = TrackStatus.CONFIRMED
 57        elif misses >= 3:
 58            status = TrackStatus.DELETED
 59        else:
 60            status = TrackStatus.TENTATIVE
 61
 62        track = Track(
 63            id=i,
 64            state=state,
 65            covariance=P,
 66            status=status,
 67            hits=hits,
 68            misses=misses,
 69            time=10.0 + rng.uniform(0, 5),
 70        )
 71        tracks.append(track)
 72
 73    return TrackList(tracks)
 74
 75
 76def create_sample_measurements(n_times: int = 5, n_per_time: int = 8, seed: int = 42):
 77    """Create sample measurements across multiple time steps."""
 78    rng = np.random.default_rng(seed)
 79
 80    measurements = []
 81    meas_id = 0
 82    for t in range(n_times):
 83        time = float(t)
 84        for _ in range(n_per_time):
 85            # 2D position measurement
 86            value = rng.uniform(-50, 50, size=2)
 87            covariance = np.eye(2) * rng.uniform(0.5, 2.0)
 88            sensor_id = rng.integers(0, 3)  # 3 sensors
 89
 90            meas = Measurement(
 91                value=value,
 92                time=time,
 93                covariance=covariance,
 94                sensor_id=sensor_id,
 95                id=meas_id,
 96            )
 97            measurements.append(meas)
 98            meas_id += 1
 99
100    return MeasurementSet(measurements)
101
102
103def demo_track_list():
104    """Demonstrate TrackList container operations."""
105    print("=" * 70)
106    print("TrackList Container Demo")
107    print("=" * 70)
108
109    # Create sample tracks
110    tracks = create_sample_tracks(n_tracks=15)
111    print(f"\nCreated TrackList with {len(tracks)} tracks")
112
113    # Get statistics
114    stats = tracks.stats()
115    print("\nTrack Statistics:")
116    print(f"  Total tracks: {stats.n_tracks}")
117    print(f"  Confirmed: {stats.n_confirmed}")
118    print(f"  Tentative: {stats.n_tentative}")
119    print(f"  Deleted: {stats.n_deleted}")
120    print(f"  Mean hits: {stats.mean_hits:.1f}")
121    print(f"  Mean misses: {stats.mean_misses:.1f}")
122
123    # Filter by status
124    confirmed = tracks.filter_by_status(TrackStatus.CONFIRMED)
125    tentative = tracks.filter_by_status(TrackStatus.TENTATIVE)
126    print("\nFiltered by status:")
127    print(f"  Confirmed tracks: {len(confirmed)}")
128    print(f"  Tentative tracks: {len(tentative)}")
129
130    # Shortcut properties
131    print(f"  Using .confirmed property: {len(tracks.confirmed)}")
132    print(f"  Using .tentative property: {len(tracks.tentative)}")
133
134    # Filter by region (tracks near origin)
135    center = np.array([0.0, 0.0])
136    nearby = tracks.filter_by_region(center, radius=50.0, state_indices=(0, 2))
137    print(f"\nTracks within 50 units of origin: {len(nearby)}")
138
139    # Filter by time
140    recent = tracks.filter_by_time(min_time=12.0)
141    print(f"Tracks updated after t=12.0: {len(recent)}")
142
143    # Custom predicate filter
144    high_confidence = tracks.filter_by_predicate(lambda t: t.hits >= 10)
145    print(f"Tracks with 10+ hits: {len(high_confidence)}")
146
147    # Batch data extraction
148    if len(confirmed) > 0:
149        states = confirmed.states()
150        positions = confirmed.positions(indices=(0, 2))
151        print("\nBatch extraction from confirmed tracks:")
152        print(f"  States shape: {states.shape}")
153        print(f"  Positions shape: {positions.shape}")
154
155    # Access by ID
156    track_ids = tracks.track_ids
157    if track_ids:
158        track = tracks.get_by_id(track_ids[0])
159        print(f"\nTrack {track.id}:")
160        print(f"  Position: ({track.state[0]:.1f}, {track.state[2]:.1f})")
161        print(f"  Velocity: ({track.state[1]:.1f}, {track.state[3]:.1f})")
162        print(f"  Status: {track.status.name}")
163
164    # Immutable operations
165    new_track = Track(
166        id=100,
167        state=np.array([0, 0, 0, 0]),
168        covariance=np.eye(4),
169        status=TrackStatus.TENTATIVE,
170        hits=1,
171        misses=0,
172        time=15.0,
173    )
174    tracks_with_new = tracks.add(new_track)
175    print("\nAfter adding track:")
176    print(f"  Original TrackList: {len(tracks)} tracks")
177    print(f"  New TrackList: {len(tracks_with_new)} tracks")
178
179    # Merge two track lists
180    merged = confirmed.merge(tentative)
181    print(f"\nMerged confirmed + tentative: {len(merged)} tracks")
182
183
184def demo_measurement_set():
185    """Demonstrate MeasurementSet container operations."""
186    print("\n" + "=" * 70)
187    print("MeasurementSet Container Demo")
188    print("=" * 70)
189
190    # Create sample measurements
191    meas_set = create_sample_measurements(n_times=5, n_per_time=8)
192    print(f"\nCreated MeasurementSet with {len(meas_set)} measurements")
193
194    # Time properties
195    times = meas_set.times
196    time_range = meas_set.time_range
197    print("\nTime information:")
198    print(f"  Unique times: {times}")
199    print(f"  Time range: {time_range}")
200
201    # Query by time
202    at_t2 = meas_set.at_time(2.0)
203    print(f"\nMeasurements at t=2.0: {len(at_t2)}")
204
205    # Query time window
206    window = meas_set.in_time_window(1.0, 3.0)
207    print(f"Measurements in window [1.0, 3.0]: {len(window)}")
208
209    # Query by sensor
210    sensors = meas_set.sensors
211    print(f"\nSensors: {sensors}")
212    for sensor_id in sensors:
213        sensor_meas = meas_set.by_sensor(sensor_id)
214        print(f"  Sensor {sensor_id}: {len(sensor_meas)} measurements")
215
216    # Spatial queries
217    center = np.array([0.0, 0.0])
218    nearby = meas_set.in_region(center, radius=25.0)
219    print(f"\nMeasurements within 25 units of origin: {len(nearby)}")
220
221    # K-nearest neighbors
222    query_point = np.array([10.0, 10.0])
223    nearest = meas_set.nearest_to(query_point, k=3)
224    print("\n3 nearest measurements to (10, 10):")
225    for meas in nearest.measurements:
226        dist = np.linalg.norm(meas.value - query_point)
227        print(f"  ID {meas.id}: value={meas.value}, distance={dist:.2f}")
228
229    # Batch extraction
230    values = meas_set.values()
231    print("\nBatch extraction:")
232    print(f"  All values shape: {values.shape}")
233
234    values_at_t1 = meas_set.values_at_time(1.0)
235    print(f"  Values at t=1.0 shape: {values_at_t1.shape}")
236
237    # Create from arrays
238    new_values = np.random.randn(5, 2) * 10
239    new_times = np.array([10.0, 10.0, 10.1, 10.1, 10.2])
240    meas_from_arrays = MeasurementSet.from_arrays(new_values, new_times)
241    print(f"\nCreated from arrays: {len(meas_from_arrays)} measurements")
242
243
244def demo_cluster_set():
245    """Demonstrate ClusterSet container operations."""
246    print("\n" + "=" * 70)
247    print("ClusterSet Container Demo")
248    print("=" * 70)
249
250    # Create tracks with some spatial clustering
251    rng = np.random.default_rng(42)
252    tracks = []
253
254    # Cluster 1: tracks near (50, 50)
255    for i in range(5):
256        state = np.array(
257            [
258                50 + rng.normal(0, 3),  # x
259                2 + rng.normal(0, 0.5),  # vx
260                50 + rng.normal(0, 3),  # y
261                1 + rng.normal(0, 0.5),  # vy
262            ]
263        )
264        tracks.append(
265            Track(
266                id=i,
267                state=state,
268                covariance=np.eye(4),
269                status=TrackStatus.CONFIRMED,
270                hits=10,
271                misses=0,
272                time=0.0,
273            )
274        )
275
276    # Cluster 2: tracks near (-30, -30)
277    for i in range(4):
278        state = np.array(
279            [
280                -30 + rng.normal(0, 3),
281                -1 + rng.normal(0, 0.5),
282                -30 + rng.normal(0, 3),
283                2 + rng.normal(0, 0.5),
284            ]
285        )
286        tracks.append(
287            Track(
288                id=5 + i,
289                state=state,
290                covariance=np.eye(4),
291                status=TrackStatus.CONFIRMED,
292                hits=8,
293                misses=1,
294                time=0.0,
295            )
296        )
297
298    # Isolated tracks (noise)
299    for i in range(3):
300        state = np.array(
301            [
302                rng.uniform(-100, 100),
303                rng.uniform(-3, 3),
304                rng.uniform(-100, 100),
305                rng.uniform(-3, 3),
306            ]
307        )
308        tracks.append(
309            Track(
310                id=9 + i,
311                state=state,
312                covariance=np.eye(4),
313                status=TrackStatus.CONFIRMED,
314                hits=5,
315                misses=2,
316                time=0.0,
317            )
318        )
319
320    track_list = TrackList(tracks)
321    print(f"\nCreated {len(track_list)} tracks with 2 clusters + noise")
322
323    # DBSCAN clustering
324    print("\n--- DBSCAN Clustering ---")
325    clusters_dbscan = cluster_tracks_dbscan(
326        track_list,
327        eps=10.0,  # Max distance between neighbors
328        min_samples=3,  # Minimum cluster size
329        state_indices=(0, 2),  # Use x, y positions
330    )
331    print(f"Found {len(clusters_dbscan)} clusters")
332
333    for cluster in clusters_dbscan:
334        print(f"\n  Cluster {cluster.id}:")
335        print(f"    Track IDs: {cluster.track_ids}")
336        print(f"    Centroid: ({cluster.centroid[0]:.1f}, {cluster.centroid[1]:.1f})")
337        print(f"    Covariance diagonal: {np.diag(cluster.covariance)}")
338
339    # Cluster statistics
340    print("\n--- Cluster Statistics ---")
341    all_stats = clusters_dbscan.all_stats(
342        tracks=track_list,
343        state_indices=(0, 2),
344        velocity_indices=(1, 3),
345    )
346    for cluster_id, stats in all_stats.items():
347        print(f"\n  Cluster {cluster_id}:")
348        print(f"    Tracks: {stats.n_tracks}")
349        print(f"    Mean separation: {stats.mean_separation:.2f}")
350        print(f"    Max separation: {stats.max_separation:.2f}")
351        print(f"    Velocity coherence: {stats.velocity_coherence:.2f}")
352
353    # K-means clustering
354    print("\n--- K-Means Clustering ---")
355    clusters_kmeans = cluster_tracks_kmeans(
356        track_list,
357        n_clusters=3,
358        state_indices=(0, 2),
359        rng=np.random.default_rng(42),
360    )
361    print(f"Created {len(clusters_kmeans)} clusters")
362
363    for cluster in clusters_kmeans:
364        print(
365            f"  Cluster {cluster.id}: {len(cluster.track_ids)} tracks at "
366            f"({cluster.centroid[0]:.1f}, {cluster.centroid[1]:.1f})"
367        )
368
369    # Using ClusterSet.from_tracks factory
370    print("\n--- Factory Method ---")
371    clusters = ClusterSet.from_tracks(
372        track_list,
373        method="dbscan",
374        eps=10.0,
375        min_samples=2,
376    )
377    print(f"Created ClusterSet with {len(clusters)} clusters")
378
379    # Spatial query on clusters
380    center = np.array([50.0, 50.0])
381    nearby_clusters = clusters.clusters_in_region(center, radius=30.0)
382    print(f"\nClusters within 30 units of (50, 50): {len(nearby_clusters)}")
383
384    # Track to cluster lookup
385    if len(clusters) > 0:
386        track_id = 0
387        cluster = clusters.get_cluster_for_track(track_id)
388        if cluster:
389            print(f"Track {track_id} belongs to cluster {cluster.id}")
390
391    # Cluster manipulation (immutable)
392    if len(clusters) >= 2:
393        cluster_ids = clusters.cluster_ids
394        merged = clusters.merge_clusters(cluster_ids[0], cluster_ids[1])
395        print(f"\nAfter merging clusters {cluster_ids[0]} and {cluster_ids[1]}:")
396        print(f"  Original: {len(clusters)} clusters")
397        print(f"  After merge: {len(merged)} clusters")
398
399
400def demo_integration():
401    """Demonstrate integration between containers."""
402    print("\n" + "=" * 70)
403    print("Container Integration Demo")
404    print("=" * 70)
405
406    # Create tracks and measurements
407    tracks = create_sample_tracks(n_tracks=20)
408    measurements = create_sample_measurements(n_times=10, n_per_time=15)
409
410    print(f"\nDataset: {len(tracks)} tracks, {len(measurements)} measurements")
411
412    # Filter to confirmed tracks
413    confirmed = tracks.confirmed
414    print(f"\nConfirmed tracks: {len(confirmed)}")
415
416    # For each confirmed track, find nearby measurements
417    print("\nMatching tracks to nearby measurements:")
418    for track in list(confirmed)[:3]:  # Show first 3
419        pos = track.state[[0, 2]]  # x, y position
420        nearby_meas = measurements.in_region(pos, radius=20.0)
421        print(
422            f"  Track {track.id} at ({pos[0]:.1f}, {pos[1]:.1f}): "
423            f"{len(nearby_meas)} nearby measurements"
424        )
425
426    # Cluster confirmed tracks
427    if len(confirmed) >= 3:
428        clusters = ClusterSet.from_tracks(
429            confirmed,
430            method="dbscan",
431            eps=50.0,
432            min_samples=2,
433        )
434        print(f"\nClustered confirmed tracks: {len(clusters)} formations")
435
436        # For each cluster, find measurements near centroid
437        for cluster in clusters:
438            nearby = measurements.in_region(cluster.centroid, radius=30.0)
439            print(
440                f"  Cluster {cluster.id} ({len(cluster.track_ids)} tracks): "
441                f"{len(nearby)} measurements near centroid"
442            )
443
444    # Time-synchronized analysis
445    print("\n--- Time-Synchronized Analysis ---")
446    for t in [0.0, 2.0, 4.0]:
447        meas_at_t = measurements.at_time(t)
448        tracks_at_t = tracks.filter_by_time(max_time=t + 1.0)
449        print(
450            f"  t={t}: {len(meas_at_t)} measurements, "
451            f"{len(tracks_at_t)} tracks updated before t={t + 1}"
452        )
453
454
455def main():
456    """Run all demonstrations."""
457    print("\n" + "#" * 70)
458    print("# PyTCL Tracking Containers Example")
459    print("#" * 70)
460
461    demo_track_list()
462    demo_measurement_set()
463    demo_cluster_set()
464    demo_integration()
465
466    # Visualization
467    visualize_track_distribution()
468
469    print("\n" + "=" * 70)
470    print("Example complete!")
471    print("=" * 70)
472
473
474def visualize_track_distribution():
475    """Visualize track spatial distribution."""
476    print("\nGenerating track distribution visualization...")
477
478    # Create sample tracks
479    tracks = create_sample_tracks(n_tracks=15)
480
481    # Extract positions
482    positions = []
483    for track in tracks:
484        if track.state is not None:
485            # Assuming state is [x, vx, y, vy]
486            pos = track.state[[0, 2]]
487            positions.append(pos)
488
489    if positions:
490        positions = np.array(positions)
491
492        # Create scatter plot
493        fig = go.Figure()
494
495        fig.add_trace(
496            go.Scatter(
497                x=positions[:, 0],
498                y=positions[:, 1],
499                mode="markers+text",
500                text=[f"T{i}" for i in range(len(positions))],
501                marker=dict(size=10, color="blue", opacity=0.7),
502                textposition="top center",
503                name="Track Positions",
504            )
505        )
506
507        fig.update_layout(
508            title="Track Spatial Distribution",
509            xaxis_title="X Position (m)",
510            yaxis_title="Y Position (m)",
511            height=600,
512            width=700,
513            showlegend=False,
514        )
515
516        if SHOW_PLOTS:
517            fig.show()
518        else:
519            OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
520            fig.write_html(
521                str(OUTPUT_DIR / "tracking_containers.html"),
522                include_plotlyjs="cdn",
523                div_id="tracking_containers",
524            )
525
526
527if __name__ == "__main__":
528    main()

Running the Example

python examples/tracking_containers.py

See Also