astrox-python 0.2.0__py3-none-any.whl → 1.1.0__py3-none-any.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.
- astrox/__init__.py +3 -1
- astrox/catalog.py +22 -3
- astrox/celestial.py +258 -5
- astrox/libration.py +448 -0
- astrox/orbits.py +38 -6
- astrox/propagator.py +31 -0
- astrox/terrain.py +14 -1
- {astrox_python-0.2.0.dist-info → astrox_python-1.1.0.dist-info}/METADATA +3 -3
- {astrox_python-0.2.0.dist-info → astrox_python-1.1.0.dist-info}/RECORD +11 -10
- {astrox_python-0.2.0.dist-info → astrox_python-1.1.0.dist-info}/WHEEL +1 -1
- {astrox_python-0.2.0.dist-info → astrox_python-1.1.0.dist-info}/licenses/LICENSE +0 -0
astrox/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ Functions are organized by domain modules and can use package-level
|
|
|
4
4
|
configuration without requiring explicit client management:
|
|
5
5
|
|
|
6
6
|
import astrox
|
|
7
|
-
from astrox import access, celestial, catalog, components, coverage, lighting, orbits, propagator, terrain
|
|
7
|
+
from astrox import access, celestial, catalog, components, coverage, libration, lighting, orbits, propagator, terrain
|
|
8
8
|
|
|
9
9
|
astrox.configure(base_url="http://custom:8765", timeout=120)
|
|
10
10
|
orbit = orbits.keplerian(...)
|
|
@@ -35,6 +35,7 @@ from astrox import (
|
|
|
35
35
|
conjunction,
|
|
36
36
|
components,
|
|
37
37
|
coverage,
|
|
38
|
+
libration,
|
|
38
39
|
lighting,
|
|
39
40
|
orbits,
|
|
40
41
|
propagator,
|
|
@@ -60,6 +61,7 @@ __all__ = [
|
|
|
60
61
|
"conjunction",
|
|
61
62
|
"coverage",
|
|
62
63
|
"components",
|
|
64
|
+
"libration",
|
|
63
65
|
"lighting",
|
|
64
66
|
"orbits",
|
|
65
67
|
"propagator",
|
astrox/catalog.py
CHANGED
|
@@ -43,6 +43,16 @@ def _optional_boolean_query(value: bool | None, *, parameter: str) -> str | None
|
|
|
43
43
|
return "true" if value else "false"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _without_status_fields(value: Any, *, endpoint: str) -> dict[str, Any]:
|
|
47
|
+
if not isinstance(value, dict):
|
|
48
|
+
raise TypeError(f"{endpoint} response must be an object")
|
|
49
|
+
return {
|
|
50
|
+
key: item
|
|
51
|
+
for key, item in value.items()
|
|
52
|
+
if key not in {"IsSuccess", "Message"}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
46
56
|
def query_cities(
|
|
47
57
|
*,
|
|
48
58
|
city_name: str | None = None,
|
|
@@ -72,7 +82,10 @@ def query_cities(
|
|
|
72
82
|
"typeOfCity",
|
|
73
83
|
_optional_string(city_type, parameter="city_type"),
|
|
74
84
|
)
|
|
75
|
-
return
|
|
85
|
+
return _without_status_fields(
|
|
86
|
+
raw.get("/city", params=params),
|
|
87
|
+
endpoint="/city",
|
|
88
|
+
)
|
|
76
89
|
|
|
77
90
|
|
|
78
91
|
def query_facilities(
|
|
@@ -92,7 +105,10 @@ def query_facilities(
|
|
|
92
105
|
"networkName",
|
|
93
106
|
_optional_string(network_name, parameter="network_name"),
|
|
94
107
|
)
|
|
95
|
-
return
|
|
108
|
+
return _without_status_fields(
|
|
109
|
+
raw.get("/facility", params=params),
|
|
110
|
+
endpoint="/facility",
|
|
111
|
+
)
|
|
96
112
|
|
|
97
113
|
|
|
98
114
|
def query_satellites(
|
|
@@ -172,4 +188,7 @@ def query_satellites(
|
|
|
172
188
|
parameter="maximum_inclination_deg",
|
|
173
189
|
),
|
|
174
190
|
)
|
|
175
|
-
return
|
|
191
|
+
return _without_status_fields(
|
|
192
|
+
raw.get("/ssc", params=params),
|
|
193
|
+
endpoint="/ssc",
|
|
194
|
+
)
|
astrox/celestial.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Celestial read-like and computational endpoint functions."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from numbers import Real
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from astrox._http import raw
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
12
|
+
"MpcOrbitalElements",
|
|
11
13
|
"cb_axes_rotation",
|
|
12
14
|
"ephemeris",
|
|
15
|
+
"lambert_transfer_window",
|
|
13
16
|
"mpc_ephemeris",
|
|
17
|
+
"mpc_orbital_elements",
|
|
14
18
|
]
|
|
15
19
|
|
|
16
20
|
|
|
@@ -47,6 +51,235 @@ def _optional_integer(value: int | None, *, parameter: str) -> int | None:
|
|
|
47
51
|
return value
|
|
48
52
|
|
|
49
53
|
|
|
54
|
+
def _without_status_fields(value: Any, *, endpoint: str) -> dict[str, Any]:
|
|
55
|
+
if not isinstance(value, dict):
|
|
56
|
+
raise TypeError(f"{endpoint} response must be an object")
|
|
57
|
+
return {
|
|
58
|
+
key: item
|
|
59
|
+
for key, item in value.items()
|
|
60
|
+
if key not in {"IsSuccess", "Message"}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, kw_only=True)
|
|
65
|
+
class MpcOrbitalElements:
|
|
66
|
+
"""MPC heliocentric orbital elements used by celestial transfer routes."""
|
|
67
|
+
|
|
68
|
+
epoch_mjd_tdt: float | None = None
|
|
69
|
+
periapsis_time_mjd_tdt: float | None = None
|
|
70
|
+
periapsis_distance_au: float | None = None
|
|
71
|
+
semi_major_axis_au: float | None = None
|
|
72
|
+
eccentricity: float | None = None
|
|
73
|
+
inclination_deg: float | None = None
|
|
74
|
+
raan_deg: float | None = None
|
|
75
|
+
argument_of_periapsis_deg: float | None = None
|
|
76
|
+
mean_anomaly_deg: float | None = None
|
|
77
|
+
reference_frame: str | None = None
|
|
78
|
+
"""Heliocentric mean-ecliptic variant: ``MeanEclpJ2000`` (JPL) or
|
|
79
|
+
``EclpJ2000ICRF`` (MPC, server default)."""
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
for field_name in (
|
|
83
|
+
"epoch_mjd_tdt",
|
|
84
|
+
"periapsis_time_mjd_tdt",
|
|
85
|
+
"periapsis_distance_au",
|
|
86
|
+
"semi_major_axis_au",
|
|
87
|
+
"eccentricity",
|
|
88
|
+
"inclination_deg",
|
|
89
|
+
"raan_deg",
|
|
90
|
+
"argument_of_periapsis_deg",
|
|
91
|
+
"mean_anomaly_deg",
|
|
92
|
+
):
|
|
93
|
+
object.__setattr__(
|
|
94
|
+
self,
|
|
95
|
+
field_name,
|
|
96
|
+
_optional_number(
|
|
97
|
+
getattr(self, field_name),
|
|
98
|
+
parameter=field_name,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
object.__setattr__(
|
|
102
|
+
self,
|
|
103
|
+
"reference_frame",
|
|
104
|
+
_optional_string(self.reference_frame, parameter="reference_frame"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def to_wire(self) -> dict[str, Any]:
|
|
108
|
+
payload: dict[str, Any] = {}
|
|
109
|
+
_include_if_supplied(payload, "EpochMjdTdt", self.epoch_mjd_tdt)
|
|
110
|
+
_include_if_supplied(payload, "PeriTimeMjdTdt", self.periapsis_time_mjd_tdt)
|
|
111
|
+
_include_if_supplied(payload, "Q", self.periapsis_distance_au)
|
|
112
|
+
_include_if_supplied(payload, "SemimajorAxis", self.semi_major_axis_au)
|
|
113
|
+
_include_if_supplied(payload, "Eccentricity", self.eccentricity)
|
|
114
|
+
_include_if_supplied(payload, "Inclination", self.inclination_deg)
|
|
115
|
+
_include_if_supplied(payload, "Raan", self.raan_deg)
|
|
116
|
+
_include_if_supplied(
|
|
117
|
+
payload,
|
|
118
|
+
"ArgOfPeriapsis",
|
|
119
|
+
self.argument_of_periapsis_deg,
|
|
120
|
+
)
|
|
121
|
+
_include_if_supplied(payload, "MeanAnomaly", self.mean_anomaly_deg)
|
|
122
|
+
_include_if_supplied(payload, "ReferenceFrame", self.reference_frame)
|
|
123
|
+
return payload
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def mpc_orbital_elements(
|
|
127
|
+
*,
|
|
128
|
+
epoch_mjd_tdt: float | None = None,
|
|
129
|
+
periapsis_time_mjd_tdt: float | None = None,
|
|
130
|
+
periapsis_distance_au: float | None = None,
|
|
131
|
+
semi_major_axis_au: float | None = None,
|
|
132
|
+
eccentricity: float | None = None,
|
|
133
|
+
inclination_deg: float | None = None,
|
|
134
|
+
raan_deg: float | None = None,
|
|
135
|
+
argument_of_periapsis_deg: float | None = None,
|
|
136
|
+
mean_anomaly_deg: float | None = None,
|
|
137
|
+
reference_frame: str | None = None,
|
|
138
|
+
) -> MpcOrbitalElements:
|
|
139
|
+
"""Build an MPC orbital-element fragment without adding physics policy."""
|
|
140
|
+
return MpcOrbitalElements(
|
|
141
|
+
epoch_mjd_tdt=epoch_mjd_tdt,
|
|
142
|
+
periapsis_time_mjd_tdt=periapsis_time_mjd_tdt,
|
|
143
|
+
periapsis_distance_au=periapsis_distance_au,
|
|
144
|
+
semi_major_axis_au=semi_major_axis_au,
|
|
145
|
+
eccentricity=eccentricity,
|
|
146
|
+
inclination_deg=inclination_deg,
|
|
147
|
+
raan_deg=raan_deg,
|
|
148
|
+
argument_of_periapsis_deg=argument_of_periapsis_deg,
|
|
149
|
+
mean_anomaly_deg=mean_anomaly_deg,
|
|
150
|
+
reference_frame=reference_frame,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _interval(start: str, stop: str, *, parameter: str) -> str:
|
|
155
|
+
return "/".join(
|
|
156
|
+
(
|
|
157
|
+
_string(start, parameter=f"{parameter}_start"),
|
|
158
|
+
_string(stop, parameter=f"{parameter}_stop"),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _elements_to_wire(
|
|
164
|
+
value: MpcOrbitalElements | None,
|
|
165
|
+
*,
|
|
166
|
+
parameter: str,
|
|
167
|
+
) -> dict[str, Any] | None:
|
|
168
|
+
if value is None:
|
|
169
|
+
return None
|
|
170
|
+
if not isinstance(value, MpcOrbitalElements):
|
|
171
|
+
raise TypeError(f"{parameter} must be an MpcOrbitalElements value")
|
|
172
|
+
return value.to_wire()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def lambert_transfer_window(
|
|
176
|
+
*,
|
|
177
|
+
departure_body: str,
|
|
178
|
+
arrival_body: str,
|
|
179
|
+
departure_start: str,
|
|
180
|
+
departure_stop: str,
|
|
181
|
+
arrival_start: str,
|
|
182
|
+
arrival_stop: str,
|
|
183
|
+
sun_frame: str | None = None,
|
|
184
|
+
min_time_of_flight_days: int | None = None,
|
|
185
|
+
departure_step_days: float | None = None,
|
|
186
|
+
arrival_step_days: float | None = None,
|
|
187
|
+
departure_elements: MpcOrbitalElements | None = None,
|
|
188
|
+
arrival_elements: MpcOrbitalElements | None = None,
|
|
189
|
+
max_departure_delta_v_m_s: int | None = None,
|
|
190
|
+
max_arrival_delta_v_m_s: int | None = None,
|
|
191
|
+
max_time_of_flight_days: int | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Return a Lambert transfer-window scan from ASTROX.
|
|
194
|
+
|
|
195
|
+
The returned dictionary preserves every server field except the transport
|
|
196
|
+
success envelope fields ``IsSuccess`` and ``Message``. The transfer route
|
|
197
|
+
itself returns one result for each sampled departure/arrival time pair that
|
|
198
|
+
it evaluates; it is not the single-case ``orbits.lambert_delta_v`` route.
|
|
199
|
+
"""
|
|
200
|
+
payload: dict[str, Any] = {
|
|
201
|
+
"DepartureCbName": _string(departure_body, parameter="departure_body"),
|
|
202
|
+
"ArrivalCbName": _string(arrival_body, parameter="arrival_body"),
|
|
203
|
+
"DepartureInterval": _interval(
|
|
204
|
+
departure_start,
|
|
205
|
+
departure_stop,
|
|
206
|
+
parameter="departure_interval",
|
|
207
|
+
),
|
|
208
|
+
"ArrivalInterval": _interval(
|
|
209
|
+
arrival_start,
|
|
210
|
+
arrival_stop,
|
|
211
|
+
parameter="arrival_interval",
|
|
212
|
+
),
|
|
213
|
+
}
|
|
214
|
+
_include_if_supplied(
|
|
215
|
+
payload,
|
|
216
|
+
"SunFrameName",
|
|
217
|
+
_optional_string(sun_frame, parameter="sun_frame"),
|
|
218
|
+
)
|
|
219
|
+
_include_if_supplied(
|
|
220
|
+
payload,
|
|
221
|
+
"MinTofDays",
|
|
222
|
+
_optional_integer(
|
|
223
|
+
min_time_of_flight_days,
|
|
224
|
+
parameter="min_time_of_flight_days",
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
_include_if_supplied(
|
|
228
|
+
payload,
|
|
229
|
+
"DepartureStepDay",
|
|
230
|
+
_optional_number(
|
|
231
|
+
departure_step_days,
|
|
232
|
+
parameter="departure_step_days",
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
_include_if_supplied(
|
|
236
|
+
payload,
|
|
237
|
+
"ArrivalStepDay",
|
|
238
|
+
_optional_number(
|
|
239
|
+
arrival_step_days,
|
|
240
|
+
parameter="arrival_step_days",
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
_include_if_supplied(
|
|
244
|
+
payload,
|
|
245
|
+
"DepartureElements",
|
|
246
|
+
_elements_to_wire(departure_elements, parameter="departure_elements"),
|
|
247
|
+
)
|
|
248
|
+
_include_if_supplied(
|
|
249
|
+
payload,
|
|
250
|
+
"ArrivalElements",
|
|
251
|
+
_elements_to_wire(arrival_elements, parameter="arrival_elements"),
|
|
252
|
+
)
|
|
253
|
+
_include_if_supplied(
|
|
254
|
+
payload,
|
|
255
|
+
"MaxDepartureDV",
|
|
256
|
+
_optional_integer(
|
|
257
|
+
max_departure_delta_v_m_s,
|
|
258
|
+
parameter="max_departure_delta_v_m_s",
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
_include_if_supplied(
|
|
262
|
+
payload,
|
|
263
|
+
"MaxArrivalDV",
|
|
264
|
+
_optional_integer(
|
|
265
|
+
max_arrival_delta_v_m_s,
|
|
266
|
+
parameter="max_arrival_delta_v_m_s",
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
_include_if_supplied(
|
|
270
|
+
payload,
|
|
271
|
+
"MaxTofDays",
|
|
272
|
+
_optional_integer(
|
|
273
|
+
max_time_of_flight_days,
|
|
274
|
+
parameter="max_time_of_flight_days",
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
return _without_status_fields(
|
|
278
|
+
raw.post("/celestial/transfer", json=payload),
|
|
279
|
+
endpoint="/celestial/transfer",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
50
283
|
def ephemeris(
|
|
51
284
|
*,
|
|
52
285
|
target_name: str,
|
|
@@ -85,7 +318,10 @@ def ephemeris(
|
|
|
85
318
|
"Step",
|
|
86
319
|
_optional_number(step_s, parameter="step_s"),
|
|
87
320
|
)
|
|
88
|
-
return
|
|
321
|
+
return _without_status_fields(
|
|
322
|
+
raw.post("/celestial/ephemeris", json=payload),
|
|
323
|
+
endpoint="/celestial/ephemeris",
|
|
324
|
+
)
|
|
89
325
|
|
|
90
326
|
|
|
91
327
|
def cb_axes_rotation(
|
|
@@ -121,7 +357,10 @@ def cb_axes_rotation(
|
|
|
121
357
|
"Order",
|
|
122
358
|
_optional_integer(order, parameter="order"),
|
|
123
359
|
)
|
|
124
|
-
return
|
|
360
|
+
return _without_status_fields(
|
|
361
|
+
raw.post("/celestial/CbAxesRotation", json=payload),
|
|
362
|
+
endpoint="/celestial/CbAxesRotation",
|
|
363
|
+
)
|
|
125
364
|
|
|
126
365
|
|
|
127
366
|
def mpc_ephemeris(
|
|
@@ -130,8 +369,14 @@ def mpc_ephemeris(
|
|
|
130
369
|
observer_frame: str | None = None,
|
|
131
370
|
start: str | None = None,
|
|
132
371
|
stop: str | None = None,
|
|
372
|
+
target_elements: MpcOrbitalElements | None = None,
|
|
133
373
|
) -> dict[str, Any]:
|
|
134
|
-
"""Return ASTROX minor-planet ephemeris output from its MPC-backed route.
|
|
374
|
+
"""Return ASTROX minor-planet ephemeris output from its MPC-backed route.
|
|
375
|
+
|
|
376
|
+
When ``target_elements`` is supplied, the server integrates those MPC
|
|
377
|
+
orbital elements directly instead of resolving ``target_name`` through the
|
|
378
|
+
MPC network query.
|
|
379
|
+
"""
|
|
135
380
|
payload: dict[str, Any] = {
|
|
136
381
|
"TargetName": _string(target_name, parameter="target_name"),
|
|
137
382
|
}
|
|
@@ -150,4 +395,12 @@ def mpc_ephemeris(
|
|
|
150
395
|
"Stop",
|
|
151
396
|
_optional_string(stop, parameter="stop"),
|
|
152
397
|
)
|
|
153
|
-
|
|
398
|
+
_include_if_supplied(
|
|
399
|
+
payload,
|
|
400
|
+
"TargetElements",
|
|
401
|
+
_elements_to_wire(target_elements, parameter="target_elements"),
|
|
402
|
+
)
|
|
403
|
+
return _without_status_fields(
|
|
404
|
+
raw.post("/celestial/mpc", json=payload),
|
|
405
|
+
endpoint="/celestial/mpc",
|
|
406
|
+
)
|
astrox/libration.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""CRTBP and Earth-Moon libration-dynamics functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from numbers import Real
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from astrox._http import raw
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CrtbpSample",
|
|
15
|
+
"CrtbpState",
|
|
16
|
+
"CrtbpTrajectory",
|
|
17
|
+
"LibrationPoint",
|
|
18
|
+
"LibrationPoints",
|
|
19
|
+
"LibrationUnitSystem",
|
|
20
|
+
"PeriodicOrbit",
|
|
21
|
+
"correct_periodic_orbit_fixed_x",
|
|
22
|
+
"crtbp_state",
|
|
23
|
+
"crtbp_trajectory",
|
|
24
|
+
"earth_moon_dro",
|
|
25
|
+
"earth_moon_l1_halo",
|
|
26
|
+
"earth_moon_l2_halo",
|
|
27
|
+
"positions",
|
|
28
|
+
"units",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _number(value: Any, *, field: str) -> float:
|
|
33
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
34
|
+
raise TypeError(f"{field} must be a number")
|
|
35
|
+
result = float(value)
|
|
36
|
+
if not math.isfinite(result):
|
|
37
|
+
raise ValueError(f"{field} must be finite")
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _optional_number(value: float | None, *, parameter: str) -> float | None:
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
return _number(value, field=parameter)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _optional_boolean(value: bool | None, *, parameter: str) -> bool | None:
|
|
48
|
+
if value is None:
|
|
49
|
+
return None
|
|
50
|
+
if not isinstance(value, bool):
|
|
51
|
+
raise TypeError(f"{parameter} must be a boolean")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _mapping(value: Any, *, field: str) -> Mapping[str, Any]:
|
|
56
|
+
if not isinstance(value, Mapping):
|
|
57
|
+
raise TypeError(f"{field} must be an object")
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sequence(value: Any, *, field: str) -> Sequence[Any]:
|
|
62
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
63
|
+
raise TypeError(f"{field} must be an array")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _boolean(value: Any, *, field: str) -> bool:
|
|
68
|
+
if not isinstance(value, bool):
|
|
69
|
+
raise TypeError(f"{field} must be a boolean")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _include_if_supplied(payload: dict[str, Any], wire_key: str, value: Any) -> None:
|
|
74
|
+
if value is not None:
|
|
75
|
+
payload[wire_key] = value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _boolean_query(value: bool | None, *, parameter: str) -> str | None:
|
|
79
|
+
parsed = _optional_boolean(value, parameter=parameter)
|
|
80
|
+
if parsed is None:
|
|
81
|
+
return None
|
|
82
|
+
return "true" if parsed else "false"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, kw_only=True)
|
|
86
|
+
class LibrationPoint:
|
|
87
|
+
"""One nondimensional CRTBP equilibrium-point coordinate."""
|
|
88
|
+
|
|
89
|
+
x: float
|
|
90
|
+
y: float
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, kw_only=True)
|
|
94
|
+
class LibrationPoints:
|
|
95
|
+
"""The five CRTBP equilibrium points and collinear-point distances."""
|
|
96
|
+
|
|
97
|
+
l1: LibrationPoint
|
|
98
|
+
l2: LibrationPoint
|
|
99
|
+
l3: LibrationPoint
|
|
100
|
+
l4: LibrationPoint
|
|
101
|
+
l5: LibrationPoint
|
|
102
|
+
l1_distance_to_secondary: float
|
|
103
|
+
l2_distance_to_secondary: float
|
|
104
|
+
l3_distance_to_primary: float
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, kw_only=True)
|
|
108
|
+
class LibrationUnitSystem:
|
|
109
|
+
"""Dimensional scales for a nondimensional CRTBP system."""
|
|
110
|
+
|
|
111
|
+
primary_gravitational_parameter_m3_s2: float
|
|
112
|
+
secondary_gravitational_parameter_m3_s2: float
|
|
113
|
+
mass_ratio: float
|
|
114
|
+
length_unit_m: float
|
|
115
|
+
time_unit_s: float
|
|
116
|
+
velocity_unit_m_s: float
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, kw_only=True)
|
|
120
|
+
class CrtbpState:
|
|
121
|
+
"""Nondimensional CRTBP position and rotating-frame velocity."""
|
|
122
|
+
|
|
123
|
+
x: float
|
|
124
|
+
y: float
|
|
125
|
+
z: float
|
|
126
|
+
vx: float
|
|
127
|
+
vy: float
|
|
128
|
+
vz: float
|
|
129
|
+
|
|
130
|
+
def __post_init__(self) -> None:
|
|
131
|
+
for field_name in ("x", "y", "z", "vx", "vy", "vz"):
|
|
132
|
+
object.__setattr__(
|
|
133
|
+
self,
|
|
134
|
+
field_name,
|
|
135
|
+
_number(getattr(self, field_name), field=field_name),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def to_wire(self) -> list[float]:
|
|
139
|
+
"""Lower to ASTROX ``[x, y, z, vx, vy, vz]`` order."""
|
|
140
|
+
return [self.x, self.y, self.z, self.vx, self.vy, self.vz]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True, kw_only=True)
|
|
144
|
+
class CrtbpSample:
|
|
145
|
+
"""One nondimensional CRTBP state at a nondimensional time."""
|
|
146
|
+
|
|
147
|
+
time: float
|
|
148
|
+
state: CrtbpState
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, kw_only=True)
|
|
152
|
+
class CrtbpTrajectory:
|
|
153
|
+
"""A sampled nondimensional CRTBP trajectory."""
|
|
154
|
+
|
|
155
|
+
mass_ratio: float
|
|
156
|
+
is_barycentric: bool
|
|
157
|
+
samples: tuple[CrtbpSample, ...]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, kw_only=True)
|
|
161
|
+
class PeriodicOrbit:
|
|
162
|
+
"""A corrected periodic CRTBP orbit and one sampled period."""
|
|
163
|
+
|
|
164
|
+
is_barycentric: bool
|
|
165
|
+
period: float
|
|
166
|
+
initial_state: CrtbpState
|
|
167
|
+
corrected_state: CrtbpState
|
|
168
|
+
samples: tuple[CrtbpSample, ...]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def crtbp_state(
|
|
172
|
+
*,
|
|
173
|
+
x: float,
|
|
174
|
+
y: float,
|
|
175
|
+
z: float,
|
|
176
|
+
vx: float,
|
|
177
|
+
vy: float,
|
|
178
|
+
vz: float,
|
|
179
|
+
) -> CrtbpState:
|
|
180
|
+
"""Create a nondimensional rotating-frame CRTBP state."""
|
|
181
|
+
return CrtbpState(x=x, y=y, z=z, vx=vx, vy=vy, vz=vz)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _state_to_wire(value: CrtbpState, *, parameter: str) -> list[float]:
|
|
185
|
+
if not isinstance(value, CrtbpState):
|
|
186
|
+
raise TypeError(f"{parameter} must be a CrtbpState value")
|
|
187
|
+
return value.to_wire()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _state_from_wire(value: Any, *, field: str) -> CrtbpState:
|
|
191
|
+
values = _sequence(value, field=field)
|
|
192
|
+
if len(values) != 6:
|
|
193
|
+
raise TypeError(f"{field} must contain six numbers")
|
|
194
|
+
return CrtbpState(
|
|
195
|
+
x=_number(values[0], field=f"{field}[0]"),
|
|
196
|
+
y=_number(values[1], field=f"{field}[1]"),
|
|
197
|
+
z=_number(values[2], field=f"{field}[2]"),
|
|
198
|
+
vx=_number(values[3], field=f"{field}[3]"),
|
|
199
|
+
vy=_number(values[4], field=f"{field}[4]"),
|
|
200
|
+
vz=_number(values[5], field=f"{field}[5]"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _points_from_wire(value: Any) -> LibrationPoints:
|
|
205
|
+
values = _sequence(value, field="positions response")
|
|
206
|
+
if len(values) != 10:
|
|
207
|
+
raise TypeError("positions response must contain ten numbers")
|
|
208
|
+
numbers = tuple(
|
|
209
|
+
_number(item, field=f"positions response[{index}]")
|
|
210
|
+
for index, item in enumerate(values)
|
|
211
|
+
)
|
|
212
|
+
return LibrationPoints(
|
|
213
|
+
l1=LibrationPoint(x=numbers[3], y=0.0),
|
|
214
|
+
l2=LibrationPoint(x=numbers[4], y=0.0),
|
|
215
|
+
l3=LibrationPoint(x=numbers[5], y=0.0),
|
|
216
|
+
l4=LibrationPoint(x=numbers[6], y=numbers[8]),
|
|
217
|
+
l5=LibrationPoint(x=numbers[7], y=numbers[9]),
|
|
218
|
+
l1_distance_to_secondary=numbers[0],
|
|
219
|
+
l2_distance_to_secondary=numbers[1],
|
|
220
|
+
l3_distance_to_primary=numbers[2],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _units_from_wire(value: Any) -> LibrationUnitSystem:
|
|
225
|
+
payload = _mapping(value, field="unit response")
|
|
226
|
+
return LibrationUnitSystem(
|
|
227
|
+
primary_gravitational_parameter_m3_s2=_number(
|
|
228
|
+
payload["GravitationalParameter1"],
|
|
229
|
+
field="GravitationalParameter1",
|
|
230
|
+
),
|
|
231
|
+
secondary_gravitational_parameter_m3_s2=_number(
|
|
232
|
+
payload["GravitationalParameter2"],
|
|
233
|
+
field="GravitationalParameter2",
|
|
234
|
+
),
|
|
235
|
+
mass_ratio=_number(payload["U"], field="U"),
|
|
236
|
+
length_unit_m=_number(payload["UnitL"], field="UnitL"),
|
|
237
|
+
time_unit_s=_number(payload["UnitT"], field="UnitT"),
|
|
238
|
+
velocity_unit_m_s=_number(payload["UnitV"], field="UnitV"),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _trajectory_from_wire(value: Any) -> CrtbpTrajectory:
|
|
243
|
+
payload = _mapping(value, field="crtbp trajectory response")
|
|
244
|
+
positions = _sequence(payload["Positions"], field="Positions")
|
|
245
|
+
if not positions or len(positions) % 7 != 0:
|
|
246
|
+
raise TypeError("Positions must contain one or more seven-number samples")
|
|
247
|
+
samples = tuple(
|
|
248
|
+
CrtbpSample(
|
|
249
|
+
time=_number(positions[index], field=f"Positions[{index}]"),
|
|
250
|
+
state=_state_from_wire(
|
|
251
|
+
positions[index + 1 : index + 7],
|
|
252
|
+
field=f"Positions[{index + 1}:{index + 7}]",
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
for index in range(0, len(positions), 7)
|
|
256
|
+
)
|
|
257
|
+
return CrtbpTrajectory(
|
|
258
|
+
mass_ratio=_number(payload["U"], field="U"),
|
|
259
|
+
is_barycentric=_boolean(payload["IsBarycentric"], field="IsBarycentric"),
|
|
260
|
+
samples=samples,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _periodic_orbit_from_wire(value: Any) -> PeriodicOrbit:
|
|
265
|
+
payload = _mapping(value, field="periodic orbit response")
|
|
266
|
+
times = _sequence(payload["ListT"], field="ListT")
|
|
267
|
+
states = _sequence(payload["ListX"], field="ListX")
|
|
268
|
+
if not times or len(times) != len(states):
|
|
269
|
+
raise TypeError("ListT and ListX must be non-empty arrays of equal length")
|
|
270
|
+
samples = tuple(
|
|
271
|
+
CrtbpSample(
|
|
272
|
+
time=_number(time, field=f"ListT[{index}]"),
|
|
273
|
+
state=_state_from_wire(state, field=f"ListX[{index}]"),
|
|
274
|
+
)
|
|
275
|
+
for index, (time, state) in enumerate(zip(times, states, strict=True))
|
|
276
|
+
)
|
|
277
|
+
return PeriodicOrbit(
|
|
278
|
+
is_barycentric=_boolean(payload["IsBarycentric"], field="IsBarycentric"),
|
|
279
|
+
period=_number(payload["Period"], field="Period"),
|
|
280
|
+
initial_state=_state_from_wire(payload["InitialX0"], field="InitialX0"),
|
|
281
|
+
corrected_state=_state_from_wire(payload["X0"], field="X0"),
|
|
282
|
+
samples=samples,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def positions(*, mass_ratio: float) -> LibrationPoints:
|
|
287
|
+
"""Return the five barycentric CRTBP equilibrium points."""
|
|
288
|
+
return _points_from_wire(
|
|
289
|
+
raw.get(
|
|
290
|
+
"/libration/positions",
|
|
291
|
+
params={"u": _number(mass_ratio, field="mass_ratio")},
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def units(
|
|
297
|
+
*,
|
|
298
|
+
primary_gravitational_parameter_m3_s2: float | None = None,
|
|
299
|
+
secondary_gravitational_parameter_m3_s2: float | None = None,
|
|
300
|
+
mean_separation_m: float | None = None,
|
|
301
|
+
) -> LibrationUnitSystem:
|
|
302
|
+
"""Return the dimensional scales for a CRTBP primary-secondary system."""
|
|
303
|
+
params: dict[str, Any] = {}
|
|
304
|
+
_include_if_supplied(
|
|
305
|
+
params,
|
|
306
|
+
"gm1",
|
|
307
|
+
_optional_number(
|
|
308
|
+
primary_gravitational_parameter_m3_s2,
|
|
309
|
+
parameter="primary_gravitational_parameter_m3_s2",
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
_include_if_supplied(
|
|
313
|
+
params,
|
|
314
|
+
"gm2",
|
|
315
|
+
_optional_number(
|
|
316
|
+
secondary_gravitational_parameter_m3_s2,
|
|
317
|
+
parameter="secondary_gravitational_parameter_m3_s2",
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
_include_if_supplied(
|
|
321
|
+
params,
|
|
322
|
+
"meanRange",
|
|
323
|
+
_optional_number(mean_separation_m, parameter="mean_separation_m"),
|
|
324
|
+
)
|
|
325
|
+
return _units_from_wire(raw.get("/libration/unit", params=params))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def crtbp_trajectory(
|
|
329
|
+
*,
|
|
330
|
+
initial_state: CrtbpState,
|
|
331
|
+
mass_ratio: float,
|
|
332
|
+
start_time: float | None = None,
|
|
333
|
+
end_time: float | None = None,
|
|
334
|
+
barycentric: bool | None = None,
|
|
335
|
+
output_step: float | None = None,
|
|
336
|
+
) -> CrtbpTrajectory:
|
|
337
|
+
"""Integrate a nondimensional CRTBP state in the rotating frame."""
|
|
338
|
+
payload: dict[str, Any] = {
|
|
339
|
+
"RV0": _state_to_wire(initial_state, parameter="initial_state"),
|
|
340
|
+
"U": _number(mass_ratio, field="mass_ratio"),
|
|
341
|
+
}
|
|
342
|
+
_include_if_supplied(
|
|
343
|
+
payload,
|
|
344
|
+
"T0",
|
|
345
|
+
_optional_number(start_time, parameter="start_time"),
|
|
346
|
+
)
|
|
347
|
+
_include_if_supplied(
|
|
348
|
+
payload,
|
|
349
|
+
"TEnd",
|
|
350
|
+
_optional_number(end_time, parameter="end_time"),
|
|
351
|
+
)
|
|
352
|
+
_include_if_supplied(
|
|
353
|
+
payload,
|
|
354
|
+
"IsBarycentric",
|
|
355
|
+
_optional_boolean(barycentric, parameter="barycentric"),
|
|
356
|
+
)
|
|
357
|
+
_include_if_supplied(
|
|
358
|
+
payload,
|
|
359
|
+
"OutStep",
|
|
360
|
+
_optional_number(output_step, parameter="output_step"),
|
|
361
|
+
)
|
|
362
|
+
return _trajectory_from_wire(
|
|
363
|
+
raw.post("/libration/crtbp-trajectory", json=payload)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def earth_moon_l1_halo(
|
|
368
|
+
*,
|
|
369
|
+
z_amplitude: float | None = None,
|
|
370
|
+
southern: bool | None = None,
|
|
371
|
+
) -> PeriodicOrbit:
|
|
372
|
+
"""Return a corrected Earth-Moon L1 Halo orbit."""
|
|
373
|
+
params: dict[str, Any] = {}
|
|
374
|
+
_include_if_supplied(
|
|
375
|
+
params,
|
|
376
|
+
"az",
|
|
377
|
+
_optional_number(z_amplitude, parameter="z_amplitude"),
|
|
378
|
+
)
|
|
379
|
+
_include_if_supplied(
|
|
380
|
+
params,
|
|
381
|
+
"isSouth",
|
|
382
|
+
_boolean_query(southern, parameter="southern"),
|
|
383
|
+
)
|
|
384
|
+
return _periodic_orbit_from_wire(
|
|
385
|
+
raw.get("/libration/em-l1-halo", params=params)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def earth_moon_l2_halo(
|
|
390
|
+
*,
|
|
391
|
+
x_amplitude: float | None = None,
|
|
392
|
+
southern: bool | None = None,
|
|
393
|
+
) -> PeriodicOrbit:
|
|
394
|
+
"""Return a corrected Earth-Moon L2 Halo orbit."""
|
|
395
|
+
params: dict[str, Any] = {}
|
|
396
|
+
_include_if_supplied(
|
|
397
|
+
params,
|
|
398
|
+
"ax",
|
|
399
|
+
_optional_number(x_amplitude, parameter="x_amplitude"),
|
|
400
|
+
)
|
|
401
|
+
_include_if_supplied(
|
|
402
|
+
params,
|
|
403
|
+
"isSouth",
|
|
404
|
+
_boolean_query(southern, parameter="southern"),
|
|
405
|
+
)
|
|
406
|
+
return _periodic_orbit_from_wire(
|
|
407
|
+
raw.get("/libration/em-l2-halo", params=params)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def earth_moon_dro(*, x_amplitude: float | None = None) -> PeriodicOrbit:
|
|
412
|
+
"""Return a corrected planar Earth-Moon distant retrograde orbit."""
|
|
413
|
+
params: dict[str, Any] = {}
|
|
414
|
+
_include_if_supplied(
|
|
415
|
+
params,
|
|
416
|
+
"ax",
|
|
417
|
+
_optional_number(x_amplitude, parameter="x_amplitude"),
|
|
418
|
+
)
|
|
419
|
+
return _periodic_orbit_from_wire(raw.get("/libration/em-dro", params=params))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def correct_periodic_orbit_fixed_x(
|
|
423
|
+
*,
|
|
424
|
+
initial_state: CrtbpState,
|
|
425
|
+
period_guess: float,
|
|
426
|
+
mass_ratio: float,
|
|
427
|
+
barycentric: bool | None = None,
|
|
428
|
+
output_step: float | None = None,
|
|
429
|
+
) -> PeriodicOrbit:
|
|
430
|
+
"""Correct an XZ-plane-crossing seed while holding its x coordinate fixed."""
|
|
431
|
+
payload: dict[str, Any] = {
|
|
432
|
+
"RV0": _state_to_wire(initial_state, parameter="initial_state"),
|
|
433
|
+
"TEnd": _number(period_guess, field="period_guess"),
|
|
434
|
+
"U": _number(mass_ratio, field="mass_ratio"),
|
|
435
|
+
}
|
|
436
|
+
_include_if_supplied(
|
|
437
|
+
payload,
|
|
438
|
+
"IsBarycentric",
|
|
439
|
+
_optional_boolean(barycentric, parameter="barycentric"),
|
|
440
|
+
)
|
|
441
|
+
_include_if_supplied(
|
|
442
|
+
payload,
|
|
443
|
+
"OutStep",
|
|
444
|
+
_optional_number(output_step, parameter="output_step"),
|
|
445
|
+
)
|
|
446
|
+
return _periodic_orbit_from_wire(
|
|
447
|
+
raw.post("/libration/crtbp-period-orbit-fixed-x", json=payload)
|
|
448
|
+
)
|
astrox/orbits.py
CHANGED
|
@@ -11,6 +11,7 @@ from astrox._http import raw
|
|
|
11
11
|
__all__ = [
|
|
12
12
|
"CartesianState",
|
|
13
13
|
"KeplerianElements",
|
|
14
|
+
"LambertResult",
|
|
14
15
|
"MeanKeplerianElements",
|
|
15
16
|
"Tle",
|
|
16
17
|
"cartesian_state",
|
|
@@ -517,14 +518,37 @@ def geo_ym_lambert_delta_v(
|
|
|
517
518
|
)
|
|
518
519
|
|
|
519
520
|
|
|
521
|
+
@dataclass(frozen=True, kw_only=True)
|
|
522
|
+
class LambertResult:
|
|
523
|
+
"""Single-case Lambert solution with the sampled transfer positions."""
|
|
524
|
+
|
|
525
|
+
departure_delta_v_m_s: tuple[float, float, float]
|
|
526
|
+
arrival_delta_v_m_s: tuple[float, float, float]
|
|
527
|
+
positions: tuple[float, ...]
|
|
528
|
+
"""Flat ``(x, y, z, ...)`` transfer position sequence in meters.
|
|
529
|
+
|
|
530
|
+
The server samples ``path_point_count`` points including the departure and
|
|
531
|
+
arrival positions, so the sequence length is ``path_point_count * 3``.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
|
|
520
535
|
def lambert_delta_v(
|
|
521
536
|
*,
|
|
522
537
|
departure_state: CartesianState,
|
|
523
538
|
arrival_state: CartesianState,
|
|
524
539
|
time_of_flight_s: float,
|
|
525
540
|
gravitational_parameter_m3_s2: float | None = None,
|
|
526
|
-
|
|
527
|
-
|
|
541
|
+
get_path_points: bool = False,
|
|
542
|
+
path_point_count: int | None = None,
|
|
543
|
+
) -> tuple[tuple[float, float, float], tuple[float, float, float]] | LambertResult:
|
|
544
|
+
"""Return single-revolution Lambert delta-v between two Cartesian states.
|
|
545
|
+
|
|
546
|
+
By default returns ``(departure_delta_v_m_s, arrival_delta_v_m_s)``. When
|
|
547
|
+
``get_path_points`` is true, the server also samples the transfer orbit and
|
|
548
|
+
the return is a :class:`LambertResult` carrying the delta-v pair plus the
|
|
549
|
+
flat position sequence; ``path_point_count`` sets the sample count
|
|
550
|
+
(server default 100, including both endpoints).
|
|
551
|
+
"""
|
|
528
552
|
if not isinstance(departure_state, CartesianState):
|
|
529
553
|
raise TypeError("departure_state must be a CartesianState instance")
|
|
530
554
|
if not isinstance(arrival_state, CartesianState):
|
|
@@ -536,12 +560,20 @@ def lambert_delta_v(
|
|
|
536
560
|
"TOF": [time_of_flight_s],
|
|
537
561
|
}
|
|
538
562
|
_include_if_supplied(payload, "Gm", gravitational_parameter_m3_s2)
|
|
563
|
+
if get_path_points:
|
|
564
|
+
payload["GetPathPoints"] = True
|
|
565
|
+
_include_if_supplied(payload, "NumberOfPathPoints", path_point_count)
|
|
539
566
|
|
|
540
567
|
result = raw.post("/orbit/lambert", json=payload)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
568
|
+
departure_delta_v = (result["DV1"][0], result["DV1"][1], result["DV1"][2])
|
|
569
|
+
arrival_delta_v = (result["DV2"][0], result["DV2"][1], result["DV2"][2])
|
|
570
|
+
if get_path_points:
|
|
571
|
+
return LambertResult(
|
|
572
|
+
departure_delta_v_m_s=departure_delta_v,
|
|
573
|
+
arrival_delta_v_m_s=arrival_delta_v,
|
|
574
|
+
positions=tuple(result["Positions"]),
|
|
575
|
+
)
|
|
576
|
+
return departure_delta_v, arrival_delta_v
|
|
545
577
|
|
|
546
578
|
|
|
547
579
|
def convert_czml_position(
|
astrox/propagator.py
CHANGED
|
@@ -42,6 +42,7 @@ __all__ = [
|
|
|
42
42
|
"sgp4",
|
|
43
43
|
"simple_ascent",
|
|
44
44
|
"two_body",
|
|
45
|
+
"two_body_rv",
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
|
|
@@ -672,6 +673,36 @@ def two_body(
|
|
|
672
673
|
return _success_path(result)
|
|
673
674
|
|
|
674
675
|
|
|
676
|
+
def two_body_rv(
|
|
677
|
+
*,
|
|
678
|
+
state: CartesianState,
|
|
679
|
+
time_of_flight_s: float,
|
|
680
|
+
gravitational_parameter_m3_s2: float | None = None,
|
|
681
|
+
step_s: float | None = None,
|
|
682
|
+
) -> tuple[float, ...]:
|
|
683
|
+
"""Propagate a Cartesian state with two-body dynamics and return the sampled ephemeris.
|
|
684
|
+
|
|
685
|
+
Returns the flat ``[t, x, y, z, vx, vy, vz, ...]`` sequence, where ``t`` is
|
|
686
|
+
seconds relative to the initial epoch, positions are in meters, and
|
|
687
|
+
velocities are in m/s. The sequence includes the start and stop samples;
|
|
688
|
+
intermediate samples follow ``step_s`` (server default 60 s).
|
|
689
|
+
"""
|
|
690
|
+
if not isinstance(state, CartesianState):
|
|
691
|
+
raise TypeError("state must be a CartesianState instance")
|
|
692
|
+
|
|
693
|
+
payload: dict[str, Any] = {
|
|
694
|
+
"RV0": state.to_wire(),
|
|
695
|
+
"TimeOfFlight": time_of_flight_s,
|
|
696
|
+
}
|
|
697
|
+
if gravitational_parameter_m3_s2 is not None:
|
|
698
|
+
payload["Gm"] = gravitational_parameter_m3_s2
|
|
699
|
+
if step_s is not None:
|
|
700
|
+
payload["Step"] = step_s
|
|
701
|
+
|
|
702
|
+
result = raw.post("/Propagator/TwoBodyRV", json=payload)
|
|
703
|
+
return tuple(result["Positions"])
|
|
704
|
+
|
|
705
|
+
|
|
675
706
|
def j2(
|
|
676
707
|
*,
|
|
677
708
|
start: str,
|
astrox/terrain.py
CHANGED
|
@@ -45,6 +45,16 @@ def _optional_integer(value: int | None, *, parameter: str) -> int | None:
|
|
|
45
45
|
return value
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _without_status_fields(value: Any, *, endpoint: str) -> dict[str, Any]:
|
|
49
|
+
if not isinstance(value, dict):
|
|
50
|
+
raise TypeError(f"{endpoint} response must be an object")
|
|
51
|
+
return {
|
|
52
|
+
key: item
|
|
53
|
+
for key, item in value.items()
|
|
54
|
+
if key not in {"IsSuccess", "Message"}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
def _site_position_to_wire(
|
|
49
59
|
value: components.SitePosition,
|
|
50
60
|
*,
|
|
@@ -164,7 +174,10 @@ def _mask_request(
|
|
|
164
174
|
"TerrainMaskPara",
|
|
165
175
|
_terrain_mask_config_to_wire(config, parameter="config"),
|
|
166
176
|
)
|
|
167
|
-
return
|
|
177
|
+
return _without_status_fields(
|
|
178
|
+
raw.post(endpoint, json=payload),
|
|
179
|
+
endpoint=endpoint,
|
|
180
|
+
)
|
|
168
181
|
|
|
169
182
|
|
|
170
183
|
def azimuth_elevation_mask(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: astrox-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python SDK for the ASTROX Web API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Mtrya/astrox-python
|
|
6
6
|
Project-URL: Repository, https://github.com/Mtrya/astrox-python.git
|
|
@@ -10,7 +10,7 @@ Author-email: Mtrya <erchanmion20@outlook.com>
|
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: astrodynamics,astrox,orbit,propagation,sdk,space
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
astrox/__init__.py,sha256=
|
|
1
|
+
astrox/__init__.py,sha256=2OlHH2Bx83mXoazHMd6hOD3laKNqrYQrgK-JNmRvtAM,1566
|
|
2
2
|
astrox/_http.py,sha256=ZZDkJ6qPG4VbMDRZO7xonIib88aYS6PPTVRxRYwXU3A,15606
|
|
3
3
|
astrox/access.py,sha256=pjyCher_yQNmkoojf2sWpG1HRDnLLK9gI90jM1iEoTo,5817
|
|
4
4
|
astrox/cat.py,sha256=doZqy6lGDtgrVe9QlU57l1lccmzirc14RDSR41tyGUU,15057
|
|
5
|
-
astrox/catalog.py,sha256=
|
|
6
|
-
astrox/celestial.py,sha256=
|
|
5
|
+
astrox/catalog.py,sha256=WDb3adiDO41YY6IdivjN3ri1vIQMHi6DPI3dBa8W1Gc,5255
|
|
6
|
+
astrox/celestial.py,sha256=ILZjn1MrjYS4cqpp4aBDnwJGBkAv5dW-V87eh6bJFxg,12444
|
|
7
7
|
astrox/conjunction.py,sha256=sXhTNAlMai27ejuJekksaAw-850nHPimo21YdFpuzQo,9108
|
|
8
8
|
astrox/exceptions.py,sha256=apClRT1r-N3XY1ibXxkW3oxpsJgiHos4tC0cMJ8eBRk,2592
|
|
9
|
+
astrox/libration.py,sha256=YBgcZi2pTAjL5VUlE8y6jLhVbYnwRxv-PIUxJQmFBSg,13454
|
|
9
10
|
astrox/lighting.py,sha256=aWMty64rU3wv1H1Q5q-yXG5mINbIY31omyd-pjdNaVs,3876
|
|
10
|
-
astrox/orbits.py,sha256=
|
|
11
|
-
astrox/propagator.py,sha256=
|
|
11
|
+
astrox/orbits.py,sha256=SdRE7f9W9zO4N6Xxjg5quHK--gsVh1E5qdvJigpwxLA,19223
|
|
12
|
+
astrox/propagator.py,sha256=GPcg5fpra_jsof4ipxgtaeJNof2PrWBvUTnDG1aH940,38434
|
|
12
13
|
astrox/rocket.py,sha256=tb6wXc4bZ0EMAd9bVzva_7k3I9S2lsYA7Q2RD6byqfk,2755
|
|
13
|
-
astrox/terrain.py,sha256=
|
|
14
|
+
astrox/terrain.py,sha256=2v_Ca5OnXjmMtIKNFQJ_Kbdsh42ZuIc4ZDe-0aWnpdg,6146
|
|
14
15
|
astrox/astrogator/__init__.py,sha256=rgIHos9rHjQ3i-qkpLeLRwZL6zsVthc2bOVbAdwHWHU,267
|
|
15
16
|
astrox/astrogator/_api.py,sha256=ovfBHzstXmhLj1QKIFFSpraEQb5ZRlMlAwaUCXvIiqA,1882
|
|
16
17
|
astrox/astrogator/_models.py,sha256=jIFts5jH6RjAb6NneU4WLqpbwQU2IWGX_1h3OMs8Tww,50620
|
|
@@ -32,7 +33,7 @@ astrox/coverage/number_of_assets.py,sha256=Bf1OQRu1r9lunyARyQoV4x7KetkgY_7OMNcEc
|
|
|
32
33
|
astrox/coverage/response_time.py,sha256=yLnv8eEDL6CS0PJHC1JIKBCoOachOwo3m8dNoHH3HfQ,5164
|
|
33
34
|
astrox/coverage/revisit_time.py,sha256=R5vF2egixCF9elvsGYXzpgWbD5lfcQW8uZqi1ifIDX8,5155
|
|
34
35
|
astrox/coverage/simple_coverage.py,sha256=ZPj3HrP3M59rG47pz4eHt1rq-vTlk-oZ7c2ZBmFsz1U,4992
|
|
35
|
-
astrox_python-
|
|
36
|
-
astrox_python-
|
|
37
|
-
astrox_python-
|
|
38
|
-
astrox_python-
|
|
36
|
+
astrox_python-1.1.0.dist-info/METADATA,sha256=2oZePQ_YT4pqPYJP2PqhKlPrLxWt3YfcxUml36z_9pQ,2520
|
|
37
|
+
astrox_python-1.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
38
|
+
astrox_python-1.1.0.dist-info/licenses/LICENSE,sha256=Fbt55lCQnv_n5yGhbG_VDlowjaV_FPHBQWsJIyuBpDk,1062
|
|
39
|
+
astrox_python-1.1.0.dist-info/RECORD,,
|
|
File without changes
|