satnogs-predict 0.2__tar.gz → 0.4__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.
Files changed (58) hide show
  1. {satnogs_predict-0.2 → satnogs_predict-0.4}/PKG-INFO +4 -3
  2. {satnogs_predict-0.2 → satnogs_predict-0.4}/README.md +3 -2
  3. {satnogs_predict-0.2 → satnogs_predict-0.4}/docs/docs.md +144 -6
  4. satnogs_predict-0.4/src/satnogs_predict/__init__.py +24 -0
  5. satnogs_predict-0.4/src/satnogs_predict/constraints/__init__.py +15 -0
  6. satnogs_predict-0.4/src/satnogs_predict/constraints/constraints.py +433 -0
  7. satnogs_predict-0.4/src/satnogs_predict/core/__init__.py +3 -0
  8. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/core/engine.py +37 -9
  9. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/__init__.py +4 -2
  10. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/constraints.py +10 -0
  11. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/geometry.py +9 -0
  12. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/orbit.py +12 -6
  13. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/time.py +16 -12
  14. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/window.py +1 -1
  15. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/planning/planner.py +2 -2
  16. satnogs_predict-0.4/src/satnogs_predict/propagation/__init__.py +19 -0
  17. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/propagation/propagator.py +77 -1
  18. satnogs_predict-0.4/src/satnogs_predict/tle.py +107 -0
  19. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict.egg-info/PKG-INFO +4 -3
  20. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict.egg-info/SOURCES.txt +3 -0
  21. satnogs_predict-0.4/tests/constraints/test_constraints.py +745 -0
  22. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/core/test_engine.py +129 -6
  23. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/domain/test_orbit.py +14 -3
  24. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/domain/test_required_fields.py +10 -0
  25. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/domain/test_time.py +3 -4
  26. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/helpers.py +22 -4
  27. satnogs_predict-0.4/tests/integration/test_fake_tle_integration.py +99 -0
  28. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/propagation/test_propagator.py +181 -17
  29. satnogs_predict-0.4/tests/test_tle.py +53 -0
  30. satnogs_predict-0.2/src/satnogs_predict/__init__.py +0 -9
  31. satnogs_predict-0.2/src/satnogs_predict/constraints/__init__.py +0 -3
  32. satnogs_predict-0.2/src/satnogs_predict/constraints/constraints.py +0 -111
  33. satnogs_predict-0.2/src/satnogs_predict/propagation/__init__.py +0 -0
  34. satnogs_predict-0.2/tests/__init__.py +0 -0
  35. satnogs_predict-0.2/tests/constraints/test_constraints.py +0 -102
  36. {satnogs_predict-0.2 → satnogs_predict-0.4}/.gitignore +0 -0
  37. {satnogs_predict-0.2 → satnogs_predict-0.4}/.gitlab-ci.yml +0 -0
  38. {satnogs_predict-0.2 → satnogs_predict-0.4}/.pre-commit-config.yaml +0 -0
  39. {satnogs_predict-0.2 → satnogs_predict-0.4}/.python-version +0 -0
  40. {satnogs_predict-0.2 → satnogs_predict-0.4}/CONTRIBUTING.md +0 -0
  41. {satnogs_predict-0.2 → satnogs_predict-0.4}/LICENSE +0 -0
  42. {satnogs_predict-0.2 → satnogs_predict-0.4}/pyproject.toml +0 -0
  43. {satnogs_predict-0.2 → satnogs_predict-0.4}/setup.cfg +0 -0
  44. {satnogs_predict-0.2 → satnogs_predict-0.4}/setup.py +0 -0
  45. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/observer.py +0 -0
  46. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/planner.py +0 -0
  47. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/domain/validation.py +0 -0
  48. {satnogs_predict-0.2/src/satnogs_predict/core → satnogs_predict-0.4/src/satnogs_predict/planning}/__init__.py +0 -0
  49. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict/py.typed +0 -0
  50. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
  51. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict.egg-info/requires.txt +0 -0
  52. {satnogs_predict-0.2 → satnogs_predict-0.4}/src/satnogs_predict.egg-info/top_level.txt +0 -0
  53. {satnogs_predict-0.2/src/satnogs_predict/planning → satnogs_predict-0.4/tests}/__init__.py +0 -0
  54. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/conftest.py +0 -0
  55. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/domain/test_planner_domain.py +0 -0
  56. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/domain/test_window.py +0 -0
  57. {satnogs_predict-0.2 → satnogs_predict-0.4}/tests/planning/test_planner.py +0 -0
  58. {satnogs_predict-0.2 → satnogs_predict-0.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.2
3
+ Version: 0.4
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
@@ -13,8 +13,7 @@ Dynamic: license-file
13
13
  [![Pipeline Status](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/pipeline.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
14
14
  [![Coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
15
15
  [![PyPI version](https://img.shields.io/pypi/v/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
16
- [![Python Versions](https://img.shields.io/pypi/pyversions/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
17
-
16
+ ![Python Version](https://img.shields.io/badge/dynamic/toml?url=https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/raw/fb6964df5384da584c1dbb92ac157104c8a3948d/pyproject.toml&query=project.requires-python&label=python)
18
17
  ---
19
18
 
20
19
  ## 🚀 Overview
@@ -45,6 +44,8 @@ pip install satnogs-predict
45
44
  from satnogs_predict import find_observation_windows
46
45
  ```
47
46
 
47
+ For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
48
+
48
49
  ## License
49
50
 
50
51
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -3,8 +3,7 @@
3
3
  [![Pipeline Status](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/pipeline.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
4
4
  [![Coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
5
5
  [![PyPI version](https://img.shields.io/pypi/v/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
6
- [![Python Versions](https://img.shields.io/pypi/pyversions/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
7
-
6
+ ![Python Version](https://img.shields.io/badge/dynamic/toml?url=https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/raw/fb6964df5384da584c1dbb92ac157104c8a3948d/pyproject.toml&query=project.requires-python&label=python)
8
7
  ---
9
8
 
10
9
  ## 🚀 Overview
@@ -35,6 +34,8 @@ pip install satnogs-predict
35
34
  from satnogs_predict import find_observation_windows
36
35
  ```
37
36
 
37
+ For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
38
+
38
39
  ## License
39
40
 
40
41
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -21,7 +21,8 @@
21
21
  - [6.2 - Windows returned](#62---windows-returned)
22
22
  - [6.3 - Implementation Details](#63---implementation-details)
23
23
  - [6.4 - Full workflow for finding passes](#64---full-workflow-for-finding-passes)
24
- - [6.5 - Full workflow for validating time ranges that belong to passes](#65---full-workflow-for-validating-time-ranges-that-belong-to-passes)
24
+ - [6.5 - Sampling point-in-time geometry](#65---sampling-point-in-time-geometry)
25
+ - [6.6 - Full workflow for validating time ranges that belong to passes](#66---full-workflow-for-validating-time-ranges-that-belong-to-passes)
25
26
 
26
27
 
27
28
  # 1. Installation
@@ -41,10 +42,15 @@ level:
41
42
  ``` python
42
43
  from satnogs_predict import (
43
44
  find_observation_windows,
45
+ get_altaz,
44
46
  get_and_validate_pass_from_range,
47
+ get_range,
45
48
  check_observation_density_limit_respected,
46
49
  has_range_list_overlap,
50
+ generate_fake_tle,
47
51
 
52
+ AngularSeparationConstraint,
53
+ AzimuthWindowConstraint,
48
54
  MaxDurationConstraint,
49
55
  MinCulminationConstraint,
50
56
  MinDurationConstraint,
@@ -52,6 +58,7 @@ from satnogs_predict import (
52
58
  ConstraintAppliedResult,
53
59
  ConstraintCheckFailedError,
54
60
  GeometrySample,
61
+ RangeSample,
55
62
  Observer,
56
63
  TLE,
57
64
  OrbitRepresentation,
@@ -86,6 +93,22 @@ satellite = Satellite.from_tle(
86
93
  )
87
94
  ```
88
95
 
96
+ The `identifier` is required. It is used both as part of the public model and as the key for the internal propagation cache.
97
+
98
+ For testing or development, you can also generate a synthetic TLE:
99
+
100
+ ```python
101
+ from datetime import datetime, timezone
102
+
103
+ fake_tle = generate_fake_tle(
104
+ latitude=37.983810,
105
+ longitude=23.727539,
106
+ date=datetime(2026, 3, 6, 12, 0, 0, tzinfo=timezone.utc),
107
+ )
108
+
109
+ line0, line1, line2 = fake_tle.to_list()
110
+ ```
111
+
89
112
 
90
113
  ## 3.2 - Observer
91
114
 
@@ -101,6 +124,8 @@ station = Observer(
101
124
  )
102
125
  ```
103
126
 
127
+ The `identifier` is required here as well, and is also used by the internal propagation cache.
128
+
104
129
 
105
130
  ## 3.3 - Time Modeling
106
131
 
@@ -123,14 +148,47 @@ now_instant = Instant.from_datetime(now)
123
148
  five_mins_dur = Duration.from_timedelta(delta)
124
149
  five_mins_dur_2 = Duration.from_seconds(5 * 60)
125
150
 
151
+ later_instant = now_instant + five_mins_dur
152
+ earlier_instant = later_instant - five_mins_dur
153
+
126
154
  time_range = TimeRange.from_datetimes(now, future)
155
+ range_duration = time_range.duration
156
+ shifted_range = time_range.shift(Duration.from_seconds(30))
157
+ padded_range = time_range.pad_duration(
158
+ left=Duration.from_seconds(10),
159
+ right=Duration.from_seconds(20),
160
+ )
161
+
162
+ assert now_instant < later_instant
163
+ assert time_range.contains(now_instant)
164
+ assert time_range.intersects(shifted_range)
127
165
  ```
128
166
 
167
+ Supported operations include:
168
+
169
+ - `Instant.from_datetime(dt)` and `instant.to_datetime()`
170
+ - `Instant + Duration -> Instant`
171
+ - `Instant - Duration -> Instant`
172
+ - ordering/comparisons between `Instant` values
173
+ - `Duration.from_timedelta(td)`, `Duration.from_seconds(seconds)`, `duration.to_timedelta()`, `duration.to_seconds()`
174
+ - `Duration + Duration -> Duration`
175
+ - `TimeRange.from_datetimes(start, end)` creates a range from two timezone-aware datetimes
176
+ - `TimeRange.duration` returns the range length as a `Duration`
177
+ - `TimeRange.contains(instant)` checks whether an instant lies inside the range
178
+ - `TimeRange.intersects(other)` checks whether two ranges overlap
179
+ - `TimeRange.intersection(other)` returns the overlapping part of two ranges, or `None`
180
+ - `TimeRange.subtract(other)` removes the overlapping part and returns the remaining pieces
181
+ - `TimeRange.shift(duration)` moves the whole range by a duration
182
+ - `TimeRange.pad_secs(left, right)` extends the range by raw second values on each side
183
+ - `TimeRange.pad_duration(left, right)` extends the range by `Duration` values on each side
184
+
129
185
  # 4. Observation Windows & samples
130
186
 
131
187
  ## 4.1 - Samples
132
188
 
133
- A sample represents information of a satellite's orbit relative to the observer at a specific instant:
189
+ A sample represents information of a satellite's orbit relative to the observer at a specific instant.
190
+
191
+ `GeometrySample` stores angular geometry:
134
192
 
135
193
  ```python
136
194
  class GeometrySample:
@@ -142,6 +200,19 @@ class GeometrySample:
142
200
 
143
201
  ```
144
202
 
203
+ `RangeSample` stores distance information:
204
+
205
+ ```python
206
+ class RangeSample:
207
+ """Range and range velocity at an instant, relative to an observer."""
208
+
209
+ instant: Instant
210
+ range_m: float
211
+ range_rate: float
212
+ ```
213
+
214
+ `range_m` is the observer-to-satellite distance in meters, and `range_rate` is the radial velocity in meters per second.
215
+
145
216
  ## 4.2 - Observation Windows
146
217
 
147
218
  An observation window represents a pass or a subset of a pass.
@@ -184,14 +255,48 @@ If due to the constraints, a window is rejected, the reason is set in `rejection
184
255
 
185
256
  If the window has an overlap with the provided already scheduled ranges, `overlapped` will be set to `True` and the ratio will be the percentage (as a decimal) that is overlapped.
186
257
 
258
+ Note: if a later constraint modifies the window length, `overlap_ratio` is currently not recalculated. It remains the value computed earlier during planning.
259
+
187
260
  ## 4.3 - Constraints
188
261
 
189
262
  When searching for observation windows (more on this below), certain constraints can be applied. Currently supported constraints are:
190
263
 
191
- - `MaxDurationConstraint` Rejects windows over a certain duration. Does not change the window.
264
+ - `AngularSeparationConstraint` Trims a window to stay within a target angular separation from a fixed pointing direction, with optional fallback behavior controlled by `min_duration`.
265
+ - `AzimuthWindowConstraint` Trims the start and end of a window so that both edge azimuths lie inside a clockwise azimuth interval.
266
+ - `MaxDurationConstraint` Rejects windows over a certain duration. If `trim=True`, it trims them symmetrically instead.
192
267
  - `MinDurationConstraint` Rejects windows under a certain duration. Does not change the window.
193
268
  - `MinCulminationConstraint` Rejects windows whose max altitude is under a certain threshold. Does not change the window.
194
269
 
270
+ For `AzimuthWindowConstraint`, the allowed region is defined by sweeping clockwise from `az_start` to `az_end`.
271
+
272
+ Examples:
273
+
274
+ - `AzimuthWindowConstraint(20.0, 100.0)` allows azimuths between `20°` and `100°`.
275
+ - `AzimuthWindowConstraint(350.0, 20.0)` wraps around north and allows azimuths between `350°..360°` and `0°..20°`.
276
+
277
+ If a window already satisfies the azimuth interval at both edges, it is left unchanged. Otherwise, the start is swept forward and the end is swept backward until both edge azimuths are inside the interval. If no valid sub-window remains, the window is rejected.
278
+
279
+ `AngularSeparationConstraint` defines a fixed antenna pointing direction with:
280
+
281
+ - `pointing_az_deg`
282
+ - `pointing_el_deg`
283
+ - `max_separation_deg`
284
+
285
+ The pass is sampled over time, and the angular separation between the satellite direction and that fixed pointing direction is checked at each sample.
286
+
287
+ If `min_duration` is `None`, the constraint trims the pass to the sampled interval that lies within `max_separation_deg`.
288
+
289
+ If `min_duration` is provided:
290
+
291
+ - if the within-separation interval is already at least `min_duration`, the pass is trimmed to that interval
292
+ - if trimming to the within-separation interval would make the pass shorter than `min_duration`, a fallback window of exactly `min_duration` is selected around the closest sampled approach, while staying inside the original pass
293
+ - if the original pass itself is shorter than `min_duration`, the constraint rejects it
294
+
295
+ `MaxDurationConstraint` supports two modes:
296
+
297
+ - by default, it rejects any window longer than `max_duration`
298
+ - if `trim=True`, it trims the window symmetrically around its center down to `max_duration`
299
+
195
300
  Constraint behavior is:
196
301
 
197
302
  - `constraint.apply(window) -> ObservationWindow`: annotates the window with `constraint_applied_result` / `rejection_reason`.
@@ -280,6 +385,8 @@ Internally, satellite & observer are converted to Skyfield's types, which is com
280
385
 
281
386
  This way, repeated consecutive calls of `find_observation_windows` with the same satellite, or with the same `observer` reuse the objects under the hood.
282
387
 
388
+ If you need to clear these caches explicitly, the public helpers `clear_satellite_cache()` and `clear_observer_cache()` are available.
389
+
283
390
  ## 6.4 - Full workflow for finding passes
284
391
 
285
392
  High-level process:
@@ -331,19 +438,50 @@ planning_config = PlanningConfig(
331
438
 
332
439
  min_duration_constraint = MinDurationConstraint(Duration.from_seconds(5))
333
440
  min_culmination_constraint = MinCulminationConstraint(55.0)
441
+ azimuth_window_constraint = AzimuthWindowConstraint(220.0, 330.0)
334
442
 
335
443
  valid_windows, rejected_windows = find_observation_windows(
336
444
  sat,
337
445
  observer,
338
446
  time_range,
339
447
  planning_config,
340
- pre_plan_constraints=[min_duration_constraint, min_culmination_constraint],
341
- post_plan_constraints=[min_duration_constraint, min_culmination_constraint],
448
+ pre_plan_constraints=[
449
+ min_duration_constraint,
450
+ min_culmination_constraint,
451
+ azimuth_window_constraint,
452
+ ],
453
+ post_plan_constraints=[
454
+ min_duration_constraint,
455
+ min_culmination_constraint,
456
+ azimuth_window_constraint,
457
+ ],
342
458
  scheduled_intervals=scheduled_ranges,
343
459
  )
344
460
  ```
345
461
 
346
- ## 6.5 - Full workflow for validating time ranges that belong to passes
462
+ ## 6.5 - Sampling point-in-time geometry
463
+
464
+ For point-in-time queries, the public API also provides helpers for sampling a satellite relative to an observer at a single `Instant`.
465
+
466
+ ```python
467
+ altaz_sample = get_altaz(
468
+ satellite,
469
+ observer,
470
+ instant,
471
+ )
472
+
473
+ range_sample = get_range(
474
+ satellite,
475
+ observer,
476
+ instant,
477
+ )
478
+ ```
479
+
480
+ - `get_altaz()` returns a `GeometrySample` with azimuth and altitude.
481
+ - `get_range()` returns a `RangeSample` with range in meters and range rate in meters per second.
482
+
483
+
484
+ ## 6.6 - Full workflow for validating time ranges that belong to passes
347
485
 
348
486
  - Given a time range, we can validate whether it belongs to a single pass and whether it satisfies constraints.
349
487
 
@@ -0,0 +1,24 @@
1
+ from . import constraints, domain
2
+ from .constraints import * # noqa: F401,F403
3
+ from .core.engine import (
4
+ find_observation_windows,
5
+ get_altaz,
6
+ get_and_validate_pass_from_range,
7
+ get_range,
8
+ )
9
+ from .domain import * # noqa: F401,F403
10
+ from .planning.planner import check_observation_density_limit_respected, has_range_list_overlap
11
+ from .propagation import clear_observer_cache, clear_satellite_cache
12
+ from .tle import generate_fake_tle
13
+
14
+ __all__ = list(domain.__all__) + list(constraints.__all__)
15
+ __all__ += ["check_observation_density_limit_respected", "has_range_list_overlap"]
16
+ __all__ += [
17
+ "clear_observer_cache",
18
+ "clear_satellite_cache",
19
+ "find_observation_windows",
20
+ "get_altaz",
21
+ "get_and_validate_pass_from_range",
22
+ "get_range",
23
+ ]
24
+ __all__ += ["generate_fake_tle"]
@@ -0,0 +1,15 @@
1
+ from .constraints import (
2
+ AngularSeparationConstraint,
3
+ AzimuthWindowConstraint,
4
+ MaxDurationConstraint,
5
+ MinCulminationConstraint,
6
+ MinDurationConstraint,
7
+ )
8
+
9
+ __all__ = [
10
+ "AngularSeparationConstraint",
11
+ "AzimuthWindowConstraint",
12
+ "MinCulminationConstraint",
13
+ "MinDurationConstraint",
14
+ "MaxDurationConstraint",
15
+ ]