pyModeS 3.4.0__tar.gz → 3.5.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.
Files changed (64) hide show
  1. {pymodes-3.4.0 → pymodes-3.5.0}/CHANGELOG.md +65 -2
  2. {pymodes-3.4.0 → pymodes-3.5.0}/PKG-INFO +5 -26
  3. {pymodes-3.4.0 → pymodes-3.5.0}/README.md +4 -25
  4. {pymodes-3.4.0 → pymodes-3.5.0}/pyproject.toml +1 -1
  5. {pymodes-3.4.0 → pymodes-3.5.0}/scripts/README.md +9 -4
  6. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/__init__.py +3 -3
  7. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_pipe.py +360 -465
  8. pymodes-3.5.0/src/pyModeS/_pipe_validation.py +234 -0
  9. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/_args.py +11 -0
  10. pymodes-3.5.0/src/pyModeS/cli/_parse.py +51 -0
  11. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/_source.py +142 -72
  12. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/_tui.py +45 -42
  13. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/decode.py +15 -26
  14. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/live.py +18 -39
  15. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/core.py +4 -2
  16. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/_infer.py +6 -1
  17. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/message.py +18 -6
  18. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/position/_cpr.py +16 -6
  19. {pymodes-3.4.0 → pymodes-3.5.0}/.gitignore +0 -0
  20. {pymodes-3.4.0 → pymodes-3.5.0}/LICENSE +0 -0
  21. {pymodes-3.4.0 → pymodes-3.5.0}/scripts/data/README.md +0 -0
  22. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_aero.py +0 -0
  23. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_altcode.py +0 -0
  24. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_bits.py +0 -0
  25. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_callsign.py +0 -0
  26. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_idcode.py +0 -0
  27. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_schema.py +0 -0
  28. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_uncertainty.py +0 -0
  29. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/_v2_removed.py +0 -0
  30. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/__init__.py +0 -0
  31. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/__main__.py +0 -0
  32. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/cli/_sink.py +0 -0
  33. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/data/__init__.py +0 -0
  34. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/data/airports.py +0 -0
  35. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/__init__.py +0 -0
  36. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/_base.py +0 -0
  37. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/acas.py +0 -0
  38. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/adsb.py +0 -0
  39. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/allcall.py +0 -0
  40. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/__init__.py +0 -0
  41. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/_helpers.py +0 -0
  42. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds05.py +0 -0
  43. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds06.py +0 -0
  44. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds08.py +0 -0
  45. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds09.py +0 -0
  46. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds10.py +0 -0
  47. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds17.py +0 -0
  48. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds20.py +0 -0
  49. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds30.py +0 -0
  50. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds40.py +0 -0
  51. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds44.py +0 -0
  52. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds45.py +0 -0
  53. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds50.py +0 -0
  54. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds60.py +0 -0
  55. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds61.py +0 -0
  56. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds62.py +0 -0
  57. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/bds/bds65.py +0 -0
  58. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/commb.py +0 -0
  59. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/decoder/surv.py +0 -0
  60. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/errors.py +0 -0
  61. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/position/__init__.py +0 -0
  62. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/position/_airports.py +0 -0
  63. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/py.typed +0 -0
  64. {pymodes-3.4.0 → pymodes-3.5.0}/src/pyModeS/util.py +0 -0
@@ -5,6 +5,69 @@ 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.0] — 2026-07-15
9
+
10
+ ### Added
11
+
12
+ - `PipeDecoder` now uses the last validated airborne position as a local CPR
13
+ reference for subsequent BDS 0,5 frames. This makes a position available on
14
+ the current streaming `decode()` call when no fresh global even/odd pair is
15
+ available, instead of relying on retroactive mutation of an earlier result.
16
+ - `local_ref_window` configures the maximum reference age (30 seconds by
17
+ default); set it to zero to disable local airborne decoding.
18
+ `PipeDecoder.stats["local_positions"]` reports positions produced by this
19
+ path.
20
+ - Position bootstrap now locks as soon as three candidates are pairwise
21
+ motion-consistent instead of always waiting for all five candidate slots.
22
+ The five-candidate limit still allows two unrelated outliers to be ignored.
23
+ - The mixed-traffic benchmark now reports immediate streaming position yield
24
+ and frame-by-frame coordinate agreement for v2.21.1, released v3.4.0, and
25
+ v3.5.0 in addition to throughput.
26
+
27
+ ### Changed
28
+
29
+ - `crc_valid` is now `None` when an address-parity message cannot be checked
30
+ independently. It remains `True` or `False` for frames whose parity can be
31
+ validated without assuming the address derived from that same parity.
32
+ - `PipeDecoder` uses each message's source timestamp for state validity while
33
+ retaining a monotonic high-water mark for eviction. Reordered capture data
34
+ therefore cannot revive expired observations or prematurely evict newer
35
+ state.
36
+ - Plausibility and position validation are isolated in a dedicated internal
37
+ module, keeping the streaming pipeline and its decision comments easier to
38
+ follow without changing the public API.
39
+ - Release publishing now runs only after the full test matrix succeeds, checks
40
+ that the Git tag matches the package version, and smoke-tests the exact wheel
41
+ artifact that will be uploaded.
42
+
43
+ ### Fixed
44
+
45
+ - Surface references remain optional and are used only for BDS 0,6 surface
46
+ positions. Positions derived from `surface_ref` can never seed local
47
+ airborne CPR decoding.
48
+ - Rejected global position candidates cannot become local CPR references; a
49
+ separate last-accepted airborne reference is maintained and TTL-evicted.
50
+ - Bootstrap promotion, including `flush()`, initializes the latest accepted
51
+ airborne reference while keeping surface-derived positions separate.
52
+ - Globally decoded surface CPR positions now select the correct longitude zone
53
+ across the antimeridian and other CPR zone boundaries.
54
+ - Beast stream input resynchronizes after malformed or truncated frames instead
55
+ of losing subsequent valid messages.
56
+ - Beast receiver timestamps are back-projected from the current wall clock with
57
+ rollover-safe receiver-tick deltas, avoiding dates near the Unix epoch and
58
+ preserving correct timing across the 12 MHz counter rollover.
59
+ - BDS inference compares headings using circular angular distance, so values
60
+ around 0/360 degrees are scored correctly.
61
+ - Trusted ICAO observations expire according to the configured state TTL and
62
+ cannot be refreshed by older, reordered messages.
63
+ - Integer messages outside the valid 56-bit and 112-bit Mode S frame ranges are
64
+ rejected instead of being silently truncated or misclassified.
65
+ - Network endpoint and surface-reference CLI arguments are validated before a
66
+ stream starts, and network sources are closed deterministically on normal
67
+ exit, errors, and interruption.
68
+ - The live terminal UI synchronizes access to shared aircraft state, preventing
69
+ concurrent decoder and renderer updates from racing.
70
+
8
71
  ## [3.4.0] — 2026-07-13
9
72
 
10
73
  ### Fixed
@@ -38,8 +101,8 @@ Comm-B reply to the wrong aircraft.
38
101
  ### Added
39
102
 
40
103
  - `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
104
+ ICAO: lat/lon emission is held until five candidate positions agree
105
+ under a motion-consistency check. Prevents an
43
106
  initial phantom frame from anchoring the rolling position history.
44
107
  On each decode the held result dicts are retro-filled once the
45
108
  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.0
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,8 +194,8 @@ 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
@@ -203,27 +203,6 @@ the full command reference.
203
203
  - Type-checked under mypy strict across all source files
204
204
  - Golden-file oracle regression test against `pyModeS 2.21.1`
205
205
 
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
206
  ## Supported messages
228
207
 
229
208
  - **DF4 / DF20:** altitude code (surveillance altitude reply)
@@ -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,8 +167,8 @@ 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
@@ -176,27 +176,6 @@ the full command reference.
176
176
  - Type-checked under mypy strict across all source files
177
177
  - Golden-file oracle regression test against `pyModeS 2.21.1`
178
178
 
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
179
  ## Supported messages
201
180
 
202
181
  - **DF4 / DF20:** altitude code (surveillance altitude reply)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyModeS"
3
- version = "3.4.0"
3
+ version = "3.5.0"
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" }
@@ -18,8 +18,12 @@ through their names; generated benchmark reports live in
18
18
 
19
19
  ## Benchmarks
20
20
 
21
- - `benchmark_mixed_traffic.py` — isolated v2, released-v3, updated-v3, and
22
- prefiltered-v3 comparison on mixed captured traffic. Writes
21
+ - `benchmark_mixed_traffic.py` — isolated v2.21.1, released v3.4.0, v3.5.0,
22
+ and prefiltered v3.5.0 comparison on mixed captured traffic. It measures
23
+ throughput, immediate streaming position yield, and coordinate agreement.
24
+ By default it fails below 80% of v2 position yield, below 99% agreement
25
+ within 100 m, or when v3.5.0 yields fewer positions than v3.4.0. The two
26
+ percentage floors are configurable through command-line options. Writes
23
27
  `benchmark_results/mixed_traffic.md` by default.
24
28
  - `benchmark_pipe.py` — synthetic high-cardinality PipeDecoder comparison.
25
29
  Writes `benchmark_results/pipe.md` by default.
@@ -54,5 +58,6 @@ access unless those packages are already cached.
54
58
  environment and checks the public API.
55
59
  - `smoke_test_cli.sh` — checks CLI decoding and a short live Beast connection.
56
60
 
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.
61
+ The CLI smoke test requires network access. The wheel smoke test accepts an
62
+ explicit wheel path; without one, it builds a fresh working-tree wheel in a
63
+ temporary directory so stale files under `dist/` cannot be selected.
@@ -18,9 +18,9 @@ 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
24
  """
25
25
 
26
26
  from importlib.metadata import version as _version