• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moeyensj / thor / 4708907600

15 Apr 2023 05:15PM UTC coverage: 48.357%. First build
4708907600

Pull #98

github

GitHub
Merge 6dfe698e9 into cc697a037
Pull Request #98: Update workflows, add Docker compose recipe, and add linting with pre-commit

956 of 956 new or added lines in 82 files covered. (100.0%)

3091 of 6392 relevant lines covered (48.36%)

0.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.75
/thor/testing/orbit_testing.py
1
import numpy as np
1✔
2
from astropy import units as u
1✔
3

4
__all__ = [
1✔
5
    "testCartesianOrbits",
6
    "testKeplerianOrbits",
7
    "testCometaryOrbits",
8
    "testOrbits",
9
]
10

11

12
def __statsToErrorMessage(stats, message):
1✔
13
    """
14
    Helper function that adds the stats derived from _evaluateDifference
15
    to an error message.
16
    """
17
    for stat, value in stats.items():
1✔
18
        message += "  {:<7}: {}\n".format(stat, value.T)
1✔
19
    message += "\n"
1✔
20
    return message
1✔
21

22

23
def _evaluateDifference(actual, desired, unit, tol, magnitude=False):
1✔
24
    """
25
    Calculate the absolute difference between actual and desired
26
    and return it in the same units of the given tolerance with
27
    some summary statistics on the absolute difference.
28

29

30
    Paramters
31
    ---------
32
    actual : `~numpy.ndarray` (N, M)
33
        Array of computed values.
34
    desired : `~numpy.ndarray` (N, M)
35
        Array of desired values.
36
    unit : `astropy.units.core.Unit`
37
        Units in which both arrays are expressed. Must be the same.
38
    tol : `~astropy.units.quantity.Quantity` (1)
39
        The tolerance to which the absolute difference will be evaluated
40
        to. Used purely to convert the absolute differences to the same
41
        units as the tolerance.
42

43
    Returns
44
    -------
45
    diff : `~astropy.units.quantity.Quantity` (N, M)
46
        |actual - desired| in units of the given tolerance.
47
    stats : dict
48
        "Mean" : mean difference per M dimension
49
        "Median" : median difference per M dimension
50
        "Min" : minimum difference per M dimension
51
        "Max" : maximum difference per M dimension
52
        "Argmin" : location of minimum difference per M dimension
53
        "Argmax" : location of maximum difference per M dimension
54
    error : bool
55
        True if diff is not within the desired tolerance.
56
    """
57
    error = False
1✔
58
    diff = np.abs(actual - desired) * unit
1✔
59
    diff = diff.to(tol.unit)
1✔
60

61
    if magnitude:
1✔
62
        diff = np.linalg.norm(diff, axis=1)
1✔
63
        diff = diff[:, np.newaxis]
1✔
64

65
    tol_ = np.empty_like(diff)
1✔
66
    tol_.fill(tol)
1✔
67

68
    stats = {
1✔
69
        "Mean": np.mean(diff, axis=0),
70
        "Median": np.median(diff, axis=0),
71
        "Std": np.std(diff, axis=0),
72
        "Min": np.min(diff, axis=0),
73
        "Max": np.max(diff, axis=0),
74
        "Argmin": np.argmin(diff, axis=0),
75
        "Argmax": np.argmax(diff, axis=0),
76
    }
77

78
    try:
1✔
79
        np.testing.assert_array_less(
1✔
80
            diff.to(tol.unit),
81
            tol_.to(tol.unit),
82
        )
83
    except AssertionError as e:
1✔
84
        error = True
1✔
85
    return diff, stats, error
1✔
86

87

88
def testCartesianOrbits(
1✔
89
    orbits_actual,
90
    orbits_desired,
91
    position_tol=1 * u.m,
92
    velocity_tol=(1 * u.mm / u.s),
93
    magnitude=True,
94
    raise_error=True,
95
):
96
    """
97
    Tests that the two sets of cartesian orbits are within the desired absolute tolerances
98
    of each other. The absolute difference is calculated as |actual - desired|.
99

100
    Parameters
101
    ----------
102
    orbits_actual : `~numpy.ndarray` (N, 6)
103
        Array of orbits to compare to the desired orbits.
104
        Assumed units for:
105
            positions : AU,
106
            velocities : AU per day
107
    orbits_desired : `~numpy.ndarray` (N, 6)
108
        Array of desired orbits to which to compare the actual orbits to.
109
        Assumed units for:
110
            positions : AU,
111
            velocities : AU per day
112
    orbit_type : {'cartesian', 'keplerian', 'cometary'}
113
        Type of the input orbits. Both actual and desired orbits must be of the same
114
        type.
115
    position_tol : `~astropy.units.quantity.Quantity` (1)
116
        Absolute tolerance positions need to satisfy (x, y, z, r).
117
    velocity_tol : `~astropy.units.quantity.Quantity` (1)
118
        Absolute tolerance velocity need to satisfy. (vx, vy, vz, v).
119
    magnitude : bool
120
        Test the magnitude of the position difference
121
        and velocity difference vectors as opposed to testing per individual coordinate.
122

123
    Raises
124
    ------
125
    AssertionError:
126
        If |orbits_actual - orbits_desired| > tolerance.
127

128
    Returns
129
    -------
130
    None
131
    """
132
    any_error = False
1✔
133
    error_message = "\n"
1✔
134
    differences = {}
1✔
135
    statistics = {}
1✔
136

137
    # Test positions
138
    if magnitude:
1✔
139
        names = ["r"]
1✔
140
    else:
141
        names = ["x", "y", "z"]
×
142
    diff, stats, error = _evaluateDifference(
1✔
143
        orbits_actual[:, :3],
144
        orbits_desired[:, :3],
145
        u.AU,
146
        position_tol,
147
        magnitude=magnitude,
148
    )
149
    for i, n in enumerate(names):
1✔
150
        differences[n] = diff[:, i]
1✔
151
        statistics[n] = {k: v[i] for k, v in stats.items()}
1✔
152

153
    # If any of the differences between desired and actual are
154
    # greater than the allowed tolerance set any_error to True
155
    # and build the error message
156
    if error:
1✔
157
        any_error = True
1✔
158
        error_message += (
1✔
159
            "{} difference (|actual - desired|) is not within {}.\n".format(
160
                ", ".join(names), position_tol
161
            )
162
        )
163
        error_message = __statsToErrorMessage(stats, error_message)
1✔
164

165
    # Test velocities
166
    if magnitude:
1✔
167
        names = ["v"]
1✔
168
    else:
169
        names = ["vx", "vy", "vz"]
×
170
    diff, stats, error = _evaluateDifference(
1✔
171
        orbits_actual[:, 3:],
172
        orbits_desired[:, 3:],
173
        (u.AU / u.d),
174
        velocity_tol,
175
        magnitude=magnitude,
176
    )
177
    for i, n in enumerate(names):
1✔
178
        differences[n] = diff[:, i]
1✔
179
        statistics[n] = {k: v[i] for k, v in stats.items()}
1✔
180

181
    # If any of the differences between desired and actual are
182
    # greater than the allowed tolerance set any_error to True
183
    # and build the error message
184
    if error:
1✔
185
        any_error = True
1✔
186
        error_message += (
1✔
187
            "{} difference (|actual - desired|) is not within {}.\n".format(
188
                ", ".join(names), velocity_tol
189
            )
190
        )
191
        error_message = __statsToErrorMessage(stats, error_message)
1✔
192

193
    if any_error and raise_error:
1✔
194
        raise AssertionError(error_message)
×
195

196
    return differences, statistics, error
1✔
197

198

199
def testKeplerianOrbits(
1✔
200
    orbits_actual,
201
    orbits_desired,
202
    position_tol=1 * u.m,
203
    unitless_tol=1e-10 * u.dimensionless_unscaled,
204
    angle_tol=1e-10 * u.degree,
205
    raise_error=True,
206
):
207
    """
208
    Tests that the two sets of keplerian orbits are within the desired absolute tolerances
209
    of each other. The absolute difference is calculated as |actual - desired|.
210

211
    Parameters
212
    ----------
213
    orbits_actual : `~numpy.ndarray` (N, 6)
214
        Array of orbits to compare to the desired orbits.
215
        Assumed units for:
216
            positions : AU,
217
            angles : degrees
218
    orbits_desired : `~numpy.ndarray` (N, 6)
219
        Array of desired orbits to which to compare the actual orbits to.
220
        Assumed units for:
221
            positions : AU,
222
            angles : degrees
223
    position_tol : `~astropy.units.quantity.Quantity` (1)
224
        Absolute tolerance positions need to satisfy (a).
225
    unitless_tol : `~astropy.units.quantity.Quantity` (1)
226
        Absolute tolerance unitless quantities need to satisfy (e).
227
    angle_tol : `~astropy.units.quantity.Quantity` (1)
228
        Absolute tolerance angle quantities need to satisfy (i, ascNode, argPeri, meanAnom).
229

230
    Raises
231
    ------
232
    AssertionError:
233
        If |orbits_actual - orbits_desired| > tolerance.
234

235
    Returns
236
    -------
237
    None
238
    """
239
    any_error = False
1✔
240
    error_message = "\n"
1✔
241
    differences = {}
1✔
242
    statistics = {}
1✔
243

244
    # Test positions
245
    names = ["a"]
1✔
246
    diff, stats, error = _evaluateDifference(
1✔
247
        orbits_actual[:, :1], orbits_desired[:, :1], u.AU, position_tol, magnitude=False
248
    )
249
    for i, n in enumerate(names):
1✔
250
        differences[n] = diff[:, i]
1✔
251
        statistics[n] = {k: v[i] for k, v in stats.items()}
1✔
252

253
    # If any of the differences between desired and actual are
254
    # greater than the allowed tolerance set any_error to True
255
    # and build the error message
256
    if error:
1✔
257
        any_error = True
×
258
        error_message += (
×
259
            "{} difference (|actual - desired|) is not within {}.\n".format(
260
                ", ".join(names), position_tol
261
            )
262
        )
263
        error_message = __statsToErrorMessage(stats, error_message)
×
264

265
    # Test unitless (eccentricity)
266
    names = ["e"]
1✔
267
    diff, stats, error = _evaluateDifference(
1✔
268
        orbits_actual[:, 1:2],
269
        orbits_desired[:, 1:2],
270
        u.dimensionless_unscaled,
271
        unitless_tol,
272
        magnitude=False,
273
    )
274
    for i, n in enumerate(names):
1✔
275
        differences[n] = diff[:, i]
1✔
276
        statistics[n] = {k: v[i] for k, v in stats.items()}
1✔
277

278
    # If any of the differences between desired and actual are
279
    # greater than the allowed tolerance set any_error to True
280
    # and build the error message
281
    if error:
1✔
282
        any_error = True
×
283
        error_message += (
×
284
            "{} difference (|actual - desired|) is not within {}.\n".format(
285
                ", ".join(names), unitless_tol
286
            )
287
        )
288
        error_message = __statsToErrorMessage(stats, error_message)
×
289

290
    # Test angles
291
    names = ["i", "ascNode", "argPeri", "meanAnom"]
1✔
292
    diff, stats, error = _evaluateDifference(
1✔
293
        orbits_actual[:, 2:],
294
        orbits_desired[:, 2:],
295
        u.degree,
296
        angle_tol,
297
        magnitude=False,
298
    )
299
    for i, n in enumerate(names):
1✔
300
        differences[n] = diff[:, i]
1✔
301
        statistics[n] = {k: v[i] for k, v in stats.items()}
1✔
302

303
    # If any of the differences between desired and actual are
304
    # greater than the allowed tolerance set any_error to True
305
    # and build the error message
306
    if error:
1✔
307
        any_error = True
×
308
        error_message += (
×
309
            "{} difference (|actual - desired|) is not within {}.\n".format(
310
                ", ".join(names), angle_tol
311
            )
312
        )
313
        error_message = __statsToErrorMessage(stats, error_message)
×
314

315
    if any_error and raise_error:
1✔
316
        raise AssertionError(error_message)
×
317

318
    return diff, stats, error
1✔
319

320

321
def testCometaryOrbits(
1✔
322
    orbits_actual,
323
    orbits_desired,
324
    position_tol=1 * u.m,
325
    unitless_tol=1e-10 * u.dimensionless_unscaled,
326
    angle_tol=1e-10 * u.degree,
327
    time_tol=1e-6 * u.s,
328
    raise_error=True,
329
):
330
    """
331
    Tests that the two sets of cometary orbits are within the desired absolute tolerances
332
    of each other. The absolute difference is calculated as |actual - desired|.
333

334
    Parameters
335
    ----------
336
    orbits_actual : `~numpy.ndarray` (N, 6)
337
        Array of orbits to compare to the desired orbits.
338
        Assumed units for:
339
            positions : AU,
340
            velocities : AU per day,
341
            angles : degrees,
342
            times : days
343
    orbits_desired : `~numpy.ndarray` (N, 6)
344
        Array of desired orbits to which to compare the actual orbits to.
345
        Assumed units for:
346
            positions : AU,
347
            velocities : AU per day,
348
            angles : degrees,
349
            times : days
350
    position_tol : `~astropy.units.quantity.Quantity` (1)
351
        Absolute tolerance positions need to satisfy (q).
352
    unitless_tol : `~astropy.units.quantity.Quantity` (1)
353
        Absolute tolerance unitless quantities need to satisfy (e).
354
    angle_tol : `~astropy.units.quantity.Quantity` (1)
355
        Absolute tolerance angle quantities need to satisfy (i, ascNode, argPeri).
356
    time_tol : `~astropy.units.quantity.Quantity` (1)
357
        Absolute tolerance time quantities need to satisfy (tPeri).
358

359
    Raises
360
    ------
361
    AssertionError:
362
        If |orbits_actual - orbits_desired| > tolerance.
363

364
    Returns
365
    -------
366
    None
367
    """
368
    any_error = False
×
369
    error_message = "\n"
×
370
    differences = {}
×
371
    statistics = {}
×
372

373
    # Test positions
374
    names = ["q"]
×
375
    diff, stats, error = _evaluateDifference(
×
376
        orbits_actual[:, :1], orbits_desired[:, :1], u.AU, position_tol, magnitude=False
377
    )
378
    for i, n in enumerate(names):
×
379
        differences[n] = diff[:, i]
×
380
        statistics[n] = {k: v[i] for k, v in stats.items()}
×
381

382
    # If any of the differences between desired and actual are
383
    # greater than the allowed tolerance set any_error to True
384
    # and build the error message
385
    if error:
×
386
        any_error = True
×
387
        error_message += (
×
388
            "{} difference (|actual - desired|) is not within {}.\n".format(
389
                ", ".join(names), position_tol
390
            )
391
        )
392
        error_message = __statsToErrorMessage(stats, error_message)
×
393

394
    # Test unitless (eccentricity)
395
    names = ["e"]
×
396
    diff, stats, error = _evaluateDifference(
×
397
        orbits_actual[:, 1:2],
398
        orbits_desired[:, 1:2],
399
        u.dimensionless_unscaled,
400
        unitless_tol,
401
        magnitude=False,
402
    )
403
    for i, n in enumerate(names):
×
404
        differences[n] = diff[:, i]
×
405
        statistics[n] = {k: v[i] for k, v in stats.items()}
×
406

407
    # If any of the differences between desired and actual are
408
    # greater than the allowed tolerance set any_error to True
409
    # and build the error message
410
    if error:
×
411
        any_error = True
×
412
        error_message += (
×
413
            "{} difference (|actual - desired|) is not within {}.\n".format(
414
                ", ".join(names), unitless_tol
415
            )
416
        )
417
        error_message = __statsToErrorMessage(stats, error_message)
×
418

419
    # Test angles
420
    names = ["i", "ascNode", "argPeri"]
×
421
    diff, stats, error = _evaluateDifference(
×
422
        orbits_actual[:, 2:5],
423
        orbits_desired[:, 2:5],
424
        u.degree,
425
        angle_tol,
426
        magnitude=False,
427
    )
428
    for i, n in enumerate(names):
×
429
        differences[n] = diff[:, i]
×
430
        statistics[n] = {k: v[i] for k, v in stats.items()}
×
431

432
    # If any of the differences between desired and actual are
433
    # greater than the allowed tolerance set any_error to True
434
    # and build the error message
435
    if error:
×
436
        any_error = True
×
437
        error_message += (
×
438
            "{} difference (|actual - desired|) is not within {}.\n".format(
439
                ", ".join(names), angle_tol
440
            )
441
        )
442
        error_message = __statsToErrorMessage(stats, error_message)
×
443

444
    # Test time (time of perihelion passage)
445
    names = ["tPeri"]
×
446
    diff, stats, error = _evaluateDifference(
×
447
        orbits_actual[:, 5:], orbits_desired[:, 5:], u.d, time_tol, magnitude=False
448
    )
449
    for i, n in enumerate(names):
×
450
        differences[n] = diff[:, i]
×
451
        statistics[n] = {k: v[i] for k, v in stats.items()}
×
452

453
    # If any of the differences between desired and actual are
454
    # greater than the allowed tolerance set any_error to True
455
    # and build the error message
456
    if error:
×
457
        any_error = True
×
458
        error_message += (
×
459
            "{} difference (|actual - desired|) is not within {}.\n".format(
460
                ", ".join(names), time_tol
461
            )
462
        )
463
        error_message = __statsToErrorMessage(stats, error_message)
×
464

465
    if any_error and raise_error:
×
466
        raise AssertionError(error_message)
×
467

468
    return diff, stats, error
×
469

470

471
def testOrbits(
1✔
472
    orbits_actual,
473
    orbits_desired,
474
    orbit_type="cartesian",
475
    position_tol=1 * u.m,
476
    velocity_tol=(1 * u.m / u.s),
477
    unitless_tol=1e-10 * u.dimensionless_unscaled,
478
    angle_tol=1e-10 * u.degree,
479
    time_tol=1e-6 * u.s,
480
    magnitude=True,
481
    raise_error=True,
482
):
483
    """
484
    Tests that the two sets of orbits are within the desired absolute tolerances
485
    of each other. The absolute difference is calculated as |actual - desired|.
486

487
    Parameters
488
    ----------
489
    orbits_actual : `~numpy.ndarray` (N, 6)
490
        Array of orbits to compare to the desired orbits.
491
        Assumed units for:
492
            positions : AU,
493
            velocities : AU per day,
494
            angles : degrees,
495
            times : days
496
    orbits_desired : `~numpy.ndarray` (N, 6)
497
        Array of desired orbits to which to compare the actual orbits to.
498
        Assumed units for:
499
            positions : AU,
500
            velocities : AU per day,
501
            angles : degrees,
502
            times : days
503
    orbit_type : {'cartesian', 'keplerian', 'cometary'}
504
        Type of the input orbits. Both actual and desired orbits must be of the same
505
        type.
506
    position_tol : `~astropy.units.quantity.Quantity` (1)
507
        Absolute tolerance positions need to satisfy (a, q, x, y, z, r).
508
    velocity_tol : `~astropy.units.quantity.Quantity` (1)
509
        Absolute tolerance velocity need to satisfy. (vx, vy, vz, v).
510
    unitless_tol : `~astropy.units.quantity.Quantity` (1)
511
        Absolute tolerance unitless quantities need to satisfy (e).
512
    angle_tol : `~astropy.units.quantity.Quantity` (1)
513
        Absolute tolerance angle quantities need to satisfy (i, ascNode, argPeri, meanAnom).
514
    time_tol : `~astropy.units.quantity.Quantity` (1)
515
        Absolute tolerance time quantities need to satisfy (tPeri).
516
    magnitude : bool
517
        Applies to cartesian orbits only. Test the magnitude of the position difference
518
        and velocity difference vectors as opposed to testing per individual coordinate.
519

520
    Raises
521
    ------
522
    AssertionError:
523
        If |orbits_actual - orbits_desired| > tolerance.
524
    ValueError:
525
        If orbit_type is not one of 'cartesian', 'keplerian' or 'cometary'.
526

527
    Returns
528
    -------
529
    None
530
    """
531
    if orbit_type == "cartesian":
1✔
532

533
        diff, stats, error = testCartesianOrbits(
1✔
534
            orbits_actual,
535
            orbits_desired,
536
            position_tol=position_tol,
537
            velocity_tol=velocity_tol,
538
            magnitude=magnitude,
539
            raise_error=raise_error,
540
        )
541

542
    elif orbit_type == "keplerian":
1✔
543

544
        diff, stats, error = testKeplerianOrbits(
1✔
545
            orbits_actual,
546
            orbits_desired,
547
            position_tol=position_tol,
548
            unitless_tol=unitless_tol,
549
            angle_tol=angle_tol,
550
            raise_error=raise_error,
551
        )
552

553
    elif orbit_type == "cometary":
×
554

555
        diff, stats, error = testCometaryOrbits(
×
556
            orbits_actual,
557
            orbits_desired,
558
            position_tol=position_tol,
559
            unitless_tol=unitless_tol,
560
            angle_tol=angle_tol,
561
            time_tol=time_tol,
562
            raise_error=raise_error,
563
        )
564

565
    else:
566
        err = "orbit_type should be one of {'cartesian', 'keplerian', 'cometary'}"
×
567
        raise ValueError(err)
×
568

569
    return diff, stats, error
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc