satnogs-predict 0.6__tar.gz → 1.0__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-1.0/CHANGELOG.md +19 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/PKG-INFO +3 -1
- {satnogs_predict-0.6 → satnogs_predict-1.0}/README.md +2 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/docs/docs.md +21 -22
- satnogs_predict-1.0/src/satnogs_predict/domain/observer.py +35 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/orbit.py +37 -12
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/window.py +3 -3
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/propagation/propagator.py +13 -14
- satnogs_predict-1.0/src/satnogs_predict/utils.py +7 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/PKG-INFO +3 -1
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/SOURCES.txt +4 -0
- satnogs_predict-1.0/src/satnogs_predict.egg-info/scm_file_list.json +52 -0
- satnogs_predict-1.0/src/satnogs_predict.egg-info/scm_version.json +8 -0
- satnogs_predict-1.0/tests/domain/test_observer.py +59 -0
- satnogs_predict-1.0/tests/domain/test_orbit.py +131 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_required_fields.py +0 -11
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/helpers.py +24 -11
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/propagation/test_propagator.py +6 -10
- satnogs_predict-0.6/src/satnogs_predict/domain/observer.py +0 -22
- satnogs_predict-0.6/tests/domain/test_observer.py +0 -44
- satnogs_predict-0.6/tests/domain/test_orbit.py +0 -146
- {satnogs_predict-0.6 → satnogs_predict-1.0}/.gitignore +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/.gitlab-ci.yml +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/.pre-commit-config.yaml +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/.python-version +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/CONTRIBUTING.md +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/LICENSE +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/pyproject.toml +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/setup.cfg +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/setup.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/constraints/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/constraints/constraints.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/core/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/core/engine.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/constraints.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/geometry.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/planner.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/time.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/validation.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/planning/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/planning/planner.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/propagation/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/py.typed +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/tle.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/requires.txt +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/top_level.txt +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/__init__.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/conftest.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/constraints/test_constraints.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/core/test_engine.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_planner_domain.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_time.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_window.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/integration/test_fake_tle_integration.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/planning/test_planner.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/test_tle.py +0 -0
- {satnogs_predict-0.6 → satnogs_predict-1.0}/uv.lock +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- `Satellite.identifier` no longer defaults to a generated UUID. It now defaults to an empty string and is treated as caller metadata.
|
|
8
|
+
- `Observer.identifier` no longer defaults to a generated UUID. It now defaults to an empty string and is treated as caller metadata.
|
|
9
|
+
- Skyfield satellite and observer caches **no longer use identifiers as cache keys**. Satellite cache identity is based on orbital data, and observer cache identity is based on latitude, longitude, and elevation.
|
|
10
|
+
- `OMM.fields` is now stored as a read-only mapping. Use `OMM.to_dict()` when a mutable dictionary is needed.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `cache_key` properties for `TLE`, `OMM`, `Orbit`, `Satellite`, and `Observer`.
|
|
15
|
+
- Added a shared internal `cache_digest()` helper.
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
|
|
19
|
+
- It is no longer the caller's responsibility to manage cache by using identifiers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: satnogs-predict
|
|
3
|
-
Version: 0
|
|
3
|
+
Version: 1.0
|
|
4
4
|
Summary: A package for calculating passes and observation windows for satellites.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -46,6 +46,8 @@ from satnogs_predict import find_observation_windows
|
|
|
46
46
|
|
|
47
47
|
For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
|
|
48
48
|
|
|
49
|
+
For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
|
|
50
|
+
|
|
49
51
|
## License
|
|
50
52
|
|
|
51
53
|
[](LICENSE)
|
|
@@ -36,6 +36,8 @@ from satnogs_predict import find_observation_windows
|
|
|
36
36
|
|
|
37
37
|
For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
|
|
38
38
|
|
|
39
|
+
For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
|
|
40
|
+
|
|
39
41
|
## License
|
|
40
42
|
|
|
41
43
|
[](LICENSE)
|
|
@@ -20,10 +20,9 @@
|
|
|
20
20
|
- [6 - Finding passes](#6---finding-passes)
|
|
21
21
|
- [6.1 - Constraints](#61---constraints)
|
|
22
22
|
- [6.2 - Windows returned](#62---windows-returned)
|
|
23
|
-
- [6.3 -
|
|
24
|
-
- [6.4 -
|
|
25
|
-
- [6.5 -
|
|
26
|
-
- [6.6 - Full workflow for validating time ranges that belong to passes](#66---full-workflow-for-validating-time-ranges-that-belong-to-passes)
|
|
23
|
+
- [6.3 - Full workflow for finding passes](#63---full-workflow-for-finding-passes)
|
|
24
|
+
- [6.4 - Sampling point-in-time geometry](#64---sampling-point-in-time-geometry)
|
|
25
|
+
- [6.5 - Full workflow for validating time ranges that belong to passes](#65---full-workflow-for-validating-time-ranges-that-belong-to-passes)
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
# 1. Installation
|
|
@@ -102,8 +101,12 @@ satellite = Satellite.from_omm(
|
|
|
102
101
|
)
|
|
103
102
|
```
|
|
104
103
|
|
|
105
|
-
The `identifier` is optional and defaults to
|
|
106
|
-
|
|
104
|
+
The `identifier` is optional metadata and defaults to an empty string. It is copied
|
|
105
|
+
to produced `ObservationWindow.sat_identifier` values so callers can conveniently connect windows back to their own satellite records.
|
|
106
|
+
|
|
107
|
+
Because the underlying skyfield object EarthSatellite is computationally expensive to create, the EarthSatellite of the last-used satellite is cached. Cache identity is based on orbital state: TLE `line1`/`line2`, or normalized OMM fields.
|
|
108
|
+
|
|
109
|
+
OMM field data are copied when the `OMM` object is created. The stored mapping is read-only, and `to_dict()` returns a mutable copy.
|
|
107
110
|
|
|
108
111
|
For testing or development, you can also generate a synthetic TLE:
|
|
109
112
|
|
|
@@ -133,8 +136,11 @@ station = Observer(
|
|
|
133
136
|
)
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
The `identifier` is optional
|
|
137
|
-
|
|
139
|
+
The `identifier` is optional metadata and defaults to an empty string. It is copied
|
|
140
|
+
to produced `ObservationWindow.observer_identifier` for convenience.
|
|
141
|
+
|
|
142
|
+
Because the underlying skyfield object Topos that represents an observer is computationally expensive, the Topos of the last-used observer is cached.
|
|
143
|
+
Observer cache identity depends on `lat_deg`, `lon_deg` and `elevation_meters`. The `horizon_deg` value is not part of the Topos object so it doesn't affect caching.
|
|
138
144
|
|
|
139
145
|
|
|
140
146
|
## 3.3 - Time Modeling
|
|
@@ -248,13 +254,14 @@ class ObservationWindow:
|
|
|
248
254
|
A schedulable observation window.
|
|
249
255
|
"""
|
|
250
256
|
|
|
251
|
-
sat_identifier: str
|
|
252
|
-
observer_identifier: str
|
|
253
|
-
|
|
254
257
|
start_sample: GeometrySample
|
|
255
258
|
end_sample: GeometrySample
|
|
256
259
|
max_altitude_sample: GeometrySample
|
|
257
260
|
|
|
261
|
+
# Caller metadata copied from Satellite.identifier and Observer.identifier.
|
|
262
|
+
sat_identifier: str = ""
|
|
263
|
+
observer_identifier: str = ""
|
|
264
|
+
|
|
258
265
|
# The altitude has multiple peaks during the window (None means unknown)
|
|
259
266
|
has_multiple_peaks: bool | None = None
|
|
260
267
|
|
|
@@ -404,15 +411,7 @@ For the helper `get_and_validate_pass_from_range`, constraints are validated via
|
|
|
404
411
|
|
|
405
412
|
The function returns a tuple of observation window lists. The first element of the tuple is the list of valid windows, and the second is the list of windows that were rejected due to constraints.
|
|
406
413
|
|
|
407
|
-
## 6.3 -
|
|
408
|
-
|
|
409
|
-
Internally, satellite & observer are converted to Skyfield's types, which is computationally expensive. For this reason, the last satellite and the last observer used is cached, differentiated by the `identifier` field.
|
|
410
|
-
|
|
411
|
-
This way, repeated consecutive calls of `find_observation_windows` with the same satellite, or with the same `observer` reuse the objects under the hood.
|
|
412
|
-
|
|
413
|
-
If you need to clear these caches explicitly, the public helpers `clear_satellite_cache()` and `clear_observer_cache()` are available.
|
|
414
|
-
|
|
415
|
-
## 6.4 - Full workflow for finding passes
|
|
414
|
+
## 6.3 - Full workflow for finding passes
|
|
416
415
|
|
|
417
416
|
High-level process:
|
|
418
417
|
|
|
@@ -484,7 +483,7 @@ valid_windows, rejected_windows = find_observation_windows(
|
|
|
484
483
|
)
|
|
485
484
|
```
|
|
486
485
|
|
|
487
|
-
## 6.
|
|
486
|
+
## 6.4 - Sampling point-in-time geometry
|
|
488
487
|
|
|
489
488
|
For point-in-time queries, the public API also provides helpers for sampling a satellite relative to an observer at a single `Instant`.
|
|
490
489
|
|
|
@@ -506,7 +505,7 @@ range_sample = get_range(
|
|
|
506
505
|
- `get_range()` returns a `RangeSample` with range in meters and range rate in meters per second.
|
|
507
506
|
|
|
508
507
|
|
|
509
|
-
## 6.
|
|
508
|
+
## 6.5 - Full workflow for validating time ranges that belong to passes
|
|
510
509
|
|
|
511
510
|
- Given a time range, we can validate whether it belongs to a single pass and whether it satisfies constraints.
|
|
512
511
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from satnogs_predict.utils import cache_digest
|
|
6
|
+
|
|
7
|
+
from .validation import ensure_required_fields_not_none
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Observer:
|
|
12
|
+
"""Observer to calculate passes over e.g. a ground station"""
|
|
13
|
+
|
|
14
|
+
lat_deg: float
|
|
15
|
+
lon_deg: float
|
|
16
|
+
elevation_meters: int
|
|
17
|
+
horizon_deg: float
|
|
18
|
+
identifier: str = ""
|
|
19
|
+
_cache_key: str = field(init=False, repr=False, compare=False, default="")
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
ensure_required_fields_not_none(self)
|
|
23
|
+
object.__setattr__(
|
|
24
|
+
self,
|
|
25
|
+
"_cache_key",
|
|
26
|
+
cache_digest(
|
|
27
|
+
str(self.lat_deg),
|
|
28
|
+
str(self.lon_deg),
|
|
29
|
+
str(self.elevation_meters),
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def cache_key(self) -> str:
|
|
35
|
+
return self._cache_key
|
|
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from enum import IntEnum
|
|
6
|
-
from
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
from satnogs_predict.utils import cache_digest
|
|
7
9
|
|
|
8
10
|
from .validation import ensure_required_fields_not_none
|
|
9
11
|
|
|
@@ -13,24 +15,41 @@ class TLE:
|
|
|
13
15
|
line0: str
|
|
14
16
|
line1: str
|
|
15
17
|
line2: str
|
|
18
|
+
_cache_key: str = field(init=False, repr=False, compare=False, default="")
|
|
16
19
|
|
|
17
20
|
def __post_init__(self) -> None:
|
|
18
21
|
ensure_required_fields_not_none(self)
|
|
22
|
+
object.__setattr__(self, "_cache_key", cache_digest(self.line1, self.line2))
|
|
19
23
|
|
|
20
24
|
def to_list(self) -> list[str]:
|
|
21
25
|
return [self.line0, self.line1, self.line2]
|
|
22
26
|
|
|
27
|
+
@property
|
|
28
|
+
def cache_key(self) -> str:
|
|
29
|
+
return self._cache_key
|
|
30
|
+
|
|
23
31
|
|
|
24
32
|
@dataclass(frozen=True, slots=True)
|
|
25
33
|
class OMM:
|
|
26
|
-
fields:
|
|
34
|
+
fields: Mapping[str, str]
|
|
35
|
+
_cache_key: str = field(init=False, repr=False, compare=False, default="")
|
|
27
36
|
|
|
28
37
|
def __post_init__(self) -> None:
|
|
29
38
|
ensure_required_fields_not_none(self)
|
|
39
|
+
fields = MappingProxyType(dict(self.fields))
|
|
40
|
+
object.__setattr__(self, "fields", fields)
|
|
41
|
+
parts: list[str] = []
|
|
42
|
+
for key, value in sorted(fields.items()):
|
|
43
|
+
parts.extend((key, value))
|
|
44
|
+
object.__setattr__(self, "_cache_key", cache_digest(*parts))
|
|
30
45
|
|
|
31
46
|
def to_dict(self) -> dict[str, str]:
|
|
32
47
|
return dict(self.fields)
|
|
33
48
|
|
|
49
|
+
@property
|
|
50
|
+
def cache_key(self) -> str:
|
|
51
|
+
return self._cache_key
|
|
52
|
+
|
|
34
53
|
|
|
35
54
|
class OrbitRepresentation(IntEnum):
|
|
36
55
|
TLE = 0
|
|
@@ -41,27 +60,37 @@ class OrbitRepresentation(IntEnum):
|
|
|
41
60
|
class Orbit:
|
|
42
61
|
representation: OrbitRepresentation
|
|
43
62
|
data: TLE | OMM
|
|
63
|
+
_cache_key: str = field(init=False, repr=False, compare=False, default="")
|
|
44
64
|
|
|
45
65
|
def __post_init__(self) -> None:
|
|
46
66
|
ensure_required_fields_not_none(self)
|
|
67
|
+
object.__setattr__(self, "_cache_key", self.data.cache_key)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def cache_key(self) -> str:
|
|
71
|
+
return self._cache_key
|
|
47
72
|
|
|
48
73
|
@classmethod
|
|
49
74
|
def from_omm(
|
|
50
75
|
cls,
|
|
51
76
|
fields: Mapping[str, str],
|
|
52
77
|
):
|
|
53
|
-
return cls(representation=OrbitRepresentation.OMM, data=OMM(fields=
|
|
78
|
+
return cls(representation=OrbitRepresentation.OMM, data=OMM(fields=fields))
|
|
54
79
|
|
|
55
80
|
|
|
56
81
|
@dataclass(frozen=True, slots=True)
|
|
57
82
|
class Satellite:
|
|
58
83
|
orbit: Orbit
|
|
59
|
-
identifier: str =
|
|
84
|
+
identifier: str = ""
|
|
85
|
+
_cache_key: str = field(init=False, repr=False, compare=False, default="")
|
|
60
86
|
|
|
61
87
|
def __post_init__(self) -> None:
|
|
62
|
-
if self.identifier is None:
|
|
63
|
-
object.__setattr__(self, "identifier", str(uuid4()))
|
|
64
88
|
ensure_required_fields_not_none(self)
|
|
89
|
+
object.__setattr__(self, "_cache_key", self.orbit.cache_key)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def cache_key(self) -> str:
|
|
93
|
+
return self._cache_key
|
|
65
94
|
|
|
66
95
|
@classmethod
|
|
67
96
|
def from_tle(
|
|
@@ -69,21 +98,17 @@ class Satellite:
|
|
|
69
98
|
line0: str,
|
|
70
99
|
line1: str,
|
|
71
100
|
line2: str,
|
|
72
|
-
identifier: str
|
|
101
|
+
identifier: str = "",
|
|
73
102
|
):
|
|
74
103
|
tle = TLE(line0=line0, line1=line1, line2=line2)
|
|
75
104
|
orbit = Orbit(representation=OrbitRepresentation.TLE, data=tle)
|
|
76
|
-
if identifier is None:
|
|
77
|
-
return cls(orbit=orbit)
|
|
78
105
|
return cls(orbit=orbit, identifier=identifier)
|
|
79
106
|
|
|
80
107
|
@classmethod
|
|
81
108
|
def from_omm(
|
|
82
109
|
cls,
|
|
83
110
|
fields: Mapping[str, str],
|
|
84
|
-
identifier: str
|
|
111
|
+
identifier: str = "",
|
|
85
112
|
):
|
|
86
113
|
orbit = Orbit.from_omm(fields)
|
|
87
|
-
if identifier is None:
|
|
88
|
-
return cls(orbit=orbit)
|
|
89
114
|
return cls(orbit=orbit, identifier=identifier)
|
|
@@ -19,13 +19,13 @@ class ObservationWindow:
|
|
|
19
19
|
A schedulable observation window.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
sat_identifier: str
|
|
23
|
-
observer_identifier: str
|
|
24
|
-
|
|
25
22
|
start_sample: GeometrySample
|
|
26
23
|
end_sample: GeometrySample
|
|
27
24
|
max_altitude_sample: GeometrySample
|
|
28
25
|
|
|
26
|
+
sat_identifier: str = ""
|
|
27
|
+
observer_identifier: str = ""
|
|
28
|
+
|
|
29
29
|
# The altitude does has multiple peaks during the window (None means unknown)
|
|
30
30
|
has_multiple_peaks: bool | None = None
|
|
31
31
|
|
|
@@ -65,20 +65,20 @@ def _instant_from_skyfield_time(t: Time):
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
_last_sat: EarthSatellite | None = None
|
|
68
|
-
|
|
68
|
+
_last_sat_cache_key: str | None = None
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def clear_satellite_cache() -> None:
|
|
72
|
-
global _last_sat,
|
|
72
|
+
global _last_sat, _last_sat_cache_key
|
|
73
73
|
|
|
74
74
|
_last_sat = None
|
|
75
|
-
|
|
75
|
+
_last_sat_cache_key = None
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
|
|
79
|
-
global _last_sat,
|
|
79
|
+
global _last_sat, _last_sat_cache_key
|
|
80
80
|
|
|
81
|
-
if
|
|
81
|
+
if _last_sat_cache_key == satellite.cache_key and _last_sat is not None:
|
|
82
82
|
return _last_sat
|
|
83
83
|
|
|
84
84
|
match satellite.orbit.representation:
|
|
@@ -87,8 +87,7 @@ def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
|
|
|
87
87
|
skyfield_sat = EarthSatellite(
|
|
88
88
|
tle.line1,
|
|
89
89
|
tle.line2,
|
|
90
|
-
|
|
91
|
-
ts,
|
|
90
|
+
ts=ts,
|
|
92
91
|
)
|
|
93
92
|
case OrbitRepresentation.OMM:
|
|
94
93
|
omm = cast(OMM, satellite.orbit.data)
|
|
@@ -96,26 +95,26 @@ def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
|
|
|
96
95
|
case _:
|
|
97
96
|
raise ValueError("Unknown orbit representation")
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
_last_sat_cache_key = satellite.cache_key
|
|
100
99
|
_last_sat = skyfield_sat
|
|
101
100
|
return skyfield_sat
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
_last_topos: Topos | None = None
|
|
105
|
-
|
|
104
|
+
_last_observer_cache_key: str | None = None
|
|
106
105
|
|
|
107
106
|
|
|
108
107
|
def clear_observer_cache() -> None:
|
|
109
|
-
global _last_topos,
|
|
108
|
+
global _last_topos, _last_observer_cache_key
|
|
110
109
|
|
|
111
110
|
_last_topos = None
|
|
112
|
-
|
|
111
|
+
_last_observer_cache_key = None
|
|
113
112
|
|
|
114
113
|
|
|
115
114
|
def _skyfield_topos_from_observer(observer: Observer) -> Topos:
|
|
116
|
-
global _last_topos,
|
|
115
|
+
global _last_topos, _last_observer_cache_key
|
|
117
116
|
|
|
118
|
-
if
|
|
117
|
+
if _last_observer_cache_key == observer.cache_key and _last_topos is not None:
|
|
119
118
|
return _last_topos
|
|
120
119
|
|
|
121
120
|
_last_topos = Topos(
|
|
@@ -123,7 +122,7 @@ def _skyfield_topos_from_observer(observer: Observer) -> Topos:
|
|
|
123
122
|
longitude_degrees=observer.lon_deg,
|
|
124
123
|
elevation_m=observer.elevation_meters,
|
|
125
124
|
)
|
|
126
|
-
|
|
125
|
+
_last_observer_cache_key = observer.cache_key
|
|
127
126
|
return _last_topos
|
|
128
127
|
|
|
129
128
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: satnogs-predict
|
|
3
|
-
Version: 0
|
|
3
|
+
Version: 1.0
|
|
4
4
|
Summary: A package for calculating passes and observation windows for satellites.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -46,6 +46,8 @@ from satnogs_predict import find_observation_windows
|
|
|
46
46
|
|
|
47
47
|
For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
|
|
48
48
|
|
|
49
|
+
For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
|
|
50
|
+
|
|
49
51
|
## License
|
|
50
52
|
|
|
51
53
|
[](LICENSE)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
.gitlab-ci.yml
|
|
3
3
|
.pre-commit-config.yaml
|
|
4
4
|
.python-version
|
|
5
|
+
CHANGELOG.md
|
|
5
6
|
CONTRIBUTING.md
|
|
6
7
|
LICENSE
|
|
7
8
|
README.md
|
|
@@ -12,10 +13,13 @@ docs/docs.md
|
|
|
12
13
|
src/satnogs_predict/__init__.py
|
|
13
14
|
src/satnogs_predict/py.typed
|
|
14
15
|
src/satnogs_predict/tle.py
|
|
16
|
+
src/satnogs_predict/utils.py
|
|
15
17
|
src/satnogs_predict.egg-info/PKG-INFO
|
|
16
18
|
src/satnogs_predict.egg-info/SOURCES.txt
|
|
17
19
|
src/satnogs_predict.egg-info/dependency_links.txt
|
|
18
20
|
src/satnogs_predict.egg-info/requires.txt
|
|
21
|
+
src/satnogs_predict.egg-info/scm_file_list.json
|
|
22
|
+
src/satnogs_predict.egg-info/scm_version.json
|
|
19
23
|
src/satnogs_predict.egg-info/top_level.txt
|
|
20
24
|
src/satnogs_predict/constraints/__init__.py
|
|
21
25
|
src/satnogs_predict/constraints/constraints.py
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".gitignore",
|
|
4
|
+
".gitlab-ci.yml",
|
|
5
|
+
".pre-commit-config.yaml",
|
|
6
|
+
"CHANGELOG.md",
|
|
7
|
+
".python-version",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CONTRIBUTING.md",
|
|
10
|
+
"uv.lock",
|
|
11
|
+
"pyproject.toml",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"setup.py",
|
|
14
|
+
"tests/conftest.py",
|
|
15
|
+
"tests/__init__.py",
|
|
16
|
+
"tests/helpers.py",
|
|
17
|
+
"tests/test_tle.py",
|
|
18
|
+
"tests/integration/test_fake_tle_integration.py",
|
|
19
|
+
"tests/domain/test_time.py",
|
|
20
|
+
"tests/domain/test_planner_domain.py",
|
|
21
|
+
"tests/domain/test_required_fields.py",
|
|
22
|
+
"tests/domain/test_orbit.py",
|
|
23
|
+
"tests/domain/test_observer.py",
|
|
24
|
+
"tests/domain/test_window.py",
|
|
25
|
+
"tests/planning/test_planner.py",
|
|
26
|
+
"tests/propagation/test_propagator.py",
|
|
27
|
+
"tests/core/test_engine.py",
|
|
28
|
+
"tests/constraints/test_constraints.py",
|
|
29
|
+
"src/satnogs_predict/__init__.py",
|
|
30
|
+
"src/satnogs_predict/py.typed",
|
|
31
|
+
"src/satnogs_predict/utils.py",
|
|
32
|
+
"src/satnogs_predict/tle.py",
|
|
33
|
+
"src/satnogs_predict/domain/constraints.py",
|
|
34
|
+
"src/satnogs_predict/domain/time.py",
|
|
35
|
+
"src/satnogs_predict/domain/observer.py",
|
|
36
|
+
"src/satnogs_predict/domain/validation.py",
|
|
37
|
+
"src/satnogs_predict/domain/planner.py",
|
|
38
|
+
"src/satnogs_predict/domain/__init__.py",
|
|
39
|
+
"src/satnogs_predict/domain/window.py",
|
|
40
|
+
"src/satnogs_predict/domain/geometry.py",
|
|
41
|
+
"src/satnogs_predict/domain/orbit.py",
|
|
42
|
+
"src/satnogs_predict/planning/planner.py",
|
|
43
|
+
"src/satnogs_predict/planning/__init__.py",
|
|
44
|
+
"src/satnogs_predict/propagation/__init__.py",
|
|
45
|
+
"src/satnogs_predict/propagation/propagator.py",
|
|
46
|
+
"src/satnogs_predict/core/engine.py",
|
|
47
|
+
"src/satnogs_predict/core/__init__.py",
|
|
48
|
+
"src/satnogs_predict/constraints/constraints.py",
|
|
49
|
+
"src/satnogs_predict/constraints/__init__.py",
|
|
50
|
+
"docs/docs.md"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from satnogs_predict.domain.observer import Observer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestObserver:
|
|
5
|
+
def test_given_topos_values_change_when_getting_cache_key_then_key_changes(self) -> None:
|
|
6
|
+
observer = Observer(
|
|
7
|
+
lat_deg=0.0,
|
|
8
|
+
lon_deg=0.0,
|
|
9
|
+
elevation_meters=0,
|
|
10
|
+
horizon_deg=0.0,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
assert (
|
|
14
|
+
observer.cache_key
|
|
15
|
+
!= Observer(
|
|
16
|
+
lat_deg=1.0,
|
|
17
|
+
lon_deg=0.0,
|
|
18
|
+
elevation_meters=0,
|
|
19
|
+
horizon_deg=0.0,
|
|
20
|
+
).cache_key
|
|
21
|
+
)
|
|
22
|
+
assert (
|
|
23
|
+
observer.cache_key
|
|
24
|
+
!= Observer(
|
|
25
|
+
lat_deg=0.0,
|
|
26
|
+
lon_deg=1.0,
|
|
27
|
+
elevation_meters=0,
|
|
28
|
+
horizon_deg=0.0,
|
|
29
|
+
).cache_key
|
|
30
|
+
)
|
|
31
|
+
assert (
|
|
32
|
+
observer.cache_key
|
|
33
|
+
!= Observer(
|
|
34
|
+
lat_deg=0.0,
|
|
35
|
+
lon_deg=0.0,
|
|
36
|
+
elevation_meters=1,
|
|
37
|
+
horizon_deg=0.0,
|
|
38
|
+
).cache_key
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def test_given_non_topos_values_changes_when_getting_cache_key_then_key_is_unchanged(
|
|
42
|
+
self,
|
|
43
|
+
) -> None:
|
|
44
|
+
first = Observer(
|
|
45
|
+
lat_deg=0.0,
|
|
46
|
+
lon_deg=0.0,
|
|
47
|
+
elevation_meters=0,
|
|
48
|
+
horizon_deg=0.0,
|
|
49
|
+
identifier="OBS-1",
|
|
50
|
+
)
|
|
51
|
+
second = Observer(
|
|
52
|
+
lat_deg=0.0,
|
|
53
|
+
lon_deg=0.0,
|
|
54
|
+
elevation_meters=0,
|
|
55
|
+
horizon_deg=10.0,
|
|
56
|
+
identifier="OBS-2",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert first.cache_key == second.cache_key
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from satnogs_predict.domain.orbit import OMM, TLE, Orbit, Satellite
|
|
6
|
+
|
|
7
|
+
OMM_FIELDS = {
|
|
8
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
9
|
+
"NORAD_CAT_ID": "25544",
|
|
10
|
+
"OBJECT_ID": "1998-067A",
|
|
11
|
+
"CLASSIFICATION_TYPE": "U",
|
|
12
|
+
"EPHEMERIS_TYPE": "0",
|
|
13
|
+
"ELEMENT_SET_NO": "999",
|
|
14
|
+
"REV_AT_EPOCH": "54773",
|
|
15
|
+
"EPOCH": "2026-01-13T05:00:55.753",
|
|
16
|
+
"MEAN_MOTION": "15.49269250",
|
|
17
|
+
"ECCENTRICITY": "0.0007741",
|
|
18
|
+
"INCLINATION": "51.6330",
|
|
19
|
+
"RA_OF_ASC_NODE": "346.6801",
|
|
20
|
+
"ARG_OF_PERICENTER": "12.6584",
|
|
21
|
+
"MEAN_ANOMALY": "347.4598",
|
|
22
|
+
"MEAN_MOTION_DOT": ".00008852",
|
|
23
|
+
"MEAN_MOTION_DDOT": "0",
|
|
24
|
+
"BSTAR": ".00016699",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestTle:
|
|
29
|
+
def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
|
|
30
|
+
tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
31
|
+
|
|
32
|
+
assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
|
|
33
|
+
|
|
34
|
+
def test_given_line1_or_line2_changes_when_getting_cache_key_then_key_changes(self) -> None:
|
|
35
|
+
tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
36
|
+
|
|
37
|
+
assert tle.cache_key != TLE(line0="LINE0", line1="CHANGED", line2="LINE2").cache_key
|
|
38
|
+
assert tle.cache_key != TLE(line0="LINE0", line1="LINE1", line2="CHANGED").cache_key
|
|
39
|
+
|
|
40
|
+
def test_given_only_line0_changes_when_getting_cache_key_then_key_is_unchanged(self) -> None:
|
|
41
|
+
tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
42
|
+
|
|
43
|
+
assert tle.cache_key == TLE(line0="CHANGED", line1="LINE1", line2="LINE2").cache_key
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestOmm:
|
|
47
|
+
def test_given_fields_when_created_then_copies_fields(self) -> None:
|
|
48
|
+
fields = dict(OMM_FIELDS)
|
|
49
|
+
|
|
50
|
+
omm = OMM(fields=fields)
|
|
51
|
+
fields["OBJECT_NAME"] = "CHANGED"
|
|
52
|
+
|
|
53
|
+
assert omm.fields == OMM_FIELDS
|
|
54
|
+
|
|
55
|
+
def test_given_omm_when_mutating_fields_then_raises(self) -> None:
|
|
56
|
+
omm = OMM(fields=OMM_FIELDS)
|
|
57
|
+
|
|
58
|
+
with pytest.raises(TypeError):
|
|
59
|
+
omm.fields["OBJECT_NAME"] = "CHANGED"
|
|
60
|
+
|
|
61
|
+
def test_given_omm_when_to_dict_then_returns_copy_of_fields(self) -> None:
|
|
62
|
+
omm = OMM(fields=OMM_FIELDS)
|
|
63
|
+
|
|
64
|
+
result = omm.to_dict()
|
|
65
|
+
result["OBJECT_NAME"] = "CHANGED"
|
|
66
|
+
|
|
67
|
+
assert result != omm.fields
|
|
68
|
+
assert omm.fields == OMM_FIELDS
|
|
69
|
+
|
|
70
|
+
def test_given_same_fields_in_different_order_when_getting_cache_key_then_key_is_stable(
|
|
71
|
+
self,
|
|
72
|
+
) -> None:
|
|
73
|
+
fields = {
|
|
74
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
75
|
+
"NORAD_CAT_ID": "25544",
|
|
76
|
+
}
|
|
77
|
+
reversed_fields = {
|
|
78
|
+
"NORAD_CAT_ID": "25544",
|
|
79
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assert OMM(fields=fields).cache_key == OMM(fields=reversed_fields).cache_key
|
|
83
|
+
|
|
84
|
+
def test_given_field_value_changes_when_getting_cache_key_then_key_changes(self) -> None:
|
|
85
|
+
fields = {
|
|
86
|
+
"OBJECT_NAME": "ISS (ZARYA)",
|
|
87
|
+
"NORAD_CAT_ID": "25544",
|
|
88
|
+
}
|
|
89
|
+
changed_fields = {
|
|
90
|
+
"OBJECT_NAME": "CHANGED",
|
|
91
|
+
"NORAD_CAT_ID": "25544",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assert OMM(fields=fields).cache_key != OMM(fields=changed_fields).cache_key
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestOrbitFromOmm:
|
|
98
|
+
def test_given_omm_orbit_when_getting_cache_key_then_delegates_to_omm_data(self) -> None:
|
|
99
|
+
orbit = Orbit.from_omm(OMM_FIELDS)
|
|
100
|
+
|
|
101
|
+
assert orbit.cache_key == orbit.data.cache_key
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestSatellite:
|
|
105
|
+
def test_given_only_identifier_changes_when_getting_cache_key_then_key_is_unchanged(
|
|
106
|
+
self,
|
|
107
|
+
) -> None:
|
|
108
|
+
first = Satellite.from_tle(
|
|
109
|
+
line0="LINE0",
|
|
110
|
+
line1="LINE1",
|
|
111
|
+
line2="LINE2",
|
|
112
|
+
identifier="SAT-1",
|
|
113
|
+
)
|
|
114
|
+
second = Satellite.from_tle(
|
|
115
|
+
line0="LINE0",
|
|
116
|
+
line1="LINE1",
|
|
117
|
+
line2="LINE2",
|
|
118
|
+
identifier="SAT-2",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert first.cache_key == second.cache_key
|
|
122
|
+
|
|
123
|
+
def test_given_tle_satellite_when_getting_cache_key_then_delegates_to_orbit(self) -> None:
|
|
124
|
+
sat = Satellite.from_tle(line0="LINE0", line1="LINE1", line2="LINE2")
|
|
125
|
+
|
|
126
|
+
assert sat.cache_key == sat.orbit.cache_key
|
|
127
|
+
|
|
128
|
+
def test_given_omm_satellite_when_getting_cache_key_then_delegates_to_orbit(self) -> None:
|
|
129
|
+
sat = Satellite.from_omm(fields=OMM_FIELDS)
|
|
130
|
+
|
|
131
|
+
assert sat.cache_key == sat.orbit.cache_key
|
|
@@ -8,7 +8,6 @@ from satnogs_predict.domain.geometry import GeometrySample
|
|
|
8
8
|
from satnogs_predict.domain.orbit import OMM, TLE, Orbit, Satellite
|
|
9
9
|
from satnogs_predict.domain.planner import PlanningConfig
|
|
10
10
|
from satnogs_predict.domain.time import Duration, Instant, TimeRange
|
|
11
|
-
from satnogs_predict.domain.window import ObservationWindow
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _sample(micros: int) -> GeometrySample:
|
|
@@ -74,16 +73,6 @@ class TestRequiredFields:
|
|
|
74
73
|
),
|
|
75
74
|
r"PlanningConfig\.overlap_handling cannot be None",
|
|
76
75
|
),
|
|
77
|
-
(
|
|
78
|
-
lambda: ObservationWindow(
|
|
79
|
-
sat_identifier=None, # type: ignore[arg-type]
|
|
80
|
-
observer_identifier="OBS",
|
|
81
|
-
start_sample=_sample(0),
|
|
82
|
-
end_sample=_sample(1_000_000),
|
|
83
|
-
max_altitude_sample=_sample(500_000),
|
|
84
|
-
),
|
|
85
|
-
r"ObservationWindow\.sat_identifier cannot be None",
|
|
86
|
-
),
|
|
87
76
|
],
|
|
88
77
|
)
|
|
89
78
|
def test_given_none_for_required_field_when_created_then_raises(
|
|
@@ -57,22 +57,35 @@ def window_seconds(
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def make_observer(
|
|
60
|
+
def make_observer(
|
|
61
|
+
identifier: str = "",
|
|
62
|
+
*,
|
|
63
|
+
lat_deg: float = 0.0,
|
|
64
|
+
lon_deg: float = 0.0,
|
|
65
|
+
elevation_meters: int = 0,
|
|
66
|
+
horizon_deg: float = 0.0,
|
|
67
|
+
) -> Observer:
|
|
61
68
|
return Observer(
|
|
62
|
-
lat_deg=
|
|
63
|
-
lon_deg=
|
|
64
|
-
elevation_meters=
|
|
65
|
-
horizon_deg=
|
|
66
|
-
identifier=identifier
|
|
69
|
+
lat_deg=lat_deg,
|
|
70
|
+
lon_deg=lon_deg,
|
|
71
|
+
elevation_meters=elevation_meters,
|
|
72
|
+
horizon_deg=horizon_deg,
|
|
73
|
+
identifier=identifier,
|
|
67
74
|
)
|
|
68
75
|
|
|
69
76
|
|
|
70
|
-
def make_satellite(
|
|
77
|
+
def make_satellite(
|
|
78
|
+
identifier: str = "",
|
|
79
|
+
*,
|
|
80
|
+
line0: str = "LINE0",
|
|
81
|
+
line1: str = "LINE1",
|
|
82
|
+
line2: str = "LINE2",
|
|
83
|
+
) -> Satellite:
|
|
71
84
|
return Satellite.from_tle(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
identifier=identifier
|
|
85
|
+
line0=line0,
|
|
86
|
+
line1=line1,
|
|
87
|
+
line2=line2,
|
|
88
|
+
identifier=identifier,
|
|
76
89
|
)
|
|
77
90
|
|
|
78
91
|
|
|
@@ -1082,9 +1082,7 @@ class TestCalculateMinAltSkyfieldSample:
|
|
|
1082
1082
|
|
|
1083
1083
|
|
|
1084
1084
|
class TestSkyfieldSatFromSatellite:
|
|
1085
|
-
def
|
|
1086
|
-
self, monkeypatch
|
|
1087
|
-
) -> None:
|
|
1085
|
+
def test_given_same_values_when_called_twice_then_reuses_instance(self, monkeypatch) -> None:
|
|
1088
1086
|
created: list[object] = []
|
|
1089
1087
|
|
|
1090
1088
|
class EarthSatelliteSpy:
|
|
@@ -1102,7 +1100,7 @@ class TestSkyfieldSatFromSatellite:
|
|
|
1102
1100
|
assert first is second
|
|
1103
1101
|
assert len(created) == 1
|
|
1104
1102
|
|
|
1105
|
-
def
|
|
1103
|
+
def test_given_different_values_when_called_then_returns_different_instances(
|
|
1106
1104
|
self, monkeypatch
|
|
1107
1105
|
) -> None:
|
|
1108
1106
|
created: list[object] = []
|
|
@@ -1115,7 +1113,7 @@ class TestSkyfieldSatFromSatellite:
|
|
|
1115
1113
|
propagator.clear_satellite_cache()
|
|
1116
1114
|
|
|
1117
1115
|
first = propagator._skyfield_sat_from_satellite(make_satellite())
|
|
1118
|
-
second = propagator._skyfield_sat_from_satellite(make_satellite())
|
|
1116
|
+
second = propagator._skyfield_sat_from_satellite(make_satellite(line1="LINE1CHANGED"))
|
|
1119
1117
|
|
|
1120
1118
|
assert first is not second
|
|
1121
1119
|
assert len(created) == 2
|
|
@@ -1164,9 +1162,7 @@ class TestSkyfieldSatFromSatellite:
|
|
|
1164
1162
|
|
|
1165
1163
|
|
|
1166
1164
|
class TestSkyfieldToposFromObserver:
|
|
1167
|
-
def
|
|
1168
|
-
self, monkeypatch
|
|
1169
|
-
) -> None:
|
|
1165
|
+
def test_given_same_values_when_called_twice_then_reuses_instance(self, monkeypatch) -> None:
|
|
1170
1166
|
created: list[object] = []
|
|
1171
1167
|
|
|
1172
1168
|
class ToposSpy:
|
|
@@ -1184,7 +1180,7 @@ class TestSkyfieldToposFromObserver:
|
|
|
1184
1180
|
assert first is second
|
|
1185
1181
|
assert len(created) == 1
|
|
1186
1182
|
|
|
1187
|
-
def
|
|
1183
|
+
def test_given_different_latlngalt_values_when_called_then_returns_different_instances(
|
|
1188
1184
|
self, monkeypatch
|
|
1189
1185
|
) -> None:
|
|
1190
1186
|
created: list[object] = []
|
|
@@ -1197,7 +1193,7 @@ class TestSkyfieldToposFromObserver:
|
|
|
1197
1193
|
propagator.clear_observer_cache()
|
|
1198
1194
|
|
|
1199
1195
|
first = propagator._skyfield_topos_from_observer(make_observer())
|
|
1200
|
-
second = propagator._skyfield_topos_from_observer(make_observer())
|
|
1196
|
+
second = propagator._skyfield_topos_from_observer(make_observer(lat_deg=1.0))
|
|
1201
1197
|
|
|
1202
1198
|
assert first is not second
|
|
1203
1199
|
assert len(created) == 2
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from uuid import uuid4
|
|
5
|
-
|
|
6
|
-
from .validation import ensure_required_fields_not_none
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass(frozen=True, slots=True)
|
|
10
|
-
class Observer:
|
|
11
|
-
"""Observer to calculate passes over e.g. a ground station"""
|
|
12
|
-
|
|
13
|
-
lat_deg: float
|
|
14
|
-
lon_deg: float
|
|
15
|
-
elevation_meters: int
|
|
16
|
-
horizon_deg: float
|
|
17
|
-
identifier: str = field(default_factory=lambda: str(uuid4()))
|
|
18
|
-
|
|
19
|
-
def __post_init__(self) -> None:
|
|
20
|
-
if self.identifier is None:
|
|
21
|
-
object.__setattr__(self, "identifier", str(uuid4()))
|
|
22
|
-
ensure_required_fields_not_none(self)
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
|
@@ -1,146 +0,0 @@
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{satnogs_predict-0.6 → satnogs_predict-1.0}/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
|