astrox-python 0.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.
@@ -0,0 +1,508 @@
1
+ """Axes component value objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any, TYPE_CHECKING, TypeAlias
8
+
9
+ from ._common import (
10
+ _axes_type,
11
+ _include_axes_metadata,
12
+ _include_if_supplied,
13
+ _number_sequence_to_list,
14
+ _optional_string,
15
+ _string,
16
+ _typed_tuple,
17
+ _validate_axis_direction,
18
+ _validate_relative_to,
19
+ )
20
+ from ._rotations import Rotation, _rotation_to_wire
21
+
22
+ if TYPE_CHECKING:
23
+ from ._vgt import VgtVector
24
+
25
+
26
+ def _vector_reference_name(value: VgtVector | str, *, parameter: str) -> str:
27
+ from ._vgt import _VGT_VECTOR_TYPES
28
+
29
+ if isinstance(value, str):
30
+ return value
31
+ if isinstance(value, _VGT_VECTOR_TYPES):
32
+ if value.name is None:
33
+ raise TypeError(
34
+ f"{parameter} object must have a name before it can be referenced"
35
+ )
36
+ return value.name
37
+ raise TypeError(
38
+ f"{parameter} must be an astrox.components VGT vector value or string name"
39
+ )
40
+
41
+ @dataclass(frozen=True, kw_only=True)
42
+ class VvlhAxes:
43
+ """Entity attitude axes using ASTROX VVLH variants."""
44
+
45
+ relative_to: str | None = None
46
+ name: str | None = None
47
+ description: str | None = None
48
+ start: str | None = None
49
+ stop: str | None = None
50
+
51
+ def to_wire(self) -> dict[str, Any]:
52
+ """Lower to an ASTROX VVLH CrdnAxes fragment."""
53
+ payload: dict[str, Any] = {"$type": _axes_type("VVLH", self.relative_to)}
54
+ _include_axes_metadata(
55
+ payload,
56
+ name=self.name,
57
+ description=self.description,
58
+ start=self.start,
59
+ stop=self.stop,
60
+ )
61
+ return payload
62
+
63
+
64
+ @dataclass(frozen=True, kw_only=True)
65
+ class LvlhAxes:
66
+ """Entity attitude axes using ASTROX LVLH variants."""
67
+
68
+ relative_to: str | None = None
69
+ name: str | None = None
70
+ description: str | None = None
71
+ start: str | None = None
72
+ stop: str | None = None
73
+
74
+ def to_wire(self) -> dict[str, Any]:
75
+ """Lower to an ASTROX LVLH CrdnAxes fragment."""
76
+ payload: dict[str, Any] = {"$type": _axes_type("LVLH", self.relative_to)}
77
+ _include_axes_metadata(
78
+ payload,
79
+ name=self.name,
80
+ description=self.description,
81
+ start=self.start,
82
+ stop=self.stop,
83
+ )
84
+ return payload
85
+
86
+
87
+ @dataclass(frozen=True, kw_only=True)
88
+ class VncAxes:
89
+ """Entity attitude axes using ASTROX VNC variants."""
90
+
91
+ relative_to: str | None = None
92
+ name: str | None = None
93
+ description: str | None = None
94
+ start: str | None = None
95
+ stop: str | None = None
96
+
97
+ def to_wire(self) -> dict[str, Any]:
98
+ """Lower to an ASTROX VNC CrdnAxes fragment."""
99
+ payload: dict[str, Any] = {"$type": _axes_type("VNC", self.relative_to)}
100
+ _include_axes_metadata(
101
+ payload,
102
+ name=self.name,
103
+ description=self.description,
104
+ start=self.start,
105
+ stop=self.stop,
106
+ )
107
+ return payload
108
+
109
+
110
+ @dataclass(frozen=True, kw_only=True)
111
+ class FixedAxes:
112
+ """Entity attitude axes fixed relative to named reference axes."""
113
+
114
+ reference_axes: EntityAxes | str
115
+ rotation: Rotation
116
+ name: str | None = None
117
+ description: str | None = None
118
+ start: str | None = None
119
+ stop: str | None = None
120
+
121
+ def to_wire(self) -> dict[str, Any]:
122
+ """Lower to an ASTROX Fixed CrdnAxes fragment."""
123
+ payload: dict[str, Any] = {
124
+ "$type": "Fixed",
125
+ "FixedOrientation": _rotation_to_wire(self.rotation),
126
+ "ReferenceAxesName": _axes_reference_name(
127
+ self.reference_axes,
128
+ parameter="reference_axes",
129
+ ),
130
+ }
131
+ _include_axes_metadata(
132
+ payload,
133
+ name=self.name,
134
+ description=self.description,
135
+ start=self.start,
136
+ stop=self.stop,
137
+ )
138
+ return payload
139
+
140
+
141
+ @dataclass(frozen=True, kw_only=True)
142
+ class FixedAtEpochAxes:
143
+ """Entity attitude axes frozen between source and reference axes at an epoch."""
144
+
145
+ source_axes: EntityAxes | str
146
+ reference_axes: EntityAxes | str
147
+ epoch: str
148
+ name: str | None = None
149
+ description: str | None = None
150
+ start: str | None = None
151
+ stop: str | None = None
152
+
153
+ def to_wire(self) -> dict[str, Any]:
154
+ """Lower to an ASTROX FixedAtEpoch CrdnAxes fragment."""
155
+ payload: dict[str, Any] = {
156
+ "$type": "FixedAtEpoch",
157
+ "SourceAxesName": _axes_reference_name(
158
+ self.source_axes,
159
+ parameter="source_axes",
160
+ ),
161
+ "ReferenceAxesName": _axes_reference_name(
162
+ self.reference_axes,
163
+ parameter="reference_axes",
164
+ ),
165
+ "Epoch": self.epoch,
166
+ }
167
+ _include_axes_metadata(
168
+ payload,
169
+ name=self.name,
170
+ description=self.description,
171
+ start=self.start,
172
+ stop=self.stop,
173
+ )
174
+ return payload
175
+
176
+
177
+ @dataclass(frozen=True, kw_only=True)
178
+ class AlignedAndConstrainedAxes:
179
+ """Entity attitude axes aligned and constrained by named vectors."""
180
+
181
+ principal: VgtVector | str
182
+ principal_axis: str
183
+ reference: VgtVector | str
184
+ reference_axis: str
185
+ name: str | None = None
186
+ description: str | None = None
187
+ start: str | None = None
188
+ stop: str | None = None
189
+
190
+ def to_wire(self) -> dict[str, Any]:
191
+ """Lower to an ASTROX AlignedAndConstrained CrdnAxes fragment."""
192
+ payload: dict[str, Any] = {
193
+ "$type": "AlignedAndConstrained",
194
+ "Principal": _vector_reference_name(
195
+ self.principal,
196
+ parameter="principal",
197
+ ),
198
+ "PrincipalAxis": self.principal_axis,
199
+ "Reference": _vector_reference_name(
200
+ self.reference,
201
+ parameter="reference",
202
+ ),
203
+ "ReferenceAxis": self.reference_axis,
204
+ }
205
+ _include_axes_metadata(
206
+ payload,
207
+ name=self.name,
208
+ description=self.description,
209
+ start=self.start,
210
+ stop=self.stop,
211
+ )
212
+ return payload
213
+
214
+
215
+ @dataclass(frozen=True, kw_only=True)
216
+ class CompositeAxes:
217
+ """Entity attitude axes made from multiple axes intervals."""
218
+
219
+ intervals: tuple[EntityAxes, ...]
220
+ name: str | None = None
221
+ description: str | None = None
222
+ start: str | None = None
223
+ stop: str | None = None
224
+
225
+ def to_wire(self) -> dict[str, Any]:
226
+ """Lower to an ASTROX Composite CrdnAxes fragment."""
227
+ payload: dict[str, Any] = {
228
+ "$type": "Composite",
229
+ "Intervals": [interval.to_wire() for interval in self.intervals],
230
+ }
231
+ _include_axes_metadata(
232
+ payload,
233
+ name=self.name,
234
+ description=self.description,
235
+ start=self.start,
236
+ stop=self.stop,
237
+ )
238
+ return payload
239
+
240
+
241
+ @dataclass(frozen=True, kw_only=True)
242
+ class CzmlAxes:
243
+ """Entity attitude axes from CZML unit-quaternion samples."""
244
+
245
+ epoch: str
246
+ unit_quaternion_xyzw: tuple[float, ...]
247
+ central_body: str | None = None
248
+ interpolation_algorithm: str | None = None
249
+ interpolation_degree: int | None = None
250
+ name: str | None = None
251
+ description: str | None = None
252
+ start: str | None = None
253
+ stop: str | None = None
254
+
255
+ def to_wire(self) -> dict[str, Any]:
256
+ """Lower to an ASTROX CzmlOrientation CrdnAxes fragment."""
257
+ payload: dict[str, Any] = {
258
+ "$type": "CzmlOrientation",
259
+ "epoch": self.epoch,
260
+ "unitQuaternion": list(self.unit_quaternion_xyzw),
261
+ }
262
+ _include_if_supplied(payload, "CentralBody", self.central_body)
263
+ _include_if_supplied(
264
+ payload,
265
+ "interpolationAlgorithm",
266
+ self.interpolation_algorithm,
267
+ )
268
+ _include_if_supplied(payload, "interpolationDegree", self.interpolation_degree)
269
+ _include_axes_metadata(
270
+ payload,
271
+ name=self.name,
272
+ description=self.description,
273
+ start=self.start,
274
+ stop=self.stop,
275
+ )
276
+ return payload
277
+
278
+
279
+ EntityAxes: TypeAlias = (
280
+ VvlhAxes
281
+ | LvlhAxes
282
+ | VncAxes
283
+ | FixedAxes
284
+ | FixedAtEpochAxes
285
+ | AlignedAndConstrainedAxes
286
+ | CompositeAxes
287
+ | CzmlAxes
288
+ )
289
+
290
+
291
+ _AXES_TYPES = (
292
+ VvlhAxes,
293
+ LvlhAxes,
294
+ VncAxes,
295
+ FixedAxes,
296
+ FixedAtEpochAxes,
297
+ AlignedAndConstrainedAxes,
298
+ CompositeAxes,
299
+ CzmlAxes,
300
+ )
301
+
302
+
303
+ def vvlh_axes(
304
+ *,
305
+ relative_to: str | None = None,
306
+ name: str | None = None,
307
+ description: str | None = None,
308
+ start: str | None = None,
309
+ stop: str | None = None,
310
+ ) -> VvlhAxes:
311
+ """Create VVLH entity attitude axes."""
312
+ return VvlhAxes(
313
+ relative_to=_validate_relative_to(relative_to, parameter="relative_to"),
314
+ name=_optional_string(name, parameter="name"),
315
+ description=_optional_string(description, parameter="description"),
316
+ start=_optional_string(start, parameter="start"),
317
+ stop=_optional_string(stop, parameter="stop"),
318
+ )
319
+
320
+
321
+ def lvlh_axes(
322
+ *,
323
+ relative_to: str | None = None,
324
+ name: str | None = None,
325
+ description: str | None = None,
326
+ start: str | None = None,
327
+ stop: str | None = None,
328
+ ) -> LvlhAxes:
329
+ """Create LVLH entity attitude axes."""
330
+ return LvlhAxes(
331
+ relative_to=_validate_relative_to(relative_to, parameter="relative_to"),
332
+ name=_optional_string(name, parameter="name"),
333
+ description=_optional_string(description, parameter="description"),
334
+ start=_optional_string(start, parameter="start"),
335
+ stop=_optional_string(stop, parameter="stop"),
336
+ )
337
+
338
+
339
+ def vnc_axes(
340
+ *,
341
+ relative_to: str | None = None,
342
+ name: str | None = None,
343
+ description: str | None = None,
344
+ start: str | None = None,
345
+ stop: str | None = None,
346
+ ) -> VncAxes:
347
+ """Create VNC entity attitude axes."""
348
+ return VncAxes(
349
+ relative_to=_validate_relative_to(relative_to, parameter="relative_to"),
350
+ name=_optional_string(name, parameter="name"),
351
+ description=_optional_string(description, parameter="description"),
352
+ start=_optional_string(start, parameter="start"),
353
+ stop=_optional_string(stop, parameter="stop"),
354
+ )
355
+
356
+
357
+ def fixed_axes(
358
+ *,
359
+ reference_axes: EntityAxes | str,
360
+ rotation: Rotation,
361
+ name: str | None = None,
362
+ description: str | None = None,
363
+ start: str | None = None,
364
+ stop: str | None = None,
365
+ ) -> FixedAxes:
366
+ """Create entity attitude axes fixed relative to named reference axes."""
367
+ _axes_reference_name(reference_axes, parameter="reference_axes")
368
+ _rotation_to_wire(rotation)
369
+ return FixedAxes(
370
+ reference_axes=reference_axes,
371
+ rotation=rotation,
372
+ name=_optional_string(name, parameter="name"),
373
+ description=_optional_string(description, parameter="description"),
374
+ start=_optional_string(start, parameter="start"),
375
+ stop=_optional_string(stop, parameter="stop"),
376
+ )
377
+
378
+
379
+ def fixed_at_epoch_axes(
380
+ *,
381
+ source_axes: EntityAxes | str,
382
+ reference_axes: EntityAxes | str,
383
+ epoch: str,
384
+ name: str | None = None,
385
+ description: str | None = None,
386
+ start: str | None = None,
387
+ stop: str | None = None,
388
+ ) -> FixedAtEpochAxes:
389
+ """Create entity attitude axes fixed at an epoch."""
390
+ _axes_reference_name(source_axes, parameter="source_axes")
391
+ _axes_reference_name(reference_axes, parameter="reference_axes")
392
+ return FixedAtEpochAxes(
393
+ source_axes=source_axes,
394
+ reference_axes=reference_axes,
395
+ epoch=_string(epoch, parameter="epoch"),
396
+ name=_optional_string(name, parameter="name"),
397
+ description=_optional_string(description, parameter="description"),
398
+ start=_optional_string(start, parameter="start"),
399
+ stop=_optional_string(stop, parameter="stop"),
400
+ )
401
+
402
+
403
+ def aligned_and_constrained_axes(
404
+ *,
405
+ principal: VgtVector | str,
406
+ principal_axis: str,
407
+ reference: VgtVector | str,
408
+ reference_axis: str,
409
+ name: str | None = None,
410
+ description: str | None = None,
411
+ start: str | None = None,
412
+ stop: str | None = None,
413
+ ) -> AlignedAndConstrainedAxes:
414
+ """Create entity attitude axes aligned and constrained by named vectors."""
415
+ _vector_reference_name(principal, parameter="principal")
416
+ _vector_reference_name(reference, parameter="reference")
417
+ return AlignedAndConstrainedAxes(
418
+ principal=principal,
419
+ principal_axis=_validate_axis_direction(
420
+ principal_axis,
421
+ parameter="principal_axis",
422
+ ),
423
+ reference=reference,
424
+ reference_axis=_validate_axis_direction(
425
+ reference_axis,
426
+ parameter="reference_axis",
427
+ ),
428
+ name=_optional_string(name, parameter="name"),
429
+ description=_optional_string(description, parameter="description"),
430
+ start=_optional_string(start, parameter="start"),
431
+ stop=_optional_string(stop, parameter="stop"),
432
+ )
433
+
434
+
435
+ def composite_axes(
436
+ *,
437
+ intervals: Sequence[EntityAxes],
438
+ name: str | None = None,
439
+ description: str | None = None,
440
+ start: str | None = None,
441
+ stop: str | None = None,
442
+ ) -> CompositeAxes:
443
+ """Create composite entity attitude axes."""
444
+ return CompositeAxes(
445
+ intervals=_axes_tuple(intervals, parameter="intervals"),
446
+ name=_optional_string(name, parameter="name"),
447
+ description=_optional_string(description, parameter="description"),
448
+ start=_optional_string(start, parameter="start"),
449
+ stop=_optional_string(stop, parameter="stop"),
450
+ )
451
+
452
+
453
+ def czml_axes(
454
+ *,
455
+ epoch: str,
456
+ unit_quaternion_xyzw: Sequence[float],
457
+ central_body: str | None = None,
458
+ interpolation_algorithm: str | None = None,
459
+ interpolation_degree: int | None = None,
460
+ name: str | None = None,
461
+ description: str | None = None,
462
+ start: str | None = None,
463
+ stop: str | None = None,
464
+ ) -> CzmlAxes:
465
+ """Create CZML-sampled entity attitude axes."""
466
+ return CzmlAxes(
467
+ epoch=_string(epoch, parameter="epoch"),
468
+ unit_quaternion_xyzw=tuple(
469
+ _number_sequence_to_list(
470
+ unit_quaternion_xyzw,
471
+ parameter="unit_quaternion_xyzw",
472
+ )
473
+ ),
474
+ central_body=_optional_string(central_body, parameter="central_body"),
475
+ interpolation_algorithm=_optional_string(
476
+ interpolation_algorithm,
477
+ parameter="interpolation_algorithm",
478
+ ),
479
+ interpolation_degree=interpolation_degree,
480
+ name=_optional_string(name, parameter="name"),
481
+ description=_optional_string(description, parameter="description"),
482
+ start=_optional_string(start, parameter="start"),
483
+ stop=_optional_string(stop, parameter="stop"),
484
+ )
485
+
486
+
487
+ def _axes_to_wire(axes: EntityAxes) -> dict[str, Any]:
488
+ if not isinstance(axes, _AXES_TYPES):
489
+ raise TypeError("axes must be an astrox.components axes value")
490
+ return axes.to_wire()
491
+
492
+
493
+ def _axes_reference_name(value: EntityAxes | str, *, parameter: str) -> str:
494
+ if isinstance(value, str):
495
+ return value
496
+ if isinstance(value, _AXES_TYPES):
497
+ if value.name is None:
498
+ raise TypeError(
499
+ f"{parameter} object must have a name before it can be referenced"
500
+ )
501
+ return value.name
502
+ raise TypeError(f"{parameter} must be an astrox.components axes value or string name")
503
+
504
+
505
+ def _axes_tuple(
506
+ values: Sequence[EntityAxes], *, parameter: str
507
+ ) -> tuple[EntityAxes, ...]:
508
+ return _typed_tuple(values, _AXES_TYPES, parameter=parameter)
@@ -0,0 +1,141 @@
1
+ """Shared helpers for ASTROX reusable component value objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from numbers import Real
7
+ from typing import Any
8
+
9
+ from astrox.orbits import CartesianState, KeplerianElements
10
+ from astrox.propagator import HpopConfig
11
+
12
+ _GROUP_RESTRICTIONS = {"AnyOf", "AtLeastN"}
13
+
14
+
15
+ _RELATIVE_TO_VALUES = {"Earth", "Moon", "Mars", "Sun", "CBF"}
16
+
17
+
18
+ _AXIS_DIRECTIONS = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"}
19
+
20
+
21
+ def _include_if_supplied(payload: dict[str, Any], wire_key: str, value: Any) -> None:
22
+ if value is not None:
23
+ payload[wire_key] = value
24
+
25
+
26
+ def _real_number(value: float, *, parameter: str) -> float:
27
+ if not isinstance(value, Real) or isinstance(value, bool):
28
+ raise TypeError(f"{parameter} must be a number")
29
+ return value
30
+
31
+
32
+ def _string(value: str, *, parameter: str) -> str:
33
+ if not isinstance(value, str):
34
+ raise TypeError(f"{parameter} must be a string")
35
+ return value
36
+
37
+
38
+ def _optional_string(value: str | None, *, parameter: str) -> str | None:
39
+ if value is None:
40
+ return None
41
+ return _string(value, parameter=parameter)
42
+
43
+
44
+ def _number_sequence_to_list(value: Sequence[float], *, parameter: str) -> list[float]:
45
+ if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
46
+ raise TypeError(f"{parameter} must be a sequence of numbers")
47
+ items = list(value)
48
+ if not all(isinstance(item, Real) and not isinstance(item, bool) for item in items):
49
+ raise TypeError(f"{parameter} must be a sequence of numbers")
50
+ return items
51
+
52
+
53
+ def _tle_lines_to_list(value: tuple[str, str] | list[str]) -> list[str]:
54
+ if (
55
+ not isinstance(value, (list, tuple))
56
+ or len(value) != 2
57
+ or not all(isinstance(line, str) for line in value)
58
+ ):
59
+ raise TypeError("tle_lines must be a two-item sequence of TLE strings")
60
+ return list(value)
61
+
62
+
63
+ def _orbit_elements_to_wire(orbit: KeplerianElements, *, parameter: str) -> list[float]:
64
+ if not isinstance(orbit, KeplerianElements):
65
+ raise TypeError(f"{parameter} must be a KeplerianElements instance")
66
+ return orbit.to_wire()
67
+
68
+
69
+ def _cartesian_state_to_wire(state: CartesianState, *, parameter: str) -> list[float]:
70
+ if not isinstance(state, CartesianState):
71
+ raise TypeError(f"{parameter} must be a CartesianState instance")
72
+ return state.to_wire()
73
+
74
+
75
+ def _hpop_config_to_wire(
76
+ config: HpopConfig | Mapping[str, Any],
77
+ *,
78
+ parameter: str,
79
+ ) -> dict[str, Any]:
80
+ if isinstance(config, HpopConfig):
81
+ return config.to_wire()
82
+ if isinstance(config, Mapping):
83
+ return dict(config)
84
+ raise TypeError(f"{parameter} must be an HpopConfig value or mapping fragment")
85
+
86
+
87
+ def _validate_group_restriction(value: str | None, *, parameter: str) -> str | None:
88
+ if value is None:
89
+ return None
90
+ if value not in _GROUP_RESTRICTIONS:
91
+ accepted = ", ".join(sorted(_GROUP_RESTRICTIONS))
92
+ raise ValueError(f"{parameter} must be one of: {accepted}")
93
+ return value
94
+
95
+
96
+ def _validate_relative_to(value: str | None, *, parameter: str) -> str | None:
97
+ if value is None:
98
+ return None
99
+ if value not in _RELATIVE_TO_VALUES:
100
+ accepted = ", ".join(sorted(_RELATIVE_TO_VALUES))
101
+ raise ValueError(f"{parameter} must be one of: {accepted}")
102
+ return value
103
+
104
+
105
+ def _validate_axis_direction(value: str, *, parameter: str) -> str:
106
+ if value not in _AXIS_DIRECTIONS:
107
+ accepted = ", ".join(sorted(_AXIS_DIRECTIONS))
108
+ raise ValueError(f"{parameter} must be one of: {accepted}")
109
+ return value
110
+
111
+
112
+ def _axes_type(family: str, relative_to: str | None) -> str:
113
+ return family if relative_to is None else f"{family}({relative_to})"
114
+
115
+
116
+ def _include_axes_metadata(
117
+ payload: dict[str, Any],
118
+ *,
119
+ name: str | None,
120
+ description: str | None,
121
+ start: str | None,
122
+ stop: str | None,
123
+ ) -> None:
124
+ _include_if_supplied(payload, "Name", name)
125
+ _include_if_supplied(payload, "Description", description)
126
+ _include_if_supplied(payload, "Start", start)
127
+ _include_if_supplied(payload, "Stop", stop)
128
+
129
+
130
+ def _typed_tuple(
131
+ values: Sequence[Any],
132
+ accepted_types: tuple[type[Any], ...],
133
+ *,
134
+ parameter: str,
135
+ ) -> tuple[Any, ...]:
136
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
137
+ raise TypeError(f"{parameter} must be a sequence")
138
+ items = tuple(values)
139
+ if not all(isinstance(item, accepted_types) for item in items):
140
+ raise TypeError(f"{parameter} contains unsupported item values")
141
+ return items