c4dynamics 2.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c4dynamics/__init__.py +240 -0
- c4dynamics/datasets/__init__.py +95 -0
- c4dynamics/datasets/_manager.py +596 -0
- c4dynamics/datasets/_registry.py +80 -0
- c4dynamics/detectors/__init__.py +37 -0
- c4dynamics/detectors/yolo3_opencv.py +686 -0
- c4dynamics/detectors/yolo3_tf.py +124 -0
- c4dynamics/eqm/__init__.py +324 -0
- c4dynamics/eqm/derivs.py +212 -0
- c4dynamics/eqm/integrate.py +359 -0
- c4dynamics/filters/__init__.py +1373 -0
- c4dynamics/filters/a.py +48 -0
- c4dynamics/filters/ekf.py +320 -0
- c4dynamics/filters/kalman.py +725 -0
- c4dynamics/filters/kalman_v0.py +1071 -0
- c4dynamics/filters/kalman_v1.py +821 -0
- c4dynamics/filters/lowpass.py +123 -0
- c4dynamics/filters/luenberger.py +97 -0
- c4dynamics/rotmat/__init__.py +141 -0
- c4dynamics/rotmat/animate.py +465 -0
- c4dynamics/rotmat/rotmat.py +351 -0
- c4dynamics/sensors/__init__.py +72 -0
- c4dynamics/sensors/lineofsight.py +78 -0
- c4dynamics/sensors/radar.py +740 -0
- c4dynamics/sensors/seeker.py +1030 -0
- c4dynamics/states/__init__.py +327 -0
- c4dynamics/states/lib/__init__.py +320 -0
- c4dynamics/states/lib/datapoint.py +660 -0
- c4dynamics/states/lib/pixelpoint.py +776 -0
- c4dynamics/states/lib/rigidbody.py +677 -0
- c4dynamics/states/state.py +1486 -0
- c4dynamics/utils/__init__.py +44 -0
- c4dynamics/utils/_struct.py +6 -0
- c4dynamics/utils/const.py +130 -0
- c4dynamics/utils/cprint.py +80 -0
- c4dynamics/utils/gen_gif.py +142 -0
- c4dynamics/utils/idx2keys.py +4 -0
- c4dynamics/utils/images_loader.py +63 -0
- c4dynamics/utils/math.py +136 -0
- c4dynamics/utils/plottools.py +140 -0
- c4dynamics/utils/plottracks.py +304 -0
- c4dynamics/utils/printpts.py +36 -0
- c4dynamics/utils/slides_gen.py +64 -0
- c4dynamics/utils/tictoc.py +167 -0
- c4dynamics/utils/video_gen.py +300 -0
- c4dynamics/utils/vidgen.py +182 -0
- c4dynamics-2.0.3.dist-info/METADATA +242 -0
- c4dynamics-2.0.3.dist-info/RECORD +50 -0
- c4dynamics-2.0.3.dist-info/WHEEL +5 -0
- c4dynamics-2.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
|
|
3
|
+
from scipy.linalg import solve_discrete_are
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.append('.')
|
|
7
|
+
import c4dynamics as c4d
|
|
8
|
+
import numpy as np
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _noncontwarning(x):
|
|
13
|
+
warnings.warn(f"""The system is not continuous. \nDid you mean {x}?""" , c4d.c4warn)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class kalman(c4d.state):
|
|
17
|
+
'''
|
|
18
|
+
Kalman Filter.
|
|
19
|
+
|
|
20
|
+
Kalman filter class for state estimation.
|
|
21
|
+
:class:`kalman` provides methods for prediction and update
|
|
22
|
+
phases of the Kalman filter, including both discrete and continuous systems.
|
|
23
|
+
|
|
24
|
+
For background material, implementation, and examples,
|
|
25
|
+
please refer to :mod:`filters <c4dynamics.filters>`.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
==========
|
|
31
|
+
X : dict
|
|
32
|
+
Initial state variables and their values.
|
|
33
|
+
dt : float, optional
|
|
34
|
+
Time step to advance the process model. The time interval between two predictions.
|
|
35
|
+
Mandatory if continuous-time matrices are provided.
|
|
36
|
+
P0 : numpy.ndarray, optional
|
|
37
|
+
Covariance matrix, or standard deviations array, of the
|
|
38
|
+
initial estimation error. Mandatory if `steadystate` is False.
|
|
39
|
+
If P0 is one-dimensional array, standard deviation values are
|
|
40
|
+
expected. Otherwise, variance values are expected.
|
|
41
|
+
steadystate : bool, optional
|
|
42
|
+
Flag to indicate if the filter is in steady-state mode. Defaults to False.
|
|
43
|
+
A : numpy.ndarray, optional
|
|
44
|
+
Continuous-time state transition matrix. Defaults to None.
|
|
45
|
+
B : numpy.ndarray, optional
|
|
46
|
+
Continuous-time control matrix. Defaults to None.
|
|
47
|
+
C : numpy.ndarray, optional
|
|
48
|
+
Continuous-time measurement matrix. Defaults to None.
|
|
49
|
+
Q : numpy.ndarray, optional
|
|
50
|
+
Process noise covariance matrix. Defaults to None.
|
|
51
|
+
R : numpy.ndarray, optional
|
|
52
|
+
Measurement noise covariance matrix. Defaults to None.
|
|
53
|
+
F : numpy.ndarray, optional
|
|
54
|
+
Discrete-time state transition matrix. Defaults to None.
|
|
55
|
+
G : numpy.ndarray, optional
|
|
56
|
+
Discrete-time control matrix. Defaults to None.
|
|
57
|
+
H : numpy.ndarray, optional
|
|
58
|
+
Discrete-time measurement matrix. Defaults to None.
|
|
59
|
+
|
|
60
|
+
Notes
|
|
61
|
+
=====
|
|
62
|
+
1. `kalman` is a subclass of :class:`state <c4dynamics.states.state.state>`,
|
|
63
|
+
as such the variables provided within the parameter `X` form its state variables.
|
|
64
|
+
Hence, `X` is a dictionary of variables and their initial values, for example:
|
|
65
|
+
``X = {'x': x0, 'y': y0, 'z': z0}``.
|
|
66
|
+
|
|
67
|
+
2. The filter may be initialized with either continuous-time matrices
|
|
68
|
+
or with discrete-time matrices.
|
|
69
|
+
However, all the necessary parameters,
|
|
70
|
+
i.e. `A` and `B` (for continuous system) or `F` and `G`
|
|
71
|
+
(for discrete system) must be provided consistently.
|
|
72
|
+
|
|
73
|
+
3. If continuous-time matrices are provided, then a time step parameter `dt`
|
|
74
|
+
has to be provided for the advance of the process model between two predictions.
|
|
75
|
+
|
|
76
|
+
4. Steady-state mode: if the underlying system is linear time-invariant (`LTI`),
|
|
77
|
+
and the noise covariance matrices are time-invariant,
|
|
78
|
+
then a steady-state mode of the Kalman filter can be employed.
|
|
79
|
+
In steady-state mode the Kalman gain (`K`) and the estimation covariance matrix
|
|
80
|
+
(`P`) are computed once and remain constant ('steady-state') for the entire run-time,
|
|
81
|
+
performing as well as the time-varying filter.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
Raises
|
|
86
|
+
======
|
|
87
|
+
TypeError:
|
|
88
|
+
If X is not a dictionary.
|
|
89
|
+
ValueError:
|
|
90
|
+
If P0 is not provided when steadystate is False.
|
|
91
|
+
ValueError:
|
|
92
|
+
If neither continuous nor discrete system matrices are fully provided.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
See Also
|
|
97
|
+
========
|
|
98
|
+
.filters
|
|
99
|
+
.ekf
|
|
100
|
+
.lowpass
|
|
101
|
+
.seeker
|
|
102
|
+
.eqm
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Examples
|
|
108
|
+
========
|
|
109
|
+
|
|
110
|
+
The examples in the introduction to the
|
|
111
|
+
:mod:`filters <c4dynamics.filters>`
|
|
112
|
+
module demonstrate the operations of
|
|
113
|
+
the Kalman filter for inputs from
|
|
114
|
+
electromagnetic devices, such as an altimeter,
|
|
115
|
+
which measures the altitude.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
An accurate Japaneese train travels 150 meters in one second
|
|
121
|
+
(:math:`F = 1, u = 1, B = 150, Q = 0.05`).
|
|
122
|
+
A sensor measures the train position with noise
|
|
123
|
+
variance of :math:`200m^2` (:math:`H = 1, R = 200`).
|
|
124
|
+
The initial position of the train is known with uncertainty
|
|
125
|
+
of :math:`0.5m` (:math:`P0 = 0.5^2`).
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
**Note**
|
|
129
|
+
|
|
130
|
+
The system may be interpreted as follows:
|
|
131
|
+
|
|
132
|
+
- :math:`F = 1` - constant position
|
|
133
|
+
|
|
134
|
+
- :math:`u = 1, B = 150` - constant velocity control input
|
|
135
|
+
|
|
136
|
+
The advantage of this model is in its being first order.
|
|
137
|
+
However, a slight difference between the actual dynamics and
|
|
138
|
+
the modeled process will result in a lag with the tracked object.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Import required packages:
|
|
143
|
+
|
|
144
|
+
.. code::
|
|
145
|
+
|
|
146
|
+
>>> from c4dynamics.filters import kalman
|
|
147
|
+
>>> from matplotlib import pyplot as plt
|
|
148
|
+
>>> import c4dynamics as c4d
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
Let's run a filter.
|
|
152
|
+
|
|
153
|
+
First, since the covariance matrices are
|
|
154
|
+
constant we can utilize the steady state mode of the filter.
|
|
155
|
+
This requires initalization with the respective flag:
|
|
156
|
+
|
|
157
|
+
.. code::
|
|
158
|
+
|
|
159
|
+
>>> v = 150
|
|
160
|
+
>>> sensor_noise = 200
|
|
161
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, G = v, H = 1
|
|
162
|
+
... , Q = 0.05, R = sensor_noise**2, steadystate = True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
This initialization uses the discrete form of the model.
|
|
166
|
+
Alternatively, the filter can be initialized with continuous matrices.
|
|
167
|
+
The only requirement for using continuous form is to provide
|
|
168
|
+
the time step at the initialization stage:
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
.. code::
|
|
172
|
+
|
|
173
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, A = 0, B = v, C = 1, Q = 0.05
|
|
174
|
+
... , R = sensor_noise**2, steadystate = True, dt = 1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
.. code::
|
|
178
|
+
|
|
179
|
+
>>> for t in range(1, 26): # seconds.
|
|
180
|
+
... # store for later
|
|
181
|
+
... kf.store(t)
|
|
182
|
+
... # predict + correct
|
|
183
|
+
... kf.predict(u = 1)
|
|
184
|
+
... kf.detect = v * t + np.random.randn() * sensor_noise
|
|
185
|
+
... kf.storeparams('detect', t)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Recall that a :class:`kalman` object, as subclass of
|
|
189
|
+
the :class:`state <c4dynamics.states.state.state>`,
|
|
190
|
+
encapsulates the process state vector:
|
|
191
|
+
|
|
192
|
+
.. code::
|
|
193
|
+
|
|
194
|
+
>>> print(kf)
|
|
195
|
+
[ x ]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
It can also employ the
|
|
199
|
+
:meth:`plot <c4dynamics.states.state.state.plot>`
|
|
200
|
+
or any other method of the `state` class:
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
.. code::
|
|
204
|
+
|
|
205
|
+
>>> kf.plot('x')
|
|
206
|
+
<Axes: ...>
|
|
207
|
+
>>> plt.gca().plot(*kf.data('detect'), 'co', label = 'detection') # doctest: +ELLIPSIS
|
|
208
|
+
[<matplotlib.lines.Line2D ...>]
|
|
209
|
+
>>> plt.gca().legend()
|
|
210
|
+
<matplotlib.legend.Legend ...>
|
|
211
|
+
>>> plt.show()
|
|
212
|
+
|
|
213
|
+
.. figure:: /_example/kf/steadystate.png
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
Let's now assume that as the
|
|
217
|
+
train moves farther from the station,
|
|
218
|
+
the sensor measurements degrade.
|
|
219
|
+
|
|
220
|
+
The measurement covariance matrix therefore increases accordingly,
|
|
221
|
+
and the steady state mode cannot be used:
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
.. code::
|
|
225
|
+
|
|
226
|
+
>>> v = 150
|
|
227
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5*2, F = 1, G = v, H = 1, Q = 0.05)
|
|
228
|
+
>>> for t in range(1, 26): # seconds.
|
|
229
|
+
... kf.store(t)
|
|
230
|
+
... sensor_noise = 200 + 8 * t
|
|
231
|
+
... kf.predict(u = 1)
|
|
232
|
+
... kf.detect = v * t + np.random.randn() * sensor_noise
|
|
233
|
+
... kf.K = kf.update(kf.detect, R = sensor_noise**2)
|
|
234
|
+
... kf.storeparams('detect', t)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
.. figure:: /_example/kf/varying_r.png
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
'''
|
|
242
|
+
# TODO maybe change 'time histories' with 'time series' or 'time evolution'
|
|
243
|
+
|
|
244
|
+
Kinf = None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def __init__(self, X: dict, dt: Optional[float] = None, P0: Optional[np.ndarray] = None, steadystate: bool = False
|
|
248
|
+
, A: Optional[np.ndarray] = None, B: Optional[np.ndarray] = None, C: Optional[np.ndarray] = None
|
|
249
|
+
, F: Optional[np.ndarray] = None, G: Optional[np.ndarray] = None, H: Optional[np.ndarray] = None
|
|
250
|
+
, Q: Optional[np.ndarray] = None, R: Optional[np.ndarray] = None):
|
|
251
|
+
#
|
|
252
|
+
# P0 is mandatory and it is either the initial state covariance matrix itself or
|
|
253
|
+
# a vector of the diagonal standard deviations.
|
|
254
|
+
# dt is for the predict integration.
|
|
255
|
+
# F and H are linear transition matrix and linear measure matrix for
|
|
256
|
+
# a linear kalman filter.
|
|
257
|
+
# Q and R are process noise and measure noise matrices when they are time invariant.
|
|
258
|
+
##
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if not isinstance(X, dict):
|
|
263
|
+
raise TypeError("""X must be a dictionary containig pairs of variables
|
|
264
|
+
and initial conditions, e.g.: {''x'': 0, ''y'': 0}""")
|
|
265
|
+
super().__init__(**X)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# initialize cont or discrete system
|
|
269
|
+
self.isdiscrete = True
|
|
270
|
+
#
|
|
271
|
+
# verify shapes consistency:
|
|
272
|
+
# x = Fx + Gu + w
|
|
273
|
+
# y = Hx + v
|
|
274
|
+
# X: nx1, F: nxn, G: nxm, u: mx1, y: 1xk, H: kxn
|
|
275
|
+
# P: nxn, Q: nxn, R: kxk
|
|
276
|
+
# state matrices should be 2dim array.
|
|
277
|
+
##
|
|
278
|
+
def vershape(M1name, M1rows, M2name, M2columns):
|
|
279
|
+
if M1rows.shape[0] != M2columns.shape[1]:
|
|
280
|
+
raise ValueError(f"The columns of {M2name} ({M2columns}) must equal """
|
|
281
|
+
f"the rows of {M1name} ({M1rows}).")
|
|
282
|
+
|
|
283
|
+
self.G = None
|
|
284
|
+
if A is not None and C is not None:
|
|
285
|
+
# continuous system
|
|
286
|
+
#
|
|
287
|
+
self.isdiscrete = False
|
|
288
|
+
if dt is None:
|
|
289
|
+
raise ValueError("""dt is necessary for a continuous system""")
|
|
290
|
+
|
|
291
|
+
self.dt = dt
|
|
292
|
+
#
|
|
293
|
+
|
|
294
|
+
self.F = np.atleast_2d(A * dt).astype(float)
|
|
295
|
+
vershape('A', self.F, 'A', self.F) # A: nxn
|
|
296
|
+
vershape('X', self.X.T, 'A', self.F) # A: n columns
|
|
297
|
+
self.F += np.eye(len(self.F)) # F = I + A*dt
|
|
298
|
+
|
|
299
|
+
self.H = np.atleast_2d(C)
|
|
300
|
+
vershape('X', self.X.T, 'C', self.H) # C: n columns
|
|
301
|
+
if B is not None:
|
|
302
|
+
self.G = np.atleast_2d(B * dt).reshape(self.F.shape[0], -1) # now G is necessarily a column vector.
|
|
303
|
+
# self.G = np.atleast_2d(B) * dt
|
|
304
|
+
# self.G = self.G.reshape(self.F.shape[0], -1) # now G is necessarily a column vector.
|
|
305
|
+
# vershape('G (or B)', self.G, 'F (or A)', self.F) # G: n rows. this one is useless because previous reshpae guarentees is has the correct size.
|
|
306
|
+
|
|
307
|
+
elif F is not None and H is not None:
|
|
308
|
+
# discrete
|
|
309
|
+
self.F = np.atleast_2d(F).astype(float)
|
|
310
|
+
vershape('F', self.F, 'F', self.F) # F: nxn
|
|
311
|
+
vershape('X', self.X.T, 'A', self.F) # F: n columns
|
|
312
|
+
|
|
313
|
+
self.H = np.atleast_2d(H)
|
|
314
|
+
vershape('X', self.X.T, 'H', self.H) # H: n columns
|
|
315
|
+
|
|
316
|
+
if G is not None:
|
|
317
|
+
self.G = np.atleast_2d(G).reshape(self.F.shape[0], -1) # now G is necessarily a column vector.
|
|
318
|
+
|
|
319
|
+
else:
|
|
320
|
+
raise ValueError("""At least one set of matrices has to be provided entirely:"""
|
|
321
|
+
"""\nFor a continuous system: A, C (B is optional). Where: x' = A*x + B*u + w, y = C*x + v"""
|
|
322
|
+
"""\nFor a dicscrete system: F, H (G is optional). Where: x(k) = F*x(k-1) + G*u(k-1) + w(k-1), y(k) = H*x(k) + v(k)""")
|
|
323
|
+
|
|
324
|
+
self.Q = None
|
|
325
|
+
self.R = None
|
|
326
|
+
if Q is not None:
|
|
327
|
+
self.Q = np.atleast_2d(Q)
|
|
328
|
+
vershape('Q', self.Q, 'Q', self.Q) # Q: nxn
|
|
329
|
+
vershape('X', self.X.T, 'Q', self.Q) # Q: n columns
|
|
330
|
+
if R is not None:
|
|
331
|
+
self.R = np.atleast_2d(R)
|
|
332
|
+
vershape('R', self.R, 'R', self.R) # R: kxk
|
|
333
|
+
vershape('H' if self.isdiscrete else 'H', self.H, 'R', self.R) # R: k columns
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if steadystate:
|
|
337
|
+
# in steady state mode Q and R must be provided:
|
|
338
|
+
if self.Q is None or self.R is None:
|
|
339
|
+
raise ValueError("""In steady-state mode, the noise matrices Q and R must be provided.""")
|
|
340
|
+
|
|
341
|
+
self.P = solve_discrete_are(self.F.T, self.H.T, self.Q, self.R)
|
|
342
|
+
self.Kinf = self.P @ self.H.T @ np.linalg.inv(self.H @ self.P @ self.H.T + self.R)
|
|
343
|
+
|
|
344
|
+
else: # steady state is off
|
|
345
|
+
if P0 is None:
|
|
346
|
+
# NOTE maybe init with zeros and raising warning is better solution.
|
|
347
|
+
raise ValueError(r'P0 must be provided (optional only in steadystate mode)')
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if np.array(P0).ndim == 1:
|
|
351
|
+
# an array of standard deviations is provided
|
|
352
|
+
self.P = np.diag(np.array(P0).ravel()**2)
|
|
353
|
+
else:
|
|
354
|
+
P0 = np.atleast_2d(P0)
|
|
355
|
+
if P0.shape[0] == P0.shape[1]:
|
|
356
|
+
# square matrix
|
|
357
|
+
self.P = P0
|
|
358
|
+
|
|
359
|
+
self._Pdata = []
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def A(self):
|
|
365
|
+
if self.isdiscrete:
|
|
366
|
+
_noncontwarning('F')
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
a = (self.F - np.eye(len(self.F))) / self.dt
|
|
370
|
+
return a
|
|
371
|
+
|
|
372
|
+
@A.setter
|
|
373
|
+
def A(self, a):
|
|
374
|
+
if self.isdiscrete:
|
|
375
|
+
_noncontwarning('F')
|
|
376
|
+
else:
|
|
377
|
+
self.F = np.eye(len(a)) + a * self.dt
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def B(self):
|
|
382
|
+
if self.isdiscrete:
|
|
383
|
+
_noncontwarning('G') # the system is not continuous. did u mean G?
|
|
384
|
+
return None
|
|
385
|
+
elif self.G is None:
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
return self.G / self.dt
|
|
389
|
+
|
|
390
|
+
@B.setter
|
|
391
|
+
def B(self, b):
|
|
392
|
+
if self.isdiscrete:
|
|
393
|
+
_noncontwarning('G')
|
|
394
|
+
else:
|
|
395
|
+
self.G = b * self.dt
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def C(self):
|
|
400
|
+
if self.isdiscrete:
|
|
401
|
+
_noncontwarning('H')
|
|
402
|
+
return None
|
|
403
|
+
return self.H
|
|
404
|
+
|
|
405
|
+
@C.setter
|
|
406
|
+
def C(self, c):
|
|
407
|
+
if self.isdiscrete:
|
|
408
|
+
_noncontwarning('H')
|
|
409
|
+
else:
|
|
410
|
+
self.H = c
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def predict(self, u: Optional[np.ndarray] = None, Q: Optional[np.ndarray] = None):
|
|
414
|
+
'''
|
|
415
|
+
Predicts the filter's next state and covariance matrix based
|
|
416
|
+
on the current state and the process model.
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
u : numpy.ndarray, optional
|
|
421
|
+
Control input. Defaults to None.
|
|
422
|
+
Q : numpy.ndarray, optional
|
|
423
|
+
Process noise covariance matrix. Defaults to None.
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
Raises
|
|
427
|
+
------
|
|
428
|
+
ValueError
|
|
429
|
+
If `Q` is not missing (neither provided
|
|
430
|
+
during construction nor passed to `predict`).
|
|
431
|
+
ValueError
|
|
432
|
+
If a control input is provided, but the number of elements in `u`
|
|
433
|
+
does not match the number of columns of the input matrix (`B` or `G`).
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
Examples
|
|
437
|
+
--------
|
|
438
|
+
For more detailed usage,
|
|
439
|
+
see the examples in the introduction to
|
|
440
|
+
the :mod:`c4dynamics.filters` module and
|
|
441
|
+
the :class:`kalman` class.
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
Import required packages:
|
|
447
|
+
|
|
448
|
+
.. code::
|
|
449
|
+
|
|
450
|
+
>>> from c4dynamics.filters import kalman
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
Plain `predict` step
|
|
455
|
+
(predict in steady-state mode where the process variance matrix
|
|
456
|
+
remains constant
|
|
457
|
+
and is provided once to initialize the filter):
|
|
458
|
+
|
|
459
|
+
.. code::
|
|
460
|
+
|
|
461
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, H = 1, Q = 0.05, R = 200, steadystate = True)
|
|
462
|
+
>>> print(kf)
|
|
463
|
+
[ x ]
|
|
464
|
+
>>> kf.X
|
|
465
|
+
array([0.])
|
|
466
|
+
>>> kf.P
|
|
467
|
+
array([[3.18...]])
|
|
468
|
+
>>> kf.predict()
|
|
469
|
+
>>> kf.X
|
|
470
|
+
array([0.])
|
|
471
|
+
>>> kf.P
|
|
472
|
+
array([[3.18...]])
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
Predict with control input:
|
|
476
|
+
|
|
477
|
+
.. code::
|
|
478
|
+
|
|
479
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, G = 150, H = 1, R = 200, Q = 0.05)
|
|
480
|
+
>>> kf.X
|
|
481
|
+
array([0.])
|
|
482
|
+
>>> kf.P
|
|
483
|
+
array([[0.25...]])
|
|
484
|
+
>>> kf.predict(u = 1)
|
|
485
|
+
>>> kf.X
|
|
486
|
+
array([150.])
|
|
487
|
+
>>> kf.P
|
|
488
|
+
array([[0.3]])
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
Predict with updated process noise covariance matrix:
|
|
493
|
+
|
|
494
|
+
.. code::
|
|
495
|
+
|
|
496
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, G = 150, H = 1, R = 200, Q = 0.05)
|
|
497
|
+
>>> kf.X
|
|
498
|
+
array([0.])
|
|
499
|
+
>>> kf.P
|
|
500
|
+
array([[0.25]])
|
|
501
|
+
>>> kf.predict(u = 1, Q = 0.01)
|
|
502
|
+
>>> kf.X
|
|
503
|
+
array([150.])
|
|
504
|
+
>>> kf.P
|
|
505
|
+
array([[0.26]])
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
'''
|
|
509
|
+
# TODO test the size of the objects.
|
|
510
|
+
# test the type.
|
|
511
|
+
# make sure the user is working with c4d modules.
|
|
512
|
+
# actually only u should be tested here all the other need be tested at the init stage.
|
|
513
|
+
# this F must be linear, but it can be linearized once for the entire
|
|
514
|
+
# process (regular kalman), or linearized and delivered at each step (extended kalman)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
if self.Kinf is None:
|
|
518
|
+
|
|
519
|
+
if Q is not None:
|
|
520
|
+
self.Q = np.atleast_2d(Q)
|
|
521
|
+
elif self.Q is None:
|
|
522
|
+
raise ValueError("""Q is missing. It can be provided once at construction """
|
|
523
|
+
"""or in every call to predict() """)
|
|
524
|
+
|
|
525
|
+
self.P = self.F @ self.P @ self.F.T + self.Q
|
|
526
|
+
|
|
527
|
+
# this F can be either linear or nonlinear function of x.
|
|
528
|
+
self.X = self.F @ self.X
|
|
529
|
+
|
|
530
|
+
if u is not None:
|
|
531
|
+
if self.G is None:
|
|
532
|
+
warnings.warn(f"""\nWarning: u={u} is introduced as control input but the input matrix is zero! (G for discrete system or B for continuous).""", c4d.c4warn)
|
|
533
|
+
else:
|
|
534
|
+
u = np.atleast_2d(u)
|
|
535
|
+
if len(u.ravel()) != self.G.shape[1]:
|
|
536
|
+
raise ValueError(f"""The number of elements in u must equal the number of columns of the input matrix (B or G), {len(u.ravel())} != {self.G.shape[1]}""")
|
|
537
|
+
self.X += self.G @ u.ravel()
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def update(self, z: np.ndarray, R: Optional[np.ndarray] = None):
|
|
542
|
+
'''
|
|
543
|
+
Updates (corrects) the state estimate based on the given measurements.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
z : numpy.ndarray
|
|
548
|
+
Measurement vector.
|
|
549
|
+
R : numpy.ndarray, optional
|
|
550
|
+
Measurement noise covariance matrix. Defaults to None.
|
|
551
|
+
|
|
552
|
+
Returns
|
|
553
|
+
-------
|
|
554
|
+
K : numpy.ndarray
|
|
555
|
+
Kalman gain.
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
Raises
|
|
559
|
+
------
|
|
560
|
+
ValueError
|
|
561
|
+
If the number of elements in `z` does not match
|
|
562
|
+
the number of rows in the measurement matrix (C or H).
|
|
563
|
+
ValueError
|
|
564
|
+
If `R` is missing (neither provided
|
|
565
|
+
during construction
|
|
566
|
+
nor passed to `update`).
|
|
567
|
+
|
|
568
|
+
Examples
|
|
569
|
+
--------
|
|
570
|
+
For more detailed usage,
|
|
571
|
+
see the examples in the introduction to
|
|
572
|
+
the :mod:`c4dynamics.filters` module and
|
|
573
|
+
the :class:`kalman` class.
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
Import required packages:
|
|
578
|
+
|
|
579
|
+
.. code::
|
|
580
|
+
|
|
581
|
+
>>> from c4dynamics.filters import kalman
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
Plain update step
|
|
586
|
+
(update in steady-state mode
|
|
587
|
+
where the measurement covariance matrix remains
|
|
588
|
+
and is provided once during filter initialization):
|
|
589
|
+
|
|
590
|
+
.. code::
|
|
591
|
+
|
|
592
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, H = 1, Q = 0.05, R = 200, steadystate = True)
|
|
593
|
+
>>> print(kf)
|
|
594
|
+
[ x ]
|
|
595
|
+
>>> kf.X
|
|
596
|
+
array([0.])
|
|
597
|
+
>>> kf.P
|
|
598
|
+
array([[3.18...]])
|
|
599
|
+
>>> kf.update(z = 100) # returns Kalman gain
|
|
600
|
+
array([[0.01568688]])
|
|
601
|
+
>>> kf.X
|
|
602
|
+
array([1.56...])
|
|
603
|
+
>>> kf.P
|
|
604
|
+
array([[3.18...]])
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
Update with modified measurement noise covariance matrix:
|
|
609
|
+
|
|
610
|
+
.. code::
|
|
611
|
+
|
|
612
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, G = 150, H = 1, R = 200, Q = 0.05)
|
|
613
|
+
>>> kf.X
|
|
614
|
+
array([0.])
|
|
615
|
+
>>> kf.P
|
|
616
|
+
array([[0.25]])
|
|
617
|
+
>>> K = kf.update(z = 150, R = 0)
|
|
618
|
+
>>> K
|
|
619
|
+
array([[1.]])
|
|
620
|
+
>>> kf.X
|
|
621
|
+
array([150.])
|
|
622
|
+
>>> kf.P
|
|
623
|
+
array([[0.]])
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
'''
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# this H must be linear, but as F may it be linearized once about an equilibrium point for
|
|
630
|
+
# the entire process (regular kalman) or at each
|
|
631
|
+
# iteration about the current state (ekf).
|
|
632
|
+
# TODO add Mahalanobis optional test
|
|
633
|
+
z = np.atleast_2d(z).ravel()
|
|
634
|
+
if len(z) != self.H.shape[0]:
|
|
635
|
+
raise ValueError(f"""The number of elements in the input z must equal """
|
|
636
|
+
f"""the number of rows of the measurement matrix (C or H), """
|
|
637
|
+
f"""{len(z.ravel())} != {self.H.shape[0]}""")
|
|
638
|
+
|
|
639
|
+
if self.Kinf is None:
|
|
640
|
+
if R is not None:
|
|
641
|
+
self.R = np.atleast_2d(R)
|
|
642
|
+
elif self.R is None:
|
|
643
|
+
raise ValueError("""R is missing. It can be provided once at construction """
|
|
644
|
+
"""or in every call to update() """)
|
|
645
|
+
|
|
646
|
+
K = self.P @ self.H.T @ np.linalg.inv(self.H @ self.P @ self.H.T + self.R)
|
|
647
|
+
self.P = self.P - K @ self.H @ self.P
|
|
648
|
+
else:
|
|
649
|
+
K = self.Kinf
|
|
650
|
+
|
|
651
|
+
# this H can be expressed as either linear or nonlinear function of x.
|
|
652
|
+
self.X += K @ (z - self.H @ self.X) # nx1 = nxm @ (mx1 - mxn @ nx1)
|
|
653
|
+
return K
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def store(self, t: int = -1):
|
|
657
|
+
'''
|
|
658
|
+
Stores the current state and diagonal elements of the covariance matrix.
|
|
659
|
+
|
|
660
|
+
The :meth:`store` method captures the current state of the Kalman filter,
|
|
661
|
+
storing the state vector (`X`) and the error covariance matrix (`P`)
|
|
662
|
+
at the specified time.
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
Parameters
|
|
666
|
+
----------
|
|
667
|
+
t : int, optional
|
|
668
|
+
The current time at which the state is being stored. Defaults to -1.
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
Notes
|
|
672
|
+
-----
|
|
673
|
+
1. The stored data can be accessed via :meth:`data` or other methods for
|
|
674
|
+
post-analysis or visualization.
|
|
675
|
+
2. The elements on the main diagonal of the covariance matrix are named
|
|
676
|
+
according to their position, starting with 'P' followed by their row and column indices.
|
|
677
|
+
For example, the first element is named 'P00', and so on.
|
|
678
|
+
3. See also :meth:`store <c4dynamics.states.state.state.store>`
|
|
679
|
+
and :meth:`data <c4dynamics.states.state.state.data>`
|
|
680
|
+
for more details.
|
|
681
|
+
|
|
682
|
+
Examples
|
|
683
|
+
--------
|
|
684
|
+
For more detailed usage,
|
|
685
|
+
see the examples in the introduction to
|
|
686
|
+
the :mod:`c4dynamics.filters` module and
|
|
687
|
+
the :class:`kalman` class.
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
Import required packages:
|
|
693
|
+
|
|
694
|
+
.. code::
|
|
695
|
+
|
|
696
|
+
>>> from c4dynamics.filters import kalman
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
.. code::
|
|
701
|
+
|
|
702
|
+
>>> kf = kalman({'x': 0}, P0 = 0.5**2, F = 1, H = 1, Q = 0.05, R = 200)
|
|
703
|
+
>>> # store initial conditions
|
|
704
|
+
>>> kf.store()
|
|
705
|
+
>>> kf.predict()
|
|
706
|
+
>>> # store X after prediction
|
|
707
|
+
>>> kf.store()
|
|
708
|
+
>>> kf.update(z = 100)
|
|
709
|
+
array([[0.00149...]])
|
|
710
|
+
>>> # store X after correct
|
|
711
|
+
>>> kf.store()
|
|
712
|
+
|
|
713
|
+
Access stored data:
|
|
714
|
+
|
|
715
|
+
.. code::
|
|
716
|
+
|
|
717
|
+
>>> kf.data('x')[1]
|
|
718
|
+
array([0. , 0. , 0.14977534])
|
|
719
|
+
>>> kf.data('P00')[1]
|
|
720
|
+
array([0.25 , 0.3 , 0.29955067])
|
|
721
|
+
|
|
722
|
+
'''
|
|
723
|
+
|
|
724
|
+
super().store(t) # store state
|
|
725
|
+
# store covariance:
|
|
726
|
+
for i, p in enumerate(np.diag(self.P)):
|
|
727
|
+
pstr = f'P{i}{i}'
|
|
728
|
+
setattr(self, pstr, p) # set
|
|
729
|
+
self.storeparams(pstr, t) # store
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@staticmethod
|
|
733
|
+
def velocitymodel(dt: float, process_noise: float, measure_noise: float):
|
|
734
|
+
'''
|
|
735
|
+
Defines a linear Kalman filter model for tracking position and velocity.
|
|
736
|
+
|
|
737
|
+
Parameters
|
|
738
|
+
----------
|
|
739
|
+
dt : float
|
|
740
|
+
Time step for the system model.
|
|
741
|
+
process_noise : float
|
|
742
|
+
Standard deviation of the process noise.
|
|
743
|
+
measure_noise : float
|
|
744
|
+
Standard deviation of the measurement noise.
|
|
745
|
+
|
|
746
|
+
Returns
|
|
747
|
+
-------
|
|
748
|
+
kf : kalman
|
|
749
|
+
A Kalman filter object initialized with the linear system model.
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
X = [x, y, w, h, vx, vy]
|
|
754
|
+
# 0 1 2 3 4 5
|
|
755
|
+
|
|
756
|
+
x' = vx
|
|
757
|
+
y' = vy
|
|
758
|
+
w' = 0
|
|
759
|
+
h' = 0
|
|
760
|
+
vx' = 0
|
|
761
|
+
vy' = 0
|
|
762
|
+
|
|
763
|
+
H = [1 0 0 0 0 0
|
|
764
|
+
0 1 0 0 0 0
|
|
765
|
+
0 0 1 0 0 0
|
|
766
|
+
0 0 0 1 0 0]
|
|
767
|
+
'''
|
|
768
|
+
from scipy.linalg import expm
|
|
769
|
+
|
|
770
|
+
A = np.zeros((6, 6))
|
|
771
|
+
A[0, 4] = A[1, 5] = 1
|
|
772
|
+
F = expm(A * dt)
|
|
773
|
+
H = np.zeros((4, 6))
|
|
774
|
+
H[0, 0] = H[1, 1] = H[2, 2] = H[3, 3] = 1
|
|
775
|
+
|
|
776
|
+
Q = np.eye(6) * process_noise**2
|
|
777
|
+
R = np.eye(4) * measure_noise**2
|
|
778
|
+
|
|
779
|
+
kf = kalman({'x': 0, 'y': 0, 'w': 0, 'h': 0, 'vx': 0, 'vy': 0}
|
|
780
|
+
, steadystate = True, F = F, H = H, Q = Q, R = R)
|
|
781
|
+
return kf
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@staticmethod
|
|
785
|
+
def nees(kf, true_obj):
|
|
786
|
+
''' normalized estimated error squared '''
|
|
787
|
+
|
|
788
|
+
Ptimes = kf.data('P00')[0]
|
|
789
|
+
err = []
|
|
790
|
+
for t in kf.data('t'):
|
|
791
|
+
|
|
792
|
+
xkf = kf.timestate(t)
|
|
793
|
+
xtrain = true_obj.timestate(t)
|
|
794
|
+
|
|
795
|
+
idx = min(range(len(Ptimes)), key = lambda i: abs(Ptimes[i] - t))
|
|
796
|
+
P = kf.data('P00')[1][idx]
|
|
797
|
+
|
|
798
|
+
xerr = xtrain - xkf
|
|
799
|
+
err.append(xerr**2 / P)
|
|
800
|
+
return np.mean(err)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
if __name__ == "__main__":
|
|
809
|
+
import contextlib
|
|
810
|
+
import doctest
|
|
811
|
+
|
|
812
|
+
doctest.testmod(optionflags = doctest.ELLIPSIS)
|
|
813
|
+
|
|
814
|
+
# # Redirect both stdout and stderr to a file within this block
|
|
815
|
+
# with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
|
|
816
|
+
# with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
|
|
817
|
+
# doctest.testmod()
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
|