satnogs-predict 0.4__tar.gz → 0.6__tar.gz
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.
- {satnogs_predict-0.4 → satnogs_predict-0.6}/PKG-INFO +1 -1
- {satnogs_predict-0.4 → satnogs_predict-0.6}/docs/docs.md +37 -12
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/__init__.py +2 -0
- satnogs_predict-0.6/src/satnogs_predict/core/__init__.py +15 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/core/engine.py +17 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/__init__.py +2 -1
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/observer.py +5 -2
- satnogs_predict-0.6/src/satnogs_predict/domain/orbit.py +89 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/propagation/propagator.py +6 -2
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/tle.py +3 -2
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/PKG-INFO +1 -1
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/SOURCES.txt +1 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/core/test_engine.py +21 -0
- satnogs_predict-0.6/tests/domain/test_observer.py +44 -0
- satnogs_predict-0.6/tests/domain/test_orbit.py +146 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/domain/test_required_fields.py +6 -22
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/propagation/test_propagator.py +35 -1
- satnogs_predict-0.4/src/satnogs_predict/core/__init__.py +0 -3
- satnogs_predict-0.4/src/satnogs_predict/domain/orbit.py +0 -53
- satnogs_predict-0.4/tests/domain/test_orbit.py +0 -37
- {satnogs_predict-0.4 → satnogs_predict-0.6}/.gitignore +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/.gitlab-ci.yml +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/.pre-commit-config.yaml +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/.python-version +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/CONTRIBUTING.md +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/LICENSE +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/README.md +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/pyproject.toml +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/setup.cfg +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/setup.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/constraints/__init__.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/constraints/constraints.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/constraints.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/geometry.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/planner.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/time.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/validation.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/domain/window.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/planning/__init__.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/planning/planner.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/propagation/__init__.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict/py.typed +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/requires.txt +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/top_level.txt +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/__init__.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/conftest.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/constraints/test_constraints.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/domain/test_planner_domain.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/domain/test_time.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/domain/test_window.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/helpers.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/integration/test_fake_tle_integration.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/planning/test_planner.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/tests/test_tle.py +0 -0
- {satnogs_predict-0.4 → satnogs_predict-0.6}/uv.lock +0 -0
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
- [3.3 - Time Modeling](#33---time-modeling)
|
|
10
10
|
- [4. Observation Windows \& samples](#4-observation-windows--samples)
|
|
11
11
|
- [4.1 - Samples](#41---samples)
|
|
12
|
-
- [4.2 -
|
|
13
|
-
- [4.3 -
|
|
12
|
+
- [4.2 - Doppler correction](#42---doppler-correction)
|
|
13
|
+
- [4.3 - Observation Windows](#43---observation-windows)
|
|
14
|
+
- [4.4 - Constraints](#44---constraints)
|
|
14
15
|
- [5. Planning](#5-planning)
|
|
15
16
|
- [5.1 - Planning is implemented by providing a config class instance:](#51---planning-is-implemented-by-providing-a-config-class-instance)
|
|
16
17
|
- [5.2 - Overlap Handling](#52---overlap-handling)
|
|
@@ -84,16 +85,25 @@ The top-level exports define the supported API contract.
|
|
|
84
85
|
|
|
85
86
|
## 3.1 - Satellite
|
|
86
87
|
|
|
87
|
-
Represents orbital information
|
|
88
|
-
(In the future, more ways may be added.)
|
|
88
|
+
Represents orbital information derived from TLE or OMM data.
|
|
89
89
|
|
|
90
90
|
```python
|
|
91
91
|
satellite = Satellite.from_tle(
|
|
92
|
-
line0=tle['tle0'], line1=tle['tle1'], line2=tle['tle2']
|
|
92
|
+
line0=tle['tle0'], line1=tle['tle1'], line2=tle['tle2']
|
|
93
93
|
)
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
You can also create a satellite from an OMM element dictionary. The field names and
|
|
97
|
+
values are passed through to Skyfield's `EarthSatellite.from_omm` support:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
satellite = Satellite.from_omm(
|
|
101
|
+
fields=omm_fields,
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `identifier` is optional and defaults to a random UUID. Provide an explicit
|
|
106
|
+
identifier when you need a stable public value or cache key.
|
|
97
107
|
|
|
98
108
|
For testing or development, you can also generate a synthetic TLE:
|
|
99
109
|
|
|
@@ -119,12 +129,12 @@ station = Observer(
|
|
|
119
129
|
lat_deg=37.983810,
|
|
120
130
|
lon_deg=23.727539,
|
|
121
131
|
elevation_meters=100,
|
|
122
|
-
horizon_deg=10.0
|
|
123
|
-
identifier="360"
|
|
132
|
+
horizon_deg=10.0
|
|
124
133
|
)
|
|
125
134
|
```
|
|
126
135
|
|
|
127
|
-
The `identifier` is
|
|
136
|
+
The `identifier` is optional here as well and defaults to a random UUID. Provide one
|
|
137
|
+
when you need a stable public value or cache key.
|
|
128
138
|
|
|
129
139
|
|
|
130
140
|
## 3.3 - Time Modeling
|
|
@@ -211,9 +221,24 @@ class RangeSample:
|
|
|
211
221
|
range_rate: float
|
|
212
222
|
```
|
|
213
223
|
|
|
214
|
-
`range_m` is the observer-to-satellite distance in meters, and `range_rate` is the
|
|
224
|
+
`range_m` is the observer-to-satellite distance in meters, and `range_rate` is the
|
|
225
|
+
rate of change of that distance in meters per second.
|
|
226
|
+
|
|
227
|
+
## 4.2 - Doppler correction
|
|
228
|
+
|
|
229
|
+
Use `get_doppler_adjusted_frequency` to apply the Doppler correction to a
|
|
230
|
+
base frequency:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from satnogs_predict import get_doppler_adjusted_frequency
|
|
234
|
+
|
|
235
|
+
adjusted_frequency_hz = get_doppler_adjusted_frequency(
|
|
236
|
+
range_rate_m_per_s=range_sample.range_rate,
|
|
237
|
+
frequency_hz=145_000_000.0,
|
|
238
|
+
)
|
|
239
|
+
```
|
|
215
240
|
|
|
216
|
-
## 4.
|
|
241
|
+
## 4.3 - Observation Windows
|
|
217
242
|
|
|
218
243
|
An observation window represents a pass or a subset of a pass.
|
|
219
244
|
|
|
@@ -257,7 +282,7 @@ If the window has an overlap with the provided already scheduled ranges, `overla
|
|
|
257
282
|
|
|
258
283
|
Note: if a later constraint modifies the window length, `overlap_ratio` is currently not recalculated. It remains the value computed earlier during planning.
|
|
259
284
|
|
|
260
|
-
## 4.
|
|
285
|
+
## 4.4 - Constraints
|
|
261
286
|
|
|
262
287
|
When searching for observation windows (more on this below), certain constraints can be applied. Currently supported constraints are:
|
|
263
288
|
|
|
@@ -4,6 +4,7 @@ from .core.engine import (
|
|
|
4
4
|
find_observation_windows,
|
|
5
5
|
get_altaz,
|
|
6
6
|
get_and_validate_pass_from_range,
|
|
7
|
+
get_doppler_adjusted_frequency,
|
|
7
8
|
get_range,
|
|
8
9
|
)
|
|
9
10
|
from .domain import * # noqa: F401,F403
|
|
@@ -19,6 +20,7 @@ __all__ += [
|
|
|
19
20
|
"find_observation_windows",
|
|
20
21
|
"get_altaz",
|
|
21
22
|
"get_and_validate_pass_from_range",
|
|
23
|
+
"get_doppler_adjusted_frequency",
|
|
22
24
|
"get_range",
|
|
23
25
|
]
|
|
24
26
|
__all__ += ["generate_fake_tle"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .engine import (
|
|
2
|
+
find_observation_windows,
|
|
3
|
+
get_altaz,
|
|
4
|
+
get_and_validate_pass_from_range,
|
|
5
|
+
get_doppler_adjusted_frequency,
|
|
6
|
+
get_range,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"find_observation_windows",
|
|
11
|
+
"get_altaz",
|
|
12
|
+
"get_and_validate_pass_from_range",
|
|
13
|
+
"get_doppler_adjusted_frequency",
|
|
14
|
+
"get_range",
|
|
15
|
+
]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Sequence
|
|
2
2
|
|
|
3
|
+
from skyfield.constants import C # type: ignore[import-untyped]
|
|
4
|
+
|
|
3
5
|
from satnogs_predict.constraints.constraints import Constraint
|
|
4
6
|
from satnogs_predict.domain.constraints import ConstraintAppliedResult, ConstraintContext
|
|
5
7
|
from satnogs_predict.domain.geometry import GeometrySample, RangeSample
|
|
@@ -101,6 +103,21 @@ def get_range(satellite: Satellite, observer: Observer, instant: Instant) -> Ran
|
|
|
101
103
|
return _propagation_get_range(satellite, observer, instant)
|
|
102
104
|
|
|
103
105
|
|
|
106
|
+
def get_doppler_adjusted_frequency(range_rate_m_per_s: float, frequency_hz: float) -> float:
|
|
107
|
+
"""
|
|
108
|
+
Return the Doppler-adjusted frequency in Hz.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
range_rate_m_per_s: Range rate in meters per second. Positive values mean the
|
|
112
|
+
satellite is moving away from the observer.
|
|
113
|
+
frequency_hz: Nominal transmit or receive frequency in Hz.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Doppler-corrected frequency
|
|
117
|
+
"""
|
|
118
|
+
return frequency_hz * (1 - (range_rate_m_per_s / C))
|
|
119
|
+
|
|
120
|
+
|
|
104
121
|
def get_and_validate_pass_from_range(
|
|
105
122
|
satellite: Satellite,
|
|
106
123
|
observer: Observer,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .constraints import ConstraintAppliedResult, ConstraintCheckFailedError, ConstraintContext
|
|
2
2
|
from .geometry import GeometrySample, RangeSample
|
|
3
3
|
from .observer import Observer
|
|
4
|
-
from .orbit import TLE, Orbit, OrbitRepresentation, Satellite
|
|
4
|
+
from .orbit import OMM, TLE, Orbit, OrbitRepresentation, Satellite
|
|
5
5
|
from .planner import OverlapHandling, PlanningConfig
|
|
6
6
|
from .time import (
|
|
7
7
|
Duration,
|
|
@@ -17,6 +17,7 @@ __all__ = [
|
|
|
17
17
|
"GeometrySample",
|
|
18
18
|
"RangeSample",
|
|
19
19
|
"Observer",
|
|
20
|
+
"OMM",
|
|
20
21
|
"TLE",
|
|
21
22
|
"OrbitRepresentation",
|
|
22
23
|
"Orbit",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from .validation import ensure_required_fields_not_none
|
|
6
7
|
|
|
@@ -13,7 +14,9 @@ class Observer:
|
|
|
13
14
|
lon_deg: float
|
|
14
15
|
elevation_meters: int
|
|
15
16
|
horizon_deg: float
|
|
16
|
-
identifier: str
|
|
17
|
+
identifier: str = field(default_factory=lambda: str(uuid4()))
|
|
17
18
|
|
|
18
19
|
def __post_init__(self) -> None:
|
|
20
|
+
if self.identifier is None:
|
|
21
|
+
object.__setattr__(self, "identifier", str(uuid4()))
|
|
19
22
|
ensure_required_fields_not_none(self)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .validation import ensure_required_fields_not_none
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class TLE:
|
|
13
|
+
line0: str
|
|
14
|
+
line1: str
|
|
15
|
+
line2: str
|
|
16
|
+
|
|
17
|
+
def __post_init__(self) -> None:
|
|
18
|
+
ensure_required_fields_not_none(self)
|
|
19
|
+
|
|
20
|
+
def to_list(self) -> list[str]:
|
|
21
|
+
return [self.line0, self.line1, self.line2]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class OMM:
|
|
26
|
+
fields: dict[str, str]
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
ensure_required_fields_not_none(self)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, str]:
|
|
32
|
+
return dict(self.fields)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OrbitRepresentation(IntEnum):
|
|
36
|
+
TLE = 0
|
|
37
|
+
OMM = 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class Orbit:
|
|
42
|
+
representation: OrbitRepresentation
|
|
43
|
+
data: TLE | OMM
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
ensure_required_fields_not_none(self)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_omm(
|
|
50
|
+
cls,
|
|
51
|
+
fields: Mapping[str, str],
|
|
52
|
+
):
|
|
53
|
+
return cls(representation=OrbitRepresentation.OMM, data=OMM(fields=dict(fields)))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class Satellite:
|
|
58
|
+
orbit: Orbit
|
|
59
|
+
identifier: str = field(default_factory=lambda: str(uuid4()))
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if self.identifier is None:
|
|
63
|
+
object.__setattr__(self, "identifier", str(uuid4()))
|
|
64
|
+
ensure_required_fields_not_none(self)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_tle(
|
|
68
|
+
cls,
|
|
69
|
+
line0: str,
|
|
70
|
+
line1: str,
|
|
71
|
+
line2: str,
|
|
72
|
+
identifier: str | None = None,
|
|
73
|
+
):
|
|
74
|
+
tle = TLE(line0=line0, line1=line1, line2=line2)
|
|
75
|
+
orbit = Orbit(representation=OrbitRepresentation.TLE, data=tle)
|
|
76
|
+
if identifier is None:
|
|
77
|
+
return cls(orbit=orbit)
|
|
78
|
+
return cls(orbit=orbit, identifier=identifier)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_omm(
|
|
82
|
+
cls,
|
|
83
|
+
fields: Mapping[str, str],
|
|
84
|
+
identifier: str | None = None,
|
|
85
|
+
):
|
|
86
|
+
orbit = Orbit.from_omm(fields)
|
|
87
|
+
if identifier is None:
|
|
88
|
+
return cls(orbit=orbit)
|
|
89
|
+
return cls(orbit=orbit, identifier=identifier)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import cast
|
|
2
3
|
|
|
3
4
|
from skyfield.api import Angle, EarthSatellite, Time, Topos, load # type: ignore[import-untyped]
|
|
4
5
|
from skyfield.searchlib import find_maxima # type: ignore[import-untyped]
|
|
@@ -6,7 +7,7 @@ from skyfield.units import Distance, Velocity # type: ignore[import-untyped]
|
|
|
6
7
|
|
|
7
8
|
from satnogs_predict.domain.geometry import GeometrySample, RangeSample
|
|
8
9
|
from satnogs_predict.domain.observer import Observer
|
|
9
|
-
from satnogs_predict.domain.orbit import TLE, OrbitRepresentation, Satellite
|
|
10
|
+
from satnogs_predict.domain.orbit import OMM, TLE, OrbitRepresentation, Satellite
|
|
10
11
|
from satnogs_predict.domain.time import (
|
|
11
12
|
Instant,
|
|
12
13
|
TimeRange,
|
|
@@ -82,13 +83,16 @@ def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
|
|
|
82
83
|
|
|
83
84
|
match satellite.orbit.representation:
|
|
84
85
|
case OrbitRepresentation.TLE:
|
|
85
|
-
tle
|
|
86
|
+
tle = cast(TLE, satellite.orbit.data)
|
|
86
87
|
skyfield_sat = EarthSatellite(
|
|
87
88
|
tle.line1,
|
|
88
89
|
tle.line2,
|
|
89
90
|
satellite.identifier,
|
|
90
91
|
ts,
|
|
91
92
|
)
|
|
93
|
+
case OrbitRepresentation.OMM:
|
|
94
|
+
omm = cast(OMM, satellite.orbit.data)
|
|
95
|
+
skyfield_sat = EarthSatellite.from_omm(ts, omm.to_dict())
|
|
92
96
|
case _:
|
|
93
97
|
raise ValueError("Unknown orbit representation")
|
|
94
98
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
|
|
6
|
-
from skyfield.api import load # type: ignore[import-untyped]
|
|
6
|
+
from skyfield.api import load, wgs84 # type: ignore[import-untyped]
|
|
7
7
|
|
|
8
8
|
from satnogs_predict.domain.orbit import TLE
|
|
9
9
|
|
|
@@ -67,7 +67,8 @@ def generate_fake_tle(
|
|
|
67
67
|
skyfield_time = _ts.from_datetime(epoch)
|
|
68
68
|
|
|
69
69
|
# Derived values
|
|
70
|
-
|
|
70
|
+
observer_location = wgs84.latlon(latitude, longitude)
|
|
71
|
+
right_ascension = (observer_location.lst_hours_at(skyfield_time) * 15.0) % 360.0
|
|
71
72
|
mean_anomaly = latitude % 360.0
|
|
72
73
|
|
|
73
74
|
epoch_year = epoch.year
|
|
@@ -272,6 +272,27 @@ class TestGetRange:
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
class TestGetDopplerAdjustedFrequency:
|
|
276
|
+
def test_given_zero_range_rate_when_getting_then_returns_nominal_frequency(self) -> None:
|
|
277
|
+
assert engine.get_doppler_adjusted_frequency(
|
|
278
|
+
range_rate_m_per_s=0.0, frequency_hz=145_000_000.0
|
|
279
|
+
) == pytest.approx(145_000_000.0)
|
|
280
|
+
|
|
281
|
+
def test_given_receding_range_rate_when_getting_then_returns_lower_frequency(self) -> None:
|
|
282
|
+
assert engine.get_doppler_adjusted_frequency(
|
|
283
|
+
range_rate_m_per_s=2_997.92458,
|
|
284
|
+
frequency_hz=145_000_000.0,
|
|
285
|
+
) == pytest.approx(144_998_550.0)
|
|
286
|
+
|
|
287
|
+
def test_given_approaching_range_rate_when_getting_then_returns_higher_frequency(
|
|
288
|
+
self,
|
|
289
|
+
) -> None:
|
|
290
|
+
assert engine.get_doppler_adjusted_frequency(
|
|
291
|
+
range_rate_m_per_s=-2_997.92458,
|
|
292
|
+
frequency_hz=145_000_000.0,
|
|
293
|
+
) == pytest.approx(145_001_450.0)
|
|
294
|
+
|
|
295
|
+
|
|
275
296
|
class TestEnsureWindowRespectConstraints:
|
|
276
297
|
def test_given_valid_and_rejected_windows_then_returns_expected(self, monkeypatch) -> None:
|
|
277
298
|
thepass = window_seconds(0, 10)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from satnogs_predict.domain.observer import Observer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestObserver:
|
|
9
|
+
def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
|
|
10
|
+
observer = Observer(
|
|
11
|
+
lat_deg=0.0,
|
|
12
|
+
lon_deg=0.0,
|
|
13
|
+
elevation_meters=0,
|
|
14
|
+
horizon_deg=0.0,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assert UUID(observer.identifier)
|
|
18
|
+
|
|
19
|
+
def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
|
|
20
|
+
observer = Observer(
|
|
21
|
+
lat_deg=0.0,
|
|
22
|
+
lon_deg=0.0,
|
|
23
|
+
elevation_meters=0,
|
|
24
|
+
horizon_deg=0.0,
|
|
25
|
+
identifier=None, # type: ignore[arg-type]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert UUID(observer.identifier)
|
|
29
|
+
|
|
30
|
+
def test_given_no_identifier_when_created_twice_then_generates_unique_identifiers(self) -> None:
|
|
31
|
+
first = Observer(
|
|
32
|
+
lat_deg=0.0,
|
|
33
|
+
lon_deg=0.0,
|
|
34
|
+
elevation_meters=0,
|
|
35
|
+
horizon_deg=0.0,
|
|
36
|
+
)
|
|
37
|
+
second = Observer(
|
|
38
|
+
lat_deg=0.0,
|
|
39
|
+
lon_deg=0.0,
|
|
40
|
+
elevation_meters=0,
|
|
41
|
+
horizon_deg=0.0,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert first.identifier != second.identifier
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from satnogs_predict.domain.orbit import OMM, TLE, Orbit, OrbitRepresentation, Satellite
|
|
6
|
+
from tests.helpers import next_satellite_identifier
|
|
7
|
+
|
|
8
|
+
OMM_FIELDS = {
|
|
9
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
10
|
+
"NORAD_CAT_ID": "25544",
|
|
11
|
+
"OBJECT_ID": "1998-067A",
|
|
12
|
+
"CLASSIFICATION_TYPE": "U",
|
|
13
|
+
"EPHEMERIS_TYPE": "0",
|
|
14
|
+
"ELEMENT_SET_NO": "999",
|
|
15
|
+
"REV_AT_EPOCH": "54773",
|
|
16
|
+
"EPOCH": "2026-01-13T05:00:55.753",
|
|
17
|
+
"MEAN_MOTION": "15.49269250",
|
|
18
|
+
"ECCENTRICITY": "0.0007741",
|
|
19
|
+
"INCLINATION": "51.6330",
|
|
20
|
+
"RA_OF_ASC_NODE": "346.6801",
|
|
21
|
+
"ARG_OF_PERICENTER": "12.6584",
|
|
22
|
+
"MEAN_ANOMALY": "347.4598",
|
|
23
|
+
"MEAN_MOTION_DOT": ".00008852",
|
|
24
|
+
"MEAN_MOTION_DDOT": "0",
|
|
25
|
+
"BSTAR": ".00016699",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestTle:
|
|
30
|
+
def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
|
|
31
|
+
tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
32
|
+
|
|
33
|
+
assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestOmm:
|
|
37
|
+
def test_given_omm_when_to_dict_then_returns_copy_of_fields(self) -> None:
|
|
38
|
+
omm = OMM(fields=OMM_FIELDS)
|
|
39
|
+
|
|
40
|
+
result = omm.to_dict()
|
|
41
|
+
result["OBJECT_NAME"] = "CHANGED"
|
|
42
|
+
|
|
43
|
+
assert result != omm.fields
|
|
44
|
+
assert omm.fields == OMM_FIELDS
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestOrbitFromOmm:
|
|
48
|
+
def test_given_fields_when_created_then_builds_omm_orbit(self) -> None:
|
|
49
|
+
orbit = Orbit.from_omm(OMM_FIELDS)
|
|
50
|
+
|
|
51
|
+
assert orbit.representation == OrbitRepresentation.OMM
|
|
52
|
+
assert orbit.data == OMM(fields=OMM_FIELDS)
|
|
53
|
+
|
|
54
|
+
def test_given_fields_when_created_then_copies_fields(self) -> None:
|
|
55
|
+
fields = dict(OMM_FIELDS)
|
|
56
|
+
|
|
57
|
+
orbit = Orbit.from_omm(fields)
|
|
58
|
+
fields["OBJECT_NAME"] = "CHANGED"
|
|
59
|
+
|
|
60
|
+
assert orbit.data == OMM(fields=OMM_FIELDS)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestSatelliteFromTle:
|
|
64
|
+
def test_given_identifier_when_created_then_builds_satellite(self) -> None:
|
|
65
|
+
line0 = "ISS (ZARYA)"
|
|
66
|
+
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
67
|
+
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
68
|
+
identifier = next_satellite_identifier()
|
|
69
|
+
|
|
70
|
+
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
|
|
71
|
+
|
|
72
|
+
assert sat.identifier == identifier
|
|
73
|
+
assert sat.orbit.representation == OrbitRepresentation.TLE
|
|
74
|
+
assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
|
|
75
|
+
|
|
76
|
+
def test_given_identifier_when_created_then_uses_custom_value(self) -> None:
|
|
77
|
+
line0 = "ISS (ZARYA)"
|
|
78
|
+
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
79
|
+
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
80
|
+
identifier = "ISS"
|
|
81
|
+
|
|
82
|
+
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
|
|
83
|
+
|
|
84
|
+
assert sat.identifier == identifier
|
|
85
|
+
assert sat.orbit.representation == OrbitRepresentation.TLE
|
|
86
|
+
assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
|
|
87
|
+
|
|
88
|
+
def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
|
|
89
|
+
line0 = "ISS (ZARYA)"
|
|
90
|
+
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
91
|
+
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
92
|
+
|
|
93
|
+
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2)
|
|
94
|
+
|
|
95
|
+
assert UUID(sat.identifier)
|
|
96
|
+
|
|
97
|
+
def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
|
|
98
|
+
line0 = "ISS (ZARYA)"
|
|
99
|
+
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
100
|
+
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
101
|
+
|
|
102
|
+
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=None)
|
|
103
|
+
|
|
104
|
+
assert UUID(sat.identifier)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestSatelliteFromOmm:
|
|
108
|
+
def test_given_identifier_when_created_then_builds_satellite(self) -> None:
|
|
109
|
+
identifier = next_satellite_identifier()
|
|
110
|
+
|
|
111
|
+
sat = Satellite.from_omm(fields=OMM_FIELDS, identifier=identifier)
|
|
112
|
+
|
|
113
|
+
assert sat.identifier == identifier
|
|
114
|
+
assert sat.orbit.representation == OrbitRepresentation.OMM
|
|
115
|
+
assert sat.orbit.data == OMM(fields=OMM_FIELDS)
|
|
116
|
+
|
|
117
|
+
def test_given_fields_when_created_then_copies_fields(self) -> None:
|
|
118
|
+
fields = dict(OMM_FIELDS)
|
|
119
|
+
|
|
120
|
+
sat = Satellite.from_omm(fields=fields, identifier=next_satellite_identifier())
|
|
121
|
+
fields["OBJECT_NAME"] = "CHANGED"
|
|
122
|
+
|
|
123
|
+
assert sat.orbit.data == OMM(fields=OMM_FIELDS)
|
|
124
|
+
|
|
125
|
+
def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
|
|
126
|
+
sat = Satellite.from_omm(fields=OMM_FIELDS)
|
|
127
|
+
|
|
128
|
+
assert UUID(sat.identifier)
|
|
129
|
+
|
|
130
|
+
def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
|
|
131
|
+
sat = Satellite.from_omm(fields=OMM_FIELDS, identifier=None)
|
|
132
|
+
|
|
133
|
+
assert UUID(sat.identifier)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestSatellite:
|
|
137
|
+
def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
|
|
138
|
+
sat = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
|
|
139
|
+
|
|
140
|
+
assert UUID(sat.identifier)
|
|
141
|
+
|
|
142
|
+
def test_given_no_identifier_when_created_twice_then_generates_unique_identifiers(self) -> None:
|
|
143
|
+
first = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
|
|
144
|
+
second = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
|
|
145
|
+
|
|
146
|
+
assert first.identifier != second.identifier
|
|
@@ -5,8 +5,7 @@ from collections.abc import Callable
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from satnogs_predict.domain.geometry import GeometrySample
|
|
8
|
-
from satnogs_predict.domain.
|
|
9
|
-
from satnogs_predict.domain.orbit import TLE, Orbit, Satellite
|
|
8
|
+
from satnogs_predict.domain.orbit import OMM, TLE, Orbit, Satellite
|
|
10
9
|
from satnogs_predict.domain.planner import PlanningConfig
|
|
11
10
|
from satnogs_predict.domain.time import Duration, Instant, TimeRange
|
|
12
11
|
from satnogs_predict.domain.window import ObservationWindow
|
|
@@ -20,16 +19,6 @@ class TestRequiredFields:
|
|
|
20
19
|
@pytest.mark.parametrize(
|
|
21
20
|
("factory", "expected_message"),
|
|
22
21
|
[
|
|
23
|
-
(
|
|
24
|
-
lambda: Observer(
|
|
25
|
-
lat_deg=0.0,
|
|
26
|
-
lon_deg=0.0,
|
|
27
|
-
elevation_meters=0,
|
|
28
|
-
horizon_deg=0.0,
|
|
29
|
-
identifier=None, # type: ignore[arg-type]
|
|
30
|
-
),
|
|
31
|
-
r"Observer\.identifier cannot be None",
|
|
32
|
-
),
|
|
33
22
|
(
|
|
34
23
|
lambda: GeometrySample(
|
|
35
24
|
instant=None, # type: ignore[arg-type]
|
|
@@ -54,21 +43,16 @@ class TestRequiredFields:
|
|
|
54
43
|
r"Orbit\.representation cannot be None",
|
|
55
44
|
),
|
|
56
45
|
(
|
|
57
|
-
lambda:
|
|
58
|
-
|
|
59
|
-
identifier="SAT",
|
|
46
|
+
lambda: OMM(
|
|
47
|
+
fields=None, # type: ignore[arg-type]
|
|
60
48
|
),
|
|
61
|
-
r"
|
|
49
|
+
r"OMM\.fields cannot be None",
|
|
62
50
|
),
|
|
63
51
|
(
|
|
64
52
|
lambda: Satellite(
|
|
65
|
-
orbit=
|
|
66
|
-
representation=0,
|
|
67
|
-
data=TLE("LINE0", "LINE1", "LINE2"),
|
|
68
|
-
),
|
|
69
|
-
identifier=None, # type: ignore[arg-type]
|
|
53
|
+
orbit=None, # type: ignore[arg-type]
|
|
70
54
|
),
|
|
71
|
-
r"Satellite\.
|
|
55
|
+
r"Satellite\.orbit cannot be None",
|
|
72
56
|
),
|
|
73
57
|
(
|
|
74
58
|
lambda: Duration(
|
|
@@ -1120,8 +1120,42 @@ class TestSkyfieldSatFromSatellite:
|
|
|
1120
1120
|
assert first is not second
|
|
1121
1121
|
assert len(created) == 2
|
|
1122
1122
|
|
|
1123
|
+
def test_given_omm_satellite_when_called_then_uses_skyfield_from_omm(self, monkeypatch) -> None:
|
|
1124
|
+
omm_fields = {
|
|
1125
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
1126
|
+
"NORAD_CAT_ID": "25544",
|
|
1127
|
+
}
|
|
1128
|
+
satellite = Satellite.from_omm(
|
|
1129
|
+
fields=omm_fields,
|
|
1130
|
+
identifier=next_satellite_identifier(),
|
|
1131
|
+
)
|
|
1132
|
+
skyfield_satellite = object()
|
|
1133
|
+
captured: dict[str, object] = {}
|
|
1134
|
+
|
|
1135
|
+
class EarthSatelliteSpy:
|
|
1136
|
+
@classmethod
|
|
1137
|
+
def from_omm(cls, timescale, element_dict):
|
|
1138
|
+
captured["timescale"] = timescale
|
|
1139
|
+
captured["element_dict"] = element_dict
|
|
1140
|
+
return skyfield_satellite
|
|
1141
|
+
|
|
1142
|
+
monkeypatch.setattr(propagator, "EarthSatellite", EarthSatelliteSpy)
|
|
1143
|
+
propagator.clear_satellite_cache()
|
|
1144
|
+
|
|
1145
|
+
result = propagator._skyfield_sat_from_satellite(satellite)
|
|
1146
|
+
omm_fields["OBJECT_NAME"] = "CHANGED"
|
|
1147
|
+
|
|
1148
|
+
assert result is skyfield_satellite
|
|
1149
|
+
assert captured == {
|
|
1150
|
+
"timescale": propagator.ts,
|
|
1151
|
+
"element_dict": {
|
|
1152
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
1153
|
+
"NORAD_CAT_ID": "25544",
|
|
1154
|
+
},
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1123
1157
|
def test_given_unknown_representation_when_called_then_raises(self) -> None:
|
|
1124
|
-
orbit = Orbit(representation=
|
|
1158
|
+
orbit = Orbit(representation=999, data=TLE("LINE0", "LINE1", "LINE2"))
|
|
1125
1159
|
sat = Satellite(orbit=orbit, identifier=next_satellite_identifier())
|
|
1126
1160
|
propagator.clear_satellite_cache()
|
|
1127
1161
|
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from enum import IntEnum
|
|
5
|
-
|
|
6
|
-
from .validation import ensure_required_fields_not_none
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass(frozen=True, slots=True)
|
|
10
|
-
class TLE:
|
|
11
|
-
line0: str
|
|
12
|
-
line1: str
|
|
13
|
-
line2: str
|
|
14
|
-
|
|
15
|
-
def __post_init__(self) -> None:
|
|
16
|
-
ensure_required_fields_not_none(self)
|
|
17
|
-
|
|
18
|
-
def to_list(self) -> list[str]:
|
|
19
|
-
return [self.line0, self.line1, self.line2]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class OrbitRepresentation(IntEnum):
|
|
23
|
-
TLE = 0
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass(frozen=True, slots=True)
|
|
27
|
-
class Orbit:
|
|
28
|
-
representation: OrbitRepresentation
|
|
29
|
-
data: TLE
|
|
30
|
-
|
|
31
|
-
def __post_init__(self) -> None:
|
|
32
|
-
ensure_required_fields_not_none(self)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass(frozen=True, slots=True)
|
|
36
|
-
class Satellite:
|
|
37
|
-
orbit: Orbit
|
|
38
|
-
identifier: str
|
|
39
|
-
|
|
40
|
-
def __post_init__(self) -> None:
|
|
41
|
-
ensure_required_fields_not_none(self)
|
|
42
|
-
|
|
43
|
-
@classmethod
|
|
44
|
-
def from_tle(
|
|
45
|
-
cls,
|
|
46
|
-
line0: str,
|
|
47
|
-
line1: str,
|
|
48
|
-
line2: str,
|
|
49
|
-
identifier: str,
|
|
50
|
-
):
|
|
51
|
-
tle = TLE(line0=line0, line1=line1, line2=line2)
|
|
52
|
-
orbit = Orbit(representation=OrbitRepresentation.TLE, data=tle)
|
|
53
|
-
return cls(orbit=orbit, identifier=identifier)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from satnogs_predict.domain.orbit import TLE, OrbitRepresentation, Satellite
|
|
4
|
-
from tests.helpers import next_satellite_identifier
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestTle:
|
|
8
|
-
def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
|
|
9
|
-
tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
10
|
-
|
|
11
|
-
assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestSatelliteFromTle:
|
|
15
|
-
def test_given_identifier_when_created_then_builds_satellite(self) -> None:
|
|
16
|
-
line0 = "ISS (ZARYA)"
|
|
17
|
-
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
18
|
-
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
19
|
-
identifier = next_satellite_identifier()
|
|
20
|
-
|
|
21
|
-
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
|
|
22
|
-
|
|
23
|
-
assert sat.identifier == identifier
|
|
24
|
-
assert sat.orbit.representation == OrbitRepresentation.TLE
|
|
25
|
-
assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
|
|
26
|
-
|
|
27
|
-
def test_given_identifier_when_created_then_uses_custom_value(self) -> None:
|
|
28
|
-
line0 = "ISS (ZARYA)"
|
|
29
|
-
line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
|
|
30
|
-
line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
|
|
31
|
-
identifier = "ISS"
|
|
32
|
-
|
|
33
|
-
sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
|
|
34
|
-
|
|
35
|
-
assert sat.identifier == identifier
|
|
36
|
-
assert sat.orbit.representation == OrbitRepresentation.TLE
|
|
37
|
-
assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{satnogs_predict-0.4 → satnogs_predict-0.6}/src/satnogs_predict.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|