satkit 0.20.0__tar.gz → 0.20.2__tar.gz

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 (133) hide show
  1. {satkit-0.20.0 → satkit-0.20.2}/Cargo.toml +3 -2
  2. {satkit-0.20.0 → satkit-0.20.2}/MANIFEST.in +1 -0
  3. {satkit-0.20.0 → satkit-0.20.2}/PKG-INFO +1 -1
  4. satkit-0.20.2/benches/hotpaths.rs +189 -0
  5. {satkit-0.20.0 → satkit-0.20.2}/pyproject.toml +1 -1
  6. {satkit-0.20.0 → satkit-0.20.2}/python/Cargo.toml +4 -4
  7. {satkit-0.20.0 → satkit-0.20.2}/python/satkit.egg-info/PKG-INFO +1 -1
  8. {satkit-0.20.0 → satkit-0.20.2}/python/satkit.egg-info/SOURCES.txt +1 -0
  9. {satkit-0.20.0 → satkit-0.20.2}/python/src/pypropsettings.rs +1 -4
  10. {satkit-0.20.0 → satkit-0.20.2}/python/src/pysgp4.rs +4 -4
  11. {satkit-0.20.0 → satkit-0.20.2}/src/kepler.rs +21 -12
  12. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/tides.rs +6 -0
  13. {satkit-0.20.0 → satkit-0.20.2}/src/time/instant.rs +2 -2
  14. {satkit-0.20.0 → satkit-0.20.2}/src/time/timescale.rs +1 -1
  15. {satkit-0.20.0 → satkit-0.20.2}/LICENSE-APACHE +0 -0
  16. {satkit-0.20.0 → satkit-0.20.2}/LICENSE-MIT +0 -0
  17. {satkit-0.20.0 → satkit-0.20.2}/README.md +0 -0
  18. {satkit-0.20.0 → satkit-0.20.2}/build.rs +0 -0
  19. {satkit-0.20.0 → satkit-0.20.2}/python/build.rs +0 -0
  20. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/__init__.py +0 -0
  21. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/__init__.pyi +0 -0
  22. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/density.pyi +0 -0
  23. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/frametransform.pyi +0 -0
  24. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/jplephem.pyi +0 -0
  25. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/moon.pyi +0 -0
  26. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/planets.pyi +0 -0
  27. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/py.typed +0 -0
  28. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/satkit.pyi +0 -0
  29. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/spaceweather.pyi +0 -0
  30. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/sun.pyi +0 -0
  31. {satkit-0.20.0 → satkit-0.20.2}/python/satkit/utils.pyi +0 -0
  32. {satkit-0.20.0 → satkit-0.20.2}/python/satkit.egg-info/dependency_links.txt +0 -0
  33. {satkit-0.20.0 → satkit-0.20.2}/python/satkit.egg-info/requires.txt +0 -0
  34. {satkit-0.20.0 → satkit-0.20.2}/python/satkit.egg-info/top_level.txt +0 -0
  35. {satkit-0.20.0 → satkit-0.20.2}/python/src/lib.rs +0 -0
  36. {satkit-0.20.0 → satkit-0.20.2}/python/src/mod_utils.rs +0 -0
  37. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyconsts.rs +0 -0
  38. {satkit-0.20.0 → satkit-0.20.2}/python/src/pydensity.rs +0 -0
  39. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyduration.rs +0 -0
  40. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyframes.rs +0 -0
  41. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyframetransform.rs +0 -0
  42. {satkit-0.20.0 → satkit-0.20.2}/python/src/pygravity.rs +0 -0
  43. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyinstant.rs +0 -0
  44. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyitrfcoord.rs +0 -0
  45. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyjplephem.rs +0 -0
  46. {satkit-0.20.0 → satkit-0.20.2}/python/src/pykepler.rs +0 -0
  47. {satkit-0.20.0 → satkit-0.20.2}/python/src/pylambert.rs +0 -0
  48. {satkit-0.20.0 → satkit-0.20.2}/python/src/pylpephem_moon.rs +0 -0
  49. {satkit-0.20.0 → satkit-0.20.2}/python/src/pylpephem_planets.rs +0 -0
  50. {satkit-0.20.0 → satkit-0.20.2}/python/src/pylpephem_sun.rs +0 -0
  51. {satkit-0.20.0 → satkit-0.20.2}/python/src/pynrlmsise.rs +0 -0
  52. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyomm.rs +0 -0
  53. {satkit-0.20.0 → satkit-0.20.2}/python/src/pypropagate.rs +0 -0
  54. {satkit-0.20.0 → satkit-0.20.2}/python/src/pypropresult.rs +0 -0
  55. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyquaternion.rs +0 -0
  56. {satkit-0.20.0 → satkit-0.20.2}/python/src/pysatproperties.rs +0 -0
  57. {satkit-0.20.0 → satkit-0.20.2}/python/src/pysatstate.rs +0 -0
  58. {satkit-0.20.0 → satkit-0.20.2}/python/src/pysolarsystem.rs +0 -0
  59. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyspaceweather.rs +0 -0
  60. {satkit-0.20.0 → satkit-0.20.2}/python/src/pythrust.rs +0 -0
  61. {satkit-0.20.0 → satkit-0.20.2}/python/src/pytle.rs +0 -0
  62. {satkit-0.20.0 → satkit-0.20.2}/python/src/pytlefitstatus.rs +0 -0
  63. {satkit-0.20.0 → satkit-0.20.2}/python/src/pyutils.rs +0 -0
  64. {satkit-0.20.0 → satkit-0.20.2}/setup.cfg +0 -0
  65. {satkit-0.20.0 → satkit-0.20.2}/src/consts.rs +0 -0
  66. {satkit-0.20.0 → satkit-0.20.2}/src/earth_orientation_params.rs +0 -0
  67. {satkit-0.20.0 → satkit-0.20.2}/src/earthgravity.rs +0 -0
  68. {satkit-0.20.0 → satkit-0.20.2}/src/error.rs +0 -0
  69. {satkit-0.20.0 → satkit-0.20.2}/src/frames.rs +0 -0
  70. {satkit-0.20.0 → satkit-0.20.2}/src/frametransform/dispatch.rs +0 -0
  71. {satkit-0.20.0 → satkit-0.20.2}/src/frametransform/error.rs +0 -0
  72. {satkit-0.20.0 → satkit-0.20.2}/src/frametransform/ierstable.rs +0 -0
  73. {satkit-0.20.0 → satkit-0.20.2}/src/frametransform/mod.rs +0 -0
  74. {satkit-0.20.0 → satkit-0.20.2}/src/frametransform/qcirs2gcrs.rs +0 -0
  75. {satkit-0.20.0 → satkit-0.20.2}/src/itrfcoord.rs +0 -0
  76. {satkit-0.20.0 → satkit-0.20.2}/src/jplephem.rs +0 -0
  77. {satkit-0.20.0 → satkit-0.20.2}/src/lambert.rs +0 -0
  78. {satkit-0.20.0 → satkit-0.20.2}/src/lib.rs +0 -0
  79. {satkit-0.20.0 → satkit-0.20.2}/src/lpephem/mod.rs +0 -0
  80. {satkit-0.20.0 → satkit-0.20.2}/src/lpephem/moon.rs +0 -0
  81. {satkit-0.20.0 → satkit-0.20.2}/src/lpephem/planets.rs +0 -0
  82. {satkit-0.20.0 → satkit-0.20.2}/src/lpephem/sun.rs +0 -0
  83. {satkit-0.20.0 → satkit-0.20.2}/src/mathtypes.rs +0 -0
  84. {satkit-0.20.0 → satkit-0.20.2}/src/nrlmsise.rs +0 -0
  85. {satkit-0.20.0 → satkit-0.20.2}/src/omm/error.rs +0 -0
  86. {satkit-0.20.0 → satkit-0.20.2}/src/omm/mod.rs +0 -0
  87. {satkit-0.20.0 → satkit-0.20.2}/src/omm/xml.rs +0 -0
  88. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/drag.rs +0 -0
  89. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/error.rs +0 -0
  90. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/mod.rs +0 -0
  91. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/ode/gauss_jackson.rs +0 -0
  92. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/ode/mod.rs +0 -0
  93. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/point_gravity.rs +0 -0
  94. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/precomputed.rs +0 -0
  95. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/propagator.rs +0 -0
  96. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/relativity.rs +0 -0
  97. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/satproperties.rs +0 -0
  98. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/satstate.rs +0 -0
  99. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/settings.rs +0 -0
  100. {satkit-0.20.0 → satkit-0.20.2}/src/orbitprop/thrust.rs +0 -0
  101. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/dpper.rs +0 -0
  102. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/dscom.rs +0 -0
  103. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/dsinit.rs +0 -0
  104. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/dspace.rs +0 -0
  105. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/error.rs +0 -0
  106. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/getgravconst.rs +0 -0
  107. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/initl.rs +0 -0
  108. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/mod.rs +0 -0
  109. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/satrec.rs +0 -0
  110. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/sgp4_impl.rs +0 -0
  111. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/sgp4_lowlevel.rs +0 -0
  112. {satkit-0.20.0 → satkit-0.20.2}/src/sgp4/sgp4init.rs +0 -0
  113. {satkit-0.20.0 → satkit-0.20.2}/src/solar_cycle_forecast.rs +0 -0
  114. {satkit-0.20.0 → satkit-0.20.2}/src/solarsystem.rs +0 -0
  115. {satkit-0.20.0 → satkit-0.20.2}/src/spaceweather.rs +0 -0
  116. {satkit-0.20.0 → satkit-0.20.2}/src/time/chrono.rs +0 -0
  117. {satkit-0.20.0 → satkit-0.20.2}/src/time/duration.rs +0 -0
  118. {satkit-0.20.0 → satkit-0.20.2}/src/time/instant_err.rs +0 -0
  119. {satkit-0.20.0 → satkit-0.20.2}/src/time/instant_ops.rs +0 -0
  120. {satkit-0.20.0 → satkit-0.20.2}/src/time/instantparse.rs +0 -0
  121. {satkit-0.20.0 → satkit-0.20.2}/src/time/mod.rs +0 -0
  122. {satkit-0.20.0 → satkit-0.20.2}/src/time/tests.rs +0 -0
  123. {satkit-0.20.0 → satkit-0.20.2}/src/time/timelike.rs +0 -0
  124. {satkit-0.20.0 → satkit-0.20.2}/src/time/weekday.rs +0 -0
  125. {satkit-0.20.0 → satkit-0.20.2}/src/tle/error.rs +0 -0
  126. {satkit-0.20.0 → satkit-0.20.2}/src/tle/fitting.rs +0 -0
  127. {satkit-0.20.0 → satkit-0.20.2}/src/tle/mod.rs +0 -0
  128. {satkit-0.20.0 → satkit-0.20.2}/src/utils/datadir.rs +0 -0
  129. {satkit-0.20.0 → satkit-0.20.2}/src/utils/download.rs +0 -0
  130. {satkit-0.20.0 → satkit-0.20.2}/src/utils/mod.rs +0 -0
  131. {satkit-0.20.0 → satkit-0.20.2}/src/utils/singleton.rs +0 -0
  132. {satkit-0.20.0 → satkit-0.20.2}/src/utils/test.rs +0 -0
  133. {satkit-0.20.0 → satkit-0.20.2}/src/utils/update_data.rs +0 -0
@@ -3,7 +3,7 @@ members = [".", "python"]
3
3
 
4
4
  [package]
5
5
  name = "satkit"
6
- version = "0.20.0"
6
+ version = "0.20.2"
7
7
  edition = "2021"
8
8
  description = "Satellite Toolkit"
9
9
  readme = "README.md"
@@ -28,7 +28,7 @@ ureq = { version = "3.1.2", optional = true }
28
28
  process_path = "0.1.4"
29
29
  serde = { version = "1.0", features = ["derive"] }
30
30
  serde_json = "1.0"
31
- quick-xml = { version = "0.38", features = ["serialize"], optional = true }
31
+ quick-xml = { version = "0.41", features = ["serialize"], optional = true }
32
32
  chrono = { version = "0.4", optional = true }
33
33
 
34
34
  [dev-dependencies]
@@ -37,6 +37,7 @@ rand = "0.9.2"
37
37
  approx = "0.5"
38
38
  rand_distr = "0.5.1"
39
39
  criterion = "0.8.2"
40
+ proptest = "1"
40
41
 
41
42
  [[bench]]
42
43
  name = "hotpaths"
@@ -6,6 +6,7 @@ include README.md
6
6
  include pyproject.toml
7
7
  include build.rs
8
8
  recursive-include src *.rs
9
+ recursive-include benches *.rs
9
10
  recursive-include python/src *.rs
10
11
  include python/Cargo.toml
11
12
  include python/build.rs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satkit
3
- Version: 0.20.0
3
+ Version: 0.20.2
4
4
  Summary: Satellite Orbital Dynamics Toolkit
5
5
  Author-email: Steven Michael <ssmichael@gmail.com>
6
6
  Maintainer-email: Steven Michael <ssmichael@gmail.com>
@@ -0,0 +1,189 @@
1
+ //! Criterion benchmarks for satkit hot paths
2
+ //!
3
+ //! Covers the per-call costs that dominate real workloads: high-precision
4
+ //! propagation (per-regime and per-integrator), SGP4, frame transforms,
5
+ //! spherical-harmonic gravity, JPL ephemeris lookup, and the NRLMSISE-00
6
+ //! density model.
7
+ //!
8
+ //! Data files (EOP, gravity models, JPL ephemeris, space weather) are
9
+ //! resolved through the normal `satkit::utils::datadir()` discovery, same
10
+ //! as the test suite. Run with `cargo bench`.
11
+
12
+ use criterion::{criterion_group, criterion_main, Criterion};
13
+ use std::hint::black_box;
14
+
15
+ use satkit::consts;
16
+ use satkit::frametransform;
17
+ use satkit::mathtypes::{Matrix6, Matrix67, Vector3, Vector6};
18
+ use satkit::orbitprop::{propagate, Integrator, PropSettings, SatPropertiesSimple};
19
+ use satkit::{Duration, Instant};
20
+
21
+ fn epoch() -> Instant {
22
+ Instant::from_rfc3339("2024-01-01T00:00:00Z").unwrap()
23
+ }
24
+
25
+ /// Circular orbit state at radius `r` (meters) with inclination `incl` (radians)
26
+ fn circular_state(r: f64, incl: f64) -> Vector6 {
27
+ let v = (consts::MU_EARTH / r).sqrt();
28
+ numeris::vector![r, 0.0, 0.0, 0.0, v * incl.cos(), v * incl.sin()]
29
+ }
30
+
31
+ const ISS_INCL_RAD: f64 = 51.6 * std::f64::consts::PI / 180.0;
32
+
33
+ fn bench_propagation(c: &mut Criterion) {
34
+ let mut group = c.benchmark_group("propagation");
35
+ group
36
+ .sample_size(10)
37
+ .warm_up_time(std::time::Duration::from_secs(2))
38
+ .measurement_time(std::time::Duration::from_secs(20));
39
+
40
+ let t0 = epoch();
41
+ let t1 = t0 + Duration::from_days(1.0);
42
+
43
+ // LEO with drag + full default force model, adaptive RKV 9(8)
44
+ let leo = circular_state(consts::WGS84_A + 500.0e3, ISS_INCL_RAD);
45
+ let satprops = SatPropertiesSimple::new(0.02, 0.01);
46
+ let settings = PropSettings::default();
47
+ group.bench_function("leo_drag_1day_rkv98", |b| {
48
+ b.iter(|| propagate(black_box(&leo), &t0, &t1, &settings, Some(&satprops)).unwrap())
49
+ });
50
+
51
+ // GEO, sun/moon gravity dominated, adaptive RKV 9(8)
52
+ let geo = circular_state(consts::GEO_R, 0.0);
53
+ group.bench_function("geo_1day_rkv98", |b| {
54
+ b.iter(|| propagate(black_box(&geo), &t0, &t1, &settings, None).unwrap())
55
+ });
56
+
57
+ // GEO, fixed-step Gauss-Jackson 8
58
+ let settings_gj8 = PropSettings {
59
+ integrator: Integrator::GaussJackson8,
60
+ gj_step_seconds: 300.0,
61
+ ..PropSettings::default()
62
+ };
63
+ group.bench_function("geo_1day_gj8", |b| {
64
+ b.iter(|| propagate(black_box(&geo), &t0, &t1, &settings_gj8, None).unwrap())
65
+ });
66
+
67
+ // LEO with 6x6 state transition matrix (C = 7 state columns)
68
+ let mut leo_stm = Matrix67::zeros();
69
+ leo_stm.set_block(0, 0, &leo);
70
+ leo_stm.set_block(0, 1, &Matrix6::eye());
71
+ group.bench_function("leo_stm_1day_rkv98", |b| {
72
+ b.iter(|| propagate(black_box(&leo_stm), &t0, &t1, &settings, Some(&satprops)).unwrap())
73
+ });
74
+
75
+ group.finish();
76
+ }
77
+
78
+ fn bench_sgp4(c: &mut Criterion) {
79
+ let mut group = c.benchmark_group("sgp4");
80
+
81
+ let line0 = "0 INTELSAT 902";
82
+ let line1 = "1 26900U 01039A 06106.74503247 .00000045 00000-0 10000-3 0 8290";
83
+ let line2 = "2 26900 0.0164 266.5378 0003319 86.1794 182.2590 1.00273847 16981";
84
+ let tle = satkit::TLE::load_3line(line0, line1, line2).unwrap();
85
+
86
+ // 1 day of output at 1-minute cadence
87
+ let times: Vec<Instant> = (0..1441)
88
+ .map(|i| tle.epoch + Duration::from_seconds(60.0 * i as f64))
89
+ .collect();
90
+
91
+ // Includes per-TLE SGP4 initialization (fresh TLE each iteration)
92
+ group.bench_function("init_plus_1day_1min", |b| {
93
+ b.iter(|| {
94
+ let mut t = tle.clone();
95
+ satkit::sgp4::sgp4(&mut t, black_box(times.as_slice())).unwrap()
96
+ })
97
+ });
98
+
99
+ // Cached init state (same TLE reused), measures the propagation only
100
+ let mut warm = tle.clone();
101
+ let _ = satkit::sgp4::sgp4(&mut warm, &times[..1]).unwrap();
102
+ group.bench_function("cached_1day_1min", |b| {
103
+ b.iter(|| satkit::sgp4::sgp4(&mut warm, black_box(times.as_slice())).unwrap())
104
+ });
105
+
106
+ group.finish();
107
+ }
108
+
109
+ fn bench_frametransform(c: &mut Criterion) {
110
+ let mut group = c.benchmark_group("frametransform");
111
+ let tm = epoch();
112
+
113
+ // Warm the EOP singleton so first-load cost isn't measured
114
+ let _ = frametransform::qgcrf2itrf(&tm);
115
+
116
+ group.bench_function("qgcrf2itrf", |b| {
117
+ b.iter(|| frametransform::qgcrf2itrf(black_box(&tm)))
118
+ });
119
+ group.bench_function("qgcrf2itrf_approx", |b| {
120
+ b.iter(|| frametransform::qgcrf2itrf_approx(black_box(&tm)))
121
+ });
122
+ group.bench_function("qteme2itrf", |b| {
123
+ b.iter(|| frametransform::qteme2itrf(black_box(&tm)))
124
+ });
125
+ group.bench_function("gmst", |b| b.iter(|| frametransform::gmst(black_box(&tm))));
126
+
127
+ group.finish();
128
+ }
129
+
130
+ fn bench_earthgravity(c: &mut Criterion) {
131
+ let mut group = c.benchmark_group("earthgravity");
132
+
133
+ // LEO position in ITRF (slightly off-axis to exercise all terms)
134
+ let pos: Vector3 = numeris::vector![consts::WGS84_A + 400.0e3, 1000.0e3, 2000.0e3];
135
+ let gravity = satkit::earthgravity::jgm3();
136
+
137
+ group.bench_function("accel_deg4", |b| {
138
+ b.iter(|| gravity.accel(black_box(&pos), 4, 4))
139
+ });
140
+ group.bench_function("accel_deg16", |b| {
141
+ b.iter(|| gravity.accel(black_box(&pos), 16, 16))
142
+ });
143
+ group.bench_function("accel_and_partials_deg4", |b| {
144
+ b.iter(|| gravity.accel_and_partials(black_box(&pos), 4, 4))
145
+ });
146
+
147
+ group.finish();
148
+ }
149
+
150
+ fn bench_jplephem(c: &mut Criterion) {
151
+ let mut group = c.benchmark_group("jplephem");
152
+ let tm = epoch();
153
+
154
+ // Warm the ephemeris singleton
155
+ let _ = satkit::jplephem::geocentric_pos(satkit::SolarSystem::Moon, &tm);
156
+
157
+ group.bench_function("moon_geocentric_pos", |b| {
158
+ b.iter(|| satkit::jplephem::geocentric_pos(satkit::SolarSystem::Moon, black_box(&tm)))
159
+ });
160
+ group.bench_function("sun_geocentric_state", |b| {
161
+ b.iter(|| satkit::jplephem::geocentric_state(satkit::SolarSystem::Sun, black_box(&tm)))
162
+ });
163
+
164
+ group.finish();
165
+ }
166
+
167
+ fn bench_nrlmsise(c: &mut Criterion) {
168
+ let mut group = c.benchmark_group("nrlmsise");
169
+ let tm = epoch();
170
+
171
+ group.bench_function("density_400km", |b| {
172
+ b.iter(|| {
173
+ satkit::nrlmsise::nrlmsise(black_box(400.0), Some(0.5), Some(0.5), Some(&tm), true)
174
+ })
175
+ });
176
+
177
+ group.finish();
178
+ }
179
+
180
+ criterion_group!(
181
+ benches,
182
+ bench_propagation,
183
+ bench_sgp4,
184
+ bench_frametransform,
185
+ bench_earthgravity,
186
+ bench_jplephem,
187
+ bench_nrlmsise
188
+ );
189
+ criterion_main!(benches);
@@ -9,7 +9,7 @@ requires-python = ">= 3.10"
9
9
  authors = [{ name = "Steven Michael", email = "ssmichael@gmail.com" }]
10
10
  maintainers = [{ name = "Steven Michael", email = "ssmichael@gmail.com" }]
11
11
  readme = "README.md"
12
- version = "0.20.0"
12
+ version = "0.20.2"
13
13
  license = "MIT OR Apache-2.0"
14
14
  description = "Satellite Orbital Dynamics Toolkit"
15
15
  keywords = [
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "satkit-python"
3
- version = "0.20.0"
3
+ version = "0.20.2"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -10,8 +10,8 @@ crate-type = ["cdylib"]
10
10
 
11
11
  [dependencies]
12
12
  satkit = { path = "..", features = ["download"] }
13
- pyo3 = { version = "0.28.2", features = ["extension-module", "anyhow"] }
14
- numpy = "0.28"
13
+ pyo3 = { version = "0.29", features = ["extension-module", "anyhow"] }
14
+ numpy = "0.29"
15
15
  numeris = { version = "0.5.14", features = ["serde", "ode"] }
16
16
  anyhow = "1"
17
17
  serde = { version = "1.0", features = ["derive"] }
@@ -19,4 +19,4 @@ serde-pickle = "1.2.0"
19
19
  process_path = "0.1.4"
20
20
 
21
21
  [build-dependencies]
22
- pyo3-build-config = "0.28.2"
22
+ pyo3-build-config = "0.29"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satkit
3
- Version: 0.20.0
3
+ Version: 0.20.2
4
4
  Summary: Satellite Orbital Dynamics Toolkit
5
5
  Author-email: Steven Michael <ssmichael@gmail.com>
6
6
  Maintainer-email: Steven Michael <ssmichael@gmail.com>
@@ -5,6 +5,7 @@ MANIFEST.in
5
5
  README.md
6
6
  build.rs
7
7
  pyproject.toml
8
+ benches/hotpaths.rs
8
9
  python/Cargo.toml
9
10
  python/build.rs
10
11
  python/satkit/__init__.py
@@ -394,10 +394,7 @@ impl PyPropSettings {
394
394
  Some(d.0.as_seconds())
395
395
  } else if let Ok(secs) = obj.extract::<f64>() {
396
396
  Some(secs)
397
- } else if let Ok(delta) = {
398
- #[allow(deprecated)]
399
- obj.downcast::<PyDelta>()
400
- } {
397
+ } else if let Ok(delta) = obj.cast::<PyDelta>() {
401
398
  Some(
402
399
  delta.get_days() as f64 * 86400.0
403
400
  + delta.get_seconds() as f64
@@ -391,7 +391,7 @@ pub fn sgp4(
391
391
  // TLE sources keep a handle to the originating Python object so the
392
392
  // cached SGP4 init state can be written back afterward.
393
393
  enum Sgp4Source {
394
- Tle(Py<PyTLE>, satkit::TLE),
394
+ Tle(Py<PyTLE>, Box<satkit::TLE>),
395
395
  Omm(Box<satkit::omm::OMM>),
396
396
  }
397
397
 
@@ -402,7 +402,7 @@ pub fn sgp4(
402
402
  let pytle: Py<PyTLE> = item.extract().map_err(|e| {
403
403
  pyo3::exceptions::PyValueError::new_err(format!("Invalid TLE: {}", e))
404
404
  })?;
405
- let rtle = pytle.borrow(item.py()).0.clone();
405
+ let rtle = Box::new(pytle.borrow(item.py()).0.clone());
406
406
  Ok(Sgp4Source::Tle(pytle, rtle))
407
407
  } else if item.is_instance_of::<PyDict>() {
408
408
  let dict: &Bound<'_, PyDict> = item.cast().map_err(|e| {
@@ -428,7 +428,7 @@ pub fn sgp4(
428
428
  .map(|src| -> Result<psgp4::SGP4State> {
429
429
  match src {
430
430
  Sgp4Source::Tle(_, rtle) => {
431
- Ok(psgp4::sgp4_full(rtle, tmarray.as_slice(), gc, om)?)
431
+ Ok(psgp4::sgp4_full(rtle.as_mut(), tmarray.as_slice(), gc, om)?)
432
432
  }
433
433
  Sgp4Source::Omm(omm) => {
434
434
  Ok(psgp4::sgp4_full(omm.as_mut(), tmarray.as_slice(), gc, om)?)
@@ -441,7 +441,7 @@ pub fn sgp4(
441
441
  // Write the TLEs back to preserve their cached SGP4 init state
442
442
  for src in &sources {
443
443
  if let Sgp4Source::Tle(pytle, rtle) = src {
444
- pytle.borrow_mut(tle.py()).0 = rtle.clone();
444
+ pytle.borrow_mut(tle.py()).0 = rtle.as_ref().clone();
445
445
  }
446
446
  }
447
447
 
@@ -76,23 +76,32 @@ pub struct Kepler {
76
76
  // Convert mean to eccentric anomaly
77
77
  // iterative solution required
78
78
  fn mean2eccentric(m: f64, eccen: f64) -> f64 {
79
- use std::f64::consts::PI;
79
+ use std::f64::consts::TAU;
80
+ // Range-reduce the mean anomaly to [0, 2π). Kepler's equation shifts by
81
+ // 2πk in E and M together, so the solution for the reduced M is shifted
82
+ // back at the end. Without this, an unwrapped M (e.g. after propagating
83
+ // multiple revolutions) puts the naive initial guess in a near-flat region
84
+ // of the equation at high eccentricity, where Newton's trajectory turns
85
+ // chaotic and can exhaust the iteration cap with a wildly wrong root.
86
+ let k = (m / TAU).floor();
87
+ let mr = m - k * TAU;
88
+
89
+ // Danby (1987) initial guess: E₀ = M + 0.85·e·sign(sin M). Together with
90
+ // the range reduction this keeps plain Newton convergent in < ~10
91
+ // iterations for all eccentricities below 1, including e > 0.9.
80
92
  #[allow(non_snake_case)]
81
- let mut E = match (m > PI) || ((m < 0.0) && (m > -PI)) {
82
- true => m - eccen,
83
- false => m + eccen,
84
- };
85
- // Newton's method converges quadratically for bound orbits (< ~10 steps
86
- // for e < 0.99). Cap the iteration count so a pathological eccentricity
87
- // (e >= 1, where the step can go non-finite) cannot spin forever.
88
- for _ in 0..30 {
89
- let de = eccen.mul_add(E.sin(), m - E) / eccen.mul_add(-E.cos(), 1.0);
93
+ let mut E = mr + 0.85 * eccen * if mr.sin() >= 0.0 { 1.0 } else { -1.0 };
94
+
95
+ // Cap the iteration count so a pathological eccentricity (e >= 1, where
96
+ // the step can go non-finite) cannot spin forever.
97
+ for _ in 0..50 {
98
+ let de = eccen.mul_add(E.sin(), mr - E) / eccen.mul_add(-E.cos(), 1.0);
90
99
  E += de;
91
- if de.abs() < 1.0e-12 {
100
+ if de.abs() < 1.0e-13 {
92
101
  break;
93
102
  }
94
103
  }
95
- E
104
+ E + k * TAU
96
105
  }
97
106
 
98
107
  fn eccentric2true(ea: f64, eccen: f64) -> f64 {
@@ -51,6 +51,9 @@ pub struct TideDeltas {
51
51
  // Index convention: K_RE[n][m], K_IM[n][m].
52
52
  // Entries outside the (n,m) range listed in Table 6.3 are zero.
53
53
  // ---------------------------------------------------------------------------
54
+ // The k_22 Love number (0.30102) coincidentally resembles log10(2); it is a
55
+ // physical constant from Table 6.3, not a math constant.
56
+ #[allow(clippy::approx_constant)]
54
57
  const K_RE: [[f64; 4]; 4] = [
55
58
  [0.0, 0.0, 0.0, 0.0],
56
59
  [0.0, 0.0, 0.0, 0.0],
@@ -71,7 +74,10 @@ const K_PLUS: [f64; 3] = [-0.00089, -0.00080, -0.00057];
71
74
  // Fully-normalized associated-Legendre normalization factors
72
75
  // N_nm = sqrt((2 - δ_0m)(2n+1)(n-m)!/(n+m)!). Constants chosen to allow
73
76
  // the recursion-free closed-form evaluation of P̄_nm used here.
77
+ // Written to full precision deliberately (clippy flags a digit beyond f64's
78
+ // round-trip length on N21; keeping the mathematical value verbatim).
74
79
  // ---------------------------------------------------------------------------
80
+ #[allow(clippy::excessive_precision)]
75
81
  const N20: f64 = 2.2360679774997896; // sqrt(5)
76
82
  const N21: f64 = 1.2909944487358056; // sqrt(5/3)
77
83
  const N22: f64 = 0.6454972243679028; // sqrt(5/12)
@@ -320,7 +320,7 @@ impl Instant {
320
320
  TimeScale::UT1 => {
321
321
  // This will be approximately correct for computing ut1
322
322
  let eop = crate::earth_orientation_params::eop_from_mjd_utc_or_zero(mjd);
323
- let dut1 = eop[0] as f64;
323
+ let dut1 = eop[0];
324
324
  Self::from_mjd_with_scale(mjd - dut1 / 86_400.0, TimeScale::UTC)
325
325
  }
326
326
  TimeScale::GPS => {
@@ -415,7 +415,7 @@ impl Instant {
415
415
  TimeScale::UT1 => {
416
416
  let mjd_utc = self.as_mjd_utc();
417
417
  let eop = crate::earth_orientation_params::eop_from_mjd_utc_or_zero(mjd_utc);
418
- let dut1 = eop[0] as f64;
418
+ let dut1 = eop[0];
419
419
  mjd_utc + dut1 / 86_400.0
420
420
  }
421
421
  TimeScale::TAI => (self.raw - Self::MJD_EPOCH.raw) as f64 / 86_400_000_000.0,
@@ -10,7 +10,7 @@
10
10
  /// * `TDB` - Barycentric Dynamical Time
11
11
  /// * `Invalid` - Invalid
12
12
  ///
13
- #[derive(PartialEq, Eq, Debug)]
13
+ #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
14
14
  #[allow(clippy::upper_case_acronyms)]
15
15
  pub enum TimeScale {
16
16
  /// Invalid
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes