open-space-toolkit-astrodynamics 9.1.6__py39-none-manylinux2014_aarch64.whl → 13.0.2__py39-none-manylinux2014_aarch64.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 (30) hide show
  1. {open_space_toolkit_astrodynamics-9.1.6.dist-info → open_space_toolkit_astrodynamics-13.0.2.dist-info}/METADATA +5 -5
  2. {open_space_toolkit_astrodynamics-9.1.6.dist-info → open_space_toolkit_astrodynamics-13.0.2.dist-info}/RECORD +29 -27
  3. {open_space_toolkit_astrodynamics-9.1.6.dist-info → open_space_toolkit_astrodynamics-13.0.2.dist-info}/WHEEL +1 -1
  4. ostk/astrodynamics/OpenSpaceToolkitAstrodynamicsPy.cpython-39-aarch64-linux-gnu.so +0 -0
  5. ostk/astrodynamics/converters.py +35 -90
  6. ostk/astrodynamics/dataframe.py +479 -0
  7. ostk/astrodynamics/display.py +2 -0
  8. ostk/astrodynamics/libopen-space-toolkit-astrodynamics.so.13 +0 -0
  9. ostk/astrodynamics/pytrajectory/pystate.py +216 -1
  10. ostk/astrodynamics/test/conftest.py +2 -2
  11. ostk/astrodynamics/test/flight/test_profile.py +155 -55
  12. ostk/astrodynamics/test/test_converters.py +43 -140
  13. ostk/astrodynamics/test/test_dataframe.py +875 -0
  14. ostk/astrodynamics/test/test_display.py +2 -3
  15. ostk/astrodynamics/test/test_event_condition.py +27 -7
  16. ostk/astrodynamics/test/test_trajectory.py +116 -38
  17. ostk/astrodynamics/test/test_utilities.py +31 -46
  18. ostk/astrodynamics/test/trajectory/orbit/models/kepler/test_coe.py +13 -0
  19. ostk/astrodynamics/test/trajectory/orbit/test_pass.py +9 -0
  20. ostk/astrodynamics/test/trajectory/state/test_coordinate_subset.py +3 -0
  21. ostk/astrodynamics/test/trajectory/state/test_numerical_solver.py +2 -2
  22. ostk/astrodynamics/test/trajectory/test_local_orbital_frame_factory.py +34 -2
  23. ostk/astrodynamics/test/trajectory/test_orbit.py +26 -2
  24. ostk/astrodynamics/test/trajectory/test_segment.py +99 -1
  25. ostk/astrodynamics/test/trajectory/test_sequence.py +53 -0
  26. ostk/astrodynamics/test/trajectory/test_state.py +306 -0
  27. ostk/astrodynamics/utilities.py +125 -36
  28. ostk/astrodynamics/libopen-space-toolkit-astrodynamics.so.9 +0 -0
  29. {open_space_toolkit_astrodynamics-9.1.6.dist-info → open_space_toolkit_astrodynamics-13.0.2.dist-info}/top_level.txt +0 -0
  30. {open_space_toolkit_astrodynamics-9.1.6.dist-info → open_space_toolkit_astrodynamics-13.0.2.dist-info}/zip-safe +0 -0
@@ -1,13 +1,24 @@
1
1
  # Apache License 2.0
2
2
 
3
-
4
3
  # Python-only State functionality
4
+ import re
5
+
5
6
  import numpy as np
6
7
 
7
8
  from ostk.physics.coordinate import Frame
8
9
  from ostk.physics.time import Instant
9
10
 
10
11
  from ostk.astrodynamics.trajectory import State, StateBuilder
12
+ from ostk.astrodynamics.trajectory.state import CoordinateSubset
13
+ from ostk.astrodynamics.converters import coerce_to_instant
14
+ from ostk.astrodynamics.trajectory.state.coordinate_subset import (
15
+ CartesianPosition,
16
+ CartesianVelocity,
17
+ AttitudeQuaternion,
18
+ AngularVelocity,
19
+ )
20
+
21
+ CANONICAL_FORMAT: str = r"(r|v)_(.*?)_(x|y|z)"
11
22
 
12
23
 
13
24
  @staticmethod
@@ -33,4 +44,208 @@ def custom_class_generator(frame: Frame, coordinate_subsets: list) -> type:
33
44
  return StateTemplateType
34
45
 
35
46
 
47
+ @staticmethod
48
+ def from_dict(data: dict) -> State:
49
+ """
50
+ Create a State from a dictionary.
51
+
52
+ Note: Implicit assumption that ECEF = ITRF, and ECI = GCRF.
53
+
54
+ The dictionary must contain the following:
55
+ - 'timestamp': The timestamp of the state.
56
+ - 'r_ITRF_x'/'rx'/'rx_eci'/'rx_ecef': The x-coordinate of the position.
57
+ - 'r_ITRF_y'/'ry'/'ry_eci'/'ry_ecef': The y-coordinate of the position.
58
+ - 'r_ITRF_z'/'rz'/'rz_eci'/'rz_ecef': The z-coordinate of the position.
59
+ - 'v_ITRF_x'/'vx'/'vx_eci'/'vx_ecef': The x-coordinate of the velocity.
60
+ - 'v_ITRF_y'/'vy'/'vy_eci'/'vy_ecef': The y-coordinate of the velocity.
61
+ - 'v_ITRF_z'/'vz'/'vz_eci'/'vz_ecef': The z-coordinate of the velocity.
62
+ - 'frame': The frame of the state. Required if 'rx', 'ry', 'rz', 'vx', 'vy', 'vz' are provided.
63
+ - 'q_B_ECI_x': The x-coordinate of the quaternion. Optional.
64
+ - 'q_B_ECI_y': The y-coordinate of the quaternion. Optional.
65
+ - 'q_B_ECI_z': The z-coordinate of the quaternion. Optional.
66
+ - 'q_B_ECI_s': The s-coordinate of the quaternion. Optional.
67
+ - 'w_B_ECI_in_B_x': The x-coordinate of the angular velocity. Optional.
68
+ - 'w_B_ECI_in_B_y': The y-coordinate of the angular velocity. Optional.
69
+ - 'w_B_ECI_in_B_z': The z-coordinate of the angular velocity. Optional.
70
+ - 'drag_coefficient'/'cd': The drag coefficient. Optional.
71
+ - 'cross_sectional_area'/'surface_area': The cross-sectional area. Optional.
72
+ - 'mass': The mass. Optional.
73
+
74
+ Args:
75
+ data (dict): The dictionary.
76
+
77
+ Returns:
78
+ State: The State.
79
+ """
80
+
81
+ instant: Instant = coerce_to_instant(data["timestamp"])
82
+
83
+ eci_columns: list[str] = [
84
+ "rx_eci",
85
+ "ry_eci",
86
+ "rz_eci",
87
+ "vx_eci",
88
+ "vy_eci",
89
+ "vz_eci",
90
+ ]
91
+ ecef_columns: list[str] = [
92
+ "rx_ecef",
93
+ "ry_ecef",
94
+ "rz_ecef",
95
+ "vx_ecef",
96
+ "vy_ecef",
97
+ "vz_ecef",
98
+ ]
99
+ generic_columns: list[str] = [
100
+ "rx",
101
+ "ry",
102
+ "rz",
103
+ "vx",
104
+ "vy",
105
+ "vz",
106
+ ]
107
+
108
+ # Replace non-standard position keys with canonical representation
109
+ if all(key in data.keys() for key in ("x_eci", "y_eci", "z_eci")):
110
+ data["rx_eci"] = data["x_eci"]
111
+ data["ry_eci"] = data["y_eci"]
112
+ data["rz_eci"] = data["z_eci"]
113
+
114
+ if all(key in data.keys() for key in ("x_ecef", "y_ecef", "z_ecef")):
115
+ data["rx_ecef"] = data["x_ecef"]
116
+ data["ry_ecef"] = data["y_ecef"]
117
+ data["rz_ecef"] = data["z_ecef"]
118
+
119
+ frame: Frame
120
+ coordinates: np.ndarray
121
+
122
+ coordinate_subsets: list[CoordinateSubset] = [
123
+ CartesianPosition.default(),
124
+ CartesianVelocity.default(),
125
+ ]
126
+
127
+ match_groups: list[re.Match] = [
128
+ re.match(CANONICAL_FORMAT, column) for column in data.keys()
129
+ ]
130
+
131
+ if len(matches := [match for match in match_groups if match is not None]) == 6:
132
+ frame_name: str = matches[0].group(2)
133
+ try:
134
+ frame: Frame = Frame.with_name(frame_name) or getattr(Frame, frame_name)()
135
+ except Exception:
136
+ raise ValueError(f"No frame exists with name [{frame_name}].")
137
+
138
+ coordinates = np.array([data[match.group(0)] for match in matches])
139
+
140
+ elif all(column in data for column in eci_columns):
141
+ frame: Frame = Frame.GCRF()
142
+ coordinates = np.array(
143
+ [
144
+ data["rx_eci"],
145
+ data["ry_eci"],
146
+ data["rz_eci"],
147
+ data["vx_eci"],
148
+ data["vy_eci"],
149
+ data["vz_eci"],
150
+ ]
151
+ )
152
+
153
+ elif all(column in data for column in ecef_columns):
154
+ frame = Frame.ITRF()
155
+ coordinates = np.array(
156
+ [
157
+ data["rx_ecef"],
158
+ data["ry_ecef"],
159
+ data["rz_ecef"],
160
+ data["vx_ecef"],
161
+ data["vy_ecef"],
162
+ data["vz_ecef"],
163
+ ]
164
+ )
165
+
166
+ elif all(column in data for column in generic_columns):
167
+ if "frame" not in data:
168
+ raise ValueError("Frame must be provided for generic columns.")
169
+
170
+ if isinstance(data["frame"], str):
171
+ if Frame.exists(data["frame"]):
172
+ frame = Frame.with_name(data["frame"])
173
+ else:
174
+ raise ValueError(f"No frame exists with name [{data['frame']}].")
175
+ elif isinstance(data["frame"], Frame):
176
+ frame = data["frame"]
177
+ else:
178
+ raise ValueError(f"Invalid frame data [{data['frame']}].")
179
+
180
+ coordinates = np.array(
181
+ [
182
+ data["rx"],
183
+ data["ry"],
184
+ data["rz"],
185
+ data["vx"],
186
+ data["vy"],
187
+ data["vz"],
188
+ ]
189
+ )
190
+ else:
191
+ raise ValueError("Invalid state data.")
192
+
193
+ if all(
194
+ column in data for column in ["q_B_ECI_x", "q_B_ECI_y", "q_B_ECI_z", "q_B_ECI_s"]
195
+ ):
196
+ coordinate_subsets.append(AttitudeQuaternion.default())
197
+ coordinates = np.append(
198
+ coordinates,
199
+ [
200
+ data["q_B_ECI_x"],
201
+ data["q_B_ECI_y"],
202
+ data["q_B_ECI_z"],
203
+ data["q_B_ECI_s"],
204
+ ],
205
+ )
206
+
207
+ if all(
208
+ column in data
209
+ for column in ["w_B_ECI_in_B_x", "w_B_ECI_in_B_y", "w_B_ECI_in_B_z"]
210
+ ):
211
+ coordinate_subsets.append(AngularVelocity.default())
212
+ coordinates = np.append(
213
+ coordinates,
214
+ [
215
+ data["w_B_ECI_in_B_x"],
216
+ data["w_B_ECI_in_B_y"],
217
+ data["w_B_ECI_in_B_z"],
218
+ ],
219
+ )
220
+
221
+ if "drag_coefficient" in data or "cd" in data:
222
+ coordinate_subsets.append(CoordinateSubset.drag_coefficient())
223
+ coordinates = np.append(
224
+ coordinates,
225
+ data.get("drag_coefficient", data.get("cd")),
226
+ )
227
+
228
+ if "cross_sectional_area" in data or "surface_area" in data:
229
+ coordinate_subsets.append(CoordinateSubset.surface_area())
230
+ coordinates = np.append(
231
+ coordinates,
232
+ data.get("cross_sectional_area", data.get("surface_area")),
233
+ )
234
+
235
+ if "mass" in data:
236
+ coordinate_subsets.append(CoordinateSubset.mass())
237
+ coordinates = np.append(
238
+ coordinates,
239
+ data["mass"],
240
+ )
241
+
242
+ return State(
243
+ instant=instant,
244
+ coordinates=coordinates,
245
+ frame=frame,
246
+ coordinate_subsets=coordinate_subsets,
247
+ )
248
+
249
+
250
+ State.from_dict = from_dict
36
251
  State.template = custom_class_generator
@@ -115,5 +115,5 @@ def aer(azimuth: Angle, elevation: Angle, range: Length) -> LLA:
115
115
 
116
116
 
117
117
  @pytest.fixture
118
- def trajectory(position: Position) -> Trajectory:
119
- return Trajectory.position(position)
118
+ def trajectory(position: Position, instant_1: Instant) -> Trajectory:
119
+ return Trajectory.position(position.in_frame(Frame.ITRF(), instant_1))
@@ -8,6 +8,7 @@ from ostk.mathematics.geometry.d3.transformation.rotation import Quaternion
8
8
 
9
9
  from ostk.physics import Environment
10
10
  from ostk.physics.time import DateTime
11
+ from ostk.physics.time import Duration
11
12
  from ostk.physics.time import Time
12
13
  from ostk.physics.time import Scale
13
14
  from ostk.physics.time import Instant
@@ -29,27 +30,121 @@ from ostk.astrodynamics.flight.profile.model import Tabulated as TabulatedModel
29
30
 
30
31
  @pytest.fixture
31
32
  def instant() -> Instant:
32
- return Instant.date_time(DateTime(2020, 1, 3), Scale.UTC)
33
+ return Instant.date_time(DateTime(2020, 1, 1, 0, 0, 30), Scale.UTC)
33
34
 
34
35
 
35
36
  @pytest.fixture
36
- def profile() -> Profile:
37
+ def environment() -> Environment:
38
+ return Environment.default()
39
+
40
+
41
+ @pytest.fixture
42
+ def orbit(instant: Instant, environment: Environment) -> Orbit:
43
+ return Orbit.sun_synchronous(
44
+ epoch=instant,
45
+ altitude=Length.kilometers(500.0),
46
+ local_time_at_descending_node=Time(14, 0, 0),
47
+ celestial_object=environment.access_celestial_object_with_name("Earth"),
48
+ )
49
+
50
+
51
+ @pytest.fixture
52
+ def transform_model() -> TransformModel:
37
53
  def dynamic_provider_generator(instant: Instant):
38
54
  return Transform.identity(instant)
39
55
 
40
- return Profile(
41
- model=TransformModel(
42
- dynamic_provider=DynamicProvider(dynamic_provider_generator),
43
- frame=Frame.GCRF(),
44
- ),
56
+ return TransformModel(
57
+ dynamic_provider=DynamicProvider(dynamic_provider_generator),
58
+ frame=Frame.GCRF(),
45
59
  )
46
60
 
47
61
 
62
+ @pytest.fixture
63
+ def tabulated_model() -> TabulatedModel:
64
+ return TabulatedModel(
65
+ states=[
66
+ State(
67
+ instant=Instant.date_time(datetime(2020, 1, 1, 0, 0, 0), Scale.UTC),
68
+ position=Position.meters((0.0, 0.0, 0.0), Frame.GCRF()),
69
+ velocity=Velocity.meters_per_second((0.0, 0.0, 0.0), Frame.GCRF()),
70
+ attitude=Quaternion.unit(),
71
+ angular_velocity=(0.0, 0.0, 0.0),
72
+ attitude_frame=Frame.GCRF(),
73
+ ),
74
+ State(
75
+ instant=Instant.date_time(datetime(2020, 1, 1, 0, 1, 0), Scale.UTC),
76
+ position=Position.meters((0.0, 0.0, 0.0), Frame.GCRF()),
77
+ velocity=Velocity.meters_per_second((0.0, 0.0, 0.0), Frame.GCRF()),
78
+ attitude=Quaternion.unit(),
79
+ angular_velocity=(0.0, 0.0, 0.0),
80
+ attitude_frame=Frame.GCRF(),
81
+ ),
82
+ ],
83
+ )
84
+
85
+
86
+ @pytest.fixture(
87
+ params=[
88
+ "transform_model",
89
+ "tabulated_model",
90
+ ]
91
+ )
92
+ def profile(request) -> Profile:
93
+ model: TransformModel | TabulatedModel = request.getfixturevalue(request.param)
94
+ return Profile(model=model)
95
+
96
+
97
+ @pytest.fixture(
98
+ params=[
99
+ Profile.Target(Profile.TargetType.GeocentricNadir, Profile.Axis.X),
100
+ Profile.TrajectoryTarget(
101
+ Trajectory.position(Position.meters((0.0, 0.0, 0.0), Frame.ITRF())),
102
+ Profile.Axis.X,
103
+ ),
104
+ Profile.OrientationProfileTarget(
105
+ [
106
+ (Instant.J2000(), [1.0, 0.0, 0.0]),
107
+ (Instant.J2000() + Duration.minutes(1.0), [1.0, 0.0, 0.0]),
108
+ (Instant.J2000() + Duration.minutes(2.0), [1.0, 0.0, 0.0]),
109
+ (Instant.J2000() + Duration.minutes(3.0), [1.0, 0.0, 0.0]),
110
+ ],
111
+ Profile.Axis.X,
112
+ ),
113
+ Profile.CustomTarget(
114
+ lambda state: [1.0, 0.0, 0.0],
115
+ Profile.Axis.X,
116
+ ),
117
+ ]
118
+ )
119
+ def alignment_target() -> Profile.Target:
120
+ return Profile.Target(Profile.TargetType.GeocentricNadir, Profile.Axis.X)
121
+
122
+
123
+ @pytest.fixture
124
+ def clocking_target() -> Profile.Target:
125
+ return Profile.Target(Profile.TargetType.VelocityECI, Profile.Axis.Y)
126
+
127
+
48
128
  class TestProfile:
49
129
  def test_constructors(self, profile: Profile):
50
130
  assert profile is not None
51
131
  assert isinstance(profile, Profile)
52
132
 
133
+ def test_profile_target(self, alignment_target: Profile.Target):
134
+ assert alignment_target is not None
135
+ assert isinstance(alignment_target, Profile.Target)
136
+
137
+ def test_access_model(self, profile: Profile):
138
+ model = profile.access_model()
139
+
140
+ assert model is not None
141
+
142
+ if model.is_transform():
143
+ assert model.as_transform() is not None
144
+
145
+ if model.is_tabulated():
146
+ assert model.as_tabulated() is not None
147
+
53
148
  def test_get_state_at(self, profile: Profile, instant: Instant):
54
149
  state: State = profile.get_state_at(instant)
55
150
 
@@ -68,7 +163,10 @@ class TestProfile:
68
163
  assert axes is not None
69
164
  assert isinstance(axes, Axes)
70
165
 
71
- def test_get_body_frame(self, profile: Profile, instant: Instant):
166
+ def test_get_body_frame(self, profile: Profile):
167
+ if Frame.exists("Name"):
168
+ Frame.destruct("Name")
169
+
72
170
  frame = profile.get_body_frame("Name")
73
171
 
74
172
  assert frame is not None
@@ -85,7 +183,7 @@ class TestProfile:
85
183
  quaternion: Quaternion = Quaternion([0.0, 0.0, 0.0, 1.0], Quaternion.Format.XYZS)
86
184
 
87
185
  trajectory: Trajectory = Trajectory.position(
88
- Position.meters((0.0, 0.0, 0.0), Frame.GCRF())
186
+ Position.meters((0.0, 0.0, 0.0), Frame.ITRF())
89
187
  )
90
188
 
91
189
  profile: Profile = Profile.inertial_pointing(trajectory, quaternion)
@@ -94,60 +192,62 @@ class TestProfile:
94
192
  assert isinstance(profile, Profile)
95
193
  assert profile.is_defined()
96
194
 
97
- def test_nadir_pointing(self):
98
- environment = Environment.default()
99
-
100
- orbit = Orbit.sun_synchronous(
101
- epoch=Instant.date_time(datetime(2020, 1, 1, 0, 0, 0), Scale.UTC),
102
- altitude=Length.kilometers(500.0),
103
- local_time_at_descending_node=Time(14, 0, 0),
104
- celestial_object=environment.access_celestial_object_with_name("Earth"),
105
- )
106
-
195
+ def test_nadir_pointing(
196
+ self,
197
+ orbit: Orbit,
198
+ ):
107
199
  profile: Profile = Profile.nadir_pointing(orbit, Orbit.FrameType.VVLH)
108
200
 
109
201
  assert profile is not None
110
202
  assert isinstance(profile, Profile)
111
203
  assert profile.is_defined()
112
204
 
113
- def test_tabulated(self):
114
- profile = Profile(
115
- model=TabulatedModel(
116
- states=[
117
- State(
118
- instant=Instant.date_time(
119
- datetime(2020, 1, 1, 0, 0, 0), Scale.UTC
120
- ),
121
- position=Position.meters((0.0, 0.0, 0.0), Frame.GCRF()),
122
- velocity=Velocity.meters_per_second(
123
- (0.0, 0.0, 0.0), Frame.GCRF()
124
- ),
125
- attitude=Quaternion.unit(),
126
- angular_velocity=(0.0, 0.0, 0.0),
127
- attitude_frame=Frame.GCRF(),
128
- ),
129
- State(
130
- instant=Instant.date_time(
131
- datetime(2020, 1, 1, 0, 1, 0), Scale.UTC
132
- ),
133
- position=Position.meters((0.0, 0.0, 0.0), Frame.GCRF()),
134
- velocity=Velocity.meters_per_second(
135
- (0.0, 0.0, 0.0), Frame.GCRF()
136
- ),
137
- attitude=Quaternion.unit(),
138
- angular_velocity=(0.0, 0.0, 0.0),
139
- attitude_frame=Frame.GCRF(),
140
- ),
141
- ],
142
- ),
205
+ def test_custom_pointing(
206
+ self,
207
+ orbit: Orbit,
208
+ alignment_target: Profile.Target,
209
+ clocking_target: Profile.Target,
210
+ ):
211
+ profile = Profile.custom_pointing(
212
+ orbit, Profile.align_and_constrain(alignment_target, clocking_target)
143
213
  )
144
214
 
145
- assert isinstance(profile, Profile)
215
+ assert profile is not None
146
216
  assert profile.is_defined()
147
217
 
148
- assert (
149
- profile.get_state_at(
150
- Instant.date_time(datetime(2020, 1, 1, 0, 0, 30), Scale.UTC)
151
- )
152
- is not None
218
+ def test_align_and_constrain(
219
+ self,
220
+ orbit: Orbit,
221
+ instant: Instant,
222
+ alignment_target: Profile.Target,
223
+ clocking_target: Profile.Target,
224
+ ):
225
+ orientation = Profile.align_and_constrain(
226
+ alignment_target=alignment_target,
227
+ clocking_target=clocking_target,
228
+ )
229
+
230
+ assert orientation is not None
231
+ assert orientation(orbit.get_state_at(instant)) is not None
232
+
233
+ def test_custom_pointing(
234
+ self,
235
+ orbit: Orbit,
236
+ alignment_target: Profile.Target,
237
+ clocking_target: Profile.Target,
238
+ ):
239
+ profile = Profile.custom_pointing(
240
+ orbit, Profile.align_and_constrain(alignment_target, clocking_target)
153
241
  )
242
+
243
+ assert profile is not None
244
+ assert profile.is_defined()
245
+
246
+ profile = Profile.custom_pointing(
247
+ orbit,
248
+ alignment_target,
249
+ clocking_target,
250
+ )
251
+
252
+ assert profile is not None
253
+ assert profile.is_defined()