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,115 @@
1
+ """Rotation component value objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, TypeAlias
7
+
8
+ from ._common import _real_number, _string
9
+
10
+ @dataclass(frozen=True, kw_only=True)
11
+ class AzElRotation:
12
+ """Azimuth/elevation rotation fragment."""
13
+
14
+ azimuth_deg: float
15
+ elevation_deg: float
16
+
17
+ def to_wire(self) -> dict[str, Any]:
18
+ """Lower to an ASTROX AzEl orientation fragment."""
19
+ return {
20
+ "$type": "AzEl",
21
+ "Azimuth": self.azimuth_deg,
22
+ "Elevation": self.elevation_deg,
23
+ }
24
+
25
+
26
+ @dataclass(frozen=True, kw_only=True)
27
+ class QuaternionRotation:
28
+ """Quaternion rotation fragment using scalar-first Python arguments."""
29
+
30
+ scalar: float
31
+ x: float
32
+ y: float
33
+ z: float
34
+
35
+ def to_wire(self) -> dict[str, Any]:
36
+ """Lower to an ASTROX quaternion orientation fragment."""
37
+ return {
38
+ "$type": "Quaternion",
39
+ "QS": self.scalar,
40
+ "QX": self.x,
41
+ "QY": self.y,
42
+ "QZ": self.z,
43
+ }
44
+
45
+
46
+ @dataclass(frozen=True, kw_only=True)
47
+ class EulerRotation:
48
+ """Euler-angle rotation fragment."""
49
+
50
+ sequence: str
51
+ a_deg: float
52
+ b_deg: float
53
+ c_deg: float
54
+
55
+ def to_wire(self) -> dict[str, Any]:
56
+ """Lower to an ASTROX EulerAngles orientation fragment."""
57
+ return {
58
+ "$type": "EulerAngles",
59
+ "Sequence": self.sequence,
60
+ "A": self.a_deg,
61
+ "B": self.b_deg,
62
+ "C": self.c_deg,
63
+ }
64
+
65
+
66
+ Rotation: TypeAlias = AzElRotation | QuaternionRotation | EulerRotation
67
+
68
+
69
+ _ROTATION_TYPES = (AzElRotation, QuaternionRotation, EulerRotation)
70
+
71
+
72
+ def az_el_rotation(*, azimuth_deg: float, elevation_deg: float) -> AzElRotation:
73
+ """Create an azimuth/elevation rotation fragment."""
74
+ return AzElRotation(
75
+ azimuth_deg=_real_number(azimuth_deg, parameter="azimuth_deg"),
76
+ elevation_deg=_real_number(elevation_deg, parameter="elevation_deg"),
77
+ )
78
+
79
+
80
+ def quaternion_rotation(
81
+ *,
82
+ scalar: float,
83
+ x: float,
84
+ y: float,
85
+ z: float,
86
+ ) -> QuaternionRotation:
87
+ """Create a quaternion rotation fragment."""
88
+ return QuaternionRotation(
89
+ scalar=_real_number(scalar, parameter="scalar"),
90
+ x=_real_number(x, parameter="x"),
91
+ y=_real_number(y, parameter="y"),
92
+ z=_real_number(z, parameter="z"),
93
+ )
94
+
95
+
96
+ def euler_rotation(
97
+ *,
98
+ sequence: str,
99
+ a_deg: float,
100
+ b_deg: float,
101
+ c_deg: float,
102
+ ) -> EulerRotation:
103
+ """Create an Euler-angle rotation fragment."""
104
+ return EulerRotation(
105
+ sequence=_string(sequence, parameter="sequence"),
106
+ a_deg=_real_number(a_deg, parameter="a_deg"),
107
+ b_deg=_real_number(b_deg, parameter="b_deg"),
108
+ c_deg=_real_number(c_deg, parameter="c_deg"),
109
+ )
110
+
111
+
112
+ def _rotation_to_wire(rotation: Rotation) -> dict[str, Any]:
113
+ if not isinstance(rotation, _ROTATION_TYPES):
114
+ raise TypeError("rotation must be an astrox.components rotation value")
115
+ return rotation.to_wire()
@@ -0,0 +1,134 @@
1
+ """Sensor component value objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, TypeAlias
7
+
8
+ from ._common import _include_if_supplied, _optional_string
9
+ from ._rotations import Rotation, _rotation_to_wire
10
+
11
+ @dataclass(frozen=True, kw_only=True)
12
+ class FixedSensorPointing:
13
+ """Fixed sensor pointing using a rotation fragment."""
14
+
15
+ rotation: Rotation
16
+ text: str | None = None
17
+
18
+ def to_wire(self) -> dict[str, Any]:
19
+ """Lower to an ASTROX fixed sensor-pointing fragment."""
20
+ payload: dict[str, Any] = {
21
+ "$type": "Fixed",
22
+ "Orientation": _rotation_to_wire(self.rotation),
23
+ }
24
+ _include_if_supplied(payload, "Text", self.text)
25
+ return payload
26
+
27
+
28
+ SensorPointing: TypeAlias = FixedSensorPointing
29
+
30
+
31
+ _SENSOR_POINTING_TYPES = (FixedSensorPointing,)
32
+
33
+
34
+ @dataclass(frozen=True, kw_only=True)
35
+ class ConicSensor:
36
+ """Conic sensor shape metadata."""
37
+
38
+ inner_half_angle_deg: float | None = None
39
+ outer_half_angle_deg: float | None = None
40
+ minimum_clock_angle_deg: float | None = None
41
+ maximum_clock_angle_deg: float | None = None
42
+ text: str | None = None
43
+
44
+ def to_wire(self) -> dict[str, Any]:
45
+ """Lower to an ASTROX conic sensor fragment."""
46
+ payload: dict[str, Any] = {"$type": "Conic"}
47
+ _include_if_supplied(payload, "innerHalfAngle", self.inner_half_angle_deg)
48
+ _include_if_supplied(payload, "outerHalfAngle", self.outer_half_angle_deg)
49
+ _include_if_supplied(payload, "minimumClockAngle", self.minimum_clock_angle_deg)
50
+ _include_if_supplied(payload, "maximumClockAngle", self.maximum_clock_angle_deg)
51
+ _include_if_supplied(payload, "Text", self.text)
52
+ return payload
53
+
54
+
55
+ @dataclass(frozen=True, kw_only=True)
56
+ class RectangularSensor:
57
+ """Rectangular sensor shape metadata."""
58
+
59
+ x_half_angle_deg: float | None = None
60
+ y_half_angle_deg: float | None = None
61
+ text: str | None = None
62
+
63
+ def to_wire(self) -> dict[str, Any]:
64
+ """Lower to an ASTROX rectangular sensor fragment."""
65
+ payload: dict[str, Any] = {"$type": "Rectangular"}
66
+ _include_if_supplied(payload, "xHalfAngle", self.x_half_angle_deg)
67
+ _include_if_supplied(payload, "yHalfAngle", self.y_half_angle_deg)
68
+ _include_if_supplied(payload, "Text", self.text)
69
+ return payload
70
+
71
+
72
+ EntitySensor: TypeAlias = ConicSensor | RectangularSensor
73
+
74
+
75
+ _SENSOR_TYPES = (ConicSensor, RectangularSensor)
76
+
77
+
78
+ def fixed_sensor_pointing(
79
+ *,
80
+ rotation: Rotation,
81
+ text: str | None = None,
82
+ ) -> FixedSensorPointing:
83
+ """Create fixed sensor-pointing metadata."""
84
+ _rotation_to_wire(rotation)
85
+ return FixedSensorPointing(
86
+ rotation=rotation,
87
+ text=_optional_string(text, parameter="text"),
88
+ )
89
+
90
+
91
+ def conic_sensor(
92
+ *,
93
+ inner_half_angle_deg: float | None = None,
94
+ outer_half_angle_deg: float | None = None,
95
+ minimum_clock_angle_deg: float | None = None,
96
+ maximum_clock_angle_deg: float | None = None,
97
+ text: str | None = None,
98
+ ) -> ConicSensor:
99
+ """Create conic sensor metadata."""
100
+ return ConicSensor(
101
+ inner_half_angle_deg=inner_half_angle_deg,
102
+ outer_half_angle_deg=outer_half_angle_deg,
103
+ minimum_clock_angle_deg=minimum_clock_angle_deg,
104
+ maximum_clock_angle_deg=maximum_clock_angle_deg,
105
+ text=text,
106
+ )
107
+
108
+
109
+ def rectangular_sensor(
110
+ *,
111
+ x_half_angle_deg: float | None = None,
112
+ y_half_angle_deg: float | None = None,
113
+ text: str | None = None,
114
+ ) -> RectangularSensor:
115
+ """Create rectangular sensor metadata."""
116
+ return RectangularSensor(
117
+ x_half_angle_deg=x_half_angle_deg,
118
+ y_half_angle_deg=y_half_angle_deg,
119
+ text=text,
120
+ )
121
+
122
+
123
+ def _sensor_to_wire(sensor: EntitySensor) -> dict[str, Any]:
124
+ if not isinstance(sensor, _SENSOR_TYPES):
125
+ raise TypeError("sensor must be an astrox.components sensor value")
126
+ return sensor.to_wire()
127
+
128
+
129
+ def _sensor_pointing_to_wire(pointing: SensorPointing) -> dict[str, Any]:
130
+ if not isinstance(pointing, _SENSOR_POINTING_TYPES):
131
+ raise TypeError(
132
+ "sensor_pointing must be an astrox.components sensor-pointing value"
133
+ )
134
+ return pointing.to_wire()
@@ -0,0 +1,337 @@
1
+ """VGT 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 ._axes import EntityAxes, _axes_reference_name, _axes_tuple
10
+ from ._common import (
11
+ _include_if_supplied,
12
+ _optional_string,
13
+ _real_number,
14
+ _string,
15
+ _typed_tuple,
16
+ )
17
+
18
+ @dataclass(frozen=True, kw_only=True)
19
+ class XyzDirection:
20
+ """XYZ direction fragment for named VGT vectors."""
21
+
22
+ x: float
23
+ y: float
24
+ z: float
25
+
26
+ def to_wire(self) -> dict[str, Any]:
27
+ """Lower to an ASTROX XYZ direction fragment."""
28
+ return {"$type": "XYZ", "X": self.x, "Y": self.y, "Z": self.z}
29
+
30
+
31
+ @dataclass(frozen=True, kw_only=True)
32
+ class RaDecDirection:
33
+ """Right-ascension/declination direction fragment for named VGT vectors."""
34
+
35
+ ra_deg: float
36
+ dec_deg: float
37
+ magnitude: float | None = None
38
+
39
+ def to_wire(self) -> dict[str, Any]:
40
+ """Lower to an ASTROX RADec direction fragment."""
41
+ payload: dict[str, Any] = {
42
+ "$type": "RADec",
43
+ "RA": self.ra_deg,
44
+ "Dec": self.dec_deg,
45
+ }
46
+ _include_if_supplied(payload, "Magnitude", self.magnitude)
47
+ return payload
48
+
49
+
50
+ VgtDirection: TypeAlias = XyzDirection | RaDecDirection
51
+
52
+
53
+ _DIRECTION_TYPES = (XyzDirection, RaDecDirection)
54
+
55
+
56
+ @dataclass(frozen=True, kw_only=True)
57
+ class VgtFixedVector:
58
+ """Named VGT vector fixed in reference axes."""
59
+
60
+ name: str
61
+ reference_axes: EntityAxes | str
62
+ direction: VgtDirection
63
+ description: str | None = None
64
+
65
+ def to_wire(self) -> dict[str, Any]:
66
+ """Lower to an ASTROX FixedInAxes vector fragment."""
67
+ payload: dict[str, Any] = {
68
+ "$type": "FixedInAxes",
69
+ "Direction": _direction_to_wire(self.direction),
70
+ "ReferenceAxesName": _axes_reference_name(
71
+ self.reference_axes,
72
+ parameter="reference_axes",
73
+ ),
74
+ "Name": self.name,
75
+ }
76
+ _include_if_supplied(payload, "Description", self.description)
77
+ return payload
78
+
79
+
80
+ VgtVector: TypeAlias = VgtFixedVector
81
+
82
+
83
+ _VGT_VECTOR_TYPES = (VgtFixedVector,)
84
+
85
+
86
+ @dataclass(frozen=True, kw_only=True)
87
+ class VgtPoint:
88
+ """Named VGT point definition."""
89
+
90
+ name: str
91
+ description: str | None = None
92
+
93
+ def to_wire(self) -> dict[str, Any]:
94
+ """Lower to an ASTROX VGT point fragment."""
95
+ payload: dict[str, Any] = {"Name": self.name}
96
+ _include_if_supplied(payload, "Description", self.description)
97
+ return payload
98
+
99
+
100
+ @dataclass(frozen=True, kw_only=True)
101
+ class VgtSystem:
102
+ """Named VGT coordinate-system definition."""
103
+
104
+ name: str
105
+ description: str | None = None
106
+
107
+ def to_wire(self) -> dict[str, Any]:
108
+ """Lower to an ASTROX VGT system fragment."""
109
+ payload: dict[str, Any] = {"Name": self.name}
110
+ _include_if_supplied(payload, "Description", self.description)
111
+ return payload
112
+
113
+
114
+ @dataclass(frozen=True, kw_only=True)
115
+ class VgtAngle:
116
+ """Named VGT angle between two named vectors."""
117
+
118
+ name: str
119
+ from_vector: VgtVector | str
120
+ to_vector: VgtVector | str
121
+ description: str | None = None
122
+
123
+ def to_wire(self) -> dict[str, Any]:
124
+ """Lower to an ASTROX VGT angle fragment."""
125
+ payload: dict[str, Any] = {
126
+ "$type": "BetweenVectors",
127
+ "Name": self.name,
128
+ "FromVectorName": _vector_reference_name(
129
+ self.from_vector,
130
+ parameter="from_vector",
131
+ ),
132
+ "ToVectorName": _vector_reference_name(
133
+ self.to_vector,
134
+ parameter="to_vector",
135
+ ),
136
+ }
137
+ _include_if_supplied(payload, "Description", self.description)
138
+ return payload
139
+
140
+
141
+ @dataclass(frozen=True, kw_only=True)
142
+ class VgtPlane:
143
+ """Named VGT plane definition."""
144
+
145
+ name: str
146
+ plane_type: str | None = None
147
+ description: str | None = None
148
+
149
+ def to_wire(self) -> dict[str, Any]:
150
+ """Lower to an ASTROX VGT plane fragment."""
151
+ payload: dict[str, Any] = {"Name": self.name}
152
+ _include_if_supplied(payload, "Description", self.description)
153
+ _include_if_supplied(payload, "Type", self.plane_type)
154
+ return payload
155
+
156
+
157
+ @dataclass(frozen=True, kw_only=True)
158
+ class VgtProvider:
159
+ """Named geometry definitions attached to an entity.
160
+
161
+ ASTROX exposes this field as ``Vgt``. The SDK keeps the same advanced name
162
+ because VGT definitions are name-reference objects, not general geometry
163
+ math values. ``axes`` are entity attitude frames; ``vectors`` and the other
164
+ collections are named definitions that ASTROX can resolve by name from
165
+ orientation branches such as ``Fixed`` or ``AlignedAndConstrained``.
166
+ """
167
+
168
+ axes: tuple[EntityAxes, ...]
169
+ vectors: tuple[VgtVector, ...] | None = None
170
+ points: tuple[VgtPoint, ...] | None = None
171
+ systems: tuple[VgtSystem, ...] | None = None
172
+ angles: tuple[VgtAngle, ...] | None = None
173
+ planes: tuple[VgtPlane, ...] | None = None
174
+
175
+ def to_wire(self) -> dict[str, Any]:
176
+ """Lower to an ASTROX CrdnProvider fragment."""
177
+ payload: dict[str, Any] = {
178
+ "Axes": [axes.to_wire() for axes in self.axes],
179
+ }
180
+ if self.vectors is not None:
181
+ payload["Vectors"] = [vector.to_wire() for vector in self.vectors]
182
+ if self.points is not None:
183
+ payload["Points"] = [point.to_wire() for point in self.points]
184
+ if self.systems is not None:
185
+ payload["Systems"] = [system.to_wire() for system in self.systems]
186
+ if self.angles is not None:
187
+ payload["Angles"] = [angle.to_wire() for angle in self.angles]
188
+ if self.planes is not None:
189
+ payload["Planes"] = [plane.to_wire() for plane in self.planes]
190
+ return payload
191
+
192
+
193
+ def xyz_direction(*, x: float, y: float, z: float) -> XyzDirection:
194
+ """Create an XYZ direction fragment for a VGT vector."""
195
+ return XyzDirection(
196
+ x=_real_number(x, parameter="x"),
197
+ y=_real_number(y, parameter="y"),
198
+ z=_real_number(z, parameter="z"),
199
+ )
200
+
201
+
202
+ def ra_dec_direction(
203
+ *,
204
+ ra_deg: float,
205
+ dec_deg: float,
206
+ magnitude: float | None = None,
207
+ ) -> RaDecDirection:
208
+ """Create a right-ascension/declination direction fragment."""
209
+ return RaDecDirection(
210
+ ra_deg=_real_number(ra_deg, parameter="ra_deg"),
211
+ dec_deg=_real_number(dec_deg, parameter="dec_deg"),
212
+ magnitude=_real_number(magnitude, parameter="magnitude")
213
+ if magnitude is not None
214
+ else None,
215
+ )
216
+
217
+
218
+ def vgt_fixed_vector(
219
+ *,
220
+ name: str,
221
+ reference_axes: EntityAxes | str,
222
+ direction: VgtDirection,
223
+ description: str | None = None,
224
+ ) -> VgtFixedVector:
225
+ """Create a named VGT vector fixed in reference axes."""
226
+ _axes_reference_name(reference_axes, parameter="reference_axes")
227
+ _direction_to_wire(direction)
228
+ return VgtFixedVector(
229
+ name=_string(name, parameter="name"),
230
+ reference_axes=reference_axes,
231
+ direction=direction,
232
+ description=_optional_string(description, parameter="description"),
233
+ )
234
+
235
+
236
+ def vgt_point(*, name: str, description: str | None = None) -> VgtPoint:
237
+ """Create a named VGT point definition."""
238
+ return VgtPoint(
239
+ name=_string(name, parameter="name"),
240
+ description=_optional_string(description, parameter="description"),
241
+ )
242
+
243
+
244
+ def vgt_system(*, name: str, description: str | None = None) -> VgtSystem:
245
+ """Create a named VGT system definition."""
246
+ return VgtSystem(
247
+ name=_string(name, parameter="name"),
248
+ description=_optional_string(description, parameter="description"),
249
+ )
250
+
251
+
252
+ def vgt_angle(
253
+ *,
254
+ name: str,
255
+ from_vector: VgtVector | str,
256
+ to_vector: VgtVector | str,
257
+ description: str | None = None,
258
+ ) -> VgtAngle:
259
+ """Create a named VGT angle between two named vectors."""
260
+ _vector_reference_name(from_vector, parameter="from_vector")
261
+ _vector_reference_name(to_vector, parameter="to_vector")
262
+ return VgtAngle(
263
+ name=_string(name, parameter="name"),
264
+ from_vector=from_vector,
265
+ to_vector=to_vector,
266
+ description=_optional_string(description, parameter="description"),
267
+ )
268
+
269
+
270
+ def vgt_plane(
271
+ *,
272
+ name: str,
273
+ plane_type: str | None = None,
274
+ description: str | None = None,
275
+ ) -> VgtPlane:
276
+ """Create a named VGT plane definition."""
277
+ return VgtPlane(
278
+ name=_string(name, parameter="name"),
279
+ plane_type=_optional_string(plane_type, parameter="plane_type"),
280
+ description=_optional_string(description, parameter="description"),
281
+ )
282
+
283
+
284
+ def vgt(
285
+ *,
286
+ axes: Sequence[EntityAxes],
287
+ vectors: Sequence[VgtVector] | None = None,
288
+ points: Sequence[VgtPoint] | None = None,
289
+ systems: Sequence[VgtSystem] | None = None,
290
+ angles: Sequence[VgtAngle] | None = None,
291
+ planes: Sequence[VgtPlane] | None = None,
292
+ ) -> VgtProvider:
293
+ """Create a VGT provider for named entity geometry definitions."""
294
+ return VgtProvider(
295
+ axes=_axes_tuple(axes, parameter="axes"),
296
+ vectors=_typed_tuple(vectors, _VGT_VECTOR_TYPES, parameter="vectors")
297
+ if vectors is not None
298
+ else None,
299
+ points=_typed_tuple(points, (VgtPoint,), parameter="points")
300
+ if points is not None
301
+ else None,
302
+ systems=_typed_tuple(systems, (VgtSystem,), parameter="systems")
303
+ if systems is not None
304
+ else None,
305
+ angles=_typed_tuple(angles, (VgtAngle,), parameter="angles")
306
+ if angles is not None
307
+ else None,
308
+ planes=_typed_tuple(planes, (VgtPlane,), parameter="planes")
309
+ if planes is not None
310
+ else None,
311
+ )
312
+
313
+
314
+ def _vgt_to_wire(provider: VgtProvider) -> dict[str, Any]:
315
+ if not isinstance(provider, VgtProvider):
316
+ raise TypeError("vgt must be an astrox.components.VgtProvider value")
317
+ return provider.to_wire()
318
+
319
+
320
+ def _direction_to_wire(direction: VgtDirection) -> dict[str, Any]:
321
+ if not isinstance(direction, _DIRECTION_TYPES):
322
+ raise TypeError("direction must be an astrox.components VGT direction value")
323
+ return direction.to_wire()
324
+
325
+
326
+ def _vector_reference_name(value: VgtVector | str, *, parameter: str) -> str:
327
+ if isinstance(value, str):
328
+ return value
329
+ if isinstance(value, _VGT_VECTOR_TYPES):
330
+ if value.name is None:
331
+ raise TypeError(
332
+ f"{parameter} object must have a name before it can be referenced"
333
+ )
334
+ return value.name
335
+ raise TypeError(
336
+ f"{parameter} must be an astrox.components VGT vector value or string name"
337
+ )
@@ -0,0 +1,41 @@
1
+ """Coverage analysis functions, grids, reports, and figure-of-merit routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import coverage_time, number_of_assets, response_time, revisit_time, simple_coverage
6
+ from ._core import (
7
+ CbLatLonGrid,
8
+ CoverageGrid,
9
+ GlobalGrid,
10
+ LatitudeGrid,
11
+ LatLonGrid,
12
+ cb_lat_lon_grid,
13
+ compute,
14
+ coverage_by_asset,
15
+ global_grid,
16
+ grid_points,
17
+ lat_lon_grid,
18
+ latitude_grid,
19
+ percent_coverage,
20
+ )
21
+
22
+ __all__ = [
23
+ "CbLatLonGrid",
24
+ "CoverageGrid",
25
+ "GlobalGrid",
26
+ "LatitudeGrid",
27
+ "LatLonGrid",
28
+ "cb_lat_lon_grid",
29
+ "compute",
30
+ "coverage_by_asset",
31
+ "coverage_time",
32
+ "global_grid",
33
+ "grid_points",
34
+ "lat_lon_grid",
35
+ "latitude_grid",
36
+ "number_of_assets",
37
+ "percent_coverage",
38
+ "response_time",
39
+ "revisit_time",
40
+ "simple_coverage",
41
+ ]