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,660 @@
1
+ import numpy as np
2
+ import sys
3
+ sys.path.append('.')
4
+ import c4dynamics as c4d
5
+ from c4dynamics.states.state import state
6
+ import warnings
7
+
8
+ def create(X):
9
+ if len(X) > 6:
10
+ rb = c4d.rigidbody()
11
+ rb.X = X
12
+ return rb
13
+
14
+ dp = c4d.datapoint()
15
+ dp.X = X
16
+ return dp
17
+
18
+
19
+ class datapoint(state):
20
+ '''
21
+ A data-point object.
22
+
23
+
24
+ The :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>` is the most basic element
25
+ in translational dynamics; it's a point in space with the following state vector:
26
+
27
+ .. math::
28
+
29
+ X = [x, y, z, v_x, v_y, v_z]^T
30
+
31
+ - Position coordinates, velocity coordinates.
32
+
33
+
34
+ As such, each one of the state variables is a parameter whose value determines
35
+ its initial conditions:
36
+
37
+ **Arguments**
38
+
39
+ x : float or int, optional
40
+ The x-position of the datapoint. Default value :math:`x = 0`.
41
+ y : float or int, optional
42
+ The y-position of the datapoint. Default value :math:`y = 0`.
43
+ z : float or int, optional
44
+ The z-position of the datapoint. Default value :math:`z = 0`.
45
+ vx : float or int, optional
46
+ Component of velocity along the x-axis. Default value :math:`v_x = 0`.
47
+ vy : float or int, optional
48
+ Component of velocity along the y-axis. Default value :math:`v_y = 0`.
49
+ vz : float or int, optional
50
+ Component of velocity along the z-axis. Default value :math:`v_z = 0`.
51
+
52
+
53
+
54
+ The input arguments determine the initial values of the instance.
55
+ The vector of initial conditions can be retrieved by calling
56
+ :attr:`datapoint.X0 <c4dynamics.states.state.state.X0>`:
57
+
58
+
59
+ .. code::
60
+
61
+ >>> from c4dynamics import datapoint
62
+ >>> dp = datapoint(x = 1000, vx = -100)
63
+ >>> print(dp.X0) # doctest: +NUMPY_FORMAT
64
+ [1000 0 0 -100 0 0]
65
+
66
+
67
+ When the initial values are not known at the stage of constructing
68
+ the state object, it's possible to pass zeros and override them later
69
+ by direct assignment of the state variable with a `0` suffix.
70
+ See more at :attr:`X0 <c4dynamics.states.state.state.X0>`.
71
+
72
+
73
+ Parameters
74
+ ==========
75
+
76
+ mass : float
77
+ The mass of the datapoint
78
+
79
+
80
+ See Also
81
+ ========
82
+ .lib
83
+ .state
84
+ .eqm
85
+
86
+
87
+ Example
88
+ =======
89
+
90
+ The following example simulates the
91
+ motion of a body in a free fall.
92
+
93
+ The example employs the :mod:`eqm <c4dynamics.eqm>`
94
+ module to solve the equations of motion of a point-mass
95
+ in the three dimensional space, and integrate them
96
+ using the fourth-order Runge-Kutta method.
97
+
98
+
99
+ Import required packages:
100
+
101
+ .. code::
102
+
103
+ >>> import c4dynamics as c4d
104
+ >>> from matplotlib import pyplot as plt
105
+ >>> import numpy as np
106
+
107
+
108
+ Settings and initial conditions:
109
+
110
+ .. code::
111
+
112
+ >>> dp = c4d.datapoint(z = 100)
113
+ >>> dt = 1e-2
114
+ >>> t = np.arange(0, 10 + dt, dt)
115
+
116
+
117
+ Main loop:
118
+
119
+ .. code::
120
+
121
+ >>> for ti in t:
122
+ ... dp.store(ti)
123
+ ... if dp.z < 0: break
124
+ ... dp.inteqm([0, 0, -c4d.g_ms2], dt) # doctest: +IGNORE_OUTPUT
125
+
126
+ .. code::
127
+
128
+ >>> dp.plot('z')
129
+ >>> plt.show()
130
+
131
+ .. figure:: /_examples/datapoint/intro_freefall.png
132
+
133
+
134
+ '''
135
+ x: float
136
+ y: float
137
+ z: float
138
+ vx: float
139
+ vy: float
140
+ vz: float
141
+
142
+
143
+ # Attributes
144
+ # ==========
145
+
146
+ # As mentioned earlier, reading and writing of the state vairables is allowed by using the
147
+ # :attr:`X <datapoint.X>` property. The entire attributes which support
148
+ # the reading and the updating of a datapoint instance are given in the following list:
149
+
150
+
151
+ # .. automethod:: c4dynamics.datapoint
152
+ # the datapoint object is the most basic element in the translational dynamics domain.
153
+ # --
154
+ # TBD:
155
+ # - there should be one abstrcact class \ inerface of a 'bodyw type which defines eqm(), store() etc.
156
+ # and datapoint and rigidbody impement it. the body also includes the drawing functions
157
+ # - all these nice things storage, plot etc. have to be move out of here.
158
+ # - add an option in the constructor to select the variables required for storage.
159
+ # - make a dictionary containing the variable name and the variable index in the data storage to save and to extract for plotting.
160
+ # - add total position, velocity, acceleration variables (path angles optional) and update them for each update in the cartesian components.
161
+
162
+
163
+ # #
164
+ # # position
165
+ # ##
166
+ # maybe it's a better choise to work with vectors??
167
+ # maybe there's an option to define an array which will just designate its enteries.
168
+ # namely a docker that just references its variables
169
+ # -> this is actually a function!
170
+
171
+
172
+ # In Python, all variable names are references to values.
173
+ # https://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference
174
+
175
+
176
+ # https://docs.python.org/3/library/stdtypes.html
177
+
178
+
179
+ # Lists may be constructed in several ways:
180
+ # Using a pair of square brackets to denote the empty list: []
181
+ # Using square brackets, separating items with commas: [a], [a, b, c]
182
+ # Using a list comprehension: [x for x in iterable]
183
+ # Using the type constructor: list() or list(iterable)
184
+
185
+ # Tuples may be constructed in a number of ways:
186
+ # Using a pair of parentheses to denote the empty tuple: ()
187
+ # Using a trailing comma for a singleton tuple: a, or (a,)
188
+ # Separating items with commas: a, b, c or (a, b, c)
189
+ # Using the tuple() built-in: tuple() or tuple(iterable)
190
+
191
+ # The arguments to the range constructor must be integers
192
+
193
+ # __slots__ = ['x', 'y', 'z', 'vx', 'vy', 'vz', 'mass' # , 'ax', 'ay', 'az'
194
+ # , 'x0', 'y0', 'z0', 'vx0', 'vy0', 'vz0'
195
+ # , '_data', '_vardata', '_didx', '__dict__']
196
+
197
+
198
+
199
+
200
+
201
+
202
+ _mass = 1
203
+
204
+
205
+ def __init__(self, x = 0, y = 0, z = 0, vx = 0, vy = 0, vz = 0):
206
+
207
+ dpargs = {}
208
+ dpargs.setdefault('x', x)
209
+ dpargs.setdefault('y', y)
210
+ dpargs.setdefault('z', z)
211
+ dpargs.setdefault('vx', vx)
212
+ dpargs.setdefault('vy', vy)
213
+ dpargs.setdefault('vz', vz)
214
+
215
+ super().__init__(**dpargs)
216
+
217
+
218
+ @property
219
+ def mass(self):
220
+ '''
221
+ Gets and sets the object's mass.
222
+
223
+ Default value :math:`mass = 1`.
224
+
225
+
226
+ Parameters
227
+ ----------
228
+ mass : float or int
229
+ Mass of the object.
230
+
231
+ Returns
232
+ -------
233
+ out : float or int
234
+ A scalar representing the object's mass.
235
+
236
+
237
+ Example
238
+ -------
239
+
240
+ 1. `datapoint`
241
+
242
+ Two floating balloons of 1kg and 10kg float with total force of L = 0.5N
243
+ and expreience a side wind of 10k.
244
+
245
+ Import required packages:
246
+
247
+ .. code::
248
+
249
+ >>> import c4dynamics as c4d
250
+ >>> from matplotlib import pyplot as plt
251
+ >>> import numpy as np
252
+
253
+
254
+ Settings and initial conditions:
255
+
256
+ .. code::
257
+
258
+ >>> dt = 0.01
259
+ >>> tf = 10 + dt
260
+ >>> F = [0, 0, .5]
261
+ >>> #
262
+ >>> bal1 = c4d.datapoint(vx = 10 * c4d.k2ms)
263
+ >>> bal1.mass = 1
264
+ >>> #
265
+ >>> bal10 = c4d.datapoint(vx = 10 * c4d.k2ms)
266
+ >>> bal10.mass = 10
267
+
268
+
269
+ Main loop:
270
+
271
+ .. code::
272
+
273
+ >>> for t in np.arange(0, tf, dt):
274
+ ... bal1.store(t)
275
+ ... bal10.store(t)
276
+ ... bal1.X = c4d.eqm.int3(bal1, F, dt)
277
+ ... bal10.X = c4d.eqm.int3(bal10, F, dt)
278
+
279
+
280
+ .. code::
281
+
282
+ >>> bal1.plot('side')
283
+ >>> bal10.plot('side', ax = plt.gca(), color = 'c')
284
+ >>> plt.show()
285
+
286
+ .. figure:: /_examples/datapoint/mass_balloon.png
287
+
288
+ 2. `rigidbody`
289
+
290
+ The previous example for a `datapoint` object is directly applicable
291
+ to the `rigidbody` object, as both classes share the same underlying principles
292
+ concerning translational dynamics. Simply replace :code:`c4d.datapoint(vx = 10 * c4d.k2ms)`
293
+ with :code:`c4d.rigidbody(vx = 10 * c4d.k2ms)`.
294
+
295
+
296
+ '''
297
+ return self._mass
298
+
299
+ @mass.setter
300
+ def mass(self, mass):
301
+ self._mass = mass
302
+
303
+
304
+ #
305
+ # runge kutta integration
306
+ ##
307
+ def inteqm(self, forces, dt):
308
+ '''
309
+ Advances the state vector :attr:`datapoint.X <c4dynamics.states.state.state.X>`,
310
+ with respect to the input
311
+ forces on a single step of time, `dt`.
312
+
313
+ Integrates equations of three degrees translational motion using the Runge-Kutta method.
314
+
315
+ This method numerically integrates the equations of motion for a dynamic system
316
+ using the fourth-order Runge-Kutta method as given by
317
+ :func:`int3 <c4dynamics.eqm.integrate.int3>`.
318
+
319
+ The derivatives of the equations are of three
320
+ dimensional translational motion and
321
+ produced with
322
+ :func:`eqm3 <c4dynamics.eqm.derivs.eqm3>`
323
+
324
+
325
+ Parameters
326
+ ----------
327
+ forces : numpy.array or list
328
+ An external forces vector acting on the body, `forces = [Fx, Fy, Fz]`
329
+ dt : float or int
330
+ Interval time step for integration.
331
+
332
+
333
+ Returns
334
+ -------
335
+ out : numpy.float64
336
+ An acceleration array at the final time step.
337
+
338
+ Warning
339
+ -------
340
+ This method is not recommanded when the vector
341
+ of forces depends on the state variables.
342
+ Since the force vector is provided once at the
343
+ entrance to the integration, it remains constant
344
+ for the entire steps.
345
+ Therefore, when the forces depend on the state variables
346
+ the results of this method are not accurate and may lead to instability.
347
+
348
+
349
+
350
+
351
+
352
+ Example
353
+ -------
354
+
355
+ Simulation of the motion of a body in a free fall.
356
+
357
+ Employing the :mod:`eqm <c4dynamics.eqm>`
358
+ module to solve the equations of motion of a point-mass
359
+ in the three dimensional space.
360
+ Integrating the equations of motion
361
+ using the fourth-order Runge-Kutta method.
362
+
363
+
364
+ Import required packages:
365
+
366
+ .. code::
367
+
368
+ >>> import c4dynamics as c4d
369
+ >>> from matplotlib import pyplot as plt
370
+ >>> import numpy as np
371
+
372
+
373
+ Settings and initial conditions:
374
+
375
+ .. code::
376
+
377
+ >>> dp = c4d.datapoint(z = 100)
378
+ >>> dt = 1e-2
379
+ >>> t = np.arange(0, 10 + dt, dt)
380
+
381
+
382
+ Main loop:
383
+
384
+ .. code::
385
+
386
+ >>> for ti in t:
387
+ ... dp.store(ti)
388
+ ... if dp.z < 0: break
389
+ ... dp.inteqm([0, 0, -c4d.g_ms2], dt) # doctest: +IGNORE_OUTPUT
390
+
391
+
392
+ .. code::
393
+
394
+ >>> dp.plot('z')
395
+ >>> plt.show()
396
+
397
+ .. figure:: /_examples/datapoint/intro_freefall.png
398
+
399
+
400
+
401
+ '''
402
+ self.X, acc = c4d.eqm.int3(self, forces, dt, derivs_out = True)
403
+ return acc
404
+
405
+
406
+ #
407
+ # ploting functions
408
+ ##
409
+
410
+ def plot(self, var, scale = 1, ax = None, filename = None, darkmode = True, **kwargs):
411
+ '''
412
+ Draws plots of trajectories or variable evolution over time.
413
+
414
+ `var` can be each one of the state variables, or `top`, `side`, for trajectories.
415
+
416
+
417
+ Parameters
418
+ ----------
419
+
420
+ var : str
421
+ The variable to be plotted.
422
+ Possible variables for trajectories: `top`, `side`.
423
+ For time evolution, any one of the state variables is possible:
424
+ `x`, `y`, `z`, `vx`, `vy`, `vz` - for a datapoint object, and
425
+ also `phi`, `theta`, `psi`, `p`, `q`, `r` - for a rigidbody object.
426
+
427
+ scale : float or int, optional
428
+ A scaling factor to apply to the variable values. Defaults to `1`.
429
+
430
+ ax : matplotlib.axes.Axes, optional
431
+ An existing Matplotlib axis to plot on.
432
+ If None, a new figure and axis will be created. By default None.
433
+
434
+ filename : str, optional
435
+ Full file name to save the plot image.
436
+ If None, the plot will not be saved, by default None.
437
+
438
+ darkmode : bool, optional
439
+ Directory path to save the plot image.
440
+ If None, the plot will not be saved, by default None.
441
+
442
+ **kwargs : dict, optional
443
+ Additional key-value arguments passed to `matplotlib.pyplot.plot`.
444
+ These can include any keyword arguments accepted by `plot`,
445
+ such as `color`, `linestyle`, `marker`, etc.
446
+
447
+
448
+ Notes
449
+ -----
450
+ - The method overrides the :meth:`plot <c4dynamics.states.state.state.plot>` of
451
+ the parent :class:`state <c4dynamics.states.state.state>` object and is
452
+ applicable to :class:`datapoint <c4dynamics.states.lib.datapoint.datapoint>`
453
+ and its subclass :class:`rigidbody <c4dynamics.states.lib.rigidbody.rigidbody>`.
454
+
455
+ - Uses matplotlib for plotting.
456
+
457
+ - Trajectory views (`top` and `side`) show the crossrange vs
458
+ downrange or downrange vs altitude.
459
+
460
+
461
+
462
+
463
+
464
+ Examples
465
+ --------
466
+
467
+ Import necessary packages:
468
+
469
+ .. code::
470
+
471
+ >>> import c4dynamics as c4d
472
+ >>> from matplotlib import pyplot as plt
473
+ >>> import numpy as np
474
+ >>> import scipy
475
+
476
+
477
+ 1) `datapoint`:
478
+
479
+ .. code::
480
+
481
+ >>> pt = c4d.datapoint()
482
+ >>> for t in np.arange(0, 10, .01):
483
+ ... pt.x = 10 + np.random.randn()
484
+ ... pt.store(t)
485
+ >>> pt.plot('x')
486
+
487
+ .. figure:: /_examples/datapoint/plot.png
488
+
489
+
490
+ 2) `rigidbody`:
491
+
492
+ A physical pendulum is represented by a rigidoby object.
493
+ `scipy's odeint` integrates the equations of motion to simulate
494
+ the angle of rotation of the pendulum over time.
495
+
496
+
497
+ Settings and initial conditions:
498
+
499
+ .. code::
500
+
501
+ >>> dt =.01
502
+ >>> pndlm = c4d.rigidbody(theta = 80 * c4d.d2r)
503
+ >>> pndlm.I = [0, .5, 0]
504
+
505
+
506
+ Dynamic equations:
507
+
508
+ .. code::
509
+
510
+ >>> def pendulum(yin, t, Iyy):
511
+ ... yout = np.zeros(12)
512
+ ... yout[7] = yin[10]
513
+ ... yout[10] = -c4d.g_ms2 * c4d.sin(yin[7]) / Iyy - .5 * yin[10]
514
+ ... return yout
515
+
516
+
517
+ Main loop:
518
+
519
+ .. code::
520
+
521
+ >>> for ti in np.arange(0, 4, dt):
522
+ ... pndlm.X = scipy.integrate.odeint(pendulum, pndlm.X, [ti, ti + dt], (pndlm.I[1],))[1]
523
+ ... pndlm.store(ti)
524
+
525
+
526
+ Plot results:
527
+
528
+ .. code::
529
+
530
+ >>> pndlm.plot('theta', scale = c4d.r2d)
531
+
532
+
533
+ .. figure:: /_examples/rigidbody/plot_pendulum.png
534
+
535
+
536
+ '''
537
+ from matplotlib import pyplot as plt
538
+ if var not in self._didx and var not in ['top', 'side']:
539
+ warnings.warn(f"""{var} is not a state variable or a valid trajectory to plot.""" , c4d.c4warn)
540
+ return None
541
+ if not self._data:
542
+ warnings.warn(f"""No stored data for {var}.""" , c4d.c4warn)
543
+ return None
544
+
545
+ if darkmode:
546
+ plt.style.use('dark_background')
547
+ else:
548
+ plt.style.use('default')
549
+
550
+
551
+ title = ''
552
+ ylabel = ''
553
+
554
+ if var.lower() == 'top':
555
+ # x axis: y data
556
+ # y axis: x data
557
+ x = self.data('y')[1]
558
+ y = self.data('x')[1]
559
+ xlabel = 'Crossrange'
560
+ ylabel = 'Downrange'
561
+ title = 'Top View'
562
+ elif var.lower() == 'side':
563
+ # x axis: x data
564
+ # y axis: z data
565
+ x = self.data('x')[1]
566
+ y = self.data('z')[1]
567
+ xlabel = 'Downrange'
568
+ ylabel = 'Altitude'
569
+ title = 'Side View'
570
+ # ax.invert_yaxis()
571
+ else:
572
+
573
+ if self._didx[var] >= 7: # 7 and above are angular variables
574
+ scale = 180 / np.pi
575
+
576
+
577
+ if not len(np.flatnonzero(self.data('t') != -1)): # values for t weren't stored
578
+ x = range(len(self.data('t'))) # t is just indices
579
+ xlabel = 'Sample'
580
+ else:
581
+ x = self.data('t')
582
+ xlabel = 'Time'
583
+ y = np.array(self._data)[:, self._didx[var]] * scale if self._data else np.empty(1) # used selection
584
+
585
+
586
+ if 1 <= self._didx[var] <= 6:
587
+ # x, y, z, vx, vy, vz
588
+ title = var.title()
589
+ ylabel = var.title()
590
+ elif 7 <= self._didx[var] <= 9:
591
+ # phi, theta, psi
592
+ title = '$\\' + var + '$'
593
+ ylabel = title + ' (deg)'
594
+ elif 10 <= self._didx[var] <= 12:
595
+ # p, q, r
596
+ title = var.title()
597
+ ylabel = var + ' (deg/sec)'
598
+
599
+
600
+
601
+ # Set default values in kwargs only if the user hasn't provided them
602
+ kwargs.setdefault('color', 'm')
603
+ kwargs.setdefault('linewidth', 1.2)
604
+
605
+ if ax is None:
606
+ # _, ax = plt.subplots()
607
+ # _, ax = plt.subplots(1, 1, dpi = 200, figsize = (3, 2.3)
608
+ # , gridspec_kw = {'left': .17, 'right': .9
609
+ # , 'top': .85, 'bottom': .2
610
+ # , 'hspace': 0.5, 'wspace': 0.3})
611
+
612
+ # find the legnth of the number to adjust the left axis:
613
+ # ndigits = len(str(int(np.max(y))))
614
+
615
+
616
+ factorsize = 4
617
+ aspectratio = 1080 / 1920
618
+ _, ax = plt.subplots(1, 1, dpi = 200
619
+ , figsize = (factorsize, factorsize * aspectratio)
620
+ , gridspec_kw = {'left': 0.15, 'right': .85
621
+ , 'top': .9, 'bottom': .2})
622
+
623
+
624
+
625
+ ax.plot(x, y, **kwargs)
626
+ c4d.plotdefaults(ax, title, xlabel, ylabel, 8)
627
+
628
+
629
+
630
+ if filename:
631
+ # plt.tight_layout(pad = 0)
632
+ plt.savefig(filename, bbox_inches = 'tight', pad_inches = .2, dpi = 600)
633
+
634
+
635
+
636
+
637
+ if __name__ == "__main__":
638
+
639
+ import doctest, contextlib, os
640
+ from c4dynamics import IgnoreOutputChecker, cprint
641
+
642
+ # Register the custom OutputChecker
643
+ doctest.OutputChecker = IgnoreOutputChecker
644
+
645
+ tofile = False
646
+ optionflags = doctest.FAIL_FAST
647
+
648
+ if tofile:
649
+ with open(os.path.join('tests', '_out', 'output.txt'), 'w') as f:
650
+ with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
651
+ result = doctest.testmod(optionflags = optionflags)
652
+ else:
653
+ result = doctest.testmod(optionflags = optionflags)
654
+
655
+ if result.failed == 0:
656
+ cprint(os.path.basename(__file__) + ": all tests passed!", 'g')
657
+ else:
658
+ print(f"{result.failed}")
659
+
660
+