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.
Files changed (50) hide show
  1. c4dynamics/__init__.py +240 -0
  2. c4dynamics/datasets/__init__.py +95 -0
  3. c4dynamics/datasets/_manager.py +596 -0
  4. c4dynamics/datasets/_registry.py +80 -0
  5. c4dynamics/detectors/__init__.py +37 -0
  6. c4dynamics/detectors/yolo3_opencv.py +686 -0
  7. c4dynamics/detectors/yolo3_tf.py +124 -0
  8. c4dynamics/eqm/__init__.py +324 -0
  9. c4dynamics/eqm/derivs.py +212 -0
  10. c4dynamics/eqm/integrate.py +359 -0
  11. c4dynamics/filters/__init__.py +1373 -0
  12. c4dynamics/filters/a.py +48 -0
  13. c4dynamics/filters/ekf.py +320 -0
  14. c4dynamics/filters/kalman.py +725 -0
  15. c4dynamics/filters/kalman_v0.py +1071 -0
  16. c4dynamics/filters/kalman_v1.py +821 -0
  17. c4dynamics/filters/lowpass.py +123 -0
  18. c4dynamics/filters/luenberger.py +97 -0
  19. c4dynamics/rotmat/__init__.py +141 -0
  20. c4dynamics/rotmat/animate.py +465 -0
  21. c4dynamics/rotmat/rotmat.py +351 -0
  22. c4dynamics/sensors/__init__.py +72 -0
  23. c4dynamics/sensors/lineofsight.py +78 -0
  24. c4dynamics/sensors/radar.py +740 -0
  25. c4dynamics/sensors/seeker.py +1030 -0
  26. c4dynamics/states/__init__.py +327 -0
  27. c4dynamics/states/lib/__init__.py +320 -0
  28. c4dynamics/states/lib/datapoint.py +660 -0
  29. c4dynamics/states/lib/pixelpoint.py +776 -0
  30. c4dynamics/states/lib/rigidbody.py +677 -0
  31. c4dynamics/states/state.py +1486 -0
  32. c4dynamics/utils/__init__.py +44 -0
  33. c4dynamics/utils/_struct.py +6 -0
  34. c4dynamics/utils/const.py +130 -0
  35. c4dynamics/utils/cprint.py +80 -0
  36. c4dynamics/utils/gen_gif.py +142 -0
  37. c4dynamics/utils/idx2keys.py +4 -0
  38. c4dynamics/utils/images_loader.py +63 -0
  39. c4dynamics/utils/math.py +136 -0
  40. c4dynamics/utils/plottools.py +140 -0
  41. c4dynamics/utils/plottracks.py +304 -0
  42. c4dynamics/utils/printpts.py +36 -0
  43. c4dynamics/utils/slides_gen.py +64 -0
  44. c4dynamics/utils/tictoc.py +167 -0
  45. c4dynamics/utils/video_gen.py +300 -0
  46. c4dynamics/utils/vidgen.py +182 -0
  47. c4dynamics-2.0.3.dist-info/METADATA +242 -0
  48. c4dynamics-2.0.3.dist-info/RECORD +50 -0
  49. c4dynamics-2.0.3.dist-info/WHEEL +5 -0
  50. 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
+