Skip to content

Commit e55479c

Browse files
committed
moved huber_const to the utilities and realized my Huber scale identity in the robustdiff docstring was wrong and that there is a simpler way to write scaled Huber's throughout the repo, which incidentally also helps guard against divide by zeros
1 parent 21b0b5f commit e55479c

3 files changed

Lines changed: 12 additions & 12 deletions

File tree

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from scipy.linalg import expm, sqrtm
44
from scipy.stats import norm
55
from time import time
6-
76
try: import cvxpy
87
except ImportError: pass
98

9+
from pynumdiff.utils.utility import huber_const
10+
1011

1112
def kalman_filter(y, dt_or_t, xhat0, P0, A, Q, C, R, B=None, u=None, save_P=True):
1213
"""Run the forward pass of a Kalman filter, with regular or irregular step size.
@@ -278,7 +279,7 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
278279
proper :math:`\\ell_1` normalization.
279280
280281
Note that :code:`log_q` and :code:`proc_huberM` are coupled, as are :code:`log_r` and :code:`meas_huberM`, via the relation
281-
:math:`\\text{Huber}(q^{-1/2}v, M) = q^{-1}\\text{Huber}(v, Mq^{-1/2})`, but these are still independent enough that for
282+
:math:`\\text{Huber}(q^{-1/2}v, M) = q^{-1}\\text{Huber}(v, Mq^{1/2})`, but these are still independent enough that for
282283
the purposes of optimization we cannot collapse them. Nor can :code:`log_q` and :code:`log_r` be combined into
283284
:code:`log_qr_ratio` as in RTS smoothing without the addition of a new absolute scale parameter, becausee :math:`q` and
284285
:math:`r` interact with the distinct Huber :math:`M` parameters.
@@ -329,11 +330,6 @@ def convex_smooth(y, A, Q, C, R, B=None, u=None, proc_huberM=6, meas_huberM=0):
329330
x_states = cvxpy.Variable((A.shape[0], N)) # each column is [position, velocity, acceleration, ...] at step n
330331
control = isinstance(B, np.ndarray) and isinstance(u, np.ndarray) # whether there is a control input
331332

332-
def huber_const(M): # from https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf, with correction for missing sqrt
333-
a = 2*np.exp(-M**2 / 2)/M # huber_const smoothly transitions Huber between 1-norm and 2-norm squared cases
334-
b = np.sqrt(2*np.pi)*(2*norm.cdf(M) - 1)
335-
return np.sqrt((2*a*(1 + M**2)/M**2 + b)/(a + b))
336-
337333
# It is extremely important to run time that CVXPY expressions be in vectorized form
338334
proc_resids = np.linalg.inv(sqrtm(Q)) @ (x_states[:,1:] - A @ x_states[:,:-1] - (0 if not control else B @ u[1:].T)) # all Q^(-1/2)(x_n - (A x_{n-1} + B u_n))
339335
meas_resids = np.linalg.inv(sqrtm(R)) @ (y.reshape(C.shape[0],-1) - C @ x_states) # all R^(-1/2)(y_n - C x_n)

pynumdiff/utils/evaluate.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,9 @@ def robust_rme(u, v, padding=0, M=6):
9393
"""
9494
if padding == 'auto': padding = max(1, int(0.025*len(u)))
9595
s = slice(padding, len(u)-padding) # slice out data we want to measure
96-
N = s.stop - s.start
9796

9897
sigma = stats.median_abs_deviation(u[s] - v[s], scale='normal') # M is in units of this robust scatter metric
99-
if sigma < 1e-6: sigma = 1 # guard against divide by zero
100-
return np.sqrt(2*np.mean(utility.huber((u[s] - v[s])/sigma, M))) * sigma
98+
return np.sqrt(2*np.mean(utility.huber(u[s] - v[s], M*sigma)))
10199

102100

103101
def rmse(u, v, padding=0):

pynumdiff/utils/utility.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ def huber(x, M):
102102
absx = np.abs(x)
103103
return np.where(absx <= M, 0.5*x**2, M*(absx - 0.5*M))
104104

105+
def huber_const(M):
106+
"""Scale that makes :code:`sum(huber())` interpolate :math:`\\sqrt{2}\\|\\cdot\\|_1` and :math:`\\frac{1}{2}\\|\\cdot\\|_2^2`,
107+
from https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf, with correction for missing sqrt"""
108+
a = 2*np.exp(-M**2 / 2)/M
109+
b = np.sqrt(2*np.pi)*(2*norm.cdf(M) - 1)
110+
return np.sqrt((2*a*(1 + M**2)/M**2 + b)/(a + b))
111+
105112

106113
def integrate_dxdt_hat(dxdt_hat, dt_or_t):
107114
"""Wrapper for scipy.integrate.cumulative_trapezoid. Use 0 as first value so lengths match, see #88.
@@ -136,8 +143,7 @@ def estimate_integration_constant(x, x_hat, M=6):
136143
return np.median(x - x_hat) # Solves the L1 distance minimization
137144
else:
138145
sigma = median_abs_deviation(x - x_hat, scale='normal') # M is in units of this robust scatter metric
139-
if sigma < 1e-6: sigma = 1 # guard against divide by zero
140-
return minimize(lambda x0: np.mean(huber((x - (x_hat+x0))/sigma, M)), # fn to minimize in 1st argument
146+
return minimize(lambda x0: np.sum(huber(x - (x_hat+x0), M*sigma)), # fn to minimize in 1st argument
141147
0, method='SLSQP').x[0] # result is a vector, even if initial guess is just a scalar
142148

143149

0 commit comments

Comments
 (0)