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,1030 @@
1
+ import numpy as np
2
+ # from scipy.special import erfinv
3
+ import sys
4
+ sys.path.append('.')
5
+ import c4dynamics as c4d
6
+ import warnings
7
+ from typing import Optional
8
+
9
+ class seeker(c4d.rigidbody):
10
+ '''
11
+ Direction seeker.
12
+
13
+ The :class:`seeker` class models sensors that
14
+ measure the direction to a target in terms of azimuth and elevation.
15
+ Such sensors typically use electro-optical or laser technologies,
16
+ though other technologies may also be used.
17
+
18
+ A `seeker` object can operate in an ideal mode,
19
+ providing precise direction measurements.
20
+ Alternatively, in a non-ideal mode,
21
+ the measurements may be affected by errors such as
22
+ `scale factor`, `bias`, and `noise`,
23
+ as defined by the errors model.
24
+ A random variable generation mechanism allows
25
+ for Monte Carlo simulations.
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+ Parameters
36
+ ==========
37
+ origin : :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>`, optional
38
+ A `rigidbody` object whose state vector :attr:`X <c4dynamics.states.state.state.X>` determines the seeker's initial position and attitude.
39
+ Defaults: a `rigidbody` object with zeros vector, `X = numpy.zeros(12)`.
40
+ isideal : bool, optional
41
+ A flag indicating whether the errors model is off.
42
+ Defaults False.
43
+
44
+
45
+ Keyword Arguments
46
+ =================
47
+ bias_std : float
48
+ The standard deviation of the bias error, [radians]. Defaults :math:`0.1°`.
49
+ scale_factor_std : float
50
+ The standard deviation of the scale factor error, [dimensionless]. Defaults :math:`0.05 (= 5\\%)`.
51
+ noise_std : float
52
+ The standard deviation of the seeker angular noise, [radians].
53
+ Default value for non-ideal seeker: :math:`0.4°`.
54
+ dt : float
55
+ The time-constant of the operational rate of the seeker
56
+ (below which the seeker measures return None), [seconds]. Default value: :math:`dt = -1sec`
57
+ (no limit between calls to :meth:`measure`).
58
+
59
+
60
+ See Also
61
+ ========
62
+ .filters
63
+ .eqm
64
+ .radar
65
+
66
+
67
+
68
+ **Functionality**
69
+
70
+ At each sample the seeker returns measures based on the true geometry
71
+ with the target.
72
+
73
+ Let the relative coordinates in an arbitrary frame of reference:
74
+
75
+ .. math::
76
+
77
+ dx = target.x - seeker.x
78
+
79
+ dy = target.y - seeker.y
80
+
81
+ dz = target.z - seeker.z
82
+
83
+
84
+ The relative coordinates in the seeker body frame are given by:
85
+
86
+ .. math::
87
+
88
+ x_b = [BR] \\cdot [dx, dy, dz]^T
89
+
90
+ where :math:`[BR]` is a
91
+ Body from Reference DCM (Direction Cosine Matrix)
92
+ formed by the seeker three Euler angles. See the `rigidbody` section below.
93
+
94
+ The azimuth and elevation measures are then the spatial angles:
95
+
96
+ .. math::
97
+
98
+ az = tan^{-1}{x_b[1] \\over x_b[0]}
99
+
100
+ el = tan^{-1}{x_b[2] \\over \\sqrt{x_b[0]^2 + x_b[1]^2}}
101
+
102
+
103
+
104
+ Where:
105
+
106
+ - :math:`az` is the azimuth angle
107
+ - :math:`el` is the elevation angle
108
+ - :math:`x_b` is the target-radar position vector in radar body frame
109
+
110
+ .. figure:: /_architecture/skr_definitions.svg
111
+
112
+ Fig-1: Azimuth and elevation angles definition
113
+
114
+
115
+
116
+ **Errors Model**
117
+
118
+ The azimuth and elevation angles are subject to errors: scale factor, bias, and noise.
119
+
120
+ - ``Bias``:
121
+ represents a constant offset or deviation from the
122
+ true value in the seeker's measurements.
123
+ It is a systematic error that consistently affects the measured values.
124
+ The bias of a `seeker` instance is a normally distributed variable with `mean = 0`
125
+ and `std = bias_std`, where `bias_std` is a parameter with default value of `0.1°`.
126
+ - ``Scale Factor``:
127
+ a multiplier applied to the true value of a measurement.
128
+ It represents a scaling error in the measurements made by the seeker.
129
+ The scale factor of a `seeker` instance is
130
+ a normally distributed variable
131
+ with `mean = 0` and `std = scale_factor_std`,
132
+ , where `scale_factor_std` is a parameter with default value of `0.05`.
133
+ - ``Noise``:
134
+ represents random variations or fluctuations in the measurements
135
+ that are not systematic.
136
+ The noise at each seeker sample (:meth:`measure`)
137
+ is a normally distributed variable
138
+ with `mean = 0` and `std = noise_std`, where `noise_std`
139
+ is a parameter with default value of `0.4°`.
140
+
141
+
142
+ The errors model generates random variables for each seeker instance,
143
+ allowing for the simulation of different scenarios or variations in the seeker behavior
144
+ in a technique known as Monte Carlo.
145
+ Monte Carlo simulations leverage this randomness to statistically analyze
146
+ the impact of these biases and scale factors over a large number of iterations,
147
+ providing insights into potential outcomes and system reliability.
148
+
149
+
150
+
151
+
152
+
153
+
154
+ The errors model can be disabled by applying `isideal = True` at the seeker construction stage.
155
+
156
+ The erros model operates as follows: the particular parameters
157
+ that form each error, for example the standard deviation of the bias error,
158
+ may be determined at the stage of creating the sensor object:
159
+ :code:`seeker = c4d.sensors.seeker(bias_std = 0.1 * c4d.d2r)`,
160
+ where `c4d.d2r` is a conversion from degrees to radians.
161
+ Then, the errors model generates a normal random variable
162
+ and establishes the seeker bias error.
163
+ However, the user can override the generated bias by
164
+ using the :attr:`bias` property and determine the seeker bias error:
165
+ :code:`seeker.bias = 1 * c4d.d2r`, to have a 1° bias error.
166
+
167
+
168
+
169
+
170
+ **rigidbody**
171
+
172
+ The seeker class is a subclass of :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>`, i.e.
173
+ it suggests attributes of position and attitude and the manipulation of them.
174
+
175
+ As a fundamental propety, the
176
+ rigidbody's state vector
177
+ :attr:`X <c4dynamics.states.state.state.X>`
178
+ sets the spatial coordinates of the seeker:
179
+
180
+ .. math::
181
+
182
+ X = [x, y, z, v_x, v_y, v_z, {\\varphi}, {\\theta}, {\\psi}, p, q, r]^T
183
+
184
+ The first six coordinates determine the translational position and velocity of the seeker
185
+ while the last six determine its angular attitude in terms of Euler angles and
186
+ the body rates.
187
+
188
+ Passing a rigidbody parameter as an `origin` sets
189
+ the initial conditions of the seeker.
190
+
191
+
192
+
193
+ **Construction**
194
+
195
+ A seeker instance is created by making a direct call
196
+ to the seeker constructor:
197
+
198
+ .. code::
199
+
200
+ >>> skr = c4d.sensors.seeker()
201
+
202
+ Initialization of the instance does not require any
203
+ mandatory arguments, but the seeker parameters can be
204
+ determined using the \\**kwargs argument as detailed above.
205
+
206
+
207
+
208
+
209
+ Examples
210
+ ========
211
+
212
+ Import required packages:
213
+
214
+ .. code::
215
+
216
+ >>> import c4dynamics as c4d
217
+ >>> from matplotlib import pyplot as plt
218
+ >>> import numpy as np
219
+
220
+
221
+ **Target**
222
+
223
+
224
+ For the examples below let's generate the trajectory of a target with constant velocity:
225
+
226
+ .. code::
227
+
228
+ >>> tgt = c4d.datapoint(x = 1000, y = 0, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
229
+ >>> for t in np.arange(0, 60, 0.01):
230
+ ... tgt.inteqm(np.zeros(3), .01) # doctest: +IGNORE_OUTPUT
231
+ ... tgt.store(t)
232
+
233
+ The method :meth:`inteqm <c4dynamics.states.lib.datapoint.datapoint.inteqm>`
234
+ of the
235
+ :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>` class
236
+ integrates the 3 degrees of freedom equations of motion with respect to
237
+ the input force vector (`np.zeros(3)` here).
238
+
239
+ .. figure:: /_examples/seeker/target.png
240
+
241
+ Since the call to :meth:`measure` requires a target as a `datapoint` object
242
+ we utilize a custom `create` function that returns a new `datapoint` object for
243
+ a given `X` state vector in time.
244
+
245
+ - :code:`c4d.kmh2ms` converts kilometers per hour to meters per second.
246
+ - :code:`c4d.r2d` converts radians to degrees.
247
+ - :code:`c4d.d2r` converts degrees to radians.
248
+
249
+
250
+ **Origin**
251
+
252
+ Let's also introduce a pedestal as an origin for the seeker.
253
+ The pedestal is a `rigidbody` object with position and attitude:
254
+
255
+ .. code::
256
+
257
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
258
+
259
+
260
+
261
+ **Ideal Seeker**
262
+
263
+ Measure the target position with an ideal seeker:
264
+
265
+ .. code::
266
+
267
+ >>> skr_ideal = c4d.sensors.seeker(origin = pedestal, isideal = True)
268
+ >>> for x in tgt.data():
269
+ ... skr_ideal.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
270
+
271
+ Comparing the seeker measurements with the true target angles requires
272
+ converting the relative position to the seeker body frame:
273
+
274
+ .. code::
275
+
276
+ >>> dx = tgt.data('x')[1] - skr_ideal.x
277
+ >>> dy = tgt.data('y')[1] - skr_ideal.y
278
+ >>> dz = tgt.data('z')[1] - skr_ideal.z
279
+ >>> Xb = np.array([skr_ideal.BR @ [X[1] - skr_ideal.x, X[2] - skr_ideal.y, X[3] - skr_ideal.z] for X in tgt.data()])
280
+
281
+ where :attr:`skr_ideal.BR <c4dynamics.states.lib.rigidbody.rigidbody.BR>` is a
282
+ Body from Reference DCM (Direction Cosine Matrix)
283
+ formed by the seeker three Euler angles
284
+
285
+ Now `az_true` is the true target azimuth angle with respect to the seeker, and
286
+ `el_true` is the true elevation angle (`atan2d` is an aliasing of `numpy's arctan2`
287
+ with a modification returning the angles in degrees):
288
+
289
+ .. code::
290
+
291
+ >>> az_true = c4d.atan2d(Xb[:, 1], Xb[:, 0])
292
+ >>> el_true = c4d.atan2d(Xb[:, 2], c4d.sqrt(Xb[:, 0]**2 + Xb[:, 1]**2))
293
+ >>> # plot results
294
+ >>> fig, axs = plt.subplots(2, 1) # doctest: +IGNORE_OUTPUT
295
+ >>> axs[0].plot(tgt.data('t'), az_true, label = 'target') # doctest: +IGNORE_OUTPUT
296
+ >>> axs[0].plot(*skr_ideal.data('az', scale = c4d.r2d), label = 'seeker') # doctest: +IGNORE_OUTPUT
297
+ >>> axs[1].plot(tgt.data('t'), el_true) # doctest: +IGNORE_OUTPUT
298
+ >>> axs[1].plot(*skr_ideal.data('el', scale = c4d.r2d)) # doctest: +IGNORE_OUTPUT
299
+
300
+ .. figure:: /_examples/seeker/ideal.png
301
+
302
+
303
+ **Non-ideal Seeker**
304
+
305
+
306
+ Measure the target position with a *non-ideal* seeker.
307
+ The seeker's errors model introduces bias, scale factor, and
308
+ noise that corrupt the measurements.
309
+
310
+ To reproduce the result, let's set the random generator seed (42 is arbitrary):
311
+
312
+ .. code::
313
+
314
+ >>> np.random.seed(42)
315
+
316
+ .. code::
317
+
318
+ >>> skr = c4d.sensors.seeker(origin = pedestal)
319
+ >>> for x in tgt.data():
320
+ ... skr.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
321
+
322
+
323
+ Results with respect to an ideal seeker:
324
+
325
+ .. code::
326
+
327
+ >>> fig, axs = plt.subplots(2, 1)
328
+ >>> axs[0].plot(*skr_ideal.data('az', scale = c4d.r2d), label = 'ideal') # doctest: +IGNORE_OUTPUT
329
+ >>> axs[0].plot(*skr.data('az', scale = c4d.r2d), label = 'non-ideal') # doctest: +IGNORE_OUTPUT
330
+ >>> axs[1].plot(*skr_ideal.data('el', scale = c4d.r2d)) # doctest: +IGNORE_OUTPUT
331
+ >>> axs[1].plot(*skr.data('el', scale = c4d.r2d)) # doctest: +IGNORE_OUTPUT
332
+
333
+ .. figure:: /_examples/seeker/nonideal.png
334
+
335
+
336
+ The bias, scale factor, and noise that used to generate these measures
337
+ can be examined by:
338
+
339
+ .. code::
340
+
341
+ >>> skr.bias * c4d.r2d # doctest: +ELLIPSIS
342
+ -0.01...
343
+ >>> skr.scale_factor # doctest: +ELLIPSIS
344
+ 1.02...
345
+ >>> skr.noise_std * c4d.r2d
346
+ 0.4
347
+
348
+ Points to consider:
349
+
350
+ - The scale factor error increases with the angle, such that for a :math:`5%`
351
+ scale factor,
352
+ the error of :math:`Azimuth = 100°` is :math:`5°`, whereas the error for
353
+ :math:`Elevation = -15°` is only :math:`-0.75°`.
354
+ - The standard deviation of the noise in the two channels is the same.
355
+ However, as the `Elevation` values are confined to a smaller range, the effect
356
+ appears more pronounced there.
357
+
358
+
359
+ **Rotating Seeker**
360
+
361
+
362
+ Measure the target position with a rotating seeker.
363
+ The seeker origin is yawing (performed by the increment of :math:`\\psi`) in the direction of the target motion:
364
+
365
+
366
+ .. code::
367
+
368
+ >>> skr = c4d.sensors.seeker(origin = pedestal)
369
+ >>> for x in tgt.data():
370
+ ... skr.psi += .02 * c4d.d2r
371
+ ... skr.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
372
+ ... skr.store(x[0])
373
+
374
+ The seeker yaw angle:
375
+
376
+ .. figure:: /_examples/seeker/psi.png
377
+
378
+ And the target angles with respect to the yawing seeker are:
379
+
380
+ .. code::
381
+
382
+ >>> fig, axs = plt.subplots(2, 1)
383
+ >>> axs[0].plot(*skr_ideal.data('az', c4d.r2d), label = 'ideal static seeker') # doctest: +IGNORE_OUTPUT
384
+ >>> axs[0].plot(*skr.data('az', c4d.r2d), label = 'non-ideal yawing seeker') # doctest: +IGNORE_OUTPUT
385
+ >>> axs[1].plot(*skr_ideal.data('el', scale = c4d.r2d)) # doctest: +IGNORE_OUTPUT
386
+ >>> axs[1].plot(*skr.data('el', scale = c4d.r2d)) # doctest: +IGNORE_OUTPUT
387
+
388
+ .. figure:: /_examples/seeker/yawing.png
389
+
390
+
391
+ - The rotation of the seeker with the target direction
392
+ keeps the azimuth angle limited, such that non-rotating seekers with limited FOV (field of view)
393
+ would have lost the target.
394
+
395
+
396
+
397
+ **Operation Time**
398
+
399
+ By default, the seeker returns measurments for each
400
+ call to :meth:`measure`. However, setting the parameter `dt`
401
+ to a positive number makes `measure` return `None`
402
+ for any `t < last_t + dt`, where `t` is the current measure time,
403
+ `last_t` is the last measurement time, and `dt` is the seeker time-constant:
404
+
405
+ .. code::
406
+
407
+ >>> np.random.seed(770)
408
+ >>> tgt = c4d.datapoint(x = 100, y = 100)
409
+ >>> skr = c4d.sensors.seeker(dt = 0.01)
410
+ >>> for t in np.arange(0, .025, .005): # doctest: +ELLIPSIS
411
+ ... print(f'{t}: {skr.measure(tgt, t = t)}')
412
+ 0.0: (0.73..., 0.007...)
413
+ 0.005: (None, None)
414
+ 0.01: (0.73..., 0.0006...)
415
+ 0.015: (None, None)
416
+ 0.02: (0.71..., 0.006...)
417
+
418
+
419
+ **Random Distribution**
420
+
421
+ The distribution of normally generated random variables across mutliple seeker instances
422
+ is shown for biases of two groups of seekers:
423
+
424
+ - One group generated with a default `bias_std` of `0.1°`.
425
+ - The second group with `bias_std` of 0.5°.
426
+
427
+ .. code::
428
+
429
+ >>> from c4dynamics.sensors import seeker
430
+ >>> seekers_type_A = []
431
+ >>> seekers_type_B = []
432
+ >>> B_std = 0.5 * c4d.d2r
433
+ >>> for i in range(1000):
434
+ ... seekers_type_A.append(seeker().bias * c4d.r2d)
435
+ ... seekers_type_B.append(seeker(bias_std = B_std).bias * c4d.r2d)
436
+
437
+ The histogram highlights the broadening of the distribution
438
+ as the standard deviation increases:
439
+
440
+ .. code::
441
+
442
+ >>> ax = plt.subplot()
443
+ >>> ax.hist(seekers_type_A, 30, label = 'Type A') # doctest: +IGNORE_OUTPUT
444
+ >>> ax.hist(seekers_type_B, 30, label = 'Type B') # doctest: +IGNORE_OUTPUT
445
+
446
+ .. figure:: /_examples/seeker/bias2.png
447
+
448
+
449
+
450
+
451
+ '''
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+ _scale_factor = 1.0
462
+ ''' float; The scale factor error of the seeker angels.
463
+ It is a normally distributed random variable with
464
+ standard deviation scale_factor_std.
465
+ When isideal seeker is configured scale_factor = 1.
466
+ '''
467
+
468
+ # scale_factor_std = 0.05
469
+ # ''' float; A standard deviation of the scale factor error '''
470
+
471
+
472
+ _bias = 0.0
473
+ ''' float; The bias error of the seeker angels.
474
+ It is a normally distributed random variable with
475
+ standard deviation bias_std
476
+ When isideal seeker is configured bias = 0.
477
+ '''
478
+
479
+ # bias_std = 0.1 * c4d.d2r
480
+ # ''' float; A standard deviation of the bias error. Defaults 0.1° '''
481
+
482
+ # noise_std = 0.4 * c4d.d2r
483
+ # ''' float; A standard deviation of the seeker angular noise. Default value for non-ideal
484
+ # seeker: noise_std = 0.4° '''
485
+
486
+ # dt = -1 # np.finfo(np.float64).eps
487
+ # ''' float; The time-constant of the operational rate of the seeker ''' # . default machine epsilon for float64
488
+
489
+ _lastsample = -np.inf
490
+ #
491
+
492
+ # rng_noise_std = 0
493
+
494
+ # def __init__(self, origin = None, isideal = False, **kwargs):
495
+ def __init__(self, origin: Optional['c4d.rigidbody'] = None, isideal: bool = False, **kwargs):
496
+ # A flag indicating whether to run the errors model
497
+ # Initializes the Seeker object.
498
+ # Args:
499
+ # isideal (bool): A flag indicating whether to run the errors model.
500
+ # TODO
501
+ # 1 limit field of view.
502
+
503
+ isradar = kwargs.pop('radar', False)
504
+
505
+ bias_std_def = 0.3 if isradar else .1
506
+ noise_std_def = 0.8 if isradar else .4
507
+ scale_factor_std_def = 0.07 if isradar else .05
508
+
509
+ # self.__dict__.update(kwargs)
510
+ self.dt = kwargs.pop('dt', -1)
511
+ # self.rng_noise_std = kwargs.pop('rng_noise_std', 0)
512
+ self.bias_std = kwargs.pop('bias_std', bias_std_def * c4d.d2r)
513
+ self.noise_std = kwargs.pop('noise_std', noise_std_def * c4d.d2r)
514
+ self.scale_factor_std = kwargs.pop('scale_factor_std', scale_factor_std_def)
515
+
516
+ for k in kwargs.keys():
517
+ if k == 'rng_noise_std':
518
+ if not isradar:
519
+ # c4d.cprint(f'Warning: {k} is not an attribute of seeker', 'r')
520
+ warnings.warn(f"""Warning: {k} is not an attribute of seeker""" , c4d.c4warn)
521
+
522
+ continue
523
+
524
+ # c4d.cprint(f'Warning: {k} is not an attribute of seeker or radar', 'r')
525
+ warnings.warn(f"""Warning: {k} is not an attribute of seeker or radar""" , c4d.c4warn)
526
+
527
+
528
+ super().__init__()
529
+
530
+ self.measure_data = []
531
+
532
+ self.az = 0
533
+ self.el = 0
534
+
535
+ if origin is not None:
536
+ if not isinstance(origin, c4d.rigidbody):
537
+ raise TypeError('origin must be a c4dynamics.rigidbody object whose state vector origin.X represents the seeker initial position and attitude initial conditions')
538
+
539
+ self.X = origin.X
540
+
541
+
542
+ if isideal:
543
+ self.noise_std = 0
544
+ else:
545
+ self._errors_model()
546
+
547
+
548
+
549
+ @property
550
+ def bias(self) -> float:
551
+ '''
552
+ Gets and sets the object's bias.
553
+
554
+
555
+ The bias is a random variable generated once at the stage of constructing the
556
+ instance by the errors model:
557
+
558
+ .. math::
559
+
560
+ bias = std \\cdot randn
561
+
562
+ Where `bias_std` is a parameter with default
563
+ value of `0.1°` for :class:`seeker <c4dynamics.sensors.seeker.seeker>` object, and `0.3°` for
564
+ :class:`radar <c4dynamics.sensors.radar.radar>` object.
565
+
566
+
567
+ To get the bias generated by the errors model, or to override it, the user may call
568
+ :attr:`bias` to get or set the final bias error.
569
+
570
+
571
+ Parameters
572
+ ----------
573
+ bias : float
574
+ Required bias, [radians].
575
+
576
+ Returns
577
+ -------
578
+ bias : float
579
+ Current bias, [radians].
580
+
581
+
582
+ Example
583
+ -------
584
+
585
+ The following example for a `seeker` object is directly applicable
586
+ to a `radar` object too.
587
+ Simply replace :code:`c4d.sensors.seeker(origin = pedestal,...)`
588
+ with :code:`c4d.sensors.radar(origin = pedestal,...)`.
589
+
590
+
591
+ Required packages:
592
+
593
+
594
+ .. code::
595
+
596
+ >>> import c4dynamics as c4d
597
+ >>> from matplotlib import pyplot as plt
598
+ >>> import numpy as np
599
+
600
+
601
+ Settings and initial conditions:
602
+
603
+ (see :class:`seeker <c4dynamics.sensors.seeker.seeker>` examples for more details):
604
+
605
+
606
+ Target:
607
+
608
+ .. code::
609
+
610
+ >>> tgt = c4d.datapoint(x = 1000, y = 0, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
611
+ >>> for t in np.arange(0, 60, 0.01):
612
+ ... tgt.inteqm(np.zeros(3), .01) # doctest: +IGNORE_OUTPUT
613
+ ... tgt.store(t)
614
+
615
+
616
+ Origin:
617
+
618
+
619
+ .. code::
620
+
621
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
622
+
623
+
624
+ Ground truth reference:
625
+
626
+ .. code::
627
+
628
+ >>> skr_ideal = c4d.sensors.seeker(origin = pedestal, isideal = True)
629
+
630
+
631
+ **Tracking with bias**
632
+
633
+ Define a seeker with a bias error only (mute the scale factor and the noise)
634
+ and set it to `0.5°` to track a constant velocity target:
635
+
636
+ .. code::
637
+
638
+ >>> skr = c4d.sensors.seeker(origin = pedestal, scale_factor_std = 0, noise_std = 0)
639
+ >>> skr.bias = .5 * c4d.d2r
640
+ >>> for x in tgt.data():
641
+ ... skr_ideal.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
642
+ ... skr.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
643
+
644
+
645
+ Compare the biased seeker with the true target angles (ideal seeker):
646
+
647
+ .. code::
648
+
649
+ >>> ax = plt.subplot()
650
+ >>> ax.plot(*skr_ideal.data('el', scale = c4d.r2d), label = 'target') # doctest: +IGNORE_OUTPUT
651
+ >>> ax.plot(*skr.data('el', scale = c4d.r2d), label = 'seeker') # doctest: +IGNORE_OUTPUT
652
+
653
+ .. figure:: /_examples/seeker/bias1.png
654
+
655
+
656
+ Example
657
+ -------
658
+
659
+
660
+ The distribution of normally generated random variables
661
+ is characterized by its bell-shaped curve, which is symmetric about the mean.
662
+ The area under the curve represents probability, with about `68%` of the data
663
+ falling within one standard deviation (1σ) of the mean, `95%` within two,
664
+ and `99.7%` within three,
665
+ making it a useful tool for understanding and predicting data behavior.
666
+
667
+
668
+ In `radar` objects, and `seeker` objects in general,
669
+ the `bias` and `scale factor` vary
670
+ among different instances to allow a realistic simulation
671
+ of performance behavior in a technique known as Monte Carlo.
672
+
673
+ Let's examine the `bias` distribution across
674
+ mutliple `radar` instances with a default `bias_std = 0.3°`
675
+ in comparison to `seeker` instances with a default `bias_std = 0.1°`:
676
+
677
+
678
+
679
+
680
+
681
+
682
+
683
+
684
+ .. code::
685
+
686
+ >>> from c4dynamics.sensors import seeker, radar
687
+ >>> seekers = []
688
+ >>> radars = []
689
+ >>> for i in range(1000):
690
+ ... seekers.append(seeker().bias * c4d.r2d)
691
+ ... radars.append(radar().bias * c4d.r2d)
692
+
693
+
694
+ The histogram highlights the broadening of the distribution
695
+ as the standard deviation increases:
696
+
697
+
698
+ >>> ax = plt.subplot()
699
+ >>> ax.hist(seekers, 30, label = 'Seekers') # doctest: +IGNORE_OUTPUT
700
+ >>> ax.hist(radars, 30, label = 'Radars') # doctest: +IGNORE_OUTPUT
701
+
702
+ .. figure:: /_examples/radar/bias2.png
703
+
704
+
705
+
706
+
707
+ '''
708
+ return self._bias
709
+
710
+ @bias.setter
711
+ def bias(self, bias: float):
712
+ self._bias = bias
713
+
714
+
715
+ @property
716
+ def scale_factor(self) -> float:
717
+ '''
718
+ Gets and sets the object's scale_factor.
719
+
720
+
721
+ The scale factor is a random variable generated once at the stage of constructing the
722
+ instance by the errors model:
723
+
724
+ .. math::
725
+
726
+ scalefactor = std \\cdot randn
727
+
728
+ Where `scale_factor_std` is a parameter with default
729
+ value of `0.05 (5%)` for :class:`seeker <c4dynamics.sensors.seeker.seeker>` object, and `0.07 (7%)` for
730
+ :class:`radar <c4dynamics.sensors.radar.radar>` object.
731
+
732
+
733
+
734
+ To get the scale factor generated by the errors model,
735
+ or to override it, the user may call
736
+ :attr:`scale_factor` to get or set the final scale factor error.
737
+
738
+
739
+ Parameters
740
+ ----------
741
+ scale_factor : float
742
+ Required scale factor, [dimensionless].
743
+
744
+
745
+ Returns
746
+ -------
747
+ scale_factor : float
748
+ Current scale factor, [dimensionless].
749
+
750
+
751
+ Example
752
+ -------
753
+
754
+ The following example for a `seeker` object is directly applicable
755
+ to a `radar` object too.
756
+ Simply replace :code:`c4d.sensors.seeker(...)`
757
+ with :code:`c4d.sensors.radar(...)`.
758
+
759
+
760
+
761
+ Required packages:
762
+
763
+
764
+ .. code::
765
+
766
+ >>> import c4dynamics as c4d
767
+ >>> from matplotlib import pyplot as plt
768
+ >>> import numpy as np
769
+
770
+
771
+ Settings and initial conditions:
772
+
773
+ (see :class:`seeker <c4dynamics.sensors.seeker.seeker>` or :class:`radar <c4dynamics.sensors.radar.radar>`
774
+ examples for more details):
775
+
776
+
777
+ Target:
778
+
779
+ .. code::
780
+
781
+ >>> tgt = c4d.datapoint(x = 1000, y = 0, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
782
+ >>> for t in np.arange(0, 60, 0.01):
783
+ ... tgt.inteqm(np.zeros(3), .01) # doctest: +IGNORE_OUTPUT
784
+ ... tgt.store(t)
785
+
786
+
787
+ Origin:
788
+
789
+
790
+ .. code::
791
+
792
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
793
+
794
+
795
+ Ground truth reference:
796
+
797
+ .. code::
798
+
799
+ >>> skr_ideal = c4d.sensors.seeker(origin = pedestal, isideal = True)
800
+
801
+
802
+ **Tracking with scale factor**
803
+
804
+ Define a seeker with a scale factor error only (mute the bias and the noise)
805
+ and set it to `1.2 (= 20%)` to track a constant velocity target:
806
+
807
+
808
+ .. code::
809
+
810
+ >>> skr = c4d.sensors.seeker(origin = pedestal, bias_std = 0, noise_std = 0)
811
+ >>> skr.scale_factor = 1.2
812
+ >>> for x in tgt.data():
813
+ ... skr_ideal.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
814
+ ... skr.measure(c4d.create(x[1:]), t = x[0], store = True) # doctest: +IGNORE_OUTPUT
815
+
816
+
817
+ Compare with the true target angles (ideal seeker):
818
+
819
+ .. code::
820
+
821
+ >>> ax = plt.subplot()
822
+ >>> ax.plot(*skr_ideal.data('az', scale = c4d.r2d), label = 'target') # doctest: +IGNORE_OUTPUT
823
+ >>> ax.plot(*skr.data('az', scale = c4d.r2d), label = 'seeker') # doctest: +IGNORE_OUTPUT
824
+
825
+ .. figure:: /_examples/seeker/sf.png
826
+
827
+
828
+
829
+ '''
830
+ return self._scale_factor
831
+
832
+ @scale_factor.setter
833
+ def scale_factor(self, scale_factor: float):
834
+ self._scale_factor = scale_factor
835
+
836
+
837
+
838
+ # def measure(self, target, t = -1, store = False):
839
+ def measure(self, target: 'c4d.state', t: float = -1, store: bool = False) -> tuple[Optional[float], Optional[float]]:
840
+ '''
841
+ Measures azimuth and elevation between the seeker and a `target`.
842
+
843
+
844
+ If the seeker time-constant `dt` was provided when the `seeker` was created,
845
+ then `measure` returns `None` for any `t < last_t + dt`,
846
+ where `t` is the time input, `last_t` is the last measurement time,
847
+ and `dt` is the seeker time-constant.
848
+ Default behavior, returns measurements for each call.
849
+
850
+ If `store = True`, the method stores the measured
851
+ azimuth and elevation along with a timestamp
852
+ (`t = -1` by default, if not provided otherwise).
853
+
854
+
855
+ Parameters
856
+ ----------
857
+ target : state
858
+ A Cartesian state object to measure by the seeker, including at least one position coordinate (x, y, z).
859
+ store : bool, optional
860
+ A flag indicating whether to store the measured values. Defaults `False`.
861
+ t : float, optional
862
+ Timestamp [seconds]. Defaults -1.
863
+
864
+ Returns
865
+ -------
866
+ out : tuple
867
+ azimuth and elevation, [radians].
868
+
869
+
870
+ Raises
871
+ ------
872
+ ValueError
873
+ If `target` doesn't include any position coordinate (x, y, z).
874
+
875
+
876
+ Example
877
+ -------
878
+
879
+ `measure` in a program simulating
880
+ real-time tracking of a constant velcoity target.
881
+
882
+
883
+ The target is represented by a :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>`
884
+ and is simulated using the :mod:`eqm <c4dynamics.eqm>` module,
885
+ which integrating the point-mass equations of motion.
886
+
887
+ An ideal seeker uses as reference to the true position of the target.
888
+
889
+ At each cycle, the the seekers take measurements and store the samples for
890
+ later use in plotting the results.
891
+
892
+ Import required packages:
893
+
894
+ .. code::
895
+
896
+ >>> import c4dynamics as c4d
897
+ >>> from matplotlib import pyplot as plt
898
+ >>> import numpy as np
899
+
900
+
901
+ Settings and initial conditions:
902
+
903
+ .. code::
904
+
905
+ >>> dt = 0.01
906
+ >>> np.random.seed(321)
907
+ >>> tgt = c4d.datapoint(x = 1000, vx = -80 * c4d.kmh2ms, vy = 10 * c4d.kmh2ms)
908
+ >>> pedestal = c4d.rigidbody(z = 30, theta = -1 * c4d.d2r)
909
+ >>> skr = c4d.sensors.seeker(origin = pedestal, dt = 0.05)
910
+ >>> skr_ideal = c4d.sensors.seeker(origin = pedestal, isideal = True)
911
+
912
+
913
+ Main loop:
914
+
915
+ .. code::
916
+
917
+ >>> for t in np.arange(0, 60, dt):
918
+ ... tgt.inteqm(np.zeros(3), dt) # doctest: +IGNORE_OUTPUT
919
+ ... skr_ideal.measure(tgt, t = t, store = True) # doctest: +IGNORE_OUTPUT
920
+ ... skr.measure(tgt, t = t, store = True) # doctest: +IGNORE_OUTPUT
921
+ ... tgt.store(t)
922
+
923
+
924
+
925
+ Before viewing the results, let's examine the error parameters generated by
926
+ the errors model (`c4d.r2d` converts radians to degrees):
927
+
928
+ .. code::
929
+
930
+ >>> skr.bias * c4d.r2d # doctest: +ELLIPSIS
931
+ 0.16...
932
+ >>> skr.scale_factor # doctest: +ELLIPSIS
933
+ 1.008...
934
+ >>> skr.noise_std * c4d.r2d
935
+ 0.4
936
+
937
+ Then we excpect a constant bias of -0.02° and a 'compression' or 'squeeze' of `5%` with
938
+ respect to the target (as represented by the ideal seeker):
939
+
940
+ .. code::
941
+
942
+ >>> ax = plt.subplot() # doctest: +IGNORE_OUTPUT
943
+ >>> ax.plot(*skr_ideal.data('az', scale = c4d.r2d), '.m', markersize = 1, label = 'target') # doctest: +IGNORE_OUTPUT
944
+ >>> ax.plot(*skr.data('az', scale = c4d.r2d), '.c', markersize = 1, label = 'seeker') # doctest: +IGNORE_OUTPUT
945
+
946
+ .. figure:: /_examples/seeker/measure.png
947
+
948
+ The sample rate of the seeker was set by the parameter `dt = 0.05`.
949
+ In cycles that don't satisfy `t < last_t + dt`, `measure` returns None,
950
+ as shown in a close-up view:
951
+
952
+ .. figure:: /_examples/seeker/measure_zoom.png
953
+
954
+
955
+ '''
956
+
957
+ if not(hasattr(target, 'cartesian') and target.cartesian()):
958
+ raise TypeError('target must be a state objects with at least one position coordinate, x, y, or z.')
959
+
960
+
961
+ if t < self._lastsample + self.dt - 1e-10:
962
+ return None, None
963
+
964
+ self._lastsample = t
965
+
966
+
967
+ # self: The rigid body object on which the seeker is installed
968
+ # target: A datapoint object detected by the seeker
969
+
970
+ # target-seeker position in inertial coordinates
971
+ # self.range = self.P(target) + self.rng_noise_std * np.random.randn()
972
+ # rand1 = np.random.rand() # to preserve matlab normal
973
+ # self.range = self.P(target) + self.rng_noise_std * np.sqrt(2) * erfinv(2 * rand1 - 1) # c4d.mrandn() #
974
+
975
+ # target-seeker position in seeker-body coordinates
976
+ x = target.position - self.position
977
+ x_body = self.BR @ x
978
+
979
+ # extract angles:
980
+ az_true = c4d.atan2(x_body[1], x_body[0])
981
+ el_true = c4d.atan2(x_body[2], c4d.sqrt(x_body[0]**2 + x_body[1]**2))
982
+
983
+
984
+ self.az = az_true * self._scale_factor + self._bias + self.noise_std * np.random.randn() # c4d.mrandn()
985
+ self.el = el_true * self._scale_factor + self._bias + self.noise_std * np.random.randn() # c4d.mrandn()
986
+
987
+ if store:
988
+ self.storeparams(['az', 'el'], t = t)
989
+
990
+ return self.az, self.el
991
+
992
+
993
+ def _errors_model(self) -> None:
994
+ '''
995
+ measured_angle = true_angle * scale_factor + bias + noise
996
+
997
+ Applies the errors model to azimuth and elevation angles.
998
+ Updates the scale factor, bias, and calculates noise.
999
+ '''
1000
+ self._scale_factor = 1 + self.scale_factor_std * np.random.randn()
1001
+ self._bias = self.bias_std * np.random.randn()
1002
+
1003
+
1004
+
1005
+ if __name__ == "__main__":
1006
+
1007
+ import doctest, contextlib, os
1008
+ from c4dynamics import IgnoreOutputChecker, cprint
1009
+
1010
+ # Register the custom OutputChecker
1011
+ doctest.OutputChecker = IgnoreOutputChecker
1012
+
1013
+ tofile = False
1014
+ optionflags = doctest.FAIL_FAST
1015
+
1016
+ if tofile:
1017
+ with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
1018
+ with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
1019
+ result = doctest.testmod(optionflags = optionflags)
1020
+ else:
1021
+ result = doctest.testmod(optionflags = optionflags)
1022
+
1023
+ if result.failed == 0:
1024
+ cprint(os.path.basename(__file__) + ": all tests passed!", 'g')
1025
+ else:
1026
+ print(f"{result.failed}")
1027
+
1028
+
1029
+
1030
+