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.
- astrox/__init__.py +49 -0
- astrox/_http.py +504 -0
- astrox/access.py +173 -0
- astrox/components/__init__.py +202 -0
- astrox/components/_axes.py +508 -0
- astrox/components/_common.py +141 -0
- astrox/components/_constraints.py +138 -0
- astrox/components/_objects.py +167 -0
- astrox/components/_positions.py +673 -0
- astrox/components/_rotations.py +115 -0
- astrox/components/_sensors.py +134 -0
- astrox/components/_vgt.py +337 -0
- astrox/coverage/__init__.py +41 -0
- astrox/coverage/_core.py +552 -0
- astrox/coverage/_fom.py +125 -0
- astrox/coverage/coverage_time.py +83 -0
- astrox/coverage/number_of_assets.py +160 -0
- astrox/coverage/response_time.py +160 -0
- astrox/coverage/revisit_time.py +160 -0
- astrox/coverage/simple_coverage.py +152 -0
- astrox/exceptions.py +92 -0
- astrox/lighting.py +121 -0
- astrox/orbits.py +499 -0
- astrox/propagator.py +1072 -0
- astrox/rocket.py +82 -0
- astrox_python-0.1.0.dist-info/METADATA +82 -0
- astrox_python-0.1.0.dist-info/RECORD +29 -0
- astrox_python-0.1.0.dist-info/WHEEL +4 -0
- astrox_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Constraint 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, TypeAlias
|
|
8
|
+
|
|
9
|
+
from ._common import _include_if_supplied, _number_sequence_to_list
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, kw_only=True)
|
|
12
|
+
class ElevationConstraint:
|
|
13
|
+
"""Elevation-angle constraint fragment shared by ASTROX entity and coverage inputs."""
|
|
14
|
+
|
|
15
|
+
minimum_deg: float | None = None
|
|
16
|
+
maximum_deg: float | None = None
|
|
17
|
+
maximum_enabled: bool | None = None
|
|
18
|
+
text: str | None = None
|
|
19
|
+
|
|
20
|
+
def to_wire(self) -> dict[str, Any]:
|
|
21
|
+
"""Lower to an ASTROX elevation-angle constraint fragment."""
|
|
22
|
+
payload: dict[str, Any] = {"$type": "ElevationAngle"}
|
|
23
|
+
_include_if_supplied(payload, "MinimumValue", self.minimum_deg)
|
|
24
|
+
_include_if_supplied(payload, "MaximumValue", self.maximum_deg)
|
|
25
|
+
_include_if_supplied(payload, "IsMaximumEnabled", self.maximum_enabled)
|
|
26
|
+
_include_if_supplied(payload, "Text", self.text)
|
|
27
|
+
return payload
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, kw_only=True)
|
|
31
|
+
class RangeConstraint:
|
|
32
|
+
"""Range constraint fragment shared by ASTROX entity and coverage inputs."""
|
|
33
|
+
|
|
34
|
+
minimum_km: float | None = None
|
|
35
|
+
maximum_km: float | None = None
|
|
36
|
+
maximum_enabled: bool | None = None
|
|
37
|
+
text: str | None = None
|
|
38
|
+
|
|
39
|
+
def to_wire(self) -> dict[str, Any]:
|
|
40
|
+
"""Lower to an ASTROX range constraint fragment."""
|
|
41
|
+
payload: dict[str, Any] = {"$type": "Range"}
|
|
42
|
+
_include_if_supplied(payload, "MinimumValue", self.minimum_km)
|
|
43
|
+
_include_if_supplied(payload, "MaximumValue", self.maximum_km)
|
|
44
|
+
_include_if_supplied(payload, "IsMaximumEnabled", self.maximum_enabled)
|
|
45
|
+
_include_if_supplied(payload, "Text", self.text)
|
|
46
|
+
return payload
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, kw_only=True)
|
|
50
|
+
class AzElMaskConstraint:
|
|
51
|
+
"""Azimuth/elevation mask constraint fragment shared by ASTROX entity and coverage inputs."""
|
|
52
|
+
|
|
53
|
+
az_el_mask_rad: tuple[float, ...]
|
|
54
|
+
max_range_km: float | None = None
|
|
55
|
+
text: str | None = None
|
|
56
|
+
|
|
57
|
+
def to_wire(self) -> dict[str, Any]:
|
|
58
|
+
"""Lower to an ASTROX azimuth/elevation mask constraint fragment."""
|
|
59
|
+
payload: dict[str, Any] = {
|
|
60
|
+
"$type": "AzElMask",
|
|
61
|
+
"AzElMaskData": list(self.az_el_mask_rad),
|
|
62
|
+
}
|
|
63
|
+
_include_if_supplied(payload, "MaxRange", self.max_range_km)
|
|
64
|
+
_include_if_supplied(payload, "Text", self.text)
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Constraint: TypeAlias = ElevationConstraint | RangeConstraint | AzElMaskConstraint
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_CONSTRAINT_TYPES = (ElevationConstraint, RangeConstraint, AzElMaskConstraint)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def elevation_constraint(
|
|
75
|
+
*,
|
|
76
|
+
minimum_deg: float | None = None,
|
|
77
|
+
maximum_deg: float | None = None,
|
|
78
|
+
maximum_enabled: bool | None = None,
|
|
79
|
+
text: str | None = None,
|
|
80
|
+
) -> ElevationConstraint:
|
|
81
|
+
"""Create an elevation-angle constraint fragment."""
|
|
82
|
+
return ElevationConstraint(
|
|
83
|
+
minimum_deg=minimum_deg,
|
|
84
|
+
maximum_deg=maximum_deg,
|
|
85
|
+
maximum_enabled=maximum_enabled,
|
|
86
|
+
text=text,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def range_constraint(
|
|
91
|
+
*,
|
|
92
|
+
minimum_km: float | None = None,
|
|
93
|
+
maximum_km: float | None = None,
|
|
94
|
+
maximum_enabled: bool | None = None,
|
|
95
|
+
text: str | None = None,
|
|
96
|
+
) -> RangeConstraint:
|
|
97
|
+
"""Create a range constraint fragment."""
|
|
98
|
+
return RangeConstraint(
|
|
99
|
+
minimum_km=minimum_km,
|
|
100
|
+
maximum_km=maximum_km,
|
|
101
|
+
maximum_enabled=maximum_enabled,
|
|
102
|
+
text=text,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def az_el_mask_constraint(
|
|
107
|
+
*,
|
|
108
|
+
az_el_mask_rad: Sequence[float],
|
|
109
|
+
max_range_km: float | None = None,
|
|
110
|
+
text: str | None = None,
|
|
111
|
+
) -> AzElMaskConstraint:
|
|
112
|
+
"""Create an azimuth/elevation mask constraint fragment.
|
|
113
|
+
|
|
114
|
+
``az_el_mask_rad`` is a flat sequence of alternating azimuth and elevation
|
|
115
|
+
samples in radians. ASTROX interpolates the mask piecewise-linearly in
|
|
116
|
+
azimuth and applies it at the constrained participant. Live validation shows
|
|
117
|
+
this constraint is only meaningful for ``SitePosition`` participants; the
|
|
118
|
+
server rejects it for moving position sources.
|
|
119
|
+
|
|
120
|
+
``max_range_km`` is forwarded but is not enforced by ASTROX. The live
|
|
121
|
+
OpenAPI description and server behavior both treat it as documentation-only.
|
|
122
|
+
"""
|
|
123
|
+
return AzElMaskConstraint(
|
|
124
|
+
az_el_mask_rad=tuple(
|
|
125
|
+
_number_sequence_to_list(
|
|
126
|
+
az_el_mask_rad,
|
|
127
|
+
parameter="az_el_mask_rad",
|
|
128
|
+
)
|
|
129
|
+
),
|
|
130
|
+
max_range_km=max_range_km,
|
|
131
|
+
text=text,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _constraint_to_wire(constraint: Constraint) -> dict[str, Any]:
|
|
136
|
+
if not isinstance(constraint, _CONSTRAINT_TYPES):
|
|
137
|
+
raise TypeError("constraint must be an astrox.components constraint value")
|
|
138
|
+
return constraint.to_wire()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Named ASTROX analysis object components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._axes import EntityAxes, _AXES_TYPES, _axes_to_wire
|
|
10
|
+
from ._common import (
|
|
11
|
+
_include_if_supplied,
|
|
12
|
+
_typed_tuple,
|
|
13
|
+
_validate_group_restriction,
|
|
14
|
+
)
|
|
15
|
+
from ._constraints import Constraint, _CONSTRAINT_TYPES, _constraint_to_wire
|
|
16
|
+
from ._positions import EntityPosition, _POSITION_TYPES, _position_to_wire
|
|
17
|
+
from ._sensors import (
|
|
18
|
+
EntitySensor,
|
|
19
|
+
SensorPointing,
|
|
20
|
+
_SENSOR_POINTING_TYPES,
|
|
21
|
+
_SENSOR_TYPES,
|
|
22
|
+
_sensor_pointing_to_wire,
|
|
23
|
+
_sensor_to_wire,
|
|
24
|
+
)
|
|
25
|
+
from ._vgt import VgtProvider, _vgt_to_wire
|
|
26
|
+
|
|
27
|
+
def _entities_to_tuple(
|
|
28
|
+
values: Sequence[Entity],
|
|
29
|
+
*,
|
|
30
|
+
parameter: str,
|
|
31
|
+
) -> tuple[Entity, ...]:
|
|
32
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
33
|
+
raise TypeError(f"{parameter} must be a sequence of Entity values")
|
|
34
|
+
items = tuple(values)
|
|
35
|
+
if not all(isinstance(item, Entity) for item in items):
|
|
36
|
+
raise TypeError(f"{parameter} must be a sequence of Entity values")
|
|
37
|
+
return items
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, kw_only=True)
|
|
41
|
+
class Entity:
|
|
42
|
+
"""Named ASTROX analysis object composed from a position source and metadata."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
position: EntityPosition
|
|
46
|
+
description: str | None = None
|
|
47
|
+
vgt: VgtProvider | None = None
|
|
48
|
+
orientation: EntityAxes | None = None
|
|
49
|
+
sensor: EntitySensor | None = None
|
|
50
|
+
sensor_pointing: SensorPointing | None = None
|
|
51
|
+
constraints: tuple[Constraint, ...] | None = None
|
|
52
|
+
|
|
53
|
+
def to_wire(self) -> dict[str, Any]:
|
|
54
|
+
"""Lower to the common ASTROX entity body."""
|
|
55
|
+
payload: dict[str, Any] = {
|
|
56
|
+
"Name": self.name,
|
|
57
|
+
}
|
|
58
|
+
_include_if_supplied(payload, "Description", self.description)
|
|
59
|
+
if self.vgt is not None:
|
|
60
|
+
payload["Vgt"] = _vgt_to_wire(self.vgt)
|
|
61
|
+
payload["Position"] = _position_to_wire(self.position)
|
|
62
|
+
if self.orientation is not None:
|
|
63
|
+
payload["Orientation"] = _axes_to_wire(self.orientation)
|
|
64
|
+
if self.sensor is not None:
|
|
65
|
+
payload["Sensor"] = _sensor_to_wire(self.sensor)
|
|
66
|
+
if self.sensor_pointing is not None:
|
|
67
|
+
payload["SensorPointing"] = _sensor_pointing_to_wire(self.sensor_pointing)
|
|
68
|
+
if self.constraints is not None:
|
|
69
|
+
payload["Constraints"] = [
|
|
70
|
+
_constraint_to_wire(constraint) for constraint in self.constraints
|
|
71
|
+
]
|
|
72
|
+
return payload
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, kw_only=True)
|
|
76
|
+
class EntityGroup:
|
|
77
|
+
"""Named entity group for workflows that accept grouped entities."""
|
|
78
|
+
|
|
79
|
+
name: str
|
|
80
|
+
members: tuple[Entity, ...]
|
|
81
|
+
from_restriction: str | None = None
|
|
82
|
+
from_number: int | None = None
|
|
83
|
+
to_restriction: str | None = None
|
|
84
|
+
to_number: int | None = None
|
|
85
|
+
|
|
86
|
+
def to_wire(self) -> dict[str, Any]:
|
|
87
|
+
"""Lower to the ASTROX EntityPathGroup fragment."""
|
|
88
|
+
payload: dict[str, Any] = {
|
|
89
|
+
"$type": "EntityPathGroup",
|
|
90
|
+
"Name": self.name,
|
|
91
|
+
"AssignedObjects": [member.to_wire() for member in self.members],
|
|
92
|
+
}
|
|
93
|
+
_include_if_supplied(payload, "FromAccess_Restriction", self.from_restriction)
|
|
94
|
+
_include_if_supplied(payload, "FromAccess_Number", self.from_number)
|
|
95
|
+
_include_if_supplied(payload, "ToAccess_Restriction", self.to_restriction)
|
|
96
|
+
_include_if_supplied(payload, "ToAccess_Number", self.to_number)
|
|
97
|
+
return payload
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def entity(
|
|
101
|
+
*,
|
|
102
|
+
name: str,
|
|
103
|
+
position: EntityPosition,
|
|
104
|
+
description: str | None = None,
|
|
105
|
+
vgt: VgtProvider | None = None,
|
|
106
|
+
orientation: EntityAxes | None = None,
|
|
107
|
+
sensor: EntitySensor | None = None,
|
|
108
|
+
sensor_pointing: SensorPointing | None = None,
|
|
109
|
+
constraints: Sequence[Constraint] | None = None,
|
|
110
|
+
) -> Entity:
|
|
111
|
+
"""Create a named ASTROX analysis object."""
|
|
112
|
+
if not isinstance(position, _POSITION_TYPES):
|
|
113
|
+
raise TypeError("position must be an astrox.components position value")
|
|
114
|
+
if vgt is not None and not isinstance(vgt, VgtProvider):
|
|
115
|
+
raise TypeError("vgt must be an astrox.components.VgtProvider value")
|
|
116
|
+
if orientation is not None and not isinstance(orientation, _AXES_TYPES):
|
|
117
|
+
raise TypeError("orientation must be an astrox.components axes value")
|
|
118
|
+
if sensor is not None and not isinstance(sensor, _SENSOR_TYPES):
|
|
119
|
+
raise TypeError("sensor must be an astrox.components sensor value")
|
|
120
|
+
if sensor_pointing is not None and not isinstance(
|
|
121
|
+
sensor_pointing,
|
|
122
|
+
_SENSOR_POINTING_TYPES,
|
|
123
|
+
):
|
|
124
|
+
raise TypeError(
|
|
125
|
+
"sensor_pointing must be an astrox.components sensor-pointing value"
|
|
126
|
+
)
|
|
127
|
+
constraint_items = (
|
|
128
|
+
_typed_tuple(constraints, _CONSTRAINT_TYPES, parameter="constraints")
|
|
129
|
+
if constraints is not None
|
|
130
|
+
else None
|
|
131
|
+
)
|
|
132
|
+
return Entity(
|
|
133
|
+
name=name,
|
|
134
|
+
position=position,
|
|
135
|
+
description=description,
|
|
136
|
+
vgt=vgt,
|
|
137
|
+
orientation=orientation,
|
|
138
|
+
sensor=sensor,
|
|
139
|
+
sensor_pointing=sensor_pointing,
|
|
140
|
+
constraints=constraint_items,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def entity_group(
|
|
145
|
+
*,
|
|
146
|
+
name: str,
|
|
147
|
+
members: Sequence[Entity],
|
|
148
|
+
from_restriction: str | None = None,
|
|
149
|
+
from_number: int | None = None,
|
|
150
|
+
to_restriction: str | None = None,
|
|
151
|
+
to_number: int | None = None,
|
|
152
|
+
) -> EntityGroup:
|
|
153
|
+
"""Create a named entity group."""
|
|
154
|
+
return EntityGroup(
|
|
155
|
+
name=name,
|
|
156
|
+
members=_entities_to_tuple(members, parameter="members"),
|
|
157
|
+
from_restriction=_validate_group_restriction(
|
|
158
|
+
from_restriction,
|
|
159
|
+
parameter="from_restriction",
|
|
160
|
+
),
|
|
161
|
+
from_number=from_number,
|
|
162
|
+
to_restriction=_validate_group_restriction(
|
|
163
|
+
to_restriction,
|
|
164
|
+
parameter="to_restriction",
|
|
165
|
+
),
|
|
166
|
+
to_number=to_number,
|
|
167
|
+
)
|