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