open-space-toolkit-astrodynamics 15.1.0__py312-none-manylinux2014_x86_64.whl → 15.2.1__py312-none-manylinux2014_x86_64.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.
@@ -0,0 +1,215 @@
1
+ # Apache License 2.0
2
+
3
+ import pytest
4
+
5
+ import pandas as pd
6
+
7
+ from ostk.core.type import Real
8
+ from ostk.core.type import Integer
9
+
10
+ from ostk.physics.coordinate import Frame
11
+
12
+ from ostk.astrodynamics.solver import LeastSquaresSolver
13
+ from ostk.astrodynamics.trajectory import State
14
+ from ostk.astrodynamics.trajectory import StateBuilder
15
+ from ostk.astrodynamics.trajectory import Orbit
16
+ from ostk.astrodynamics.trajectory.orbit.model.sgp4 import TLE
17
+ from ostk.astrodynamics.estimator import TLESolver
18
+ from ostk.astrodynamics.dataframe import generate_states_from_dataframe
19
+
20
+
21
+ @pytest.fixture
22
+ def least_squares_solver() -> LeastSquaresSolver:
23
+ return LeastSquaresSolver.default()
24
+
25
+
26
+ @pytest.fixture
27
+ def tle_solver(least_squares_solver: LeastSquaresSolver) -> TLESolver:
28
+ return TLESolver(
29
+ solver=least_squares_solver,
30
+ satellite_number=25544, # ISS NORAD ID
31
+ international_designator="98067A", # ISS Int'l Designator
32
+ revolution_number=12345,
33
+ estimate_b_star=True,
34
+ )
35
+
36
+
37
+ @pytest.fixture
38
+ def initial_tle() -> TLE:
39
+ return TLE(
40
+ "1 25544U 98067A 22253.00000622 .00000000 00000-0 71655-1 0 02",
41
+ "2 25544 97.5641 21.8296 0012030 155.5301 309.4836 15.14446734123455",
42
+ )
43
+
44
+
45
+ @pytest.fixture
46
+ def initial_state(observations: list[State]) -> State:
47
+ return observations[0]
48
+
49
+
50
+ @pytest.fixture
51
+ def initial_state_with_b_star(observations: list[State]) -> tuple[State, float]:
52
+ return observations[0], 1e-4
53
+
54
+
55
+ @pytest.fixture
56
+ def observations() -> list[State]:
57
+ return generate_states_from_dataframe(
58
+ pd.read_csv(
59
+ "/app/test/OpenSpaceToolkit/Astrodynamics/Estimator/OrbitDeterminationSolverData/gnss_data.csv"
60
+ ),
61
+ reference_frame=Frame.ITRF(),
62
+ )
63
+
64
+
65
+ class TestTLESolver:
66
+ def test_constructor(
67
+ self,
68
+ tle_solver: TLESolver,
69
+ ):
70
+ assert isinstance(tle_solver, TLESolver)
71
+ assert tle_solver.access_satellite_number() == 25544
72
+ assert tle_solver.access_international_designator() == "98067A"
73
+ assert tle_solver.access_revolution_number() == 12345
74
+ assert tle_solver.access_estimate_b_star() is True
75
+
76
+ def test_constructor_defaults(self):
77
+ solver = TLESolver()
78
+ assert solver.access_satellite_number() == 0
79
+ assert solver.access_international_designator() == "00001A"
80
+ assert solver.access_revolution_number() == 0
81
+ assert solver.access_estimate_b_star() is True
82
+
83
+ def test_access_methods(self, tle_solver: TLESolver):
84
+ assert isinstance(tle_solver.access_solver(), LeastSquaresSolver)
85
+ assert isinstance(tle_solver.access_default_b_star(), Real)
86
+ assert isinstance(
87
+ tle_solver.access_first_derivative_mean_motion_divided_by_2(), Real
88
+ )
89
+ assert isinstance(
90
+ tle_solver.access_second_derivative_mean_motion_divided_by_6(), Real
91
+ )
92
+ assert isinstance(tle_solver.access_ephemeris_type(), Integer)
93
+ assert isinstance(tle_solver.access_element_set_number(), Integer)
94
+ assert isinstance(tle_solver.access_tle_state_builder(), StateBuilder)
95
+
96
+ def test_estimate_from_tle(
97
+ self,
98
+ tle_solver: TLESolver,
99
+ initial_tle: TLE,
100
+ observations: list[State],
101
+ ):
102
+ analysis: TLESolver.Analysis = tle_solver.estimate(
103
+ initial_guess=initial_tle,
104
+ observations=observations,
105
+ )
106
+
107
+ assert isinstance(analysis, TLESolver.Analysis)
108
+ assert isinstance(analysis.estimated_tle, TLE)
109
+ assert isinstance(analysis.solver_analysis, LeastSquaresSolver.Analysis)
110
+
111
+ assert analysis.solver_analysis.termination_criteria == "RMS Update Threshold"
112
+
113
+ def test_estimate_from_state_b_star(
114
+ self,
115
+ tle_solver: TLESolver,
116
+ initial_state: State,
117
+ observations: list[State],
118
+ ):
119
+ analysis: TLESolver.Analysis = tle_solver.estimate(
120
+ initial_guess=(initial_state, 1e-4),
121
+ observations=observations,
122
+ )
123
+ assert isinstance(analysis, TLESolver.Analysis)
124
+ assert isinstance(analysis.estimated_tle, TLE)
125
+ assert isinstance(analysis.solver_analysis, LeastSquaresSolver.Analysis)
126
+
127
+ assert analysis.solver_analysis.termination_criteria == "RMS Update Threshold"
128
+
129
+ def test_estimate_from_state(
130
+ self,
131
+ initial_state: State,
132
+ observations: list[State],
133
+ ):
134
+ tle_solver_no_b_star = TLESolver(
135
+ satellite_number=25544,
136
+ international_designator="98067A",
137
+ revolution_number=12345,
138
+ estimate_b_star=False,
139
+ )
140
+ analysis: TLESolver.Analysis = tle_solver_no_b_star.estimate(
141
+ initial_guess=initial_state,
142
+ observations=observations,
143
+ )
144
+ assert isinstance(analysis, TLESolver.Analysis)
145
+ assert isinstance(analysis.estimated_tle, TLE)
146
+ assert isinstance(analysis.solver_analysis, LeastSquaresSolver.Analysis)
147
+
148
+ assert analysis.solver_analysis.termination_criteria == "RMS Update Threshold"
149
+
150
+ def test_estimate_invalid_initial_guess(
151
+ self,
152
+ tle_solver: TLESolver,
153
+ observations: list[State],
154
+ ):
155
+ with pytest.raises(RuntimeError) as e:
156
+ tle_solver.estimate(initial_guess="invalid", observations=observations)
157
+ assert "Initial guess must be a TLE, tuple[State, float], or State." in str(
158
+ e.value
159
+ )
160
+
161
+ def test_estimate_invalid_state_only(
162
+ self,
163
+ tle_solver: TLESolver,
164
+ initial_state: State,
165
+ observations: list[State],
166
+ ):
167
+ with pytest.raises(RuntimeError) as e:
168
+ tle_solver.estimate(initial_guess=initial_state, observations=observations)
169
+ assert (
170
+ "Initial guess must be a TLE or (State, B*) when also estimating B*."
171
+ in str(e.value)
172
+ )
173
+
174
+ def test_estimate_no_observations(
175
+ self,
176
+ tle_solver: TLESolver,
177
+ initial_state: State,
178
+ ):
179
+ with pytest.raises(Exception):
180
+ tle_solver.estimate(initial_guess=initial_state, observations=[])
181
+
182
+ def test_estimate_orbit(
183
+ self,
184
+ initial_state: State,
185
+ observations: list[State],
186
+ ):
187
+ tle_solver_no_b_star = TLESolver(
188
+ satellite_number=25544,
189
+ international_designator="98067A",
190
+ revolution_number=12345,
191
+ estimate_b_star=False,
192
+ )
193
+ orbit: Orbit = tle_solver_no_b_star.estimate_orbit(
194
+ initial_guess=initial_state,
195
+ observations=observations,
196
+ )
197
+
198
+ assert isinstance(orbit, Orbit)
199
+
200
+ def test_fit_with_different_frames(
201
+ self,
202
+ tle_solver: TLESolver,
203
+ initial_state_with_b_star: tuple[State, float],
204
+ observations: list[State],
205
+ ):
206
+ # Convert observations to TEME frame
207
+ teme_observations = [obs.in_frame(Frame.TEME()) for obs in observations]
208
+
209
+ analysis: TLESolver.Analysis = tle_solver.estimate(
210
+ initial_guess=initial_state_with_b_star, observations=teme_observations
211
+ )
212
+ assert isinstance(analysis, TLESolver.Analysis)
213
+ assert isinstance(analysis.estimated_tle, TLE)
214
+
215
+ assert analysis.solver_analysis.termination_criteria == "RMS Update Threshold"
@@ -0,0 +1,334 @@
1
+ # Apache License 2.0
2
+
3
+ from typing import Callable
4
+
5
+ import pytest
6
+ import numpy as np
7
+
8
+ from ostk.core.type import Real
9
+ from ostk.core.type import String
10
+
11
+ from ostk.physics.time import Instant
12
+ from ostk.physics.time import Duration
13
+ from ostk.physics.coordinate import Frame
14
+
15
+ from ostk.astrodynamics.solver import LeastSquaresSolver
16
+ from ostk.astrodynamics.solver import FiniteDifferenceSolver
17
+ from ostk.astrodynamics.trajectory import State
18
+ from ostk.astrodynamics.trajectory.state import CoordinateSubset
19
+
20
+
21
+ @pytest.fixture
22
+ def rms_error() -> float:
23
+ return 1.0
24
+
25
+
26
+ @pytest.fixture
27
+ def x_hat() -> np.ndarray:
28
+ return np.array([1.0, 0.0])
29
+
30
+
31
+ @pytest.fixture
32
+ def step(
33
+ rms_error: float,
34
+ x_hat: np.ndarray,
35
+ ) -> LeastSquaresSolver.Step:
36
+ return LeastSquaresSolver.Step(
37
+ rms_error=rms_error,
38
+ x_hat=x_hat,
39
+ )
40
+
41
+
42
+ @pytest.fixture
43
+ def termination_criteria() -> str:
44
+ return "RMS Update Threshold"
45
+
46
+
47
+ @pytest.fixture
48
+ def estimated_state(coordinate_subsets: list[CoordinateSubset]) -> State:
49
+ return State(
50
+ Instant.J2000(),
51
+ [1.0, 0.0],
52
+ Frame.GCRF(),
53
+ coordinate_subsets,
54
+ )
55
+
56
+
57
+ @pytest.fixture
58
+ def estimated_covariance() -> np.ndarray:
59
+ return np.array([[1.0, 0.0], [0.0, 1.0]])
60
+
61
+
62
+ @pytest.fixture
63
+ def estimated_frisbee_covariance() -> np.ndarray:
64
+ return np.array([[1.0, 0.0], [0.0, 1.0]])
65
+
66
+
67
+ @pytest.fixture
68
+ def observation_count() -> int:
69
+ return 10
70
+
71
+
72
+ @pytest.fixture
73
+ def computed_observations(
74
+ observations: list[State],
75
+ ) -> list[State]:
76
+ return observations
77
+
78
+
79
+ @pytest.fixture
80
+ def steps(step: LeastSquaresSolver.Step) -> list[LeastSquaresSolver.Step]:
81
+ return [step]
82
+
83
+
84
+ @pytest.fixture
85
+ def analysis(
86
+ termination_criteria: str,
87
+ estimated_state: State,
88
+ estimated_covariance: np.ndarray,
89
+ estimated_frisbee_covariance: np.ndarray,
90
+ computed_observations: list[State],
91
+ steps: list[LeastSquaresSolver.Step],
92
+ ) -> LeastSquaresSolver.Analysis:
93
+ return LeastSquaresSolver.Analysis(
94
+ termination_criteria=termination_criteria,
95
+ estimated_state=estimated_state,
96
+ estimated_covariance=estimated_covariance,
97
+ estimated_frisbee_covariance=estimated_frisbee_covariance,
98
+ computed_observations=computed_observations,
99
+ steps=steps,
100
+ )
101
+
102
+
103
+ @pytest.fixture
104
+ def max_iteration_count() -> int:
105
+ return 20
106
+
107
+
108
+ @pytest.fixture
109
+ def rms_update_threshold() -> float:
110
+ return 1.0
111
+
112
+
113
+ @pytest.fixture
114
+ def finite_difference_solver() -> FiniteDifferenceSolver:
115
+ return FiniteDifferenceSolver.default()
116
+
117
+
118
+ @pytest.fixture
119
+ def least_squares_solver(
120
+ max_iteration_count: int,
121
+ rms_update_threshold: float,
122
+ finite_difference_solver: FiniteDifferenceSolver,
123
+ ) -> LeastSquaresSolver:
124
+ return LeastSquaresSolver(
125
+ maximum_iteration_count=max_iteration_count,
126
+ rms_update_threshold=rms_update_threshold,
127
+ finite_difference_solver=finite_difference_solver,
128
+ )
129
+
130
+
131
+ @pytest.fixture
132
+ def coordinate_subsets() -> list[CoordinateSubset]:
133
+ return [CoordinateSubset("Position", 1), CoordinateSubset("Velocity", 1)]
134
+
135
+
136
+ @pytest.fixture
137
+ def initial_guess_sigmas(
138
+ coordinate_subsets: list[CoordinateSubset],
139
+ ) -> dict[CoordinateSubset, list[float]]:
140
+ return {
141
+ coordinate_subsets[0]: [1e-1],
142
+ coordinate_subsets[1]: [1e-2],
143
+ }
144
+
145
+
146
+ @pytest.fixture
147
+ def observation_sigmas(
148
+ coordinate_subsets: list[CoordinateSubset],
149
+ ) -> dict[CoordinateSubset, list[float]]:
150
+ return {
151
+ coordinate_subsets[0]: [1e-1],
152
+ coordinate_subsets[1]: [1e-2],
153
+ }
154
+
155
+
156
+ @pytest.fixture
157
+ def initial_instant() -> Instant:
158
+ return Instant.J2000()
159
+
160
+
161
+ @pytest.fixture
162
+ def frame() -> Frame:
163
+ return Frame.GCRF()
164
+
165
+
166
+ @pytest.fixture
167
+ def initial_guess(
168
+ initial_instant: Instant,
169
+ coordinate_subsets: list[CoordinateSubset],
170
+ frame: Frame,
171
+ ) -> State:
172
+ return State(initial_instant, [1.0, 0.0], frame, coordinate_subsets)
173
+
174
+
175
+ @pytest.fixture
176
+ def observations(
177
+ initial_instant: Instant,
178
+ coordinate_subsets: list[CoordinateSubset],
179
+ frame: Frame,
180
+ observation_count: int,
181
+ ) -> list[State]:
182
+ return [
183
+ State(
184
+ initial_instant + Duration.seconds(float(x)),
185
+ [np.cos(x), -np.sin(x)],
186
+ frame,
187
+ coordinate_subsets,
188
+ )
189
+ for x in range(0, observation_count)
190
+ ]
191
+
192
+
193
+ @pytest.fixture
194
+ def state_generator() -> Callable:
195
+ def state_fn(state, instants) -> list[State]:
196
+ x0: float = state.get_coordinates()[0]
197
+ v0: float = state.get_coordinates()[1]
198
+ omega: float = 1.0
199
+
200
+ states: list[State] = []
201
+
202
+ for instant in instants:
203
+ t: float = float((instant - state.get_instant()).in_seconds())
204
+ x: float = x0 * np.cos(omega * t) + v0 / omega * np.sin(omega * t)
205
+ v: float = -x0 * omega * np.sin(omega * t) + v0 * np.cos(omega * t)
206
+
207
+ states.append(
208
+ State(instant, [x, v], Frame.GCRF(), state.get_coordinate_subsets())
209
+ )
210
+
211
+ return states
212
+
213
+ return state_fn
214
+
215
+
216
+ class TestLeastSquaresSolverStep:
217
+
218
+ def test_constructor(
219
+ self,
220
+ step: LeastSquaresSolver.Step,
221
+ ):
222
+ assert isinstance(step, LeastSquaresSolver.Step)
223
+
224
+ def test_getters(
225
+ self,
226
+ step: LeastSquaresSolver.Step,
227
+ rms_error: float,
228
+ x_hat: np.ndarray,
229
+ ):
230
+ assert step.rms_error == rms_error
231
+ assert np.array_equal(step.x_hat, x_hat)
232
+
233
+
234
+ class TestLeastSquaresSolverAnalysis:
235
+
236
+ def test_constructor(
237
+ self,
238
+ analysis: LeastSquaresSolver.Analysis,
239
+ ):
240
+ assert isinstance(analysis, LeastSquaresSolver.Analysis)
241
+
242
+ def test_getters(
243
+ self,
244
+ analysis: LeastSquaresSolver.Analysis,
245
+ ):
246
+ assert isinstance(analysis.rms_error, Real)
247
+ assert isinstance(analysis.observation_count, int)
248
+ assert isinstance(analysis.iteration_count, int)
249
+ assert isinstance(analysis.termination_criteria, String)
250
+ assert isinstance(analysis.estimated_state, State)
251
+ assert isinstance(analysis.estimated_covariance, np.ndarray)
252
+ assert isinstance(analysis.estimated_frisbee_covariance, np.ndarray)
253
+ assert isinstance(analysis.computed_observations, list)
254
+ assert isinstance(analysis.steps, list)
255
+
256
+ def test_compute_residual_states(
257
+ self,
258
+ analysis: LeastSquaresSolver.Analysis,
259
+ observations: list[State],
260
+ ):
261
+ residuals: list[State] = analysis.compute_residual_states(
262
+ observations=observations
263
+ )
264
+
265
+ assert isinstance(residuals, list)
266
+ assert len(residuals) == len(observations)
267
+
268
+
269
+ class TestLeastSquaresSolver:
270
+ def test_constructor(
271
+ self,
272
+ least_squares_solver: LeastSquaresSolver,
273
+ ):
274
+ assert isinstance(least_squares_solver, LeastSquaresSolver)
275
+
276
+ def test_getters(
277
+ self,
278
+ least_squares_solver: LeastSquaresSolver,
279
+ max_iteration_count: int,
280
+ rms_update_threshold: float,
281
+ ):
282
+ assert least_squares_solver.get_max_iteration_count() == max_iteration_count
283
+ assert least_squares_solver.get_rms_update_threshold() == rms_update_threshold
284
+ assert least_squares_solver.get_finite_difference_solver() is not None
285
+
286
+ def test_solve_defaults(
287
+ self,
288
+ least_squares_solver: LeastSquaresSolver,
289
+ initial_guess: State,
290
+ observations: list[State],
291
+ state_generator: callable,
292
+ ):
293
+ analysis = least_squares_solver.solve(
294
+ initial_guess=initial_guess,
295
+ observations=observations,
296
+ state_generator=state_generator,
297
+ )
298
+
299
+ assert analysis is not None
300
+
301
+ def test_solve(
302
+ self,
303
+ least_squares_solver: LeastSquaresSolver,
304
+ initial_guess: State,
305
+ observations: list[State],
306
+ state_generator: callable,
307
+ initial_guess_sigmas: dict[CoordinateSubset, list[float]],
308
+ observation_sigmas: dict[CoordinateSubset, list[float]],
309
+ ):
310
+ analysis = least_squares_solver.solve(
311
+ initial_guess=initial_guess,
312
+ observations=observations,
313
+ state_generator=state_generator,
314
+ initial_guess_sigmas=initial_guess_sigmas,
315
+ observation_sigmas=observation_sigmas,
316
+ )
317
+
318
+ assert analysis is not None
319
+
320
+ def test_calculate_empirical_covariance(
321
+ self,
322
+ observations: list[State],
323
+ ):
324
+ covariance: np.ndarray = LeastSquaresSolver.calculate_empirical_covariance(
325
+ observations
326
+ )
327
+
328
+ assert isinstance(covariance, np.ndarray)
329
+ assert covariance.shape == (2, 2)
330
+
331
+ def test_default(self):
332
+ default_solver: LeastSquaresSolver = LeastSquaresSolver.default()
333
+
334
+ assert isinstance(default_solver, LeastSquaresSolver)
@@ -5,8 +5,6 @@ from __future__ import annotations
5
5
  from datetime import datetime
6
6
  from datetime import timezone
7
7
 
8
- from .OpenSpaceToolkitAstrodynamicsPy import *
9
-
10
8
  from ostk.physics import Environment
11
9
  from ostk.physics.time import Scale
12
10
  from ostk.physics.time import Instant
@@ -20,13 +18,16 @@ from ostk.physics.coordinate import Frame
20
18
  from ostk.physics.environment.object.celestial import Earth
21
19
  from ostk.physics.environment.gravitational import Earth as EarthGravitationalModel
22
20
 
21
+ from ostk.astrodynamics import Trajectory
22
+ from ostk.astrodynamics.trajectory import State
23
+
23
24
 
24
- def lla_from_state(state: trajectory.State) -> LLA:
25
+ def lla_from_state(state: State) -> LLA:
25
26
  """
26
27
  Return latitude (degrees), longitude (degrees), altitude (meters) float list from a state.
27
28
 
28
29
  Args:
29
- state (trajectory.State): A state.
30
+ state (State): A state.
30
31
 
31
32
  Returns:
32
33
  LLA: The LLA.
@@ -115,7 +116,7 @@ def compute_aer(
115
116
 
116
117
 
117
118
  def compute_time_lla_aer_coordinates(
118
- state: trajectory.State,
119
+ state: State,
119
120
  from_position: Position,
120
121
  environment: Environment,
121
122
  ) -> tuple[datetime, float, float, float, float, float, float]:
@@ -123,7 +124,7 @@ def compute_time_lla_aer_coordinates(
123
124
  Return [datetime, latitude, longitude, altitude, azimuth, elevation, range] from State and observer Position.
124
125
 
125
126
  Args:
126
- state (trajectory.State): A state.
127
+ state (State): A state.
127
128
  from_position (Position): An observer position.
128
129
  environment (Environment): An environment.
129
130
 
@@ -209,7 +210,7 @@ def compute_ground_track(
209
210
 
210
211
 
211
212
  def convert_state(
212
- state: trajectory.State,
213
+ state: State,
213
214
  ) -> tuple[str, float, float, float, float, float, float, float, float, float]:
214
215
  """
215
216
  Convert a State into dataframe-ready values.
@@ -226,7 +227,7 @@ def convert_state(
226
227
  - Altitude [meters] (float)
227
228
 
228
229
  Args:
229
- state (trajectory.State): A state.
230
+ state (State): A state.
230
231
 
231
232
  Returns:
232
233
  tuple[str, float, float, float, float, float, float, float, float, float]: The dataframe-ready values.