Gaussian Mixtures and Clustering
This example demonstrates Gaussian mixture operations and clustering algorithms.
Overview
Gaussian mixtures and clustering are essential for:
Multi-hypothesis tracking: Representing multiple target hypotheses
Mixture reduction: Limiting computational complexity
Data clustering: Grouping measurements or tracks
Clustering Algorithms
- K-Means
Partitions data into k clusters
Minimizes within-cluster variance
Fast and scalable
- DBSCAN
Density-based clustering
Handles arbitrary cluster shapes
Identifies outliers automatically
- Hierarchical
Builds cluster tree (dendrogram)
Multiple linkage options
Flexible number of clusters
Gaussian Mixture Operations
Moment matching: Reducing mixture to single Gaussian
Runnalls’ algorithm: Optimal mixture reduction
West’s algorithm: Alternative reduction method
Splitting/merging: Dynamic mixture management
Code Highlights
The example demonstrates:
K-means clustering with
kmeans()DBSCAN with
dbscan()Hierarchical clustering with
agglomerative_clustering()Gaussian mixture reduction with
reduce_mixture_runnalls()
Source Code
1"""
2Gaussian Mixtures Example
3=========================
4
5This example demonstrates Gaussian mixture operations and clustering
6algorithms in PyTCL:
7
8Gaussian Mixture Operations:
9- Component representation and manipulation
10- Moment matching (computing mean and covariance)
11- Mixture merging and reduction
12- Runnalls' and West's reduction algorithms
13
14Clustering Algorithms:
15- K-means with K-means++ initialization
16- DBSCAN (density-based clustering)
17- Hierarchical/agglomerative clustering
18- Elbow method for K selection
19
20These algorithms are essential for multi-target tracking (PHD filters),
21hypothesis reduction in MHT, and general density estimation.
22"""
23
24from pathlib import Path
25
26import numpy as np
27import plotly.graph_objects as go
28
29# Output directory for generated plots
30OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
31OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
32from plotly.subplots import make_subplots
33
34# Global flag to control plotting
35SHOW_PLOTS = True
36
37
38def create_ellipse_trace(mean, cov, color="blue", opacity=0.3, n_std=2, name=None):
39 """Create a plotly trace for an ellipse representing a 2D Gaussian covariance."""
40 eigvals, eigvecs = np.linalg.eigh(cov)
41 # Angle in radians
42 angle = np.arctan2(eigvecs[1, 0], eigvecs[0, 0])
43
44 # Semi-axes lengths
45 a = n_std * np.sqrt(eigvals[0])
46 b = n_std * np.sqrt(eigvals[1])
47
48 # Parametric ellipse
49 t = np.linspace(0, 2 * np.pi, 100)
50 x = a * np.cos(t)
51 y = b * np.sin(t)
52
53 # Rotate
54 x_rot = x * np.cos(angle) - y * np.sin(angle) + mean[0]
55 y_rot = x * np.sin(angle) + y * np.cos(angle) + mean[1]
56
57 return go.Scatter(
58 x=x_rot,
59 y=y_rot,
60 mode="lines",
61 fill="toself",
62 fillcolor=color,
63 opacity=opacity,
64 line=dict(color=color, width=2),
65 name=name,
66 showlegend=name is not None,
67 )
68
69
70from pytcl.clustering import ( # Gaussian mixture operations; K-means; DBSCAN; Hierarchical clustering
71 DBSCANResult,
72 GaussianComponent,
73 GaussianMixture,
74 HierarchicalResult,
75 KMeansResult,
76 agglomerative_clustering,
77 compute_distance_matrix,
78 cut_dendrogram,
79 dbscan,
80 dbscan_predict,
81 kmeans,
82 kmeans_elbow,
83 kmeans_plusplus_init,
84 merge_gaussians,
85 moment_match,
86 prune_mixture,
87 reduce_mixture_runnalls,
88 reduce_mixture_west,
89 runnalls_merge_cost,
90 west_merge_cost,
91)
92
93
94def _rng(seed: int = 42) -> np.random.Generator:
95 """A seeded Generator for library calls that take one.
96
97 ``np.random.seed`` seeds the legacy global RNG; ``kmeans`` and friends
98 default to ``np.random.default_rng()``, a separate stream that ignores it.
99 Without passing this the figures under docs/_static change every rebuild.
100 """
101 return np.random.default_rng(seed)
102
103
104def demo_gaussian_components():
105 """Demonstrate Gaussian component operations."""
106 print("=" * 70)
107 print("Gaussian Component Operations Demo")
108 print("=" * 70)
109
110 # Create individual Gaussian components
111 comp1 = GaussianComponent(
112 weight=0.4,
113 mean=np.array([0.0, 0.0]),
114 covariance=np.array([[1.0, 0.0], [0.0, 1.0]]),
115 )
116
117 comp2 = GaussianComponent(
118 weight=0.6,
119 mean=np.array([3.0, 3.0]),
120 covariance=np.array([[2.0, 0.5], [0.5, 2.0]]),
121 )
122
123 print("\nComponent 1:")
124 print(f" Weight: {comp1.weight}")
125 print(f" Mean: {comp1.mean}")
126 print(f" Covariance diagonal: {np.diag(comp1.covariance)}")
127
128 print("\nComponent 2:")
129 print(f" Weight: {comp2.weight}")
130 print(f" Mean: {comp2.mean}")
131 print(f" Covariance diagonal: {np.diag(comp2.covariance)}")
132
133 # Create a mixture
134 mixture = GaussianMixture([comp1, comp2])
135 print(f"\nMixture has {len(mixture)} components")
136 print(f"Total weight: {sum(c.weight for c in mixture.components)}")
137
138
139def demo_moment_matching():
140 """Demonstrate moment matching for mixture approximation."""
141 print("\n" + "=" * 70)
142 print("Moment Matching Demo")
143 print("=" * 70)
144
145 # Create a mixture of 3 components
146 weights = np.array([0.3, 0.5, 0.2])
147 means = [np.array([0.0, 0.0]), np.array([2.0, 1.0]), np.array([1.0, 3.0])]
148 covariances = [np.eye(2) * 0.5, np.eye(2) * 0.8, np.eye(2) * 0.3]
149
150 print("\nOriginal mixture (3 components):")
151 for i, (w, m) in enumerate(zip(weights, means)):
152 print(f" Component {i + 1}: weight={w:.1f}, mean={m}")
153
154 # Moment match to single Gaussian - takes (weights, means, covariances)
155 mean, cov = moment_match(weights, means, covariances)
156
157 print("\nMoment-matched single Gaussian:")
158 print(f" Mean: ({mean[0]:.3f}, {mean[1]:.3f})")
159 print(f" Covariance:\n{cov}")
160
161 # The mean should be the weighted average
162 weighted_mean = sum(w * m for w, m in zip(weights, means))
163 print(
164 f"\nVerification - weighted mean: ({weighted_mean[0]:.3f}, "
165 f"{weighted_mean[1]:.3f})"
166 )
167
168
169def demo_mixture_merging():
170 """Demonstrate merging Gaussian components."""
171 print("\n" + "=" * 70)
172 print("Mixture Merging Demo")
173 print("=" * 70)
174
175 # Two nearby components
176 comp1 = GaussianComponent(0.4, np.array([0.0, 0.0]), np.eye(2) * 1.0)
177 comp2 = GaussianComponent(0.3, np.array([0.5, 0.5]), np.eye(2) * 1.0)
178
179 # Two far apart components
180 comp3 = GaussianComponent(0.3, np.array([5.0, 5.0]), np.eye(2) * 1.0)
181
182 print("\nThree components:")
183 print(f" 1: mean={comp1.mean}, weight={comp1.weight}")
184 print(f" 2: mean={comp2.mean}, weight={comp2.weight}")
185 print(f" 3: mean={comp3.mean}, weight={comp3.weight}")
186
187 # Merge costs
188 cost_12 = runnalls_merge_cost(comp1, comp2)
189 cost_13 = runnalls_merge_cost(comp1, comp3)
190 cost_23 = runnalls_merge_cost(comp2, comp3)
191
192 print("\nRunnalls merge costs (lower = better candidates for merging):")
193 print(f" Cost(1,2): {cost_12:.4f}")
194 print(f" Cost(1,3): {cost_13:.4f}")
195 print(f" Cost(2,3): {cost_23:.4f}")
196
197 # Merge the closest pair
198 merge_result = merge_gaussians(comp1, comp2)
199 merged = merge_result.component
200
201 print("\nMerged component (1+2):")
202 print(f" Weight: {merged.weight:.2f}")
203 print(f" Mean: ({merged.mean[0]:.3f}, {merged.mean[1]:.3f})")
204 print(f" Merge cost: {merge_result.cost:.4f}")
205
206
207def demo_mixture_reduction():
208 """Demonstrate mixture reduction algorithms."""
209 print("\n" + "=" * 70)
210 print("Mixture Reduction Demo")
211 print("=" * 70)
212
213 np.random.seed(42)
214
215 # Create a large mixture (e.g., from PHD filter output)
216 n_components = 20
217 components = []
218
219 # Cluster 1: around (0, 0)
220 for _ in range(8):
221 mean = np.array([0, 0]) + np.random.randn(2) * 0.5
222 cov = np.eye(2) * (0.3 + np.random.rand() * 0.2)
223 weight = 0.3 + np.random.rand() * 0.4
224 components.append(GaussianComponent(weight, mean, cov))
225
226 # Cluster 2: around (5, 3)
227 for _ in range(7):
228 mean = np.array([5, 3]) + np.random.randn(2) * 0.5
229 cov = np.eye(2) * (0.3 + np.random.rand() * 0.2)
230 weight = 0.2 + np.random.rand() * 0.3
231 components.append(GaussianComponent(weight, mean, cov))
232
233 # Scattered components
234 for _ in range(5):
235 mean = np.random.rand(2) * 10
236 cov = np.eye(2) * 0.5
237 weight = 0.05 + np.random.rand() * 0.1
238 components.append(GaussianComponent(weight, mean, cov))
239
240 print(f"\nOriginal mixture: {len(components)} components")
241 print(f"Total weight: {sum(c.weight for c in components):.2f}")
242
243 # Prune low-weight components
244 pruned = prune_mixture(components, weight_threshold=0.1)
245 print(f"\nAfter pruning (weight_threshold=0.1): {len(pruned)} components")
246
247 # Reduce using Runnalls' algorithm
248 n_target = 5
249 result_runnalls = reduce_mixture_runnalls(components, n_target)
250 print(f"\nRunnalls reduction to {n_target} components:")
251 print(f" Final components: {result_runnalls.n_reduced}")
252 print(f" Total merge cost: {result_runnalls.total_cost:.4f}")
253 for i, c in enumerate(result_runnalls.components):
254 print(
255 f" {i + 1}: weight={c.weight:.3f}, "
256 f"mean=({c.mean[0]:.2f}, {c.mean[1]:.2f})"
257 )
258
259 # Reduce using West's algorithm
260 result_west = reduce_mixture_west(components, n_target)
261 print(f"\nWest reduction to {n_target} components:")
262 print(f" Final components: {result_west.n_reduced}")
263 print(f" Total merge cost: {result_west.total_cost:.4f}")
264
265
266def demo_kmeans():
267 """Demonstrate K-means clustering."""
268 print("\n" + "=" * 70)
269 print("K-Means Clustering Demo")
270 print("=" * 70)
271
272 np.random.seed(42)
273
274 # Generate clustered data
275 n_per_cluster = 50
276 centers_true = np.array(
277 [
278 [0, 0],
279 [5, 5],
280 [0, 5],
281 ]
282 )
283
284 data = []
285 for center in centers_true:
286 cluster = center + np.random.randn(n_per_cluster, 2) * 0.8
287 data.append(cluster)
288 data = np.vstack(data)
289
290 print(f"\nGenerated {len(data)} points in 3 clusters")
291 print(f"True centers:\n{centers_true}")
292
293 # K-means clustering
294 result = kmeans(data, n_clusters=3, max_iter=100, rng=_rng())
295
296 print(f"\nK-means result:")
297 print(f" Iterations: {result.n_iter}")
298 print(f" Inertia (within-cluster sum of squares): {result.inertia:.2f}")
299 print(f" Found centers:\n{result.centers}")
300
301 # Cluster sizes
302 unique, counts = np.unique(result.labels, return_counts=True)
303 print("\n Cluster sizes:", dict(zip(unique, counts)))
304
305 # Plot K-means result
306 if SHOW_PLOTS:
307 fig = make_subplots(
308 rows=1,
309 cols=2,
310 subplot_titles=["Ground Truth Clusters", "K-means Clustering Result"],
311 )
312
313 # True clusters
314 colors = ["blue", "green", "orange"]
315 for i, center in enumerate(centers_true):
316 mask = np.arange(len(data)) // n_per_cluster == i
317 fig.add_trace(
318 go.Scatter(
319 x=data[mask, 0],
320 y=data[mask, 1],
321 mode="markers",
322 marker=dict(color=colors[i], size=6, opacity=0.6),
323 name=f"True cluster {i}",
324 ),
325 row=1,
326 col=1,
327 )
328
329 fig.add_trace(
330 go.Scatter(
331 x=centers_true[:, 0],
332 y=centers_true[:, 1],
333 mode="markers",
334 marker=dict(color="black", size=15, symbol="x", line=dict(width=3)),
335 name="True centers",
336 ),
337 row=1,
338 col=1,
339 )
340
341 # K-means result
342 colors_km = ["red", "green", "blue"]
343 for i in range(3):
344 mask = result.labels == i
345 fig.add_trace(
346 go.Scatter(
347 x=data[mask, 0],
348 y=data[mask, 1],
349 mode="markers",
350 marker=dict(color=colors_km[i], size=6, opacity=0.6),
351 name=f"Cluster {i}",
352 showlegend=True,
353 ),
354 row=1,
355 col=2,
356 )
357
358 fig.add_trace(
359 go.Scatter(
360 x=result.centers[:, 0],
361 y=result.centers[:, 1],
362 mode="markers",
363 marker=dict(color="black", size=15, symbol="x", line=dict(width=3)),
364 name="K-means centers",
365 ),
366 row=1,
367 col=2,
368 )
369
370 fig.update_xaxes(title_text="x")
371 fig.update_yaxes(title_text="y")
372 fig.update_layout(height=500, width=1000, showlegend=True)
373 fig.write_html(
374 str(OUTPUT_DIR / "gaussian_kmeans.html"),
375 include_plotlyjs="cdn",
376 div_id="gaussian_kmeans",
377 )
378 print("\n [Plot saved to gaussian_kmeans.html]")
379
380
381def demo_kmeans_plusplus():
382 """Demonstrate K-means++ initialization."""
383 print("\n" + "=" * 70)
384 print("K-Means++ Initialization Demo")
385 print("=" * 70)
386
387 np.random.seed(42)
388
389 # Generate data with 4 well-separated clusters
390 data = np.vstack(
391 [
392 np.random.randn(30, 2) + [0, 0],
393 np.random.randn(30, 2) + [10, 0],
394 np.random.randn(30, 2) + [0, 10],
395 np.random.randn(30, 2) + [10, 10],
396 ]
397 )
398
399 n_clusters = 4
400
401 # Random initialization
402 random_centers = data[np.random.choice(len(data), n_clusters, replace=False)]
403
404 # K-means++ initialization
405 plusplus_centers = kmeans_plusplus_init(data, n_clusters)
406
407 print(f"\nComparing initialization methods for n_clusters={n_clusters}:")
408
409 # Run K-means with each initialization
410 # For random, run multiple times (use init='random' for random init)
411 results_random = []
412 for _ in range(10):
413 idx = np.random.choice(len(data), n_clusters, replace=False)
414 result = kmeans(
415 data, n_clusters=n_clusters, init=data[idx], n_init=1, rng=_rng()
416 )
417 results_random.append(result.inertia)
418
419 result_plusplus = kmeans(
420 data, n_clusters=n_clusters, init=plusplus_centers, n_init=1, rng=_rng()
421 )
422
423 print(f"\n Random initialization (10 runs):")
424 print(f" Mean inertia: {np.mean(results_random):.2f}")
425 print(f" Std inertia: {np.std(results_random):.2f}")
426 print(f" Best inertia: {np.min(results_random):.2f}")
427
428 print(f"\n K-means++ initialization:")
429 print(f" Inertia: {result_plusplus.inertia:.2f}")
430
431 print("\nNote: K-means++ typically provides better, more consistent results.")
432
433
434def demo_elbow_method():
435 """Demonstrate elbow method for K selection."""
436 print("\n" + "=" * 70)
437 print("Elbow Method Demo")
438 print("=" * 70)
439
440 np.random.seed(42)
441
442 # Generate data with 3 true clusters
443 data = np.vstack(
444 [
445 np.random.randn(40, 2) + [0, 0],
446 np.random.randn(40, 2) + [4, 4],
447 np.random.randn(40, 2) + [8, 0],
448 ]
449 )
450
451 print(f"\nData: 120 points from 3 true clusters")
452 print("\nInertia for different K values:")
453 print("-" * 40)
454
455 elbow_result = kmeans_elbow(data, k_range=range(1, 8), rng=_rng())
456 k_values = elbow_result["k_values"]
457 inertias = elbow_result["inertias"]
458
459 for k, inertia in zip(k_values, inertias):
460 bar = "#" * int(inertia / max(inertias) * 30)
461 print(f" K={k}: {inertia:>8.1f} {bar}")
462
463 print("\nThe 'elbow' should appear around K=3")
464 print("(where adding more clusters gives diminishing returns)")
465
466 # Plot elbow method
467 if SHOW_PLOTS:
468 fig = go.Figure()
469
470 fig.add_trace(
471 go.Scatter(
472 x=list(k_values),
473 y=inertias,
474 mode="lines+markers",
475 line=dict(color="blue", width=2),
476 marker=dict(size=10),
477 name="Inertia",
478 )
479 )
480
481 fig.add_vline(
482 x=3, line_dash="dash", line_color="red", annotation_text="True K=3"
483 )
484
485 fig.update_layout(
486 title="Elbow Method for K Selection",
487 xaxis_title="Number of Clusters (K)",
488 yaxis_title="Inertia (Within-cluster Sum of Squares)",
489 height=500,
490 width=700,
491 showlegend=True,
492 )
493 fig.write_html(
494 str(OUTPUT_DIR / "gaussian_elbow.html"),
495 include_plotlyjs="cdn",
496 div_id="gaussian_elbow",
497 )
498 print("\n [Plot saved to gaussian_elbow.html]")
499
500
501def demo_dbscan():
502 """Demonstrate DBSCAN clustering."""
503 print("\n" + "=" * 70)
504 print("DBSCAN Clustering Demo")
505 print("=" * 70)
506
507 np.random.seed(42)
508
509 # Generate data: two dense clusters + noise
510 cluster1 = np.random.randn(50, 2) * 0.5 + [0, 0]
511 cluster2 = np.random.randn(50, 2) * 0.5 + [4, 4]
512 noise = np.random.uniform(-2, 8, (20, 2)) # Scattered noise points
513
514 data = np.vstack([cluster1, cluster2, noise])
515
516 print(f"\nData: 100 cluster points + 20 noise points")
517
518 # DBSCAN clustering
519 result = dbscan(data, eps=0.8, min_samples=5)
520
521 print(f"\nDBSCAN result (eps=0.8, min_samples=5):")
522 print(f" Clusters found: {result.n_clusters}")
523 print(f" Core sample indices: {len(result.core_sample_indices)}")
524 print(f" Noise points: {result.n_noise}")
525
526 # Cluster sizes
527 unique_labels = np.unique(result.labels)
528 for label in unique_labels:
529 count = np.sum(result.labels == label)
530 if label == -1:
531 print(f" Noise points: {count}")
532 else:
533 print(f" Cluster {label}: {count} points")
534
535 print("\nNote: DBSCAN identifies noise points (label=-1)")
536 print("and doesn't require specifying the number of clusters.")
537
538 # Plot DBSCAN result
539 if SHOW_PLOTS:
540 fig = make_subplots(
541 rows=1,
542 cols=2,
543 subplot_titles=[
544 "Ground Truth",
545 f"DBSCAN Result ({result.n_clusters} clusters)",
546 ],
547 )
548
549 # Ground truth
550 fig.add_trace(
551 go.Scatter(
552 x=cluster1[:, 0],
553 y=cluster1[:, 1],
554 mode="markers",
555 marker=dict(color="blue", size=8, opacity=0.6),
556 name="Cluster 1",
557 ),
558 row=1,
559 col=1,
560 )
561
562 fig.add_trace(
563 go.Scatter(
564 x=cluster2[:, 0],
565 y=cluster2[:, 1],
566 mode="markers",
567 marker=dict(color="green", size=8, opacity=0.6),
568 name="Cluster 2",
569 ),
570 row=1,
571 col=1,
572 )
573
574 fig.add_trace(
575 go.Scatter(
576 x=noise[:, 0],
577 y=noise[:, 1],
578 mode="markers",
579 marker=dict(color="red", size=8, opacity=0.6),
580 name="Noise",
581 ),
582 row=1,
583 col=1,
584 )
585
586 # DBSCAN result
587 colors = ["blue", "green", "purple", "orange"]
588 for label in unique_labels:
589 mask = result.labels == label
590 if label == -1:
591 fig.add_trace(
592 go.Scatter(
593 x=data[mask, 0],
594 y=data[mask, 1],
595 mode="markers",
596 marker=dict(color="red", size=8, opacity=0.6, symbol="x"),
597 name="Noise",
598 showlegend=True,
599 ),
600 row=1,
601 col=2,
602 )
603 else:
604 fig.add_trace(
605 go.Scatter(
606 x=data[mask, 0],
607 y=data[mask, 1],
608 mode="markers",
609 marker=dict(
610 color=colors[label % len(colors)], size=8, opacity=0.6
611 ),
612 name=f"Cluster {label}",
613 showlegend=True,
614 ),
615 row=1,
616 col=2,
617 )
618
619 # Mark core samples
620 core_mask = np.zeros(len(data), dtype=bool)
621 core_mask[result.core_sample_indices] = True
622 fig.add_trace(
623 go.Scatter(
624 x=data[core_mask, 0],
625 y=data[core_mask, 1],
626 mode="markers",
627 marker=dict(
628 color="rgba(0,0,0,0)", size=15, line=dict(color="black", width=1)
629 ),
630 name="Core samples",
631 showlegend=True,
632 ),
633 row=1,
634 col=2,
635 )
636
637 fig.update_xaxes(title_text="x")
638 fig.update_yaxes(title_text="y")
639 fig.update_layout(height=500, width=1000, showlegend=True)
640 fig.write_html(
641 str(OUTPUT_DIR / "gaussian_dbscan.html"),
642 include_plotlyjs="cdn",
643 div_id="gaussian_dbscan",
644 )
645 print("\n [Plot saved to gaussian_dbscan.html]")
646
647
648def demo_hierarchical():
649 """Demonstrate hierarchical clustering."""
650 print("\n" + "=" * 70)
651 print("Hierarchical Clustering Demo")
652 print("=" * 70)
653
654 np.random.seed(42)
655
656 # Generate small dataset for visualization
657 data = np.array(
658 [
659 [0, 0],
660 [0.5, 0.5],
661 [1, 0], # Cluster A
662 [5, 5],
663 [5.5, 5.5],
664 [5, 6], # Cluster B
665 [2.5, 2.5], # Between clusters
666 ]
667 )
668
669 print(f"\nData points:\n{data}")
670
671 # Compute distance matrix
672 dist_matrix = compute_distance_matrix(data)
673 print(f"\nDistance matrix:\n{np.round(dist_matrix, 2)}")
674
675 # Agglomerative clustering
676 result = agglomerative_clustering(data, linkage="average")
677
678 print("\nHierarchical clustering (average linkage):")
679 print(f" Labels: {result.labels}")
680 print(f" Number of clusters: {result.n_clusters}")
681
682 # Cut at different thresholds
683 n_samples = len(data)
684 for threshold in [1.0, 3.0, 5.0]:
685 labels = cut_dendrogram(
686 result.linkage_matrix, n_samples, distance_threshold=threshold
687 )
688 n_clusters = len(set(labels))
689 print(
690 f" Threshold {threshold:.1f}: {n_clusters} clusters, labels={list(labels)}"
691 )
692
693
694def demo_tracking_application():
695 """Demonstrate mixture reduction in tracking context."""
696 print("\n" + "=" * 70)
697 print("Tracking Application Demo")
698 print("=" * 70)
699
700 np.random.seed(42)
701
702 # Simulated PHD filter output: mixture representing target density
703 # After several updates, the mixture can have many components
704
705 # True targets at these locations
706 true_targets = np.array(
707 [
708 [10.0, 20.0],
709 [30.0, 40.0],
710 [25.0, 25.0],
711 ]
712 )
713
714 print(f"\nTrue target positions: {len(true_targets)} targets")
715 for i, t in enumerate(true_targets):
716 print(f" Target {i + 1}: ({t[0]:.1f}, {t[1]:.1f})")
717
718 # Create mixture with components clustered around true targets
719 # Plus some spurious components (false alarms, etc.)
720 components = []
721
722 # Components near true targets (higher weight)
723 for target in true_targets:
724 for _ in range(4):
725 mean = target + np.random.randn(2) * 1.0
726 cov = np.eye(2) * (0.5 + np.random.rand() * 0.5)
727 weight = 0.6 + np.random.rand() * 0.4
728 components.append(GaussianComponent(weight, mean, cov))
729
730 # Spurious components (lower weight)
731 for _ in range(8):
732 mean = np.random.uniform(5, 45, 2)
733 cov = np.eye(2) * 2.0
734 weight = 0.05 + np.random.rand() * 0.1
735 components.append(GaussianComponent(weight, mean, cov))
736
737 print(f"\nPHD mixture: {len(components)} components")
738 total_weight = sum(c.weight for c in components)
739 print(f" Total weight (expected target count): {total_weight:.2f}")
740
741 # Reduce to extract target estimates
742 n_expected = int(round(total_weight))
743 reduced = reduce_mixture_runnalls(components, n_expected)
744
745 print(f"\nAfter reduction to {n_expected} components:")
746 for i, c in enumerate(reduced.components):
747 # Find closest true target
748 dists = [np.linalg.norm(c.mean - t) for t in true_targets]
749 closest = np.argmin(dists)
750 error = min(dists)
751 print(
752 f" Estimate {i + 1}: ({c.mean[0]:.1f}, {c.mean[1]:.1f}), "
753 f"weight={c.weight:.2f}, error to target {closest + 1}={error:.2f}"
754 )
755
756
757def main():
758 """Run all demonstrations."""
759 # Seed once for the whole run. Individual demos below reseed, but
760 # not all of them do, and the figures are committed under
761 # docs/_static -- an unseeded draw makes every rebuild produce a
762 # different file and a spurious diff.
763 np.random.seed(42)
764 print("\n" + "#" * 70)
765 print("# PyTCL Gaussian Mixtures and Clustering Example")
766 print("#" * 70)
767
768 # Gaussian mixture operations
769 demo_gaussian_components()
770 demo_moment_matching()
771 demo_mixture_merging()
772 demo_mixture_reduction()
773
774 # Clustering algorithms
775 demo_kmeans()
776 demo_kmeans_plusplus()
777 demo_elbow_method()
778 demo_dbscan()
779 demo_hierarchical()
780
781 # Application
782 demo_tracking_application()
783
784 print("\n" + "=" * 70)
785 print("Example complete!")
786 if SHOW_PLOTS:
787 print(
788 "Plots saved: gaussian_kmeans.html, gaussian_elbow.html, gaussian_dbscan.html"
789 )
790 print("=" * 70)
791
792
793if __name__ == "__main__":
794 main()
Running the Example
python examples/gaussian_mixtures.py
See Also
Spatial Data Structures - KD-trees for clustering
Advanced Filters Comparison - Gaussian sum filters