Skip to content

Commit 6310343

Browse files
committed
fixed convex_smooth to only transition to sum_squares at M=\infty
1 parent e1a7e07 commit 6310343

1 file changed

Lines changed: 26 additions & 32 deletions

File tree

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
264264
return rtsdiff(x, dt, 3, np.log10(q/r), forwardbackward)
265265

266266

267-
def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
267+
def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
268268
"""Perform outlier-robust differentiation by solving the Maximum A Priori optimization problem:
269269
:math:`\\min_{\\{x_n\\}} \\sum_{n=0}^{N-1} V(R^{-1/2}(y_n - C x_n)) + \\sum_{n=1}^{N-1} J(Q^{-1/2}(x_n - A x_{n-1}))`,
270270
where :math:`A,Q,C,R` come from an assumed constant derivative model and :math:`V,J` are the :math:`\\ell_1` norm or Huber
@@ -275,13 +275,12 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
275275
all inputs to Huber are normalized by noise level, :code:`q` or :code:`r`, :code:`M` is in units of standard deviation.
276276
In other words, this choice affects which portion of inputs are treated as outliers. For example, assuming Gaussian inliers,
277277
the portion beyond :math:`M\\sigma` is :code:`outlier_portion = 2*(1 - scipy.stats.norm.cdf(M))`. The inverse of this is
278-
:code:`M = scipy.stats.norm.ppf(1 - outlier_portion/2)`. By :code:`M=6`, Huber is essentially indistinguishable from the
279-
1/2-sum-of-squares case, :math:`\\frac{1}{2}\\|\\cdot\\|_2^2`, because the portion of examples beyond :math:`6\\sigma` is
280-
miniscule, about :code:`1.97e-9`, and the normalization constant of the Huber loss approaches 1 as :math:`M` increases.
281-
(See :math:`c_2` `in section 6 <https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf>`_, missing a :math:`\\sqrt{\\cdot}`
282-
term there, see p2700). Similarly, as :code:`M` approaches 0, Huber reduces to the :math:`\\ell_1` norm case, because
283-
:math:`c_2` approaches :math:`\\frac{\\sqrt{2}}{M}`, cancelling the :math:`M` multiplying :math:`|\\cdot|` and leaving
284-
behind :math:`\\sqrt{2}`, the proper normalization.
278+
:code:`M = scipy.stats.norm.ppf(1 - outlier_portion/2)`. As :math:`M \\to \\infty`, Huber becomes the 1/2-sum-of-squares
279+
case, :math:`\\frac{1}{2}\\|\\cdot\\|_2^2`, and the normalization constant of the Huber loss approaches 1 as :math:`M`
280+
increases. (See :math:`c_2` `in section 6 <https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf>`_, missing a
281+
:math:`\\sqrt{\\cdot}` term there, see p2700). Similarly, as :code:`M` approaches 0, Huber reduces to the :math:`\\ell_1`
282+
norm case, because :math:`c_2` approaches :math:`\\frac{\\sqrt{2}}{M}`, cancelling the :math:`M` multiplying :math:`|\\cdot|`
283+
and leaving behind :math:`\\sqrt{2}`, the proper normalization.
285284
286285
:param np.array[float] x: data series to differentiate
287286
:param float dt: step size
@@ -295,6 +294,9 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
295294
- **x_hat** -- estimated (smoothed) x
296295
- **dxdt_hat** -- estimated derivative of x
297296
"""
297+
#q = 1e4 # I found q too small worsened condition number of the Q matrix, so fixing it at a biggish value
298+
#r = q/qr_ratio
299+
298300
A_c = np.diag(np.ones(order), 1) # continuous-time A just has 1s on the first diagonal (where 0th is main diagonal)
299301
Q_c = np.zeros(A_c.shape); Q_c[-1,-1] = 10**log_q # continuous-time uncertainty around the last derivative
300302
C = np.zeros((1, order+1)); C[0,0] = 1 # we measure only y = noisy x
@@ -310,19 +312,19 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
310312
return x_states[0], x_states[1]
311313

312314

313-
def convex_smooth(y, A, Q, C, R, proc_huberM=6, meas_huberM=1.345):
315+
def convex_smooth(y, A, Q, C, R, proc_huberM, meas_huberM):
314316
"""Solve the optimization problem for robust smoothing using CVXPY. Note this currently assumes constant dt
315-
but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps as
316-
in the :code:`kalman_filter` function.
317+
but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps.
317318
318319
:param np.array[float] y: measurements
319320
:param np.array A: discrete-time state transition matrix
320321
:param np.array Q: discrete-time process noise covariance matrix
321322
:param np.array C: measurement matrix
322323
:param np.array R: measurement noise covariance matrix
323-
:param float huberM: radius where quadratic of Huber loss function turns linear. M=0 reduces to the :math:`\\ell_1` norm.
324+
:param float proc_huberM: Huber loss parameter. :math:`M=0` reduces to :math:`\\sqrt{2}\\ell_1`.
325+
:param float meas_huberM: Huber loss parameter. :math:`M=\\infty` reduces to :math:`\\frac{1}{2}\\ell_2^2`.
324326
325-
:return: np.array -- state estimates (N x state_dim)
327+
:return: np.array -- state estimates (state_dim x N)
326328
"""
327329
N = len(y)
328330
x_states = cvxpy.Variable((A.shape[0], N)) # each column is [position, velocity, acceleration, ...] at step n
@@ -334,27 +336,19 @@ def huber_const(M): # from https://jmlr.org/papers/volume14/aravkin13a/aravkin13
334336

335337
# It is extremely important to run time that CVXPY expressions be in vectorized form
336338
proc_resids = np.linalg.inv(sqrtm(Q)) @ (x_states[:,1:] - A @ x_states[:,:-1]) # all Q^(-1/2)(x_n - A x_{n-1})
337-
meas_resids = np.linalg.inv(sqrtm(R)) @ (y.reshape(1,-1) - C @ x_states) # all R^(-1/2)(y_n - C x_n)
339+
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)
338340
# Process terms: sum of J(proc_resids)
339-
objective = 0.5*cvxpy.sum_squares(proc_resids) if proc_huberM > (6 - 1e-3) \
340-
else np.sqrt(2)*cvxpy.sum(cvxpy.abs(proc_resids)) if proc_huberM < 1e-3 \
341-
else huber_const(proc_huberM)*cvxpy.sum(cvxpy.huber(proc_resids, proc_huberM)) # 1/2 l2^2, l1, or Huber
341+
objective = 0.5*cvxpy.sum_squares(proc_resids) if huberM == float('inf') \
342+
else np.sqrt(2)*cvxpy.sum(cvxpy.abs(proc_resids)) if huberM < 1e-3 \
343+
else huber_const(huberM)*cvxpy.sum(cvxpy.huber(proc_resids, huberM)) # 1/2 l2^2, l1, or Huber
342344
# Measurement terms: sum of V(meas_resids)
343-
objective += 0.5*cvxpy.sum_squares(meas_resids) if meas_huberM > (6 - 1e-3) \
344-
else np.sqrt(2)*cvxpy.sum(cvxpy.abs(meas_resids)) if meas_huberM < 1e-3 \
345-
else huber_const(meas_huberM)*cvxpy.sum(cvxpy.huber(meas_resids, meas_huberM)) # CVXPY quirk: norm(, 1) != sum(abs()) for matrices
346-
345+
objective += 0.5*cvxpy.sum_squares(meas_resids) if huberM == float('inf') \
346+
else np.sqrt(2)*cvxpy.sum(cvxpy.abs(meas_resids)) if huberM < 1e-3 \
347+
else huber_const(huberM)*cvxpy.sum(cvxpy.huber(meas_resids, huberM)) # CVXPY quirk: norm(, 1) != sum(abs()) for matrices
348+
347349
problem = cvxpy.Problem(cvxpy.Minimize(objective))
348-
try:
349-
before = time()
350-
problem.solve(solver=cvxpy.CLARABEL)
351-
print(f"CLARABEL succeeded in {time() - before}s")
352-
except cvxpy.error.SolverError: pass
353-
#print("CLARABEL failed with status {problem.status}. Returning NaNs.")
354-
#return np.full((A.shape[0], N), np.nan)
355-
# problem.solve(solver=cvxpy.SCS) # Alternatively, SCS is a lot slower but pretty bulletproof even with big condition numbers
356-
if x_states.value is None: # There can be solver failure
357-
#print(f"Convex solvers failed with status {problem.status}. Returning NaNs.")
358-
return np.full((A.shape[0], N), np.nan)
350+
try: problem.solve(solver=cvxpy.CLARABEL); print("CLARABEL succeeded")
351+
except cvxpy.error.SolverError: pass # Could try a different solver here, like SCS, but slows things down
359352

353+
if x_states.value is None: return np.full((A.shape[0], N), np.nan) # There can be solver failure, even without exception
360354
return x_states.value

0 commit comments

Comments
 (0)