Skip to content

Commit 16d3fe1

Browse files
keithalegrandAndrea De Vittori
andauthored
Stable cov (#9)
* Fixed cov(self) to handle small covariances and large means in a vectorized fashion * Add unit test to ensure covariance is not altered after splitting --------- Co-authored-by: Andrea De Vittori <adevitto@purdue.edu>
1 parent a2904ff commit 16d3fe1

File tree

2 files changed

+72
-6
lines changed

2 files changed

+72
-6
lines changed

pyest/gm/gm.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77
import pandas as pd
88
from numpy import pi, sqrt
9+
from copy import copy
910
from numpy.random import rand, randn
1011
from scipy.stats._multivariate import _LOG_2PI, _PSD, _squeeze_output
1112
from scipy.stats import Covariance, _covariance
@@ -871,18 +872,30 @@ def mean(self):
871872
return np.sum(self.w[:, np.newaxis] * self.m, axis=0)
872873

873874
def cov(self):
874-
""" return covariance of the distribution
875+
"""Compute the covariance matrix of the distribution.
875876
876877
Returns
877878
-------
878879
ndarray
879-
(nx,nx) full covariance matrix of distribution
880+
(nx, nx) full covariance matrix of the distribution
880881
"""
881-
wsum = np.sum(self.w)
882+
# Compute the mean of the distribution
882883
mean = self.mean()
883-
return np.sum([
884-
w/wsum*(P + np.outer(m, m)) for w, m, P in self
885-
], axis=0) - np.outer(mean, mean)
884+
885+
w = copy(self.w)
886+
w = w / np.sum(w)
887+
888+
# Extract individual covariances
889+
covs = self.P
890+
891+
# Compute the difference between each mixand mean and the distribution mean
892+
diffs = self.m - mean # shape (nC, nx)
893+
894+
# Compute the outer product of diffs for each sample
895+
outer_diffs = diffs[:, :, None] * diffs[:, None, :] # shape (nC, nx, nx)
896+
897+
# Weighted sum of covariances and outer products
898+
return np.sum(w[:, None, None] * (covs + outer_diffs), axis=0)
886899

887900
def cdf(self, x, allow_singular=False):
888901
""" evaluate GM CDF at points x"""
@@ -1024,3 +1037,4 @@ def __init__(
10241037
self.state_idxs = np.array(state_idxs)
10251038
self.spectral_direction_only = spectral_direction_only
10261039
self.variance_preserving = variance_preserving
1040+

tests/test_gm.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pyest.sensors import ConvexPolyhedralFieldOfView
99
from pyest.utils import fail
1010
import pytest
11+
from pyest.metrics import max_covariance_ratio
1112

1213

1314
def test_gm_equal_weights():
@@ -555,6 +556,57 @@ def test_init_mismatch_fail():
555556

556557
fail(gm.GaussianMixture, ValueError, w, m, P)
557558

559+
def test_cov():
560+
# Initial covariance matrix P0 (6×6)
561+
P0 = np.array([
562+
[2.25e-06, 0, 0, 0, 0, 0],
563+
[0, 2.25e-06, 0, 0, 0, 0],
564+
[0, 0, 2.25e-06, 0, 0, 0],
565+
[0, 0, 0, 1.77777778e-14, 0, 0],
566+
[0, 0, 0, 0, 1.77777778e-14, 0],
567+
[0, 0, 0, 0, 0, 1.77777778e-14]
568+
])
569+
570+
# Initial large mean vector m0
571+
m0 = np.array([
572+
-3.95911227e+05, -1.80422801e+05, -8.03243134e+04,
573+
5.48813474e-01, -1.02480997e+00, -5.93612009e-01
574+
])
575+
576+
# Single-component GMM (weight 1)
577+
weights = np.array([1])
578+
p0 = gm.GaussianMixture(weights, m0, P0)
579+
580+
# Options for Gaussian splitting
581+
split_opts = gm.GaussSplitOptions(
582+
L=3, # number of components created per split
583+
lam=1e-3, # scaling parameter
584+
recurse_depth=3, # maximum recursion depth
585+
min_weight=-np.inf
586+
)
587+
588+
# No weight threshold
589+
split_tol = -np.inf
590+
591+
# Arguments passed to recursive_split: splitting method + tolerance
592+
recursive_split_args = (gm.split.id_variance, split_tol)
593+
594+
# Apply recursive splitting to the initial GMM
595+
p0 = gm.split.recursive_split(p0, split_opts, *recursive_split_args)
596+
597+
# Cholesky decomposition of the resulting GMM covariance
598+
Schol_gmm = np.atleast_2d(np.linalg.cholesky(p0.cov()))
599+
600+
# Cholesky decomposition of the original covariance
601+
Schol_P0 = np.atleast_2d(np.linalg.cholesky(P0))
602+
603+
# Compute the maximum covariance ratio between the two matrices
604+
max_cov_ratio = abs(max_covariance_ratio(Schol_gmm, Schol_P0))
605+
606+
# Test: the covariance should not increase by more than 0.01%
607+
assert max_cov_ratio <= 1.0001, f"max_cov_ratio is too large: {max_cov_ratio}"
558608

559609
if __name__ == '__main__':
560610
pytest.main([__file__])
611+
612+

0 commit comments

Comments
 (0)