astrox-python 0.2.0__py3-none-any.whl → 1.0.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 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 raw.get("/city", params=params)
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 raw.get("/facility", params=params)
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 raw.get("/ssc", params=params)
191
+ return _without_status_fields(
192
+ raw.get("/ssc", params=params),
193
+ endpoint="/ssc",
194
+ )
astrox/celestial.py CHANGED
@@ -1,16 +1,20 @@
1
- """Read-like celestial endpoint functions."""
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,197 @@ 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
+
78
+ def __post_init__(self) -> None:
79
+ for field_name in (
80
+ "epoch_mjd_tdt",
81
+ "periapsis_time_mjd_tdt",
82
+ "periapsis_distance_au",
83
+ "semi_major_axis_au",
84
+ "eccentricity",
85
+ "inclination_deg",
86
+ "raan_deg",
87
+ "argument_of_periapsis_deg",
88
+ "mean_anomaly_deg",
89
+ ):
90
+ object.__setattr__(
91
+ self,
92
+ field_name,
93
+ _optional_number(
94
+ getattr(self, field_name),
95
+ parameter=field_name,
96
+ ),
97
+ )
98
+
99
+ def to_wire(self) -> dict[str, float]:
100
+ payload: dict[str, float] = {}
101
+ _include_if_supplied(payload, "EpochMjdTdt", self.epoch_mjd_tdt)
102
+ _include_if_supplied(payload, "PeriTimeMjdTdt", self.periapsis_time_mjd_tdt)
103
+ _include_if_supplied(payload, "Q", self.periapsis_distance_au)
104
+ _include_if_supplied(payload, "SemimajorAxis", self.semi_major_axis_au)
105
+ _include_if_supplied(payload, "Eccentricity", self.eccentricity)
106
+ _include_if_supplied(payload, "Inclination", self.inclination_deg)
107
+ _include_if_supplied(payload, "Raan", self.raan_deg)
108
+ _include_if_supplied(
109
+ payload,
110
+ "ArgOfPeriapsis",
111
+ self.argument_of_periapsis_deg,
112
+ )
113
+ _include_if_supplied(payload, "MeanAnomaly", self.mean_anomaly_deg)
114
+ return payload
115
+
116
+
117
+ def mpc_orbital_elements(
118
+ *,
119
+ epoch_mjd_tdt: float | None = None,
120
+ periapsis_time_mjd_tdt: float | None = None,
121
+ periapsis_distance_au: float | None = None,
122
+ semi_major_axis_au: float | None = None,
123
+ eccentricity: float | None = None,
124
+ inclination_deg: float | None = None,
125
+ raan_deg: float | None = None,
126
+ argument_of_periapsis_deg: float | None = None,
127
+ mean_anomaly_deg: float | None = None,
128
+ ) -> MpcOrbitalElements:
129
+ """Build an MPC orbital-element fragment without adding physics policy."""
130
+ return MpcOrbitalElements(
131
+ epoch_mjd_tdt=epoch_mjd_tdt,
132
+ periapsis_time_mjd_tdt=periapsis_time_mjd_tdt,
133
+ periapsis_distance_au=periapsis_distance_au,
134
+ semi_major_axis_au=semi_major_axis_au,
135
+ eccentricity=eccentricity,
136
+ inclination_deg=inclination_deg,
137
+ raan_deg=raan_deg,
138
+ argument_of_periapsis_deg=argument_of_periapsis_deg,
139
+ mean_anomaly_deg=mean_anomaly_deg,
140
+ )
141
+
142
+
143
+ def _interval(start: str, stop: str, *, parameter: str) -> str:
144
+ return "/".join(
145
+ (
146
+ _string(start, parameter=f"{parameter}_start"),
147
+ _string(stop, parameter=f"{parameter}_stop"),
148
+ )
149
+ )
150
+
151
+
152
+ def _elements_to_wire(
153
+ value: MpcOrbitalElements | None,
154
+ *,
155
+ parameter: str,
156
+ ) -> dict[str, float] | None:
157
+ if value is None:
158
+ return None
159
+ if not isinstance(value, MpcOrbitalElements):
160
+ raise TypeError(f"{parameter} must be an MpcOrbitalElements value")
161
+ return value.to_wire()
162
+
163
+
164
+ def lambert_transfer_window(
165
+ *,
166
+ departure_body: str,
167
+ arrival_body: str,
168
+ departure_start: str,
169
+ departure_stop: str,
170
+ arrival_start: str,
171
+ arrival_stop: str,
172
+ sun_frame: str | None = None,
173
+ min_time_of_flight_days: int | None = None,
174
+ departure_step_days: float | None = None,
175
+ arrival_step_days: float | None = None,
176
+ departure_elements: MpcOrbitalElements | None = None,
177
+ arrival_elements: MpcOrbitalElements | None = None,
178
+ ) -> dict[str, Any]:
179
+ """Return a Lambert transfer-window scan from ASTROX.
180
+
181
+ The returned dictionary preserves every server field except the transport
182
+ success envelope fields ``IsSuccess`` and ``Message``. The transfer route
183
+ itself returns one result for each sampled departure/arrival time pair that
184
+ it evaluates; it is not the single-case ``orbits.lambert_delta_v`` route.
185
+ """
186
+ payload: dict[str, Any] = {
187
+ "DepartureCbName": _string(departure_body, parameter="departure_body"),
188
+ "ArrivalCbName": _string(arrival_body, parameter="arrival_body"),
189
+ "DepartureInterval": _interval(
190
+ departure_start,
191
+ departure_stop,
192
+ parameter="departure_interval",
193
+ ),
194
+ "ArrivalInterval": _interval(
195
+ arrival_start,
196
+ arrival_stop,
197
+ parameter="arrival_interval",
198
+ ),
199
+ }
200
+ _include_if_supplied(
201
+ payload,
202
+ "SunFrameName",
203
+ _optional_string(sun_frame, parameter="sun_frame"),
204
+ )
205
+ _include_if_supplied(
206
+ payload,
207
+ "MinTofDays",
208
+ _optional_integer(
209
+ min_time_of_flight_days,
210
+ parameter="min_time_of_flight_days",
211
+ ),
212
+ )
213
+ _include_if_supplied(
214
+ payload,
215
+ "DepartureStepDay",
216
+ _optional_number(
217
+ departure_step_days,
218
+ parameter="departure_step_days",
219
+ ),
220
+ )
221
+ _include_if_supplied(
222
+ payload,
223
+ "ArrivalStepDay",
224
+ _optional_number(
225
+ arrival_step_days,
226
+ parameter="arrival_step_days",
227
+ ),
228
+ )
229
+ _include_if_supplied(
230
+ payload,
231
+ "DepartureElements",
232
+ _elements_to_wire(departure_elements, parameter="departure_elements"),
233
+ )
234
+ _include_if_supplied(
235
+ payload,
236
+ "ArrivalElements",
237
+ _elements_to_wire(arrival_elements, parameter="arrival_elements"),
238
+ )
239
+ return _without_status_fields(
240
+ raw.post("/celestial/transfer", json=payload),
241
+ endpoint="/celestial/transfer",
242
+ )
243
+
244
+
50
245
  def ephemeris(
51
246
  *,
52
247
  target_name: str,
@@ -85,7 +280,10 @@ def ephemeris(
85
280
  "Step",
86
281
  _optional_number(step_s, parameter="step_s"),
87
282
  )
88
- return raw.post("/celestial/ephemeris", json=payload)
283
+ return _without_status_fields(
284
+ raw.post("/celestial/ephemeris", json=payload),
285
+ endpoint="/celestial/ephemeris",
286
+ )
89
287
 
90
288
 
91
289
  def cb_axes_rotation(
@@ -121,7 +319,10 @@ def cb_axes_rotation(
121
319
  "Order",
122
320
  _optional_integer(order, parameter="order"),
123
321
  )
124
- return raw.post("/celestial/CbAxesRotation", json=payload)
322
+ return _without_status_fields(
323
+ raw.post("/celestial/CbAxesRotation", json=payload),
324
+ endpoint="/celestial/CbAxesRotation",
325
+ )
125
326
 
126
327
 
127
328
  def mpc_ephemeris(
@@ -150,4 +351,7 @@ def mpc_ephemeris(
150
351
  "Stop",
151
352
  _optional_string(stop, parameter="stop"),
152
353
  )
153
- return raw.post("/celestial/mpc", json=payload)
354
+ return _without_status_fields(
355
+ raw.post("/celestial/mpc", json=payload),
356
+ endpoint="/celestial/mpc",
357
+ )
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/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 raw.post(endpoint, json=payload)
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: astrox-python
3
- Version: 0.2.0
3
+ Version: 1.0.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 :: 3 - Alpha
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=9wJZtYZQoUqgmSBDMYGXQHskNmZDqcTbGV-KbK0eeIU,1523
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=OK1Qp9J4CJBkD7sn4qpZCuXT78VgAzMY0s1pZLMkPIk,4754
6
- astrox/celestial.py,sha256=rOCRBQukYPJSkUcSHYzDTeSrtj0HmpmKLyoLiBIoefc,4163
5
+ astrox/catalog.py,sha256=WDb3adiDO41YY6IdivjN3ri1vIQMHi6DPI3dBa8W1Gc,5255
6
+ astrox/celestial.py,sha256=Mh8VChM-YWGAM4Jn2fs09SxfOuV4kR56j36LmKv6MP0,10840
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
11
  astrox/orbits.py,sha256=JiSkCyWqYmeV-yX5WBL9554_JNhec1Li8EdmkZmzXQc,17823
11
12
  astrox/propagator.py,sha256=kiW4kFSBy2_quZPZ2VqP49vfSYDKkJ1dna5rM5bo-9M,37334
12
13
  astrox/rocket.py,sha256=tb6wXc4bZ0EMAd9bVzva_7k3I9S2lsYA7Q2RD6byqfk,2755
13
- astrox/terrain.py,sha256=mAcjIAVjUMch4z8M1asMdmtof-7ox-tpXE218sPR2jY,5777
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-0.2.0.dist-info/METADATA,sha256=wgkeBpIfyYGbDhlj84CC55b2WR84zkKrrIFKAI-ha9c,2508
36
- astrox_python-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
37
- astrox_python-0.2.0.dist-info/licenses/LICENSE,sha256=Fbt55lCQnv_n5yGhbG_VDlowjaV_FPHBQWsJIyuBpDk,1062
38
- astrox_python-0.2.0.dist-info/RECORD,,
36
+ astrox_python-1.0.0.dist-info/METADATA,sha256=zlaPEYsPsqHp6bnpp655SzjcopUFyKAAK-sC1-Z7r80,2520
37
+ astrox_python-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
38
+ astrox_python-1.0.0.dist-info/licenses/LICENSE,sha256=Fbt55lCQnv_n5yGhbG_VDlowjaV_FPHBQWsJIyuBpDk,1062
39
+ astrox_python-1.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any