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,740 @@
1
+ import numpy as np
2
+ # from scipy.special import erfinv
3
+ import sys
4
+ sys.path.append('.')
5
+ import c4dynamics as c4d
6
+ from c4dynamics.sensors.seeker import seeker
7
+ import warnings
8
+ from typing import Optional
9
+
10
+ # np.warnings.filterwarnings('ignore', category = np.VisibleDeprecationWarning)
11
+
12
+ class radar(seeker):
13
+ '''
14
+ Range-direction detector.
15
+
16
+ `radar` is a subclass of :class:`seeker <c4dynamics.sensors.seeker.seeker>`
17
+ and utilizes its functionality and errors model for angular measurements.
18
+ This documentation supplaments the information concerning range measurements.
19
+ Refer to :class:`seeker <c4dynamics.sensors.seeker.seeker>` for the full documentation.
20
+
21
+
22
+ The `radar` class models sensors that
23
+ measure both the range and the direction to a target.
24
+ The direction is measured in terms of azimuth and elevation.
25
+ Sensors that provide precise range measurements
26
+ typically use electro-magnetical technology,
27
+ though other technologies may also be employed.
28
+
29
+ As a subclass of `seeker`, the `radar`
30
+ can operate in one of two modes: ideal mode,
31
+ providing precise range and direction measurements,
32
+ or non-ideal mode,
33
+ where measurements may be affected by errors such as
34
+ `scale factor`, `bias`, and `noise`,
35
+ according to the errors model.
36
+ A random variable generation mechanism allows
37
+ for Monte Carlo simulations.
38
+
39
+
40
+
41
+ Parameters
42
+ ==========
43
+
44
+ origin : :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>`, optional
45
+ A `rigidbody` object whose state vector :attr:`X <c4dynamics.states.state.state.X>`
46
+ determines the radar's initial position and attitude.
47
+ Defaults: a `rigidbody` object with zeros vector, `X = numpy.zeros(12)`.
48
+ isideal : bool, optional
49
+ A flag indicating whether the errors model is off.
50
+ Defaults False.
51
+
52
+
53
+
54
+ Keyword Arguments
55
+ =================
56
+ rng_noise_std : float
57
+ A standard deviation of the radar range. Default value for non-ideal radar: `rng_noise_std = 1m`.
58
+ bias_std : float
59
+ The standard deviation of the bias error, [radians]. Defaults :math:`0.3°`.
60
+ scale_factor_std : float
61
+ The standard deviation of the scale factor error, [dimensionless]. Defaults :math:`0.07 (= 7\\%)`.
62
+ noise_std : float
63
+ The standard deviation of the radar angular noise, [radians].
64
+ Default value for non-ideal radar: :math:`0.8°`.
65
+ dt : float
66
+ The time-constant of the operational rate of the radar
67
+ (below which the radar measures return None), [seconds]. Default value: :math:`dt = -1sec`
68
+ (no limit between calls to :meth:`measure`).
69
+
70
+
71
+ * Note the default values for angular parameters, `bias_std`, `scale_factor_std`,
72
+ and `noise_std`, differ from those in a `seeker` object.
73
+
74
+
75
+
76
+ See Also
77
+ ========
78
+ .filters
79
+ .eqm
80
+ .seeker
81
+
82
+
83
+
84
+
85
+ **Functionality**
86
+
87
+
88
+ At each sample the seeker returns measures based on the true geometry
89
+ with the target.
90
+
91
+ Let the relative coordinates in an arbitrary frame of reference:
92
+
93
+ .. math::
94
+
95
+ dx = target.x - seeker.x
96
+
97
+ dy = target.y - seeker.y
98
+
99
+ dz = target.z - seeker.z
100
+
101
+
102
+ The relative coordinates in the seeker body frame are given by:
103
+
104
+ .. math::
105
+
106
+ x_b = [BR] \\cdot [dx, dy, dz]^T
107
+
108
+ where :math:`[BR]` is a
109
+ Body from Reference DCM (Direction Cosine Matrix)
110
+ formed by the seeker three Euler angles. See the `rigidbody` section below.
111
+
112
+ The azimuth and elevation measures are then the spatial angles:
113
+
114
+ .. math::
115
+
116
+ az = tan^{-1}{x_b[1] \\over x_b[0]}
117
+
118
+ el = tan^{-1}{x_b[2] \\over \\sqrt{x_b[0]^2 + x_b[1]^2}}
119
+
120
+
121
+ Where:
122
+
123
+ - :math:`az` is the azimuth angle
124
+ - :math:`el` is the elevation angle
125
+ - :math:`x_b` is the target-radar position vector in radar body frame
126
+
127
+
128
+ For a `radar` object, the range is defined as:
129
+
130
+ .. math::
131
+
132
+ range = \\sqrt{x_b^2 + y_b^2 + z_b^2}
133
+
134
+ Where:
135
+
136
+ - :math:`range` is the target-radar distance
137
+ - :math:`x_b` is the target-radar position vector in radar body frame
138
+
139
+
140
+ .. figure:: /_architecture/rdr_definitions.svg
141
+
142
+ Fig-1: Range and angles definition
143
+
144
+
145
+ **Errors Model**
146
+
147
+ The azimuth and elevation angles are subject to errors: scale factor, bias, and noise,
148
+ as detailed in `seeker`.
149
+ A `radar` instance has in addition range noise:
150
+
151
+ - ``Range Noise``:
152
+ represents random variations or fluctuations in the measurements
153
+ that are not systematic.
154
+ The noise at each sample (:meth:`measure`)
155
+ is a normally distributed variable
156
+ with `mean = 0` and `std = rng_noise_std`, where `rng_noise_std`
157
+ is a `radar` parameter with default value of `1m`.
158
+
159
+ Angular errors:
160
+
161
+ - ``Bias``:
162
+ represents a constant offset or deviation from the
163
+ true value in the seeker's measurements.
164
+ It is a systematic error that consistently affects the measured values.
165
+ The bias of a `seeker` instance is a normally distributed variable with `mean = 0`
166
+ and `std = bias_std`, where `bias_std` is a parameter with default value of `0.3°`.
167
+ - ``Scale Factor``:
168
+ a multiplier applied to the true value of a measurement.
169
+ It represents a scaling error in the measurements made by the seeker.
170
+ The scale factor of a `seeker` instance is
171
+ a normally distributed variable
172
+ with `mean = 0` and `std = scale_factor_std`,
173
+ , where `scale_factor_std` is a parameter with default value of `0.07`.
174
+ - ``Noise``:
175
+ represents random variations or fluctuations in the measurements
176
+ that are not systematic.
177
+ The noise at each seeker sample (:meth:`measure`)
178
+ is a normally distributed variable
179
+ with `mean = 0` and `std = noise_std`, where `noise_std`
180
+ is a parameter with default value of `0.8°`.
181
+
182
+
183
+
184
+ The errors model generates random variables for each radar instance,
185
+ allowing for the simulation of different scenarios or variations in the radar behavior
186
+ in a technique known as Monte Carlo.
187
+ Monte Carlo simulations leverage this randomness to statistically analyze
188
+ the impact of these biases and scale factors over a large number of iterations,
189
+ providing insights into potential outcomes and system reliability.
190
+
191
+
192
+
193
+ **Radar vs Seeker**
194
+
195
+
196
+ The following table
197
+ lists the main differences between
198
+ :class:`seeker <c4dynamics.sensors.seeker.seeker>` and :class:`radar`
199
+ in terms of measurements and
200
+ default error parameters:
201
+
202
+
203
+
204
+ .. list-table::
205
+ :widths: 22 13 13 13 13 13 13
206
+ :header-rows: 1
207
+
208
+ * -
209
+ - Angles
210
+ - Range
211
+ - :math:`σ_{Bias}`
212
+ - :math:`σ_{Scale Factor}`
213
+ - :math:`σ_{Angular Noise}`
214
+ - :math:`σ_{Range Noise}`
215
+
216
+ * - Seeker
217
+ - ✔️
218
+ - ❌
219
+ - :math:`0.1°`
220
+ - :math:`5%`
221
+ - :math:`0.4°`
222
+ - :math:`--`
223
+
224
+ * - Radar
225
+ - ✔️
226
+ - ✔️
227
+ - :math:`0.3°`
228
+ - :math:`7%`
229
+ - :math:`0.8°`
230
+ - :math:`1m`
231
+
232
+
233
+
234
+
235
+ **rigidbody**
236
+
237
+ The radar class is also a subclass of
238
+ :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>`, i.e.
239
+ it suggests attributes of position and attitude and the manipulation of them.
240
+
241
+ As a fundamental propety, the
242
+ rigidbody's state vector
243
+ :attr:`X <c4dynamics.states.state.state.X>`
244
+ sets the spatial coordinates of the radar:
245
+
246
+ .. math::
247
+
248
+ X = [x, y, z, v_x, v_y, v_z, {\\varphi}, {\\theta}, {\\psi}, p, q, r]^T
249
+
250
+ The first six coordinates determine the translational position and velocity of the radar
251
+ while the last six determine its angular attitude in terms of Euler angles and
252
+ the body rates.
253
+
254
+ Passing a rigidbody parameter as an `origin` sets
255
+ the initial conditions of the radar.
256
+
257
+
258
+
259
+ **Construction**
260
+
261
+ A radar instance is created by making a direct call
262
+ to the radar constructor:
263
+
264
+ .. code::
265
+
266
+ >>> rdr = c4d.sensors.radar()
267
+
268
+ Initialization of the instance does not require any
269
+ mandatory arguments, but the radar parameters can be
270
+ determined using the \\**kwargs argument as detailed above.
271
+
272
+
273
+
274
+
275
+ Examples
276
+ ========
277
+
278
+
279
+ Import required packages:
280
+
281
+ .. code::
282
+
283
+ >>> import c4dynamics as c4d
284
+ >>> from matplotlib import pyplot as plt
285
+ >>> import numpy as np
286
+
287
+
288
+
289
+ **Target**
290
+
291
+
292
+ For the examples below let's generate the trajectory of a target with constant velocity:
293
+
294
+ .. code::
295
+
296
+ >>> tgt = c4d.datapoint(x = 1000, y = 0, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
297
+ >>> for t in np.arange(0, 60, 0.01):
298
+ ... tgt.inteqm(np.zeros(3), .01) # doctest: +IGNORE_OUTPUT
299
+ ... tgt.store(t)
300
+
301
+ The method :meth:`inteqm <c4dynamics.states.lib.datapoint.datapoint.inteqm>`
302
+ of the
303
+ :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>` class
304
+ integrates the 3 degrees of freedom equations of motion with respect to
305
+ the input force vector (`np.zeros(3)` here).
306
+
307
+ .. figure:: /_examples/radar/target.png
308
+
309
+ Since the call to :meth:`measure` requires a target as a `datapoint` object
310
+ we utilize a custom `create` function that returns a new `datapoint` object for
311
+ a given `X` state vector in time.
312
+
313
+ - :code:`c4d.kmh2ms` converts kilometers per hour to meters per second.
314
+ - :code:`c4d.r2d` converts radians to degrees.
315
+ - :code:`c4d.d2r` converts degrees to radians.
316
+
317
+
318
+ **Origin**
319
+
320
+ Let's also introduce a pedestal as an origin for the radar.
321
+ The pedestal is a `rigidbody` object with position and attitude:
322
+
323
+ .. code::
324
+
325
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
326
+
327
+
328
+
329
+
330
+
331
+ **Ideal Radar**
332
+
333
+ Measure the target position with an ideal radar:
334
+
335
+ .. code::
336
+
337
+ >>> rdr_ideal = c4d.sensors.radar(origin = pedestal, isideal = True)
338
+ >>> for x in tgt.data():
339
+ ... rdr_ideal.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
340
+
341
+ Comparing the radar measurements with the true target angles requires
342
+ converting the relative position to the radar body frame:
343
+
344
+ .. code::
345
+
346
+ >>> dx = tgt.data('x')[1] - rdr_ideal.x
347
+ >>> dy = tgt.data('y')[1] - rdr_ideal.y
348
+ >>> dz = tgt.data('z')[1] - rdr_ideal.z
349
+ >>> Xb = np.array([rdr_ideal.BR @ [X[1] - rdr_ideal.x, X[2] - rdr_ideal.y, X[3] - rdr_ideal.z] for X in tgt.data()])
350
+
351
+ where :attr:`rdr_ideal.BR <c4dynamics.states.lib.rigidbody.rigidbody.BR>` is a
352
+ Body from Reference DCM (Direction Cosine Matrix)
353
+ formed by the radar three Euler angles
354
+
355
+ Now `az_true` and `el_true` are the true target angles with respect to the radar, and
356
+ `rng_true` is the true range (`atan2d` is an aliasing of `numpy's arctan2`
357
+ with a modification returning the angles in degrees):
358
+
359
+ .. code::
360
+
361
+ >>> az_true = c4d.atan2d(Xb[:, 1], Xb[:, 0])
362
+ >>> el_true = c4d.atan2d(Xb[:, 2], c4d.sqrt(Xb[:, 0]**2 + Xb[:, 1]**2))
363
+ >>> # plot results
364
+ >>> fig, axs = plt.subplots(2, 1) # doctest: +IGNORE_OUTPUT
365
+ >>> # range
366
+ >>> axs[0].plot(tgt.data('t'), c4d.norm(Xb, axis = 1), label = 'target') # doctest: +IGNORE_OUTPUT
367
+ >>> axs[0].plot(*rdr_ideal.data('range'), label = 'radar') # doctest: +IGNORE_OUTPUT
368
+ >>> # angles
369
+ >>> axs[1].plot(tgt.data('t'), c4d.atan2d(Xb[:, 1], Xb[:, 0]), label = 'target azimuth') # doctest: +IGNORE_OUTPUT
370
+ >>> axs[1].plot(*rdr_ideal.data('az', scale = c4d.r2d), label = 'radar azimuth') # doctest: +IGNORE_OUTPUT
371
+ >>> axs[1].plot(tgt.data('t'), c4d.atan2d(Xb[:, 2], c4d.sqrt(Xb[:, 0]**2 + Xb[:, 1]**2)), label = 'target elevation') # doctest: +IGNORE_OUTPUT
372
+ >>> axs[1].plot(*rdr_ideal.data('el', scale = c4d.r2d), label = 'radar elevation') # doctest: +IGNORE_OUTPUT
373
+
374
+ .. figure:: /_examples/radar/ideal.png
375
+
376
+
377
+
378
+
379
+
380
+ **Non-ideal Radar**
381
+
382
+
383
+ Measure the target position with a *non-ideal* radar.
384
+ The radar's errors model introduces bias, scale factor, and
385
+ noise that corrupt the measurements:
386
+
387
+
388
+ To reproduce the result, let's set the random generator seed (61 is arbitrary):
389
+
390
+ .. code::
391
+
392
+ >>> np.random.seed(61)
393
+
394
+
395
+ .. code::
396
+
397
+ >>> rdr = c4d.sensors.radar(origin = pedestal)
398
+ >>> for x in tgt.data():
399
+ ... rdr.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
400
+
401
+ Results with respect to an ideal radar:
402
+
403
+ .. code::
404
+
405
+ >>> fig, axs = plt.subplots(2, 1)
406
+ >>> # range
407
+ >>> axs[0].plot(*rdr_ideal.data('range'), label = 'target') # doctest: +IGNORE_OUTPUT
408
+ >>> axs[0].plot(*rdr.data('range'), label = 'radar') # doctest: +IGNORE_OUTPUT
409
+ >>> # angles
410
+ >>> axs[1].plot(*rdr_ideal.data('az', scale = c4d.r2d), label = 'target azimuth') # doctest: +IGNORE_OUTPUT
411
+ >>> axs[1].plot(*rdr.data('az', scale = c4d.r2d), label = 'radar azimuth') # doctest: +IGNORE_OUTPUT
412
+ >>> axs[1].plot(*rdr_ideal.data('el', scale = c4d.r2d), label = 'target elevation') # doctest: +IGNORE_OUTPUT
413
+ >>> axs[1].plot(*rdr.data('el', scale = c4d.r2d), label = 'radar elevation') # doctest: +IGNORE_OUTPUT
414
+
415
+ `target` labels mean the true position as measured by an ideal radar.
416
+
417
+ .. figure:: /_examples/radar/nonideal.png
418
+
419
+
420
+ The bias, scale factor, and noise that used to generate these measures
421
+ can be examined by:
422
+
423
+ .. code::
424
+
425
+ >>> rdr.rng_noise_std # doctest: +ELLIPSIS
426
+ 1.0
427
+ >>> rdr.bias * c4d.r2d # doctest: +ELLIPSIS
428
+ 0.13...
429
+ >>> rdr.scale_factor # doctest: +ELLIPSIS
430
+ 0.96...
431
+ >>> rdr.noise_std * c4d.r2d
432
+ 0.8
433
+
434
+ Points to consider here:
435
+
436
+ - The scale factor error increases with the angle, such that for a :math:`7%`
437
+ scale factor,
438
+ the error of :math:`Azimuth = 100°` is :math:`7°`, whereas the error for
439
+ :math:`Elevation = -15°` is only :math:`-1.05°`.
440
+ - The standard deviation of the noise in the two angle channels is the same.
441
+ However, as the `Elevation` values are confined to a smaller range, the effect
442
+ appears more pronounced there.
443
+
444
+
445
+
446
+
447
+ **Rotating Radar**
448
+
449
+
450
+ Measure the target position with a rotating radar.
451
+ The radar origin is yawing (performed by the increment of :math:`\\psi`) in the direction of the target motion:
452
+
453
+
454
+ .. code::
455
+
456
+ >>> rdr = c4d.sensors.radar(origin = pedestal)
457
+ >>> for x in tgt.data():
458
+ ... rdr.psi += .02 * c4d.d2r
459
+ ... rdr.measure(c4d.create(x[1:]), t = x[0], store = True)# doctest: +IGNORE_OUTPUT
460
+ ... rdr.store(x[0])
461
+
462
+ The radar yaw angle:
463
+
464
+ .. figure:: /_examples/radar/psi.png
465
+
466
+ And the target angles with respect to the yawing radar are:
467
+
468
+ .. code::
469
+
470
+ >>> fig, axs = plt.subplots(2, 1)
471
+ >>> # range
472
+ >>> axs[0].plot(*rdr_ideal.data('range'), label = 'ideal static') # doctest: +IGNORE_OUTPUT
473
+ >>> axs[0].plot(*rdr.data('range'),label = 'non-ideal yawing') # doctest: +IGNORE_OUTPUT
474
+ >>> # angles
475
+ >>> axs[1].plot(*rdr_ideal.data('az', c4d.r2d), label = 'az: ideal static') # doctest: +IGNORE_OUTPUT
476
+ >>> axs[1].plot(*rdr.data('az', c4d.r2d), label = 'az: non-ideal yawing') # doctest: +IGNORE_OUTPUT
477
+ >>> axs[1].plot(*rdr_ideal.data('el', scale = c4d.r2d), label = 'el: ideal static') # doctest: +IGNORE_OUTPUT
478
+ >>> axs[1].plot(*rdr.data('el', scale = c4d.r2d), label = 'el: non-ideal yawing') # doctest: +IGNORE_OUTPUT
479
+
480
+ .. figure:: /_examples/radar/yawing.png
481
+
482
+
483
+ - The rotation of the radar with the target direction
484
+ keeps the azimuth angle limited, such that non-rotating radars with limited FOV (field of view)
485
+ would have lost the target.
486
+
487
+
488
+
489
+ **Operation Time**
490
+
491
+ By default, the radar returns measurments for each
492
+ call to :meth:`measure`. However, setting the parameter `dt`
493
+ to a positive number makes `measure` return `None`
494
+ for any `t < last_t + dt`, where `t` is the current measure time,
495
+ `last_t` is the last measurement time, and `dt` is the radar time-constant:
496
+
497
+ .. code::
498
+
499
+ >>> np.random.seed(770)
500
+ >>> tgt = c4d.datapoint(x = 100, y = 100)
501
+ >>> rdr = c4d.sensors.radar(dt = 0.01)
502
+ >>> for t in np.arange(0, .025, .005): # doctest: +ELLIPSIS
503
+ ... print(f'{t}: {rdr.measure(tgt, t = t)}')
504
+ 0.0: (0.7..., 0.01..., 140.1...)
505
+ 0.005: (None, None, None)
506
+ 0.01: (0.72..., -0.04..., 142.1...)
507
+ 0.015: (None, None, None)
508
+ 0.02: (0.72..., -0.003..., 140.4...)
509
+
510
+
511
+ **Random Distribution**
512
+
513
+
514
+ The distribution of normally generated random variables
515
+ is characterized by its bell-shaped curve, which is symmetric about the mean.
516
+ The area under the curve represents probability, with about `68%` of the data
517
+ falling within one standard deviation (1σ) of the mean, `95%` within two,
518
+ and `99.7%` within three,
519
+ making it a useful tool for understanding and predicting data behavior.
520
+
521
+
522
+ In `radar` objects, and `seeker` objects in general,
523
+ the `bias` and `scale factor` vary
524
+ among different instances to allow a realistic simulation
525
+ of performance behavior in a technique known as Monte Carlo.
526
+
527
+ Let's examine the `bias` distribution across
528
+
529
+ mutliple `radar` instances with a default `bias_std = 0.3°`
530
+ in comparison to `seeker` instances with a default `bias_std = 0.1°`:
531
+
532
+
533
+
534
+
535
+
536
+
537
+
538
+
539
+ .. code::
540
+
541
+ >>> from c4dynamics.sensors import seeker, radar
542
+ >>> seekers = []
543
+ >>> radars = []
544
+ >>> for i in range(1000):
545
+ ... seekers.append(seeker().bias * c4d.r2d)
546
+ ... radars.append(radar().bias * c4d.r2d)
547
+
548
+
549
+ The histogram highlights the broadening of the distribution
550
+ as the standard deviation increases:
551
+
552
+
553
+ >>> ax = plt.subplot()
554
+ >>> ax.hist(seekers, 30, label = 'Seekers') # doctest: +IGNORE_OUTPUT
555
+ >>> ax.hist(radars, 30, label = 'Radars') # doctest: +IGNORE_OUTPUT
556
+
557
+ .. figure:: /_examples/radar/bias2.png
558
+
559
+ '''
560
+
561
+ rng_noise_std = 0.0
562
+
563
+ def __init__(self, origin = None, isideal = False, **kwargs):
564
+
565
+ kwargs['radar'] = True
566
+ super().__init__(origin = origin, isideal = isideal, **kwargs)
567
+ self.rng_noise_std = kwargs.pop('rng_noise_std', 1.0)
568
+ self.range = 0.0
569
+
570
+ if isideal:
571
+ self.rng_noise_std = 0.0
572
+
573
+
574
+
575
+
576
+
577
+ def measure(self, target: 'c4d.state', t: float = -1, store: bool = False) -> tuple[Optional[float], Optional[float], Optional[float]]: # type: ignore
578
+ '''
579
+ Measures range, azimuth and elevation between the radar and a `target`.
580
+
581
+
582
+ If the radar time-constant `dt` was provided when the `radar` was created,
583
+ then `measure` returns `None` for any `t < last_t + dt`,
584
+ where `t` is the time input, `last_t` is the last measurement time,
585
+ and `dt` is the radar time-constant.
586
+ Default behavior, returns measurements for each call.
587
+
588
+ If `store = True`, the method stores the measured
589
+ azimuth and elevation along with a timestamp
590
+ (`t = -1` by default, if not provided otherwise).
591
+
592
+
593
+ Parameters
594
+ ----------
595
+ target : state
596
+ A Cartesian state object to measure by the radar, including at least one position coordinate (x, y, z).
597
+ store : bool, optional
598
+ A flag indicating whether to store the measured values. Defaults `False`.
599
+ t : float, optional
600
+ Timestamp [seconds]. Defaults -1.
601
+
602
+ Returns
603
+ -------
604
+ out : tuple
605
+ range [meters, float], azimuth and elevation, [radians, float].
606
+
607
+
608
+ Raises
609
+ ------
610
+ ValueError
611
+ If `target` doesn't include any position coordinate (x, y, z).
612
+
613
+
614
+ Example
615
+ -------
616
+
617
+ `measure` in a program simulating
618
+ real-time tracking of a constant velcoity target.
619
+
620
+
621
+ The target is represented by a :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>`
622
+ and is simulated using the :mod:`eqm <c4dynamics.eqm>` module,
623
+ which integrating the point-mass equations of motion.
624
+
625
+ An ideal radar uses as reference to the true position of the target.
626
+
627
+ At each cycle, the the radars take measurements and store the samples for
628
+ later use in plotting the results.
629
+
630
+
631
+ Import required packages:
632
+
633
+ .. code::
634
+
635
+ >>> import c4dynamics as c4d
636
+ >>> from matplotlib import pyplot as plt
637
+ >>> import numpy as np
638
+
639
+
640
+
641
+ Settings and initial conditions:
642
+
643
+ .. code::
644
+
645
+ >>> dt = 0.01
646
+ >>> np.random.seed(321)
647
+ >>> tgt = c4d.datapoint(x = 1000, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
648
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
649
+ >>> rdr = c4d.sensors.radar(origin = pedestal, dt = 0.05)
650
+ >>> rdr_ideal = c4d.sensors.radar(origin = pedestal, isideal = True)
651
+
652
+ Main loop:
653
+
654
+ .. code::
655
+
656
+ >>> for t in np.arange(0, 60, dt):
657
+ ... tgt.inteqm(np.zeros(3), dt) # doctest: +IGNORE_OUTPUT
658
+ ... rdr_ideal.measure(tgt, t = t, store = True) # doctest: +IGNORE_OUTPUT
659
+ ... rdr.measure(tgt, t = t, store = True) # doctest: +IGNORE_OUTPUT
660
+ ... tgt.store(t)
661
+
662
+ Before viewing the results, let's examine the error parameters generated by
663
+ the errors model (`c4d.r2d` converts radians to degrees):
664
+
665
+ .. code::
666
+
667
+ >>> rdr.rng_noise_std
668
+ 1.0
669
+ >>> rdr.bias * c4d.r2d # doctest: +ELLIPSIS
670
+ 0.49...
671
+ >>> rdr.scale_factor # doctest: +ELLIPSIS
672
+ 1.01...
673
+ >>> rdr.noise_std * c4d.r2d
674
+ 0.8
675
+
676
+ Then we excpect a constant bias of -0.02° and a 'compression' or 'squeeze' of `5%` with
677
+ respect to the target (as represented by the ideal radar):
678
+
679
+ .. code::
680
+
681
+ >>> _, axs = plt.subplots(2, 1) # doctest: +IGNORE_OUTPUT
682
+ >>> # range
683
+ >>> axs[0].plot(*rdr_ideal.data('range'), '.m', label = 'target') # doctest: +IGNORE_OUTPUT
684
+ >>> axs[0].plot(*rdr.data('range'), '.c', label = 'radar') # doctest: +IGNORE_OUTPUT
685
+ >>> # angles
686
+ >>> axs[1].plot(*rdr_ideal.data('az', scale = c4d.r2d), '.m', label = 'target') # doctest: +IGNORE_OUTPUT
687
+ >>> axs[1].plot(*rdr.data('az', scale = c4d.r2d), '.c', label = 'radar') # doctest: +IGNORE_OUTPUT
688
+
689
+ .. figure:: /_examples/radar/measure.png
690
+
691
+ The sample rate of the radar was set by the parameter `dt = 0.05`.
692
+ In cycles that don't satisfy `t < last_t + dt`, `measure` returns None,
693
+ as shown in a close-up view:
694
+
695
+ .. figure:: /_examples/radar/measure_zoom.png
696
+
697
+ '''
698
+
699
+ az, _ = super().measure(target, t = t, store = False)
700
+
701
+ if az == None: # elapsed time is not enough
702
+ return None, None, None
703
+
704
+ self.range = self.P(target) + self.rng_noise_std * np.random.randn()
705
+
706
+
707
+ if store:
708
+ self.storeparams(['az', 'el', 'range'], t = t)
709
+
710
+ return self.az, self.el, self.range
711
+
712
+
713
+
714
+
715
+ if __name__ == "__main__":
716
+
717
+ import doctest, contextlib, os
718
+ from c4dynamics import IgnoreOutputChecker, cprint
719
+
720
+ # Register the custom OutputChecker
721
+ doctest.OutputChecker = IgnoreOutputChecker
722
+
723
+ tofile = False
724
+ optionflags = doctest.FAIL_FAST
725
+
726
+ if tofile:
727
+ with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
728
+ with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
729
+ result = doctest.testmod(optionflags = optionflags)
730
+ else:
731
+ result = doctest.testmod(optionflags = optionflags)
732
+
733
+ if result.failed == 0:
734
+ cprint(os.path.basename(__file__) + ": all tests passed!", 'g')
735
+ else:
736
+ print(f"{result.failed}")
737
+
738
+
739
+
740
+