open-space-toolkit-astrodynamics 11.1.0__py38-none-manylinux2014_x86_64.whl → 12.0.0__py38-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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: open-space-toolkit-astrodynamics
3
- Version: 11.1.0
3
+ Version: 12.0.0
4
4
  Summary: Orbit, attitude, access.
5
5
  Author: Open Space Collective
6
6
  Author-email: contact@open-space-collective.org
@@ -1,25 +1,25 @@
1
1
  ostk/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
2
- ostk/astrodynamics/OpenSpaceToolkitAstrodynamicsPy.cpython-38-x86_64-linux-gnu.so,sha256=3bvj1fIMXdepvj34w1YeTpe6TOXiVrxnYBjw3321T_Y,2320224
2
+ ostk/astrodynamics/OpenSpaceToolkitAstrodynamicsPy.cpython-38-x86_64-linux-gnu.so,sha256=1Mx9mtNqk9Pzghl93sq0b7eeNT-wuBhF_qjzqF1jloE,2422656
3
3
  ostk/astrodynamics/__init__.py,sha256=3gWyqFIbhAfcdeMhmfBPQPlPQTmaOzm-6flkJe745Zk,251
4
- ostk/astrodynamics/converters.py,sha256=xr1BDaY2od8IsNq-3isubaxainncgH8_yihZ6Mf38n4,2697
4
+ ostk/astrodynamics/converters.py,sha256=Ld-DLTaqg7pBkIJAcYeoaA-sDOQbR_uksRR-lg9dfXo,3278
5
5
  ostk/astrodynamics/dataframe.py,sha256=njimcfT2cvnSOp9eBR1Ej-LuYpXbVieAVEBk2tYW4Vg,18761
6
6
  ostk/astrodynamics/display.py,sha256=KiEGsjNftQfoUe-HfZeYpQQQ4TkaEnxtEl6p5_LX5f0,6303
7
- ostk/astrodynamics/libopen-space-toolkit-astrodynamics.so.11,sha256=AWsoHlLV00oLzGFj4rOY2FyTM71N-GMn7ZLV6ChYlbk,81607000
8
- ostk/astrodynamics/utilities.py,sha256=Yu5Yydq22gNIHi6ylqeujDENcueb1JfUE6d93h1m9XE,4795
7
+ ostk/astrodynamics/libopen-space-toolkit-astrodynamics.so.12,sha256=Z614bFw3ulUzM58S9gKVtCeYeINU2wMuWNfb_O8qpfk,84586128
8
+ ostk/astrodynamics/utilities.py,sha256=f0ySFhD6X9jhAZW1hLnbLVEOuy3QgVq2C6k4m8MFGYA,6963
9
9
  ostk/astrodynamics/viewer.py,sha256=8UxqsvU5wui7gPUwxXaKcIB-509NJiih9Df3mQgXyXY,11776
10
10
  ostk/astrodynamics/pytrajectory/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
11
- ostk/astrodynamics/pytrajectory/pystate.py,sha256=ByOq8szit74xQ-pnLz0uGGxM6Jqfa17dFJyFB8qIXUg,1033
11
+ ostk/astrodynamics/pytrajectory/pystate.py,sha256=qtGofajIODveyy_g_lEeBqJQ1GddqmzRBVNCExF8pdk,5734
12
12
  ostk/astrodynamics/test/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
13
13
  ostk/astrodynamics/test/conftest.py,sha256=fsdUQnLxyfvBzfaSCkgipkPIhkPHchQqbeAro1pF648,2717
14
14
  ostk/astrodynamics/test/test_access.py,sha256=MCBsUPtuVm7NgHZR0z0DpWnPZ_qBu4aRhLI2PnRNUYs,3940
15
- ostk/astrodynamics/test/test_converters.py,sha256=ommyONkpTnhm3GHtCRe_NMtD-SBtb0vD4LGXK6nhNuU,9098
15
+ ostk/astrodynamics/test/test_converters.py,sha256=mFpDD0YM8o356lj91GGIwYdMk2ut6xZFV3uYcgZepMY,8744
16
16
  ostk/astrodynamics/test/test_dataframe.py,sha256=IJMOODzTVYtiPxekmltH6lOMArPXbHwSEAl-jg0Ab2g,28414
17
17
  ostk/astrodynamics/test/test_display.py,sha256=Ykvw2rUWWw8jsX0Suy11X27bHjwm_ira__xj0ZyHHxw,3672
18
18
  ostk/astrodynamics/test/test_event_condition.py,sha256=mhMTH7wAoYFWRYt_8l2d1vjNPrFhVjMAEET4INLCVXY,1472
19
19
  ostk/astrodynamics/test/test_import.py,sha256=py_hALBR0IYuUzv9dfgQZzrrLHJIpnyKvt3Oi1XBqCg,1251
20
20
  ostk/astrodynamics/test/test_root_solver.py,sha256=hQ8O6g-WP49gZH_H3Rdufv0F0gQorpzJyIcjBGGUQ34,1831
21
21
  ostk/astrodynamics/test/test_trajectory.py,sha256=WC9X-O_prHeVLIZlmmYzgkskrs5ZwvmOfhz8yth2lgI,1345
22
- ostk/astrodynamics/test/test_utilities.py,sha256=kw8quvFTRuoXTbdNdac6IWrjnmA897btNntTMBn-30Q,3614
22
+ ostk/astrodynamics/test/test_utilities.py,sha256=xmRt-5z9-FFYTMQxBpjGrcKP39trbOiH-NLxGcTWOJE,3048
23
23
  ostk/astrodynamics/test/test_viewer.py,sha256=rbVAiclzBZ1bS4Zdh4xUNJ-STCN4G0O7Gr8suSPJrSc,3986
24
24
  ostk/astrodynamics/test/access/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
25
25
  ostk/astrodynamics/test/access/test_generator.py,sha256=7l_MJCZIhWIyPpZ-XouBXlSd5-BfqFDGVOMMxjoRIHA,8032
@@ -45,7 +45,7 @@ ostk/astrodynamics/test/event_condition/test_logical_condition.py,sha256=09h5TYW
45
45
  ostk/astrodynamics/test/event_condition/test_real_condition.py,sha256=tle6HVzMFMIIkfRY7CuaA0mPtw3riJBG_JQkc1L0dpk,1374
46
46
  ostk/astrodynamics/test/flight/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
47
47
  ostk/astrodynamics/test/flight/test_maneuver.py,sha256=6LhnNhaiwId-IveUraHnhx8dYxeOgzntP_WPtv9GFjQ,6197
48
- ostk/astrodynamics/test/flight/test_profile.py,sha256=FOYo-GXGTCtXhKQosXqX6hhrkXPrSyenPkM1lazyEEc,4819
48
+ ostk/astrodynamics/test/flight/test_profile.py,sha256=ZuK2FWWnFzvnJsMgZYeQyG6G0yolUihL4TjGt0BMrZI,7518
49
49
  ostk/astrodynamics/test/flight/test_system.py,sha256=MVaE7lJYisH4vmJPD-G-Hw4wNj-Xe8yMksgu8IXCLvg,1322
50
50
  ostk/astrodynamics/test/flight/system/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
51
51
  ostk/astrodynamics/test/flight/system/test_propulsion_system.py,sha256=jLUC74YjIQz7APr-kQcdYRYiKm8lfequjosptG29kmI,1964
@@ -58,13 +58,13 @@ ostk/astrodynamics/test/solvers/test_finite_difference_solver.py,sha256=49hS9EF7
58
58
  ostk/astrodynamics/test/solvers/test_temporal_condition_solver.py,sha256=ukDuIiEbBBvfqbHfiMIsPk5eyeRsWywCvNT1A3YnmIQ,4862
59
59
  ostk/astrodynamics/test/trajectory/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
60
60
  ostk/astrodynamics/test/trajectory/test_local_orbital_frame_direction.py,sha256=d72J50UGG9m0Atei0UQ8sITU1WdJmws5xgUiacLZMbw,2515
61
- ostk/astrodynamics/test/trajectory/test_local_orbital_frame_factory.py,sha256=33SuZgkXEgYwXhFftRBaSr9Vv60DDJBi5MiXD5H-n6A,2173
61
+ ostk/astrodynamics/test/trajectory/test_local_orbital_frame_factory.py,sha256=hXA23AFZ9RFroBZiGZMxTWGCFJ4fxQpYx50eml1WFjI,3116
62
62
  ostk/astrodynamics/test/trajectory/test_model.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
63
63
  ostk/astrodynamics/test/trajectory/test_orbit.py,sha256=iKVgScEo1osMIXjON4ToURCgp2LDZV18fKQuy49RktI,6484
64
64
  ostk/astrodynamics/test/trajectory/test_propagator.py,sha256=dG31S5FY0NjJ5bnisqJ_HdL7amj_AZAWWLiOmdHfJAc,14233
65
65
  ostk/astrodynamics/test/trajectory/test_segment.py,sha256=ACVMYvjlTyCQHZ2-hFEurK08-LQxt6VlcGTMOrgDHaE,9333
66
66
  ostk/astrodynamics/test/trajectory/test_sequence.py,sha256=gswWqEBTYvbi5TQjF0DNPkAj5COo9Nb9IK7TdwJ_5C0,12882
67
- ostk/astrodynamics/test/trajectory/test_state.py,sha256=PZUpGZKMFRDIyDAfKvSMmmOkiFqkquMz8Hgn_nK_SvI,6878
67
+ ostk/astrodynamics/test/trajectory/test_state.py,sha256=IRtdEyiYHCdBXSRmNEKZy355s7i8STYddOzQAMXh5EY,10805
68
68
  ostk/astrodynamics/test/trajectory/test_state_builder.py,sha256=PSlXtmGURCrlf0w50tn3dEBRo6p0x4NmIF9gJsNOR3k,4782
69
69
  ostk/astrodynamics/test/trajectory/orbit/__init__.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
70
70
  ostk/astrodynamics/test/trajectory/orbit/test_model.py,sha256=epnVn2PwdQkUDZ1msqBRO5nEZIOUBIq-IfK3IlNPijE,21
@@ -93,8 +93,8 @@ ostk/astrodynamics/test/trajectory/state/coordinate_subset/test_angular_velocity
93
93
  ostk/astrodynamics/test/trajectory/state/coordinate_subset/test_attitude_quaternion.py,sha256=UEu9ApzQLmT87eeISw6_gcHTlX-4b2scIvHz-uE1p_c,393
94
94
  ostk/astrodynamics/test/trajectory/state/coordinate_subset/test_cartesian_position.py,sha256=XvHdk1KjacTwtkgx2jUAc9I9N3nvjPDv03FAanpv8jQ,2702
95
95
  ostk/astrodynamics/test/trajectory/state/coordinate_subset/test_cartesian_velocity.py,sha256=-kd5TZO5TICihbkqDTew2i_tDpggdpe3Yf23046FATM,3057
96
- open_space_toolkit_astrodynamics-11.1.0.dist-info/METADATA,sha256=yYiRpTugeOyvQWU2cK_8raeWP7cLw3GtemFlFxudmqU,1916
97
- open_space_toolkit_astrodynamics-11.1.0.dist-info/WHEEL,sha256=q7bGkHj5-uO8ZVDbDA9JExe7sSDQsY_6S4LJ3oOcV_Y,109
98
- open_space_toolkit_astrodynamics-11.1.0.dist-info/top_level.txt,sha256=zOR18699uDYnafgarhL8WU_LmTZY_5NVqutv-flp_x4,5
99
- open_space_toolkit_astrodynamics-11.1.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
100
- open_space_toolkit_astrodynamics-11.1.0.dist-info/RECORD,,
96
+ open_space_toolkit_astrodynamics-12.0.0.dist-info/METADATA,sha256=CLVAoU2IU_TzyOATZkppzbeCyWXNA81cWvySMniQCa4,1916
97
+ open_space_toolkit_astrodynamics-12.0.0.dist-info/WHEEL,sha256=q7bGkHj5-uO8ZVDbDA9JExe7sSDQsY_6S4LJ3oOcV_Y,109
98
+ open_space_toolkit_astrodynamics-12.0.0.dist-info/top_level.txt,sha256=zOR18699uDYnafgarhL8WU_LmTZY_5NVqutv-flp_x4,5
99
+ open_space_toolkit_astrodynamics-12.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
100
+ open_space_toolkit_astrodynamics-12.0.0.dist-info/RECORD,,
@@ -1,5 +1,7 @@
1
1
  # Apache License 2.0
2
2
 
3
+ import re
4
+
3
5
  from datetime import datetime, timezone
4
6
 
5
7
  from ostk.physics.time import Instant
@@ -30,7 +32,9 @@ def coerce_to_datetime(value: Instant | datetime | str) -> datetime:
30
32
 
31
33
  return datetime.fromisoformat(value)
32
34
 
33
- raise TypeError("Argument must be a datetime, an Instant, or a str.")
35
+ raise TypeError(
36
+ f"Argument is of type [{type(value)}]. Must be a datetime, an Instant, or a str."
37
+ )
34
38
 
35
39
 
36
40
  def coerce_to_instant(value: Instant | datetime | str) -> Instant:
@@ -53,7 +57,9 @@ def coerce_to_instant(value: Instant | datetime | str) -> Instant:
53
57
  if isinstance(value, str):
54
58
  return coerce_to_instant(coerce_to_datetime(value))
55
59
 
56
- raise TypeError("Argument must be a datetime, an Instant, or a str.")
60
+ raise TypeError(
61
+ f"Argument is of type [{type(value)}]. Must be a datetime, an Instant, or a str."
62
+ )
57
63
 
58
64
 
59
65
  def coerce_to_iso(value: Instant | datetime | str, timespec: str = "microseconds") -> str:
@@ -77,12 +83,18 @@ def coerce_to_iso(value: Instant | datetime | str, timespec: str = "microseconds
77
83
  if isinstance(value, Instant):
78
84
  return coerce_to_iso(coerce_to_datetime(value), timespec=timespec)
79
85
 
80
- raise TypeError("Argument must be a datetime, an Instant, or a str.")
86
+ raise TypeError(
87
+ f"Argument is of type [{type(value)}]. Must be a datetime, an Instant, or a str."
88
+ )
81
89
 
82
90
 
83
91
  def coerce_to_interval(
84
92
  value: (
85
- Interval | tuple[Instant, Instant] | tuple[datetime, datetime] | tuple[str, str]
93
+ Interval
94
+ | tuple[Instant, Instant]
95
+ | tuple[datetime, datetime]
96
+ | tuple[str, str]
97
+ | str
86
98
  ),
87
99
  ) -> Interval:
88
100
  """
@@ -98,6 +110,18 @@ def coerce_to_interval(
98
110
  if isinstance(value, Interval):
99
111
  return value
100
112
 
113
+ if isinstance(value, str):
114
+ regex = r"\[(.*) - (.*)\] \[UTC\]"
115
+ matches = re.search(regex, value)
116
+
117
+ if matches:
118
+ return Interval.closed(
119
+ start_instant=coerce_to_instant(matches.group(1)),
120
+ end_instant=coerce_to_instant(matches.group(2)),
121
+ )
122
+
123
+ raise ValueError(f"String [{value}] does not match the expected format.")
124
+
101
125
  return Interval.closed(
102
126
  start_instant=coerce_to_instant(value[0]),
103
127
  end_instant=coerce_to_instant(value[1]),
@@ -8,6 +8,14 @@ from ostk.physics.coordinate import Frame
8
8
  from ostk.physics.time import Instant
9
9
 
10
10
  from ostk.astrodynamics.trajectory import State, StateBuilder
11
+ from ostk.astrodynamics.trajectory.state import CoordinateSubset
12
+ from ostk.astrodynamics.converters import coerce_to_instant
13
+ from ostk.astrodynamics.trajectory.state.coordinate_subset import (
14
+ CartesianPosition,
15
+ CartesianVelocity,
16
+ AttitudeQuaternion,
17
+ AngularVelocity,
18
+ )
11
19
 
12
20
 
13
21
  @staticmethod
@@ -33,4 +41,156 @@ def custom_class_generator(frame: Frame, coordinate_subsets: list) -> type:
33
41
  return StateTemplateType
34
42
 
35
43
 
44
+ @staticmethod
45
+ def from_dict(data: dict) -> State:
46
+ """
47
+ Create a State from a dictionary.
48
+ The dictionary must contain the following:
49
+ - 'timestamp': The timestamp of the state.
50
+ - 'rx'/'x_eci'/'x_ecef': The x-coordinate of the position.
51
+ - 'ry'/'y_eci'/'y_ecef': The y-coordinate of the position.
52
+ - 'rz'/'z_eci'/'z_ecef': The z-coordinate of the position.
53
+ - 'vx'/'vx_eci'/'vx_ecef': The x-coordinate of the velocity.
54
+ - 'vy'/'vy_eci'/'vy_ecef': The y-coordinate of the velocity.
55
+ - 'vz'/'vz_eci'/'vz_ecef': The z-coordinate of the velocity.
56
+ - 'frame': The frame of the state. Required if 'rx', 'ry', 'rz', 'vx', 'vy', 'vz' are provided.
57
+ - 'q_B_ECI_x': The x-coordinate of the quaternion.
58
+ - 'q_B_ECI_y': The y-coordinate of the quaternion.
59
+ - 'q_B_ECI_z': The z-coordinate of the quaternion.
60
+ - 'q_B_ECI_s': The s-coordinate of the quaternion.
61
+ - 'w_B_ECI_in_B_x': The x-coordinate of the angular velocity.
62
+ - 'w_B_ECI_in_B_y': The y-coordinate of the angular velocity.
63
+ - 'w_B_ECI_in_B_z': The z-coordinate of the angular velocity.
64
+
65
+
66
+ Args:
67
+ data (dict): The dictionary.
68
+
69
+ Returns:
70
+ State: The State.
71
+ """
72
+
73
+ instant: Instant = coerce_to_instant(data["timestamp"])
74
+
75
+ eci_columns: list[str] = [
76
+ "rx_eci",
77
+ "ry_eci",
78
+ "rz_eci",
79
+ "vx_eci",
80
+ "vy_eci",
81
+ "vz_eci",
82
+ ]
83
+ ecef_columns: list[str] = [
84
+ "rx_ecef",
85
+ "ry_ecef",
86
+ "rz_ecef",
87
+ "vx_ecef",
88
+ "vy_ecef",
89
+ "vz_ecef",
90
+ ]
91
+ generic_columns: list[str] = [
92
+ "rx",
93
+ "ry",
94
+ "rz",
95
+ "vx",
96
+ "vy",
97
+ "vz",
98
+ ]
99
+
100
+ frame: Frame
101
+ coordinates: np.ndarray
102
+
103
+ coordinate_subsets: list[CoordinateSubset] = [
104
+ CartesianPosition.default(),
105
+ CartesianVelocity.default(),
106
+ ]
107
+
108
+ if all(column in data for column in eci_columns):
109
+ frame = Frame.GCRF()
110
+ coordinates = np.array(
111
+ [
112
+ data["rx_eci"],
113
+ data["ry_eci"],
114
+ data["rz_eci"],
115
+ data["vx_eci"],
116
+ data["vy_eci"],
117
+ data["vz_eci"],
118
+ ]
119
+ )
120
+ elif all(column in data for column in ecef_columns):
121
+ frame = Frame.ITRF()
122
+ coordinates = np.array(
123
+ [
124
+ data["rx_ecef"],
125
+ data["ry_ecef"],
126
+ data["rz_ecef"],
127
+ data["vx_ecef"],
128
+ data["vy_ecef"],
129
+ data["vz_ecef"],
130
+ ]
131
+ )
132
+ elif all(column in data for column in generic_columns):
133
+ if "frame" not in data:
134
+ raise ValueError("Frame must be provided for generic columns.")
135
+
136
+ if isinstance(data["frame"], str):
137
+ if Frame.exists(data["frame"]):
138
+ frame = Frame.with_name(data["frame"])
139
+ else:
140
+ raise ValueError(f"No frame exists with name [{data['frame']}].")
141
+ elif isinstance(data["frame"], Frame):
142
+ frame = data["frame"]
143
+ else:
144
+ raise ValueError(f"Invalid frame data [{data['frame']}].")
145
+
146
+ coordinates = np.array(
147
+ [
148
+ data["rx"],
149
+ data["ry"],
150
+ data["rz"],
151
+ data["vx"],
152
+ data["vy"],
153
+ data["vz"],
154
+ ]
155
+ )
156
+ else:
157
+ raise ValueError("Invalid state data.")
158
+
159
+ if all(
160
+ column in data for column in ["q_B_ECI_x", "q_B_ECI_y", "q_B_ECI_z", "q_B_ECI_s"]
161
+ ):
162
+ coordinate_subsets.append(AttitudeQuaternion.default())
163
+ coordinates = np.append(
164
+ coordinates,
165
+ [
166
+ data["q_B_ECI_x"],
167
+ data["q_B_ECI_y"],
168
+ data["q_B_ECI_z"],
169
+ data["q_B_ECI_s"],
170
+ ],
171
+ )
172
+
173
+ if all(
174
+ column in data
175
+ for column in ["w_B_ECI_in_B_x", "w_B_ECI_in_B_y", "w_B_ECI_in_B_z"]
176
+ ):
177
+ coordinate_subsets.append(AngularVelocity.default())
178
+ coordinates = np.append(
179
+ coordinates,
180
+ [
181
+ data["w_B_ECI_in_B_x"],
182
+ data["w_B_ECI_in_B_y"],
183
+ data["w_B_ECI_in_B_z"],
184
+ ],
185
+ )
186
+
187
+ return State(
188
+ instant=instant,
189
+ coordinates=coordinates,
190
+ frame=frame,
191
+ coordinate_subsets=coordinate_subsets,
192
+ )
193
+
194
+
195
+ State.from_dict = from_dict
36
196
  State.template = custom_class_generator
@@ -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
@@ -32,6 +33,21 @@ def instant() -> Instant:
32
33
  return Instant.date_time(DateTime(2020, 1, 1, 0, 0, 30), Scale.UTC)
33
34
 
34
35
 
36
+ @pytest.fixture
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
+
35
51
  @pytest.fixture
36
52
  def transform_model() -> TransformModel:
37
53
  def dynamic_provider_generator(instant: Instant):
@@ -78,11 +94,46 @@ def profile(request) -> Profile:
78
94
  return Profile(model=model)
79
95
 
80
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
+
81
128
  class TestProfile:
82
129
  def test_constructors(self, profile: Profile):
83
130
  assert profile is not None
84
131
  assert isinstance(profile, Profile)
85
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
+
86
137
  def test_get_state_at(self, profile: Profile, instant: Instant):
87
138
  state: State = profile.get_state_at(instant)
88
139
 
@@ -130,18 +181,62 @@ class TestProfile:
130
181
  assert isinstance(profile, Profile)
131
182
  assert profile.is_defined()
132
183
 
133
- def test_nadir_pointing(self):
134
- environment = Environment.default()
184
+ def test_nadir_pointing(
185
+ self,
186
+ orbit: Orbit,
187
+ ):
188
+ profile: Profile = Profile.nadir_pointing(orbit, Orbit.FrameType.VVLH)
135
189
 
136
- orbit = Orbit.sun_synchronous(
137
- epoch=Instant.date_time(datetime(2020, 1, 1, 0, 0, 0), Scale.UTC),
138
- altitude=Length.kilometers(500.0),
139
- local_time_at_descending_node=Time(14, 0, 0),
140
- celestial_object=environment.access_celestial_object_with_name("Earth"),
190
+ assert profile is not None
191
+ assert isinstance(profile, Profile)
192
+ assert profile.is_defined()
193
+
194
+ def test_custom_pointing(
195
+ self,
196
+ orbit: Orbit,
197
+ alignment_target: Profile.Target,
198
+ clocking_target: Profile.Target,
199
+ ):
200
+ profile = Profile.custom_pointing(
201
+ orbit, Profile.align_and_constrain(alignment_target, clocking_target)
141
202
  )
142
203
 
143
- profile: Profile = Profile.nadir_pointing(orbit, Orbit.FrameType.VVLH)
204
+ assert profile is not None
205
+ assert profile.is_defined()
206
+
207
+ def test_align_and_constrain(
208
+ self,
209
+ orbit: Orbit,
210
+ instant: Instant,
211
+ alignment_target: Profile.Target,
212
+ clocking_target: Profile.Target,
213
+ ):
214
+ orientation = Profile.align_and_constrain(
215
+ alignment_target=alignment_target,
216
+ clocking_target=clocking_target,
217
+ )
218
+
219
+ assert orientation is not None
220
+ assert orientation(orbit.get_state_at(instant)) is not None
221
+
222
+ def test_custom_pointing(
223
+ self,
224
+ orbit: Orbit,
225
+ alignment_target: Profile.Target,
226
+ clocking_target: Profile.Target,
227
+ ):
228
+ profile = Profile.custom_pointing(
229
+ orbit, Profile.align_and_constrain(alignment_target, clocking_target)
230
+ )
231
+
232
+ assert profile is not None
233
+ assert profile.is_defined()
234
+
235
+ profile = Profile.custom_pointing(
236
+ orbit,
237
+ alignment_target,
238
+ clocking_target,
239
+ )
144
240
 
145
241
  assert profile is not None
146
- assert isinstance(profile, Profile)
147
242
  assert profile.is_defined()
@@ -241,52 +241,49 @@ def test_coerce_to_iso_failure():
241
241
  coerce_to_iso("some_ill_formed_iso")
242
242
 
243
243
 
244
- def test_coerce_to_interval_success_interval():
245
- value = Interval.closed(
246
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
247
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
248
- )
249
- assert coerce_to_interval(value) == value
250
-
251
-
252
- def test_coerce_to_interval_success_tuple_instant():
253
- value = (
254
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
255
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
256
- )
257
- assert coerce_to_interval(value) == Interval.closed(
258
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
259
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
260
- )
261
-
262
-
263
- def test_coerce_to_interval_success_list_instant():
264
- value = [
265
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
266
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
267
- ]
268
- assert coerce_to_interval(value) == Interval.closed(
269
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
270
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
271
- )
272
-
273
-
274
- def test_coerce_to_interval_success_tuple_datetime():
275
- value = (
276
- datetime(2020, 1, 1, tzinfo=timezone.utc),
277
- datetime(2020, 1, 2, tzinfo=timezone.utc),
278
- )
279
- assert coerce_to_interval(value) == Interval.closed(
280
- Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
281
- Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
282
- )
283
-
284
-
285
- def test_coerce_to_interval_success_list_datetime():
286
- value = [
287
- datetime(2020, 1, 1, tzinfo=timezone.utc),
288
- datetime(2020, 1, 2, tzinfo=timezone.utc),
289
- ]
244
+ @pytest.mark.parametrize(
245
+ "value",
246
+ [
247
+ (
248
+ Interval.closed(
249
+ Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
250
+ Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
251
+ )
252
+ ),
253
+ (
254
+ (
255
+ Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
256
+ Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
257
+ )
258
+ ),
259
+ (
260
+ [
261
+ Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
262
+ Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
263
+ ]
264
+ ),
265
+ (
266
+ (
267
+ datetime(2020, 1, 1, tzinfo=timezone.utc),
268
+ datetime(2020, 1, 2, tzinfo=timezone.utc),
269
+ )
270
+ ),
271
+ (
272
+ [
273
+ datetime(2020, 1, 1, tzinfo=timezone.utc),
274
+ datetime(2020, 1, 2, tzinfo=timezone.utc),
275
+ ]
276
+ ),
277
+ (
278
+ [
279
+ "2020-01-01T00:00:00Z",
280
+ "2020-01-02T00:00:00Z",
281
+ ]
282
+ ),
283
+ ("[2020-01-01T00:00:00Z - 2020-01-02T00:00:00Z] [UTC]"),
284
+ ],
285
+ )
286
+ def test_coerce_to_interval(value):
290
287
  assert coerce_to_interval(value) == Interval.closed(
291
288
  Instant.date_time(DateTime(2020, 1, 1), Scale.UTC),
292
289
  Instant.date_time(DateTime(2020, 1, 2), Scale.UTC),
@@ -1,25 +1,15 @@
1
1
  # Apache License 2.0
2
2
 
3
- import pytest
3
+ from datetime import datetime
4
4
 
5
- import ostk.physics as physics
6
- import ostk.astrodynamics as astrodynamics
7
- from ostk.astrodynamics import utilities
5
+ from ostk.physics import Environment
6
+ from ostk.physics.time import Instant, Interval
7
+ from ostk.physics.coordinate import Position
8
+ from ostk.physics.coordinate.spherical import LLA, AER
8
9
 
9
- Length = physics.unit.Length
10
- Angle = physics.unit.Angle
11
- DateTime = physics.time.DateTime
12
- Scale = physics.time.Scale
13
- Instant = physics.time.Instant
14
- Interval = physics.time.Interval
15
- Position = physics.coordinate.Position
16
- Velocity = physics.coordinate.Velocity
17
- Frame = physics.coordinate.Frame
18
- LLA = physics.coordinate.spherical.LLA
19
- AER = physics.coordinate.spherical.AER
20
- Environment = physics.Environment
21
- Trajectory = astrodynamics.Trajectory
22
- State = astrodynamics.trajectory.State
10
+ from ostk.astrodynamics import utilities
11
+ from ostk.astrodynamics import Trajectory
12
+ from ostk.astrodynamics.trajectory import State
23
13
 
24
14
 
25
15
  class TestUtility:
@@ -27,22 +17,16 @@ class TestUtility:
27
17
  self,
28
18
  state: State,
29
19
  ):
30
- lla: tuple[float, float, float] = utilities.lla_from_state(state)
20
+ lla: LLA = utilities.lla_from_state(state)
31
21
 
32
22
  assert lla is not None
33
- assert len(lla) == 3
34
- assert isinstance(lla, tuple)
35
- assert isinstance(lla[0], float)
36
- assert lla[0] >= -90.0 and lla[0] <= 90.0
37
- assert isinstance(lla[1], float)
38
- assert lla[0] >= -180.0 and lla[0] <= 180.0
39
- assert isinstance(lla[2], float)
23
+ assert isinstance(lla, LLA)
24
+ assert lla.is_defined()
40
25
 
41
26
  def test_lla_from_position(
42
27
  self,
43
28
  instant: Instant,
44
29
  position: Position,
45
- state: State,
46
30
  ):
47
31
  lla: LLA = utilities.lla_from_position(position, instant)
48
32
 
@@ -50,12 +34,6 @@ class TestUtility:
50
34
  assert isinstance(lla, LLA)
51
35
  assert lla.is_defined()
52
36
 
53
- lla_from_state: tuple[float, float, float] = utilities.lla_from_state(state)
54
-
55
- assert lla.get_latitude().in_degrees() == lla_from_state[0]
56
- assert lla.get_longitude().in_degrees() == lla_from_state[1]
57
- assert lla.get_altitude().in_meters() == lla_from_state[2]
58
-
59
37
  def test_position_from_lla(
60
38
  self,
61
39
  lla: LLA,
@@ -72,28 +50,24 @@ class TestUtility:
72
50
  position: Position,
73
51
  environment: Environment,
74
52
  ):
75
- aer: tuple[float, float, float] = utilities.compute_aer(
76
- instant, position, position, environment
77
- )
53
+ aer: AER = utilities.compute_aer(instant, position, position, environment)
78
54
 
79
55
  assert aer is not None
80
- assert len(aer) == 3
81
- assert aer[2] == 0.0
56
+ assert isinstance(aer, AER)
82
57
 
83
- def test_compute_time_lla_aer_state(
58
+ def test_compute_time_lla_aer_coordinates(
84
59
  self,
85
60
  state: State,
86
61
  position: Position,
87
62
  environment: Environment,
88
63
  ):
89
- time_lla_aer: float[Instant, float, float, float, float, float, float] = (
90
- utilities.compute_time_lla_aer_state(state, position, environment)
64
+ time_lla_aer: float[datetime, float, float, float, float, float, float] = (
65
+ utilities.compute_time_lla_aer_coordinates(state, position, environment)
91
66
  )
92
67
 
93
68
  assert time_lla_aer is not None
94
69
  assert len(time_lla_aer) == 7
95
- assert isinstance(time_lla_aer[0], Instant)
96
- assert time_lla_aer[0] == state.get_instant()
70
+ assert isinstance(time_lla_aer[0], datetime)
97
71
  for i in range(1, 7):
98
72
  assert isinstance(time_lla_aer[i], float)
99
73
 
@@ -102,12 +76,23 @@ class TestUtility:
102
76
  interval: Interval,
103
77
  trajectory: Trajectory,
104
78
  ):
105
- output: list[tuple[float, float, float]] = utilities.compute_trajectory_geometry(
106
- trajectory, interval
107
- )
79
+ output: list[LLA] = utilities.compute_trajectory_geometry(trajectory, interval)
80
+
81
+ assert output is not None
82
+ assert len(output) == 2
83
+ assert isinstance(output[0], LLA)
84
+
85
+ def test_compute_ground_track(
86
+ self,
87
+ interval: Interval,
88
+ trajectory: Trajectory,
89
+ ):
90
+ output: list[LLA] = utilities.compute_ground_track(trajectory, interval)
108
91
 
109
92
  assert output is not None
110
93
  assert len(output) == 2
94
+ assert isinstance(output[0], LLA)
95
+ assert output[0].get_altitude().in_meters() <= 15.0
111
96
 
112
97
  def test_convert_state(self, state: State):
113
98
  output: tuple[
@@ -5,11 +5,11 @@ import pytest
5
5
  from ostk.physics.time import Instant
6
6
  from ostk.physics.time import DateTime
7
7
  from ostk.physics.time import Scale
8
- from ostk.physics.coordinate import Position
9
- from ostk.physics.coordinate import Velocity
10
8
  from ostk.physics.coordinate import Frame
9
+ from ostk.physics.coordinate import Transform
11
10
 
12
11
  from ostk.astrodynamics.trajectory import LocalOrbitalFrameFactory
12
+ from ostk.astrodynamics.trajectory import LocalOrbitalFrameTransformProvider
13
13
 
14
14
 
15
15
  @pytest.fixture
@@ -17,6 +17,16 @@ def parent_frame() -> Frame:
17
17
  return Frame.GCRF()
18
18
 
19
19
 
20
+ @pytest.fixture
21
+ def local_orbital_transform_provider_type() -> LocalOrbitalFrameTransformProvider.Type:
22
+ return LocalOrbitalFrameTransformProvider.Type.VNC
23
+
24
+
25
+ @pytest.fixture
26
+ def transform_generator() -> callable:
27
+ return lambda instant, position, velocity: Transform.identity(Transform.Type.passive)
28
+
29
+
20
30
  @pytest.fixture
21
31
  def local_orbital_frame_factory(parent_frame: Frame) -> LocalOrbitalFrameFactory:
22
32
  return LocalOrbitalFrameFactory.VNC(parent_frame)
@@ -74,3 +84,25 @@ class TestLocalOrbitalFrameFactory:
74
84
  local_orbital_frame_factory: LocalOrbitalFrameFactory,
75
85
  ):
76
86
  assert local_orbital_frame_factory.is_defined()
87
+
88
+ def test_constructor(
89
+ self,
90
+ local_orbital_transform_provider_type: LocalOrbitalFrameTransformProvider.Type,
91
+ parent_frame: Frame,
92
+ ):
93
+ assert (
94
+ LocalOrbitalFrameFactory.construct(
95
+ local_orbital_transform_provider_type, parent_frame
96
+ )
97
+ is not None
98
+ )
99
+
100
+ def test_custom_constructor(
101
+ self,
102
+ transform_generator: callable,
103
+ parent_frame: Frame,
104
+ ):
105
+ assert (
106
+ LocalOrbitalFrameFactory.construct(transform_generator, parent_frame)
107
+ is not None
108
+ )
@@ -2,6 +2,8 @@
2
2
 
3
3
  import pytest
4
4
 
5
+ from datetime import datetime, timezone
6
+
5
7
  import numpy as np
6
8
 
7
9
  from ostk.mathematics.geometry.d3.transformation.rotation import Quaternion
@@ -177,6 +179,142 @@ class TestState:
177
179
  assert custom_state == state
178
180
  assert custom_state is not state
179
181
 
182
+ def test_from_dict_with_eci_coordinates(self):
183
+ data = {
184
+ "timestamp": datetime.now(timezone.utc),
185
+ "rx_eci": 7000.0,
186
+ "ry_eci": 0.0,
187
+ "rz_eci": 0.0,
188
+ "vx_eci": 0.0,
189
+ "vy_eci": 7.5,
190
+ "vz_eci": 0.0,
191
+ }
192
+
193
+ state: State = State.from_dict(data)
194
+
195
+ assert state is not None
196
+ assert isinstance(state, State)
197
+ assert state.get_frame() == Frame.GCRF()
198
+
199
+ def test_from_dict_with_ecef_coordinates(self):
200
+ data = {
201
+ "timestamp": datetime.now(timezone.utc).isoformat(),
202
+ "rx_ecef": 7000.0,
203
+ "ry_ecef": 0.0,
204
+ "rz_ecef": 0.0,
205
+ "vx_ecef": 0.0,
206
+ "vy_ecef": 7.5,
207
+ "vz_ecef": 0.0,
208
+ }
209
+
210
+ state: State = State.from_dict(data)
211
+
212
+ assert state is not None
213
+ assert isinstance(state, State)
214
+ assert state.get_frame() == Frame.ITRF()
215
+
216
+ def test_from_dict_with_generic_coordinates(self):
217
+ data = {
218
+ "timestamp": datetime.now(timezone.utc).isoformat(),
219
+ "rx": 7000.0,
220
+ "ry": 0.0,
221
+ "rz": 0.0,
222
+ "vx": 0.0,
223
+ "vy": 7.5,
224
+ "vz": 0.0,
225
+ "frame": "GCRF",
226
+ }
227
+
228
+ state: State = State.from_dict(data)
229
+
230
+ assert state is not None
231
+ assert isinstance(state, State)
232
+ assert state.get_frame() == Frame.GCRF()
233
+
234
+ with pytest.raises(
235
+ ValueError, match="Frame must be provided for generic columns."
236
+ ):
237
+ data = {
238
+ "timestamp": datetime.now(timezone.utc).isoformat(),
239
+ "rx": 7000.0,
240
+ "ry": 0.0,
241
+ "rz": 0.0,
242
+ "vx": 0.0,
243
+ "vy": 7.5,
244
+ "vz": 0.0,
245
+ }
246
+
247
+ State.from_dict(data)
248
+
249
+ with pytest.raises(ValueError, match="No frame exists with name \\[RANDOM\\]."):
250
+ data = {
251
+ "timestamp": datetime.now(timezone.utc).isoformat(),
252
+ "rx": 7000.0,
253
+ "ry": 0.0,
254
+ "rz": 0.0,
255
+ "vx": 0.0,
256
+ "vy": 7.5,
257
+ "vz": 0.0,
258
+ "frame": "RANDOM",
259
+ }
260
+
261
+ State.from_dict(data)
262
+
263
+ with pytest.raises(ValueError, match="Invalid frame data \\[123\\]"):
264
+ data = {
265
+ "timestamp": datetime.now(timezone.utc).isoformat(),
266
+ "rx": 7000.0,
267
+ "ry": 0.0,
268
+ "rz": 0.0,
269
+ "vx": 0.0,
270
+ "vy": 7.5,
271
+ "vz": 0.0,
272
+ "frame": 123,
273
+ }
274
+
275
+ State.from_dict(data)
276
+
277
+ def test_from_dict_with_attitude_quaternion(self):
278
+ data = {
279
+ "timestamp": datetime.now(timezone.utc).isoformat(),
280
+ "rx_eci": 7000.0,
281
+ "ry_eci": 0.0,
282
+ "rz_eci": 0.0,
283
+ "vx_eci": 0.0,
284
+ "vy_eci": 7.5,
285
+ "vz_eci": 0.0,
286
+ "q_B_ECI_x": 0.0,
287
+ "q_B_ECI_y": 0.0,
288
+ "q_B_ECI_z": 0.0,
289
+ "q_B_ECI_s": 1.0,
290
+ }
291
+
292
+ state: State = State.from_dict(data)
293
+
294
+ assert state is not None
295
+ assert isinstance(state, State)
296
+ assert state.get_frame() == Frame.GCRF()
297
+
298
+ def test_from_dict_with_angular_velocity(self):
299
+ data = {
300
+ "timestamp": datetime.now(timezone.utc).isoformat(),
301
+ "rx_eci": 7000.0,
302
+ "ry_eci": 0.0,
303
+ "rz_eci": 0.0,
304
+ "vx_eci": 0.0,
305
+ "vy_eci": 7.5,
306
+ "vz_eci": 0.0,
307
+ "w_B_ECI_in_B_x": 0.1,
308
+ "w_B_ECI_in_B_y": 0.2,
309
+ "w_B_ECI_in_B_z": 0.3,
310
+ }
311
+
312
+ state: State = State.from_dict(data)
313
+
314
+ assert state is not None
315
+ assert isinstance(state, State)
316
+ assert state.get_frame() == Frame.GCRF()
317
+
180
318
  def test_comparators(self, state: State):
181
319
  assert (state == state) is True
182
320
  assert (state != state) is False
@@ -1,5 +1,8 @@
1
1
  # Apache License 2.0
2
2
 
3
+ from datetime import datetime
4
+ from datetime import timezone
5
+
3
6
  from .OpenSpaceToolkitAstrodynamicsPy import *
4
7
 
5
8
  from ostk.physics import Environment
@@ -7,6 +10,7 @@ from ostk.physics.time import Scale
7
10
  from ostk.physics.time import Instant
8
11
  from ostk.physics.time import Duration
9
12
  from ostk.physics.time import Interval
13
+ from ostk.physics.unit import Length
10
14
  from ostk.physics.coordinate.spherical import LLA
11
15
  from ostk.physics.coordinate.spherical import AER
12
16
  from ostk.physics.coordinate import Position
@@ -15,18 +19,18 @@ from ostk.physics.environment.object.celestial import Earth
15
19
  from ostk.physics.environment.gravitational import Earth as EarthGravitationalModel
16
20
 
17
21
 
18
- def lla_from_state(state: trajectory.State) -> tuple[float, float, float]:
22
+ def lla_from_state(state: trajectory.State) -> LLA:
19
23
  """
20
24
  Return latitude (degrees), longitude (degrees), altitude (meters) float list from a state.
21
- """
22
25
 
23
- lla: LLA = lla_from_position(state.get_position(), state.get_instant())
26
+ Args:
27
+ state (trajectory.State): A state.
24
28
 
25
- return (
26
- float(lla.get_latitude().in_degrees()),
27
- float(lla.get_longitude().in_degrees()),
28
- float(lla.get_altitude().in_meters()),
29
- )
29
+ Returns:
30
+ LLA: The LLA.
31
+ """
32
+
33
+ return lla_from_position(state.get_position(), state.get_instant())
30
34
 
31
35
 
32
36
  def lla_from_position(
@@ -35,6 +39,13 @@ def lla_from_position(
35
39
  ) -> LLA:
36
40
  """
37
41
  Return LLA from position and instant.
42
+
43
+ Args:
44
+ position (Position): A position.
45
+ instant (Instant): An instant.
46
+
47
+ Returns:
48
+ LLA: The LLA.
38
49
  """
39
50
 
40
51
  if position.access_frame() != Frame.ITRF():
@@ -54,6 +65,12 @@ def lla_from_position(
54
65
  def position_from_lla(lla: LLA) -> Position:
55
66
  """
56
67
  Return position from lla.
68
+
69
+ Args:
70
+ lla (LLA): A LLA.
71
+
72
+ Returns:
73
+ Position: The position.
57
74
  """
58
75
 
59
76
  return Position.meters(
@@ -70,9 +87,18 @@ def compute_aer(
70
87
  from_position: Position,
71
88
  to_position: Position,
72
89
  environment: Environment,
73
- ) -> tuple[float, float, float]:
90
+ ) -> AER:
74
91
  """
75
- Return [azimuth (degrees), elevation (degrees), range (meters)] from Instant and Positions (observer, target).
92
+ Return AER from Instant and Positions (observer, target).
93
+
94
+ Args:
95
+ instant (Instant): An instant.
96
+ from_position (Position): An observer position.
97
+ to_position (Position): A target position.
98
+ environment (Environment): An environment.
99
+
100
+ Returns:
101
+ AER: The AER.
76
102
  """
77
103
 
78
104
  from_lla: LLA = lla_from_position(from_position, instant)
@@ -83,27 +109,30 @@ def compute_aer(
83
109
  from_position_NED: Position = from_position.in_frame(ned_frame, instant)
84
110
  to_position_NED: Position = to_position.in_frame(ned_frame, instant)
85
111
 
86
- aer: AER = AER.from_position_to_position(from_position_NED, to_position_NED, True)
112
+ return AER.from_position_to_position(from_position_NED, to_position_NED, True)
87
113
 
88
- return (
89
- float(aer.get_azimuth().in_degrees()),
90
- float(aer.get_elevation().in_degrees()),
91
- float(aer.get_range().in_meters()),
92
- )
93
114
 
94
-
95
- def compute_time_lla_aer_state(
115
+ def compute_time_lla_aer_coordinates(
96
116
  state: trajectory.State,
97
117
  from_position: Position,
98
118
  environment: Environment,
99
- ) -> tuple[Instant, float, float, float, float, float, float]:
119
+ ) -> tuple[datetime, float, float, float, float, float, float]:
100
120
  """
101
- Return [instant, latitude, longitude, altitude, azimuth, elevation, range] from State and observer Position.
121
+ Return [datetime, latitude, longitude, altitude, azimuth, elevation, range] from State and observer Position.
122
+
123
+ Args:
124
+ state (trajectory.State): A state.
125
+ from_position (Position): An observer position.
126
+ environment (Environment): An environment.
127
+
128
+ Returns:
129
+ tuple[datetime, float, float, float, float, float, float]: The [datetime, latitude, longitude, altitude, azimuth, elevation, range].
102
130
  """
103
131
 
104
132
  instant: Instant = state.get_instant()
105
133
 
106
- lla: tuple[float, float, float] = lla_from_state(state)
134
+ lla: LLA = lla_from_state(state)
135
+
107
136
  aer: AER = compute_aer(
108
137
  instant,
109
138
  from_position,
@@ -111,44 +140,102 @@ def compute_time_lla_aer_state(
111
140
  environment,
112
141
  )
113
142
 
114
- return (instant, lla[0], lla[1], lla[2], aer[0], aer[1], aer[2])
143
+ return (
144
+ instant.get_date_time(Scale.UTC).replace(tzinfo=timezone.utc),
145
+ float(lla.get_latitude().in_degrees()),
146
+ float(lla.get_longitude().in_degrees()),
147
+ float(lla.get_altitude().in_meters()),
148
+ float(aer.get_azimuth().in_degrees()),
149
+ float(aer.get_elevation().in_degrees()),
150
+ float(aer.get_range().in_meters()),
151
+ )
115
152
 
116
153
 
117
154
  def compute_trajectory_geometry(
118
155
  trajectory: Trajectory,
119
156
  interval: Interval,
120
- ) -> list[tuple[float, float, float]]:
157
+ step: Duration = Duration.minutes(1.0),
158
+ ) -> list[LLA]:
121
159
  """
122
- Return [latitude (degrees), longitude (degrees), altitude (meters)] values along a Trajectory during Interval.
160
+ Return list of LLA along a Trajectory during the Interval at the provided step.
161
+
162
+ Args:
163
+ trajectory (Trajectory): A trajectory.
164
+ interval (Interval): An interval.
165
+ step (Duration): A step. Defaults to 1.0 minutes.
166
+
167
+ Returns:
168
+ list[LLA]: The list of LLA.
123
169
  """
124
170
 
125
171
  return [
126
172
  lla_from_state(state)
127
- for state in trajectory.get_states_at(
128
- interval.generate_grid(Duration.minutes(1.0))
129
- )
173
+ for state in trajectory.get_states_at(interval.generate_grid(step))
130
174
  ]
131
175
 
132
176
 
177
+ def compute_ground_track(
178
+ trajectory: Trajectory,
179
+ interval: Interval,
180
+ step: Duration = Duration.minutes(1.0),
181
+ ) -> list[LLA]:
182
+ """
183
+ Return list of LLA along a Trajectory ground track (altitude set to 10.0 meters) during the Interval at the provided step.
184
+
185
+ Args:
186
+ trajectory (Trajectory): A trajectory.
187
+ interval (Interval): An interval.
188
+ step (Duration): A step. Defaults to 1.0 minutes.
189
+
190
+ Returns:
191
+ list[LLA]: The list of LLA.
192
+ """
193
+
194
+ ground_llas: list[LLA] = []
195
+ for state in trajectory.get_states_at(interval.generate_grid(step)):
196
+ lla: LLA = lla_from_state(state)
197
+
198
+ ground_lla: LLA = LLA(
199
+ lla.get_latitude(),
200
+ lla.get_longitude(),
201
+ Length.meters(10.0),
202
+ )
203
+
204
+ ground_llas.append(ground_lla)
205
+
206
+ return ground_llas
207
+
208
+
133
209
  def convert_state(
134
210
  state: trajectory.State,
135
211
  ) -> tuple[str, float, float, float, float, float, float, float, float, float]:
136
212
  """
137
213
  Convert a State into dataframe-ready values.
214
+ - Instant (str)
215
+ - Modified Julian Date (float)
216
+ - Position X [meters] (float)
217
+ - Position Y [meters] (float)
218
+ - Position Z [meters] (float)
219
+ - Velocity X [meters/second] (float)
220
+ - Velocity Y [meters/second] (float)
221
+ - Velocity Z [meters/second] (float)
222
+ - Latitude [degrees] (float)
223
+ - Longitude [degrees] (float)
224
+ - Altitude [meters] (float)
225
+
226
+ Args:
227
+ state (trajectory.State): A state.
228
+
229
+ Returns:
230
+ tuple[str, float, float, float, float, float, float, float, float, float]: The dataframe-ready values.
138
231
  """
139
232
 
140
- lla: LLA = LLA.cartesian(
141
- state.get_position()
142
- .in_frame(Frame.ITRF(), state.get_instant())
143
- .get_coordinates(),
144
- EarthGravitationalModel.EGM2008.equatorial_radius,
145
- EarthGravitationalModel.EGM2008.flattening,
146
- )
233
+ lla: LLA = lla_from_state(state)
147
234
 
148
235
  instant: Instant = state.get_instant()
149
236
 
150
237
  return (
151
- repr(instant),
238
+ str(instant.to_string()),
152
239
  float(instant.get_modified_julian_date(Scale.UTC)),
153
240
  *state.get_position().get_coordinates().transpose().tolist(),
154
241
  *state.get_velocity().get_coordinates().transpose().tolist(),