pyModeS 3.4.0__tar.gz → 3.5.1__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 (64) hide show
  1. {pymodes-3.4.0 → pymodes-3.5.1}/CHANGELOG.md +77 -2
  2. {pymodes-3.4.0 → pymodes-3.5.1}/PKG-INFO +10 -26
  3. {pymodes-3.4.0 → pymodes-3.5.1}/README.md +9 -25
  4. {pymodes-3.4.0 → pymodes-3.5.1}/pyproject.toml +1 -1
  5. {pymodes-3.4.0 → pymodes-3.5.1}/scripts/README.md +14 -5
  6. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/__init__.py +7 -3
  7. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_pipe.py +381 -470
  8. pymodes-3.5.1/src/pyModeS/_pipe_validation.py +234 -0
  9. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/_args.py +21 -0
  10. pymodes-3.5.1/src/pyModeS/cli/_parse.py +51 -0
  11. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/_source.py +142 -72
  12. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/_tui.py +45 -42
  13. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/decode.py +17 -26
  14. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/live.py +23 -40
  15. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/core.py +19 -3
  16. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/_infer.py +6 -1
  17. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/commb.py +7 -2
  18. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/message.py +35 -8
  19. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/position/_cpr.py +16 -6
  20. {pymodes-3.4.0 → pymodes-3.5.1}/.gitignore +0 -0
  21. {pymodes-3.4.0 → pymodes-3.5.1}/LICENSE +0 -0
  22. {pymodes-3.4.0 → pymodes-3.5.1}/scripts/data/README.md +0 -0
  23. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_aero.py +0 -0
  24. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_altcode.py +0 -0
  25. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_bits.py +0 -0
  26. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_callsign.py +0 -0
  27. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_idcode.py +0 -0
  28. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_schema.py +0 -0
  29. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_uncertainty.py +0 -0
  30. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/_v2_removed.py +0 -0
  31. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/__init__.py +0 -0
  32. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/__main__.py +0 -0
  33. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/cli/_sink.py +0 -0
  34. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/data/__init__.py +0 -0
  35. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/data/airports.py +0 -0
  36. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/__init__.py +0 -0
  37. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/_base.py +0 -0
  38. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/acas.py +0 -0
  39. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/adsb.py +0 -0
  40. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/allcall.py +0 -0
  41. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/__init__.py +0 -0
  42. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/_helpers.py +0 -0
  43. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds05.py +0 -0
  44. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds06.py +0 -0
  45. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds08.py +0 -0
  46. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds09.py +0 -0
  47. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds10.py +0 -0
  48. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds17.py +0 -0
  49. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds20.py +0 -0
  50. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds30.py +0 -0
  51. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds40.py +0 -0
  52. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds44.py +0 -0
  53. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds45.py +0 -0
  54. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds50.py +0 -0
  55. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds60.py +0 -0
  56. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds61.py +0 -0
  57. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds62.py +0 -0
  58. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/bds/bds65.py +0 -0
  59. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/decoder/surv.py +0 -0
  60. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/errors.py +0 -0
  61. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/position/__init__.py +0 -0
  62. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/position/_airports.py +0 -0
  63. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/py.typed +0 -0
  64. {pymodes-3.4.0 → pymodes-3.5.1}/src/pyModeS/util.py +0 -0
@@ -5,6 +5,81 @@ All notable changes to pyModeS are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [3.5.1] — 2026-07-16
9
+
10
+ ### Added
11
+
12
+ - `decode()`, `Message.decode()`, and `PipeDecoder` now accept
13
+ `include_meteo=True` to opt into heuristic Comm-B BDS 4,4 routine
14
+ meteorological reports and BDS 4,5 meteorological hazard reports. The
15
+ default remains `False` because these payload patterns can overlap other
16
+ Comm-B registers.
17
+ - The `modes decode`, `modes live`, and `scripts/stream_filtered.py` interfaces
18
+ expose the same behavior through `--include-meteo`.
19
+
20
+ ## [3.5.0] — 2026-07-15
21
+
22
+ ### Added
23
+
24
+ - `PipeDecoder` now uses the last validated airborne position as a local CPR
25
+ reference for subsequent BDS 0,5 frames. This makes a position available on
26
+ the current streaming `decode()` call when no fresh global even/odd pair is
27
+ available, instead of relying on retroactive mutation of an earlier result.
28
+ - `local_ref_window` configures the maximum reference age (30 seconds by
29
+ default); set it to zero to disable local airborne decoding.
30
+ `PipeDecoder.stats["local_positions"]` reports positions produced by this
31
+ path.
32
+ - Position bootstrap now locks as soon as three candidates are pairwise
33
+ motion-consistent instead of always waiting for all five candidate slots.
34
+ The five-candidate limit still allows two unrelated outliers to be ignored.
35
+ - The mixed-traffic benchmark now reports immediate streaming position yield
36
+ and frame-by-frame coordinate agreement for v2.21.1, released v3.4.0, and
37
+ v3.5.0 in addition to throughput.
38
+
39
+ ### Changed
40
+
41
+ - `crc_valid` is now `None` when an address-parity message cannot be checked
42
+ independently. It remains `True` or `False` for frames whose parity can be
43
+ validated without assuming the address derived from that same parity.
44
+ - `PipeDecoder` uses each message's source timestamp for state validity while
45
+ retaining a monotonic high-water mark for eviction. Reordered capture data
46
+ therefore cannot revive expired observations or prematurely evict newer
47
+ state.
48
+ - Plausibility and position validation are isolated in a dedicated internal
49
+ module, keeping the streaming pipeline and its decision comments easier to
50
+ follow without changing the public API.
51
+ - Release publishing now runs only after the full test matrix succeeds, checks
52
+ that the Git tag matches the package version, and smoke-tests the exact wheel
53
+ artifact that will be uploaded.
54
+
55
+ ### Fixed
56
+
57
+ - Surface references remain optional and are used only for BDS 0,6 surface
58
+ positions. Positions derived from `surface_ref` can never seed local
59
+ airborne CPR decoding.
60
+ - Rejected global position candidates cannot become local CPR references; a
61
+ separate last-accepted airborne reference is maintained and TTL-evicted.
62
+ - Bootstrap promotion, including `flush()`, initializes the latest accepted
63
+ airborne reference while keeping surface-derived positions separate.
64
+ - Globally decoded surface CPR positions now select the correct longitude zone
65
+ across the antimeridian and other CPR zone boundaries.
66
+ - Beast stream input resynchronizes after malformed or truncated frames instead
67
+ of losing subsequent valid messages.
68
+ - Beast receiver timestamps are back-projected from the current wall clock with
69
+ rollover-safe receiver-tick deltas, avoiding dates near the Unix epoch and
70
+ preserving correct timing across the 12 MHz counter rollover.
71
+ - BDS inference compares headings using circular angular distance, so values
72
+ around 0/360 degrees are scored correctly.
73
+ - Trusted ICAO observations expire according to the configured state TTL and
74
+ cannot be refreshed by older, reordered messages.
75
+ - Integer messages outside the valid 56-bit and 112-bit Mode S frame ranges are
76
+ rejected instead of being silently truncated or misclassified.
77
+ - Network endpoint and surface-reference CLI arguments are validated before a
78
+ stream starts, and network sources are closed deterministically on normal
79
+ exit, errors, and interruption.
80
+ - The live terminal UI synchronizes access to shared aircraft state, preventing
81
+ concurrent decoder and renderer updates from racing.
82
+
8
83
  ## [3.4.0] — 2026-07-13
9
84
 
10
85
  ### Fixed
@@ -38,8 +113,8 @@ Comm-B reply to the wrong aircraft.
38
113
  ### Added
39
114
 
40
115
  - `PipeDecoder` bootstrap cluster analysis for the first position per
41
- ICAO: lat/lon emission is held until `_BOOTSTRAP_K` (=5) candidate
42
- positions agree under a motion-consistency check. Prevents an
116
+ ICAO: lat/lon emission is held until five candidate positions agree
117
+ under a motion-consistency check. Prevents an
43
118
  initial phantom frame from anchoring the rolling position history.
44
119
  On each decode the held result dicts are retro-filled once the
45
120
  cluster locks. Scattered buffers reset and accumulate a fresh K.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyModeS
3
- Version: 3.4.0
3
+ Version: 3.5.1
4
4
  Summary: Python Mode-S and ADS-B Decoder
5
5
  Project-URL: homepage, https://mode-s.org
6
6
  Project-URL: repository, https://github.com/junzis/pyModeS
@@ -109,7 +109,7 @@ print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...
109
109
  `PipeDecoder` is stateful — it holds per-ICAO state across calls,
110
110
  matches CPR pairs automatically, evicts stale aircraft after a
111
111
  TTL, and flags DF20/21 messages as `icao_verified` when their
112
- CRC-derived ICAO was already seen in a clean DF17/18 plaintext.
112
+ CRC-derived ICAO was seen recently in clean DF17/18 plaintext.
113
113
 
114
114
  ```python
115
115
  from pyModeS import PipeDecoder
@@ -117,7 +117,7 @@ from pyModeS import PipeDecoder
117
117
  pipe = PipeDecoder(surface_ref="EHAM")
118
118
  for msg, timestamp in stream:
119
119
  decoded = pipe.decode(msg, timestamp=timestamp)
120
- if "latitude" in decoded:
120
+ if decoded.get("latitude") is not None:
121
121
  print(decoded["icao"], decoded["latitude"], decoded["longitude"])
122
122
  ```
123
123
 
@@ -194,36 +194,16 @@ the full command reference.
194
194
  - Unified `decode()` returns every decodable field in one dict
195
195
  - Batch mode preserves list length (errors become error-dicts, not
196
196
  exceptions)
197
- - `PipeDecoder` for streams: per-ICAO state, CPR pair accumulation,
198
- TTL eviction, DF20/21 ICAO verification via trusted-set promotion
197
+ - `PipeDecoder` for streams: per-ICAO state, global and local CPR position
198
+ decoding, TTL eviction, DF20/21 ICAO verification via trusted-set promotion
199
199
  - `full_dict=True` populates every key in the canonical 123-field
200
200
  schema for pandas / parquet workflows
201
201
  - `known=` aircraft state disambiguates Comm-B BDS 5,0/6,0 ambiguity
202
+ - `include_meteo=True` opts into heuristic Comm-B BDS 4,4/4,5 inference
202
203
  - Airport ICAO database for surface CPR resolution (`surface_ref="EHAM"`)
203
204
  - Type-checked under mypy strict across all source files
204
205
  - Golden-file oracle regression test against `pyModeS 2.21.1`
205
206
 
206
- ## Performance
207
-
208
- The committed benchmarks run each released/working-tree version in an
209
- isolated environment and reject output mismatches before reporting timings.
210
- On the captured 176,612-frame mixed Beast feed, the header-prefiltered v3 path
211
- processes 85,963 incoming messages/s:
212
-
213
- | Decoder path | Throughput |
214
- |---|---:|
215
- | pyModeS 2.21.1 selective helpers | 52,998 msg/s |
216
- | released v3.3.0 `PipeDecoder` | 3,897 msg/s |
217
- | v3.4.0 `PipeDecoder` | 49,243 msg/s |
218
- | v3.4.0 + header prefilter | **85,963 msg/s** |
219
-
220
- On the 2,000-aircraft synthetic PipeDecoder workload, v3.4.0 processes
221
- 38,838 msg/s versus v2's 39,604 msg/s, with identical normalized output.
222
-
223
- See [`scripts/README.md`](./scripts/README.md) for the reproducible commands
224
- and [`scripts/benchmark_results/`](./scripts/benchmark_results/) for the full
225
- methodology, timings, and output digests.
226
-
227
207
  ## Supported messages
228
208
 
229
209
  - **DF4 / DF20:** altitude code (surveillance altitude reply)
@@ -249,6 +229,10 @@ methodology, timings, and output digests.
249
229
  - BDS 5,0: track and turn report
250
230
  - BDS 6,0: heading and speed report
251
231
 
232
+ BDS 4,4 and 4,5 are heuristic and disabled by default. Enable them with
233
+ `decode(..., include_meteo=True)` or `PipeDecoder(include_meteo=True)` when
234
+ these meteorological registers are expected on the input feed.
235
+
252
236
  ## Migrating from pyModeS 2.x
253
237
 
254
238
  pyModeS 3 is **not backwards-compatible** with pyModeS 2.x. The
@@ -82,7 +82,7 @@ print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...
82
82
  `PipeDecoder` is stateful — it holds per-ICAO state across calls,
83
83
  matches CPR pairs automatically, evicts stale aircraft after a
84
84
  TTL, and flags DF20/21 messages as `icao_verified` when their
85
- CRC-derived ICAO was already seen in a clean DF17/18 plaintext.
85
+ CRC-derived ICAO was seen recently in clean DF17/18 plaintext.
86
86
 
87
87
  ```python
88
88
  from pyModeS import PipeDecoder
@@ -90,7 +90,7 @@ from pyModeS import PipeDecoder
90
90
  pipe = PipeDecoder(surface_ref="EHAM")
91
91
  for msg, timestamp in stream:
92
92
  decoded = pipe.decode(msg, timestamp=timestamp)
93
- if "latitude" in decoded:
93
+ if decoded.get("latitude") is not None:
94
94
  print(decoded["icao"], decoded["latitude"], decoded["longitude"])
95
95
  ```
96
96
 
@@ -167,36 +167,16 @@ the full command reference.
167
167
  - Unified `decode()` returns every decodable field in one dict
168
168
  - Batch mode preserves list length (errors become error-dicts, not
169
169
  exceptions)
170
- - `PipeDecoder` for streams: per-ICAO state, CPR pair accumulation,
171
- TTL eviction, DF20/21 ICAO verification via trusted-set promotion
170
+ - `PipeDecoder` for streams: per-ICAO state, global and local CPR position
171
+ decoding, TTL eviction, DF20/21 ICAO verification via trusted-set promotion
172
172
  - `full_dict=True` populates every key in the canonical 123-field
173
173
  schema for pandas / parquet workflows
174
174
  - `known=` aircraft state disambiguates Comm-B BDS 5,0/6,0 ambiguity
175
+ - `include_meteo=True` opts into heuristic Comm-B BDS 4,4/4,5 inference
175
176
  - Airport ICAO database for surface CPR resolution (`surface_ref="EHAM"`)
176
177
  - Type-checked under mypy strict across all source files
177
178
  - Golden-file oracle regression test against `pyModeS 2.21.1`
178
179
 
179
- ## Performance
180
-
181
- The committed benchmarks run each released/working-tree version in an
182
- isolated environment and reject output mismatches before reporting timings.
183
- On the captured 176,612-frame mixed Beast feed, the header-prefiltered v3 path
184
- processes 85,963 incoming messages/s:
185
-
186
- | Decoder path | Throughput |
187
- |---|---:|
188
- | pyModeS 2.21.1 selective helpers | 52,998 msg/s |
189
- | released v3.3.0 `PipeDecoder` | 3,897 msg/s |
190
- | v3.4.0 `PipeDecoder` | 49,243 msg/s |
191
- | v3.4.0 + header prefilter | **85,963 msg/s** |
192
-
193
- On the 2,000-aircraft synthetic PipeDecoder workload, v3.4.0 processes
194
- 38,838 msg/s versus v2's 39,604 msg/s, with identical normalized output.
195
-
196
- See [`scripts/README.md`](./scripts/README.md) for the reproducible commands
197
- and [`scripts/benchmark_results/`](./scripts/benchmark_results/) for the full
198
- methodology, timings, and output digests.
199
-
200
180
  ## Supported messages
201
181
 
202
182
  - **DF4 / DF20:** altitude code (surveillance altitude reply)
@@ -222,6 +202,10 @@ methodology, timings, and output digests.
222
202
  - BDS 5,0: track and turn report
223
203
  - BDS 6,0: heading and speed report
224
204
 
205
+ BDS 4,4 and 4,5 are heuristic and disabled by default. Enable them with
206
+ `decode(..., include_meteo=True)` or `PipeDecoder(include_meteo=True)` when
207
+ these meteorological registers are expected on the input feed.
208
+
225
209
  ## Migrating from pyModeS 2.x
226
210
 
227
211
  pyModeS 3 is **not backwards-compatible** with pyModeS 2.x. The
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyModeS"
3
- version = "3.4.0"
3
+ version = "3.5.1"
4
4
  description = "Python Mode-S and ADS-B Decoder"
5
5
  authors = [{ name = "Junzi Sun", email = "git@junzis.com" }]
6
6
  license = { text = "GNU GPL v3" }
@@ -13,13 +13,21 @@ through their names; generated benchmark reports live in
13
13
 
14
14
  ```bash
15
15
  uv run python scripts/stream_filtered.py \
16
- --network airsquitter.lr.tudelft.nl:10006 > decoded.jsonl
16
+ --network airsquitter.lr.tudelft.nl:10006 \
17
+ --include-meteo > decoded.jsonl
17
18
  ```
18
19
 
20
+ `--include-meteo` is optional and enables heuristic Comm-B BDS 4,4/4,5
21
+ reports when the feed is expected to contain meteorological data.
22
+
19
23
  ## Benchmarks
20
24
 
21
- - `benchmark_mixed_traffic.py` — isolated v2, released-v3, updated-v3, and
22
- prefiltered-v3 comparison on mixed captured traffic. Writes
25
+ - `benchmark_mixed_traffic.py` — isolated v2.21.1, released v3.5.0, v3.5.1,
26
+ and prefiltered v3.5.1 comparison on mixed captured traffic. It measures
27
+ throughput, immediate streaming position yield, and coordinate agreement.
28
+ By default it fails below 80% of v2 position yield, below 99% agreement
29
+ within 100 m, or when v3.5.1 yields fewer positions than v3.5.0. The two
30
+ percentage floors are configurable through command-line options. Writes
23
31
  `benchmark_results/mixed_traffic.md` by default.
24
32
  - `benchmark_pipe.py` — synthetic high-cardinality PipeDecoder comparison.
25
33
  Writes `benchmark_results/pipe.md` by default.
@@ -54,5 +62,6 @@ access unless those packages are already cached.
54
62
  environment and checks the public API.
55
63
  - `smoke_test_cli.sh` — checks CLI decoding and a short live Beast connection.
56
64
 
57
- The CLI smoke test requires network access. The wheel smoke test uses the most
58
- recent `dist/pymodes-*.whl` when no path is supplied.
65
+ The CLI smoke test requires network access. The wheel smoke test accepts an
66
+ explicit wheel path; without one, it builds a fresh working-tree wheel in a
67
+ temporary directory so stale files under `dist/` cannot be selected.
@@ -18,9 +18,13 @@ and are NOT interchangeable:
18
18
  45 NM of the true position — tighter than the airborne tolerance.
19
19
 
20
20
  For streaming workflows where references aren't known ahead of time,
21
- use :class:`PipeDecoder` which resolves airborne positions from
22
- even/odd CPR pairs without any reference and still accepts
23
- ``surface_ref`` for surface messages.
21
+ use :class:`PipeDecoder`. It establishes airborne positions from even/odd CPR
22
+ pairs, then resolves later frames locally from the last validated airborne
23
+ position. The optional ``surface_ref`` applies only to surface messages.
24
+
25
+ Comm-B BDS 4,4/4,5 meteorological inference is heuristic and disabled by
26
+ default. Pass ``include_meteo=True`` to :func:`decode` or
27
+ :class:`PipeDecoder` when these reports are expected on the input feed.
24
28
  """
25
29
 
26
30
  from importlib.metadata import version as _version