11from __future__ import annotations
22
3+ from math import isfinite as python_isfinite
34from numbers import Integral
45
6+ import numpy as np
57import pyrecest .backend
68
79# pylint: disable=no-name-in-module,no-member,redefined-builtin,duplicate-code
2123 zeros ,
2224 zeros_like ,
2325)
26+ from pyrecest .numerics import assert_covariance_matrix
2427from pyrecest .sampling .sigma_points import MerweScaledSigmaPoints
2528
2629from .abstract_extended_object_tracker import AbstractExtendedObjectTracker
@@ -30,6 +33,19 @@ def _pol2cart(phi, radius=1.0):
3033 return radius * stack ((cos (phi ), sin (phi )))
3134
3235
36+ def _ensure_finite_real_array (value , name ):
37+ """Reject non-finite or complex arrays before they enter the RHM recursion."""
38+ try :
39+ host_value = np .asarray (pyrecest .backend .to_numpy (value ))
40+ if np .iscomplexobj (host_value ):
41+ raise ValueError
42+ finite = bool (np .all (np .isfinite (host_value )))
43+ except (TypeError , ValueError , OverflowError , RuntimeError ) as exc :
44+ raise ValueError (f"{ name } must contain only finite real values" ) from exc
45+ if not finite :
46+ raise ValueError (f"{ name } must contain only finite real values" )
47+
48+
3349class FourierRHMTracker (
3450 AbstractExtendedObjectTracker
3551): # pylint: disable=too-many-instance-attributes
@@ -78,10 +94,16 @@ def __init__(
7894 self .n_harmonics = self ._as_integer (n_harmonics , "n_harmonics" , 0 )
7995 self .n_fourier_coefficients = 2 * self .n_harmonics + 1
8096 self .state_dim = self .n_fourier_coefficients + 2
97+ self .covariance_regularization = self ._as_finite_float (
98+ covariance_regularization , "covariance_regularization"
99+ )
100+ if self .covariance_regularization < 0.0 :
101+ raise ValueError ("covariance_regularization must be non-negative" )
81102
82103 if fourier_coefficients is None :
104+ initial_radius = self ._as_finite_float (initial_radius , "initial_radius" )
83105 fourier_coefficients = zeros (self .n_fourier_coefficients )
84- fourier_coefficients [0 ] = 2.0 * float ( initial_radius )
106+ fourier_coefficients [0 ] = 2.0 * initial_radius
85107 self .fourier_coefficients = self ._as_vector (
86108 fourier_coefficients ,
87109 self .n_fourier_coefficients ,
@@ -109,21 +131,19 @@ def __init__(
109131 covariance , self .state_dim , "covariance"
110132 )
111133 self ._validate_positive_definite (
112- self .covariance + covariance_regularization * eye (self .state_dim ),
134+ self .covariance
135+ + self .covariance_regularization * eye (self .state_dim ),
113136 "covariance" ,
114137 )
115138
116- self .scale_mean = float (scale_mean )
117- self .scale_variance = float (scale_variance )
139+ self .scale_mean = self . _as_finite_float (scale_mean , "scale_mean" )
140+ self .scale_variance = self . _as_finite_float (scale_variance , "scale_variance" )
118141 if self .scale_variance < 0.0 :
119142 raise ValueError ("scale_variance must be non-negative" )
120143
121- self .ukf_alpha = float (ukf_alpha )
122- self .ukf_beta = float (ukf_beta )
123- self .ukf_kappa = float (ukf_kappa )
124- self .covariance_regularization = float (covariance_regularization )
125- if self .covariance_regularization < 0.0 :
126- raise ValueError ("covariance_regularization must be non-negative" )
144+ self .ukf_alpha = self ._as_finite_float (ukf_alpha , "ukf_alpha" )
145+ self .ukf_beta = self ._as_finite_float (ukf_beta , "ukf_beta" )
146+ self .ukf_kappa = self ._as_finite_float (ukf_kappa , "ukf_kappa" )
127147
128148 self .latest_pseudo_measurement = None
129149 self .latest_innovation_covariance = None
@@ -159,11 +179,22 @@ def _as_integer(value, name, minimum):
159179 raise ValueError (f"{ name } must be at least { minimum } " )
160180 return integer
161181
182+ @staticmethod
183+ def _as_finite_float (value , name ):
184+ try :
185+ scalar = float (value )
186+ except (TypeError , ValueError , OverflowError ) as exc :
187+ raise ValueError (f"{ name } must be a finite scalar" ) from exc
188+ if not python_isfinite (scalar ):
189+ raise ValueError (f"{ name } must be a finite scalar" )
190+ return scalar
191+
162192 @staticmethod
163193 def _as_vector (value , dim , name ):
164194 vector = array (value ).reshape (- 1 )
165195 if vector .shape != (dim ,):
166196 raise ValueError (f"{ name } must have shape ({ dim } ,)" )
197+ _ensure_finite_real_array (vector , name )
167198 return vector
168199
169200 @classmethod
@@ -177,22 +208,25 @@ def _as_square_matrix(cls, value, dim, name):
177208 matrix = diag (matrix )
178209 if matrix .shape != (dim , dim ):
179210 raise ValueError (f"{ name } must have shape ({ dim } , { dim } )" )
180- return cls . _symmetrize (matrix )
211+ return assert_covariance_matrix (matrix , name = name , dim = dim )
181212
182213 @staticmethod
183214 def _normalize_measurements (measurements ):
184215 measurements = array (measurements )
185216 if measurements .ndim == 1 :
186217 if measurements .shape [0 ] != 2 :
187218 raise ValueError ("A single measurement vector must have shape (2,)" )
188- return reshape (measurements , (2 , 1 ))
189- if measurements .ndim != 2 :
219+ normalized = reshape (measurements , (2 , 1 ))
220+ elif measurements .ndim != 2 :
190221 raise ValueError ("measurements must be a vector or a two-dimensional array" )
191- if measurements .shape [0 ] == 2 :
192- return measurements
193- if measurements .shape [1 ] == 2 :
194- return measurements .T
195- raise ValueError ("measurements must have shape (2, n) or (n, 2)" )
222+ elif measurements .shape [0 ] == 2 :
223+ normalized = measurements
224+ elif measurements .shape [1 ] == 2 :
225+ normalized = measurements .T
226+ else :
227+ raise ValueError ("measurements must have shape (2, n) or (n, 2)" )
228+ _ensure_finite_real_array (normalized , "measurements" )
229+ return normalized
196230
197231 def _state_vector (self ):
198232 return concatenate ([self .fourier_coefficients , self .kinematic_state ])
@@ -243,9 +277,12 @@ def get_contour_points(self, n=100):
243277 def predict_identity (self , sys_noise = None ):
244278 if sys_noise is None :
245279 sys_noise = zeros ((self .state_dim , self .state_dim ))
246- self .covariance = self ._symmetrize (
247- self .covariance
248- + self ._as_square_matrix (sys_noise , self .state_dim , "sys_noise" )
280+ sys_noise = self ._as_square_matrix (sys_noise , self .state_dim , "sys_noise" )
281+ predicted_covariance = self ._symmetrize (self .covariance + sys_noise )
282+ self .covariance = assert_covariance_matrix (
283+ predicted_covariance ,
284+ name = "predicted_covariance" ,
285+ dim = self .state_dim ,
249286 )
250287 if self .log_prior_estimates :
251288 self .store_prior_estimates ()
@@ -258,16 +295,30 @@ def predict_linear(self, system_matrix, sys_noise=None, inputs=None):
258295 raise ValueError (
259296 f"system_matrix must have shape ({ self .state_dim } , { self .state_dim } )"
260297 )
298+ _ensure_finite_real_array (system_matrix , "system_matrix" )
299+
261300 state = system_matrix @ self ._state_vector ()
262301 if inputs is not None :
263302 state = state + self ._as_vector (inputs , self .state_dim , "inputs" )
264- self ._set_state_vector (state )
303+ state = self ._as_vector (state , self .state_dim , "predicted_state" )
304+
265305 if sys_noise is None :
266306 sys_noise = zeros ((self .state_dim , self .state_dim ))
267- self .covariance = self ._symmetrize (
268- system_matrix @ self .covariance @ system_matrix .T
269- + self ._as_square_matrix (sys_noise , self .state_dim , "sys_noise" )
307+ sys_noise = self ._as_square_matrix (sys_noise , self .state_dim , "sys_noise" )
308+ predicted_covariance = self ._symmetrize (
309+ system_matrix @ self .covariance @ system_matrix .T + sys_noise
310+ )
311+ predicted_covariance = assert_covariance_matrix (
312+ predicted_covariance ,
313+ name = "predicted_covariance" ,
314+ dim = self .state_dim ,
270315 )
316+
317+ # Commit only after every failure-prone validation succeeds. In particular,
318+ # invalid process noise must not leave the state advanced while covariance
319+ # remains at the previous time step.
320+ self ._set_state_vector (state )
321+ self .covariance = predicted_covariance
271322 if self .log_prior_estimates :
272323 self .store_prior_estimates ()
273324 if self .log_prior_extents :
@@ -311,7 +362,8 @@ def _update_single(self, measurement, meas_noise_cov, scale_mean, scale_variance
311362 augmented_covariance = linalg .block_diag (self .covariance , noise_covariance )
312363 augmented_dim = augmented_mean .shape [0 ]
313364 augmented_covariance = self ._symmetrize (
314- augmented_covariance + self .covariance_regularization * eye (augmented_dim )
365+ augmented_covariance
366+ + self .covariance_regularization * eye (augmented_dim )
315367 )
316368
317369 sigma_points = MerweScaledSigmaPoints (
@@ -378,6 +430,8 @@ def update(
378430 scale_mean = self .scale_mean
379431 if scale_variance is None :
380432 scale_variance = self .scale_variance
433+ scale_mean = self ._as_finite_float (scale_mean , "scale_mean" )
434+ scale_variance = self ._as_finite_float (scale_variance , "scale_variance" )
381435 if scale_variance < 0.0 :
382436 raise ValueError ("scale_variance must be non-negative" )
383437
@@ -386,8 +440,8 @@ def update(
386440 self ._update_single (
387441 measurements [:, measurement_index ],
388442 meas_noise_cov ,
389- float ( scale_mean ) ,
390- float ( scale_variance ) ,
443+ scale_mean ,
444+ scale_variance ,
391445 )
392446
393447 if self .log_posterior_estimates :
0 commit comments