aissegments 0.2.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.
- aissegments-0.2.0/.gitignore +41 -0
- aissegments-0.2.0/CITATION.cff +32 -0
- aissegments-0.2.0/LICENSE +21 -0
- aissegments-0.2.0/PKG-INFO +156 -0
- aissegments-0.2.0/README.md +121 -0
- aissegments-0.2.0/pyproject.toml +102 -0
- aissegments-0.2.0/src/aissegments/__init__.py +37 -0
- aissegments-0.2.0/src/aissegments/_types.py +153 -0
- aissegments-0.2.0/src/aissegments/adapters.py +354 -0
- aissegments-0.2.0/src/aissegments/py.typed +0 -0
- aissegments-0.2.0/src/aissegments/tdkc.py +292 -0
- aissegments-0.2.0/tests/conftest.py +166 -0
- aissegments-0.2.0/tests/data/README.md +71 -0
- aissegments-0.2.0/tests/test_adapters.py +362 -0
- aissegments-0.2.0/tests/test_aisdb_integration.py +125 -0
- aissegments-0.2.0/tests/test_local_data.py +76 -0
- aissegments-0.2.0/tests/test_tdkc.py +319 -0
- aissegments-0.2.0/tests/test_types.py +151 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Python build artefacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.eggs/
|
|
9
|
+
*.so
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
.coverage.*
|
|
13
|
+
htmlcov/
|
|
14
|
+
coverage.xml
|
|
15
|
+
.tox/
|
|
16
|
+
.nox/
|
|
17
|
+
.qgis-settings/
|
|
18
|
+
|
|
19
|
+
# Editors / IDEs
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Virtualenvs
|
|
30
|
+
.venv/
|
|
31
|
+
venv/
|
|
32
|
+
env/
|
|
33
|
+
|
|
34
|
+
# Hatch
|
|
35
|
+
.hatch/
|
|
36
|
+
|
|
37
|
+
# Ruff cache
|
|
38
|
+
.ruff_cache/
|
|
39
|
+
|
|
40
|
+
# mypy
|
|
41
|
+
.mypy_cache/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "AISsegments: AIS trajectory segmentation and feature-preserving compression"
|
|
3
|
+
message: "If you use AISsegments in academic work, please cite both this software and the underlying TDKC algorithm paper."
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Hörteborn
|
|
7
|
+
given-names: Axel
|
|
8
|
+
email: axel.horteborn@ri.se
|
|
9
|
+
affiliation: "RISE Research Institutes of Sweden"
|
|
10
|
+
license: MIT
|
|
11
|
+
repository-code: "https://github.com/axelHorteborn/AISsegments"
|
|
12
|
+
keywords:
|
|
13
|
+
- AIS
|
|
14
|
+
- maritime
|
|
15
|
+
- trajectory
|
|
16
|
+
- compression
|
|
17
|
+
- TDKC
|
|
18
|
+
|
|
19
|
+
references:
|
|
20
|
+
- type: article
|
|
21
|
+
title: "An adaptive trajectory compression and feature preservation method for maritime traffic analysis"
|
|
22
|
+
authors:
|
|
23
|
+
- family-names: Guo
|
|
24
|
+
given-names: Shaoqing
|
|
25
|
+
- family-names: Bolbot
|
|
26
|
+
given-names: Victor
|
|
27
|
+
- family-names: Valdez Banda
|
|
28
|
+
given-names: Osiris
|
|
29
|
+
journal: "Ocean Engineering"
|
|
30
|
+
volume: 312
|
|
31
|
+
year: 2024
|
|
32
|
+
doi: "10.1016/j.oceaneng.2024.119189"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axel Hörteborn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aissegments
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AIS trajectory segmentation and feature-preserving compression for maritime traffic analysis.
|
|
5
|
+
Project-URL: Homepage, https://github.com/axelHorteborn/AISsegments
|
|
6
|
+
Project-URL: Repository, https://github.com/axelHorteborn/AISsegments
|
|
7
|
+
Project-URL: Issues, https://github.com/axelHorteborn/AISsegments/issues
|
|
8
|
+
Author-email: Axel Hörteborn <axel.horteborn@ri.se>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: AIS,Douglas-Peucker,TDKC,compression,maritime,segmentation,trajectory
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Provides-Extra: aisdb
|
|
25
|
+
Requires-Dist: aisdb>=1.0; extra == 'aisdb'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: aisdb>=1.7; extra == 'dev'
|
|
28
|
+
Requires-Dist: matplotlib>=3.7; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
32
|
+
Provides-Extra: viz
|
|
33
|
+
Requires-Dist: matplotlib>=3.7; extra == 'viz'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# AISsegments
|
|
37
|
+
|
|
38
|
+
**A Python toolkit for compressing AIS vessel trajectories into compact, query-friendly linestring segments — without losing the kinematic features that maritime safety and risk analysis depend on.**
|
|
39
|
+
|
|
40
|
+
## What is this?
|
|
41
|
+
|
|
42
|
+
`aissegments` takes raw AIS position reports (millions of pings per day in a busy sea area) and outputs a small number of **constant-COG/SOG linestring segments per vessel**, one row per (vessel, time-window, course/speed). The output is shaped to drop straight into a PostGIS `LINESTRING` table.
|
|
43
|
+
|
|
44
|
+
It is the reference Python implementation of the **Top-Down Kinematic Compression (TDKC)** algorithm of Guo, Bolbot & Valdez Banda (Ocean Engineering 312 (2024), 119189), with the recursion-termination and adaptive-threshold fixes described in the paper.
|
|
45
|
+
|
|
46
|
+
## Goals
|
|
47
|
+
|
|
48
|
+
1. **Shrink AIS data without losing the maritime-relevant features.** A continent-scale AIS feed easily produces hundreds of millions of position reports per month. Most of those points are uninformative — a vessel cruising in a straight line at a steady speed needs only its endpoints. TDKC keeps the points where vessels actually do something interesting (turn, accelerate, stop, manoeuvre) and drops the rest.
|
|
49
|
+
2. **Treat AIS as a sequence of *kinematic states*, not just positions.** The classical Douglas-Peucker simplification looks only at how far points stray from a straight line. That throws away course and speed changes that lie on a straight track, which are exactly the events maritime risk analysis cares about. TDKC uses both position (Synchronous Euclidean Distance) **and** velocity (Synchronous Velocity Difference) with adaptive per-track thresholds.
|
|
50
|
+
3. **Produce database-ready output.** The output isn't a smaller list of points — it's a list of `Segment` records with start/end coordinates, mean COG/SOG, and the count of original observations spanned. Each segment is a 2-point `LINESTRING` ready for `ST_Intersects` and other PostGIS operations.
|
|
51
|
+
4. **Stay framework-neutral.** The core depends only on NumPy. AISdb is an optional adapter (`pip install "aissegments[aisdb]"`); other input paths (CSV from Marine Cadastre / institutional exports / your own pipeline) work via [`read_csv_tracks`](src/aissegments/adapters.py) without any extra dependencies.
|
|
52
|
+
|
|
53
|
+
## Who is this for?
|
|
54
|
+
|
|
55
|
+
- **Maritime risk analysts** who need to run collision/grounding/allision queries against millions of vessel positions per area-year and want the spatial+temporal index to fit in memory.
|
|
56
|
+
- **AIS data engineers** maintaining a Postgres/PostGIS warehouse of vessel tracks and looking for a principled way to densify ingestion without overwhelming storage.
|
|
57
|
+
- **Researchers** reproducing or extending Guo et al.'s adaptive trajectory compression work.
|
|
58
|
+
|
|
59
|
+
## What it produces
|
|
60
|
+
|
|
61
|
+
- `tdkc(track)` — same `Track` interface but with only the *key points* retained (typically 1-5% of the input, depending on track shape and threshold tuning).
|
|
62
|
+
- `tdkc_segments(track)` — a list of `Segment` records, one per consecutive key-point pair, ready for direct insertion as PostGIS `LINESTRING(start_lon start_lat, end_lon end_lat)` geometries with `cog_mean`, `sog_mean`, and `n_points` (count of original AIS pings each segment represents).
|
|
63
|
+
|
|
64
|
+
## Why not just use Douglas-Peucker?
|
|
65
|
+
|
|
66
|
+
DP and its variants throw away every point that lies on a straight line, regardless of whether the vessel's *behaviour* is changing. A vessel slowing from 15 to 5 knots while continuing to head east — DP keeps two points (start, end) and you lose the entire speed change. TDKC keeps the deceleration point because its **velocity vector** has shifted. See [`docs/algorithm.md`](docs/algorithm.md) for the precise math, and [`examples/output/03_min_svd_sweep.png`](examples/output/) for a visual side-by-side.
|
|
67
|
+
|
|
68
|
+
## Companion package
|
|
69
|
+
|
|
70
|
+
[OMRAT](https://github.com/axelande/OMRAT) (Open Maritime Risk Analysis Tool) — a QGIS plugin for collision/grounding/allision risk modelling — uses AISsegments as its segment-ingestion backend. The OMRAT pipeline shows a complete end-to-end flow: NMEA / CSV → aisdb decode → TDKC compression → bulk-load into a year-partitioned PostGIS schema.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install aissegments
|
|
76
|
+
|
|
77
|
+
# with the optional AISdb adapter for ingestion from raw NMEA / CSV
|
|
78
|
+
pip install "aissegments[aisdb]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For development with full test + coverage tooling:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/axelHorteborn/AISsegments
|
|
85
|
+
cd AISsegments
|
|
86
|
+
pip install -e ".[dev,aisdb]"
|
|
87
|
+
pytest --cov
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Quickstart
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import numpy as np
|
|
94
|
+
from aissegments import Track, tdkc, tdkc_segments
|
|
95
|
+
|
|
96
|
+
# Build a Track from your own arrays (lat/lon in degrees, sog in knots, cog in degrees).
|
|
97
|
+
track = Track.from_arrays(
|
|
98
|
+
mmsi=219000123,
|
|
99
|
+
t=np.array([0, 60, 120, 180, 240], dtype=float), # unix seconds
|
|
100
|
+
lon=np.array([12.0, 12.001, 12.002, 12.003, 12.004]),
|
|
101
|
+
lat=np.array([55.0, 55.0, 55.0, 55.0, 55.0]),
|
|
102
|
+
sog=np.array([10.0, 10.0, 10.0, 10.0, 10.0]),
|
|
103
|
+
cog=np.array([90.0, 90.0, 90.0, 90.0, 90.0]),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Compress: returns a Track containing only the key points.
|
|
107
|
+
compressed = tdkc(track)
|
|
108
|
+
print(len(compressed), "key points kept out of", len(track))
|
|
109
|
+
|
|
110
|
+
# Or go straight to segment records (one per consecutive key-point pair).
|
|
111
|
+
segments = tdkc_segments(track)
|
|
112
|
+
for s in segments:
|
|
113
|
+
print(s.t_start, s.t_end, s.cog_mean, s.sog_mean, s.n_points)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Using AISdb as an input adapter
|
|
117
|
+
|
|
118
|
+
`aissegments` can consume the per-vessel track dicts produced by [AISdb](https://github.com/AISViz/AISdb)'s `TrackGen()`:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import aisdb
|
|
122
|
+
from aissegments.adapters import from_aisdb_track
|
|
123
|
+
from aissegments import tdkc_segments
|
|
124
|
+
|
|
125
|
+
with aisdb.SQLiteDBConn(dbpath="ais.db") as conn:
|
|
126
|
+
qry = aisdb.DBQuery(start=..., end=..., callback=aisdb.sql_query_strs.in_bbox_time)
|
|
127
|
+
tracks = aisdb.TrackGen(qry.gen_qry(), decimate=False)
|
|
128
|
+
for t_dict in tracks:
|
|
129
|
+
track = from_aisdb_track(t_dict)
|
|
130
|
+
for seg in tdkc_segments(track):
|
|
131
|
+
... # write seg to your PostGIS table
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## What's in the package
|
|
135
|
+
|
|
136
|
+
| Module | Purpose |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| [`aissegments.tdkc`](src/aissegments/tdkc.py) | TDKC algorithm: SED + SVD, Compression Binary Tree, adaptive thresholds, key-node identification |
|
|
139
|
+
| [`aissegments._types`](src/aissegments/_types.py) | `Track` and `Segment` dataclasses, `to_segments` helper |
|
|
140
|
+
| [`aissegments.adapters`](src/aissegments/adapters.py) | Input adapters: `from_aisdb_track`, `read_csv_tracks` (Marine Cadastre etc.), `read_csv_static_records` for vessel-info extraction |
|
|
141
|
+
|
|
142
|
+
## Algorithm details
|
|
143
|
+
|
|
144
|
+
See [docs/algorithm.md](docs/algorithm.md) for the mathematical formulation, with equation references back to the source paper.
|
|
145
|
+
|
|
146
|
+
## Citation
|
|
147
|
+
|
|
148
|
+
If you use this package in academic work, please cite both the software and the underlying paper:
|
|
149
|
+
|
|
150
|
+
> Guo, S., Bolbot, V., & Valdez Banda, O. (2024). An adaptive trajectory compression and feature preservation method for maritime traffic analysis. *Ocean Engineering*, 312, 119189. https://doi.org/10.1016/j.oceaneng.2024.119189
|
|
151
|
+
|
|
152
|
+
A `CITATION.cff` is included so GitHub renders a "Cite this repository" widget.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# AISsegments
|
|
2
|
+
|
|
3
|
+
**A Python toolkit for compressing AIS vessel trajectories into compact, query-friendly linestring segments — without losing the kinematic features that maritime safety and risk analysis depend on.**
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
`aissegments` takes raw AIS position reports (millions of pings per day in a busy sea area) and outputs a small number of **constant-COG/SOG linestring segments per vessel**, one row per (vessel, time-window, course/speed). The output is shaped to drop straight into a PostGIS `LINESTRING` table.
|
|
8
|
+
|
|
9
|
+
It is the reference Python implementation of the **Top-Down Kinematic Compression (TDKC)** algorithm of Guo, Bolbot & Valdez Banda (Ocean Engineering 312 (2024), 119189), with the recursion-termination and adaptive-threshold fixes described in the paper.
|
|
10
|
+
|
|
11
|
+
## Goals
|
|
12
|
+
|
|
13
|
+
1. **Shrink AIS data without losing the maritime-relevant features.** A continent-scale AIS feed easily produces hundreds of millions of position reports per month. Most of those points are uninformative — a vessel cruising in a straight line at a steady speed needs only its endpoints. TDKC keeps the points where vessels actually do something interesting (turn, accelerate, stop, manoeuvre) and drops the rest.
|
|
14
|
+
2. **Treat AIS as a sequence of *kinematic states*, not just positions.** The classical Douglas-Peucker simplification looks only at how far points stray from a straight line. That throws away course and speed changes that lie on a straight track, which are exactly the events maritime risk analysis cares about. TDKC uses both position (Synchronous Euclidean Distance) **and** velocity (Synchronous Velocity Difference) with adaptive per-track thresholds.
|
|
15
|
+
3. **Produce database-ready output.** The output isn't a smaller list of points — it's a list of `Segment` records with start/end coordinates, mean COG/SOG, and the count of original observations spanned. Each segment is a 2-point `LINESTRING` ready for `ST_Intersects` and other PostGIS operations.
|
|
16
|
+
4. **Stay framework-neutral.** The core depends only on NumPy. AISdb is an optional adapter (`pip install "aissegments[aisdb]"`); other input paths (CSV from Marine Cadastre / institutional exports / your own pipeline) work via [`read_csv_tracks`](src/aissegments/adapters.py) without any extra dependencies.
|
|
17
|
+
|
|
18
|
+
## Who is this for?
|
|
19
|
+
|
|
20
|
+
- **Maritime risk analysts** who need to run collision/grounding/allision queries against millions of vessel positions per area-year and want the spatial+temporal index to fit in memory.
|
|
21
|
+
- **AIS data engineers** maintaining a Postgres/PostGIS warehouse of vessel tracks and looking for a principled way to densify ingestion without overwhelming storage.
|
|
22
|
+
- **Researchers** reproducing or extending Guo et al.'s adaptive trajectory compression work.
|
|
23
|
+
|
|
24
|
+
## What it produces
|
|
25
|
+
|
|
26
|
+
- `tdkc(track)` — same `Track` interface but with only the *key points* retained (typically 1-5% of the input, depending on track shape and threshold tuning).
|
|
27
|
+
- `tdkc_segments(track)` — a list of `Segment` records, one per consecutive key-point pair, ready for direct insertion as PostGIS `LINESTRING(start_lon start_lat, end_lon end_lat)` geometries with `cog_mean`, `sog_mean`, and `n_points` (count of original AIS pings each segment represents).
|
|
28
|
+
|
|
29
|
+
## Why not just use Douglas-Peucker?
|
|
30
|
+
|
|
31
|
+
DP and its variants throw away every point that lies on a straight line, regardless of whether the vessel's *behaviour* is changing. A vessel slowing from 15 to 5 knots while continuing to head east — DP keeps two points (start, end) and you lose the entire speed change. TDKC keeps the deceleration point because its **velocity vector** has shifted. See [`docs/algorithm.md`](docs/algorithm.md) for the precise math, and [`examples/output/03_min_svd_sweep.png`](examples/output/) for a visual side-by-side.
|
|
32
|
+
|
|
33
|
+
## Companion package
|
|
34
|
+
|
|
35
|
+
[OMRAT](https://github.com/axelande/OMRAT) (Open Maritime Risk Analysis Tool) — a QGIS plugin for collision/grounding/allision risk modelling — uses AISsegments as its segment-ingestion backend. The OMRAT pipeline shows a complete end-to-end flow: NMEA / CSV → aisdb decode → TDKC compression → bulk-load into a year-partitioned PostGIS schema.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install aissegments
|
|
41
|
+
|
|
42
|
+
# with the optional AISdb adapter for ingestion from raw NMEA / CSV
|
|
43
|
+
pip install "aissegments[aisdb]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For development with full test + coverage tooling:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/axelHorteborn/AISsegments
|
|
50
|
+
cd AISsegments
|
|
51
|
+
pip install -e ".[dev,aisdb]"
|
|
52
|
+
pytest --cov
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np
|
|
59
|
+
from aissegments import Track, tdkc, tdkc_segments
|
|
60
|
+
|
|
61
|
+
# Build a Track from your own arrays (lat/lon in degrees, sog in knots, cog in degrees).
|
|
62
|
+
track = Track.from_arrays(
|
|
63
|
+
mmsi=219000123,
|
|
64
|
+
t=np.array([0, 60, 120, 180, 240], dtype=float), # unix seconds
|
|
65
|
+
lon=np.array([12.0, 12.001, 12.002, 12.003, 12.004]),
|
|
66
|
+
lat=np.array([55.0, 55.0, 55.0, 55.0, 55.0]),
|
|
67
|
+
sog=np.array([10.0, 10.0, 10.0, 10.0, 10.0]),
|
|
68
|
+
cog=np.array([90.0, 90.0, 90.0, 90.0, 90.0]),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Compress: returns a Track containing only the key points.
|
|
72
|
+
compressed = tdkc(track)
|
|
73
|
+
print(len(compressed), "key points kept out of", len(track))
|
|
74
|
+
|
|
75
|
+
# Or go straight to segment records (one per consecutive key-point pair).
|
|
76
|
+
segments = tdkc_segments(track)
|
|
77
|
+
for s in segments:
|
|
78
|
+
print(s.t_start, s.t_end, s.cog_mean, s.sog_mean, s.n_points)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Using AISdb as an input adapter
|
|
82
|
+
|
|
83
|
+
`aissegments` can consume the per-vessel track dicts produced by [AISdb](https://github.com/AISViz/AISdb)'s `TrackGen()`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import aisdb
|
|
87
|
+
from aissegments.adapters import from_aisdb_track
|
|
88
|
+
from aissegments import tdkc_segments
|
|
89
|
+
|
|
90
|
+
with aisdb.SQLiteDBConn(dbpath="ais.db") as conn:
|
|
91
|
+
qry = aisdb.DBQuery(start=..., end=..., callback=aisdb.sql_query_strs.in_bbox_time)
|
|
92
|
+
tracks = aisdb.TrackGen(qry.gen_qry(), decimate=False)
|
|
93
|
+
for t_dict in tracks:
|
|
94
|
+
track = from_aisdb_track(t_dict)
|
|
95
|
+
for seg in tdkc_segments(track):
|
|
96
|
+
... # write seg to your PostGIS table
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What's in the package
|
|
100
|
+
|
|
101
|
+
| Module | Purpose |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| [`aissegments.tdkc`](src/aissegments/tdkc.py) | TDKC algorithm: SED + SVD, Compression Binary Tree, adaptive thresholds, key-node identification |
|
|
104
|
+
| [`aissegments._types`](src/aissegments/_types.py) | `Track` and `Segment` dataclasses, `to_segments` helper |
|
|
105
|
+
| [`aissegments.adapters`](src/aissegments/adapters.py) | Input adapters: `from_aisdb_track`, `read_csv_tracks` (Marine Cadastre etc.), `read_csv_static_records` for vessel-info extraction |
|
|
106
|
+
|
|
107
|
+
## Algorithm details
|
|
108
|
+
|
|
109
|
+
See [docs/algorithm.md](docs/algorithm.md) for the mathematical formulation, with equation references back to the source paper.
|
|
110
|
+
|
|
111
|
+
## Citation
|
|
112
|
+
|
|
113
|
+
If you use this package in academic work, please cite both the software and the underlying paper:
|
|
114
|
+
|
|
115
|
+
> Guo, S., Bolbot, V., & Valdez Banda, O. (2024). An adaptive trajectory compression and feature preservation method for maritime traffic analysis. *Ocean Engineering*, 312, 119189. https://doi.org/10.1016/j.oceaneng.2024.119189
|
|
116
|
+
|
|
117
|
+
A `CITATION.cff` is included so GitHub renders a "Cite this repository" widget.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aissegments"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "AIS trajectory segmentation and feature-preserving compression for maritime traffic analysis."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Axel Hörteborn", email = "axel.horteborn@ri.se" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.24",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["AIS", "maritime", "trajectory", "compression", "TDKC", "Douglas-Peucker", "segmentation"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Scientific/Engineering",
|
|
29
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
aisdb = ["aisdb>=1.0"]
|
|
34
|
+
viz = ["matplotlib>=3.7"]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7",
|
|
37
|
+
"pytest-cov>=4",
|
|
38
|
+
"ruff>=0.5",
|
|
39
|
+
"matplotlib>=3.7",
|
|
40
|
+
"aisdb>=1.7",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/axelHorteborn/AISsegments"
|
|
45
|
+
Repository = "https://github.com/axelHorteborn/AISsegments"
|
|
46
|
+
Issues = "https://github.com/axelHorteborn/AISsegments/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/aissegments"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = [
|
|
53
|
+
"src/aissegments",
|
|
54
|
+
"tests",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE",
|
|
57
|
+
"CITATION.cff",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
]
|
|
60
|
+
# Local AIS data files dropped under tests/data/ are kept out of the
|
|
61
|
+
# distribution — they can be large (tens of MB) and may have third-party
|
|
62
|
+
# attribution requirements that are easier to manage out-of-band. The
|
|
63
|
+
# directory's README.md is small and documents the format, so it stays in.
|
|
64
|
+
exclude = [
|
|
65
|
+
"tests/data/*.csv",
|
|
66
|
+
"tests/data/*.csv.gz",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
addopts = [
|
|
72
|
+
"--strict-markers",
|
|
73
|
+
"--strict-config",
|
|
74
|
+
"-ra",
|
|
75
|
+
]
|
|
76
|
+
markers = [
|
|
77
|
+
"aisdb: integration tests that require the optional aisdb package and its bundled test data",
|
|
78
|
+
"local_csv: integration tests that need a CSV file dropped under tests/data/",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.coverage.run]
|
|
82
|
+
source = ["aissegments"]
|
|
83
|
+
branch = true
|
|
84
|
+
|
|
85
|
+
[tool.coverage.report]
|
|
86
|
+
fail_under = 100
|
|
87
|
+
exclude_also = [
|
|
88
|
+
"if TYPE_CHECKING:",
|
|
89
|
+
"raise NotImplementedError",
|
|
90
|
+
"@overload",
|
|
91
|
+
"pragma: no cover",
|
|
92
|
+
]
|
|
93
|
+
show_missing = true
|
|
94
|
+
skip_covered = false
|
|
95
|
+
|
|
96
|
+
[tool.ruff]
|
|
97
|
+
line-length = 100
|
|
98
|
+
target-version = "py310"
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF"]
|
|
102
|
+
ignore = ["E501"] # line-length handled by formatter
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""AIS trajectory segmentation and feature-preserving compression.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
- :class:`Track` / :class:`Segment` — typed input/output containers.
|
|
6
|
+
- :func:`to_segments` — pair consecutive points of a Track into Segment records.
|
|
7
|
+
- :func:`tdkc` — Top-Down Kinematic Compression; returns a compressed Track.
|
|
8
|
+
- :func:`tdkc_segments` — convenience wrapper: TDKC plus segment construction
|
|
9
|
+
with original-point-count enrichment.
|
|
10
|
+
|
|
11
|
+
Reference
|
|
12
|
+
---------
|
|
13
|
+
Guo, S., Bolbot, V., & Valdez Banda, O. (2024). An adaptive trajectory
|
|
14
|
+
compression and feature preservation method for maritime traffic analysis.
|
|
15
|
+
*Ocean Engineering*, 312, 119189.
|
|
16
|
+
"""
|
|
17
|
+
from aissegments._types import Segment, Track, to_segments
|
|
18
|
+
from aissegments.adapters import (
|
|
19
|
+
from_aisdb_track,
|
|
20
|
+
read_csv_static_records,
|
|
21
|
+
read_csv_tracks,
|
|
22
|
+
)
|
|
23
|
+
from aissegments.tdkc import tdkc, tdkc_segments
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Segment",
|
|
29
|
+
"Track",
|
|
30
|
+
"__version__",
|
|
31
|
+
"from_aisdb_track",
|
|
32
|
+
"read_csv_static_records",
|
|
33
|
+
"read_csv_tracks",
|
|
34
|
+
"tdkc",
|
|
35
|
+
"tdkc_segments",
|
|
36
|
+
"to_segments",
|
|
37
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Typed containers for AIS tracks and the linestring-segment records they yield."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
_FLOAT_FIELDS = ("t", "lon", "lat", "sog", "cog")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Track:
|
|
14
|
+
"""An ordered AIS track for a single vessel.
|
|
15
|
+
|
|
16
|
+
All array fields must be the same length and ``t`` must be
|
|
17
|
+
monotonically non-decreasing. Construct via :meth:`from_arrays` to get
|
|
18
|
+
consistent ``float64`` dtypes; the constructor itself only validates.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
mmsi : int
|
|
23
|
+
Maritime Mobile Service Identity for the vessel.
|
|
24
|
+
t : numpy.ndarray
|
|
25
|
+
Unix timestamps in seconds since epoch (UTC). Shape ``(N,)``.
|
|
26
|
+
lon, lat : numpy.ndarray
|
|
27
|
+
WGS84 longitude/latitude in degrees. Shape ``(N,)``.
|
|
28
|
+
sog : numpy.ndarray
|
|
29
|
+
Speed over ground in knots. Shape ``(N,)``.
|
|
30
|
+
cog : numpy.ndarray
|
|
31
|
+
Course over ground in degrees, ``[0, 360)``. Shape ``(N,)``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
mmsi: int
|
|
35
|
+
t: np.ndarray
|
|
36
|
+
lon: np.ndarray
|
|
37
|
+
lat: np.ndarray
|
|
38
|
+
sog: np.ndarray
|
|
39
|
+
cog: np.ndarray
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
n = len(self.t)
|
|
43
|
+
for name in _FLOAT_FIELDS[1:]:
|
|
44
|
+
arr = getattr(self, name)
|
|
45
|
+
if len(arr) != n:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Track field {name!r} has length {len(arr)}, expected {n}"
|
|
48
|
+
)
|
|
49
|
+
if n > 1:
|
|
50
|
+
diffs = np.diff(self.t)
|
|
51
|
+
if np.any(diffs < 0):
|
|
52
|
+
raise ValueError("Track.t must be monotonically non-decreasing")
|
|
53
|
+
|
|
54
|
+
def __len__(self) -> int:
|
|
55
|
+
return len(self.t)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_arrays(
|
|
59
|
+
cls,
|
|
60
|
+
mmsi: int,
|
|
61
|
+
t: Iterable[float],
|
|
62
|
+
lon: Iterable[float],
|
|
63
|
+
lat: Iterable[float],
|
|
64
|
+
sog: Iterable[float],
|
|
65
|
+
cog: Iterable[float],
|
|
66
|
+
) -> Track:
|
|
67
|
+
"""Coerce Python iterables / mixed dtypes into a validated ``Track``."""
|
|
68
|
+
return cls(
|
|
69
|
+
mmsi=int(mmsi),
|
|
70
|
+
t=np.asarray(list(t), dtype=np.float64),
|
|
71
|
+
lon=np.asarray(list(lon), dtype=np.float64),
|
|
72
|
+
lat=np.asarray(list(lat), dtype=np.float64),
|
|
73
|
+
sog=np.asarray(list(sog), dtype=np.float64),
|
|
74
|
+
cog=np.asarray(list(cog), dtype=np.float64),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def take(self, indices: Iterable[int]) -> Track:
|
|
78
|
+
"""Return a new ``Track`` containing only the points at ``indices``."""
|
|
79
|
+
idx = np.asarray(list(indices), dtype=np.int64)
|
|
80
|
+
return Track(
|
|
81
|
+
mmsi=self.mmsi,
|
|
82
|
+
t=self.t[idx],
|
|
83
|
+
lon=self.lon[idx],
|
|
84
|
+
lat=self.lat[idx],
|
|
85
|
+
sog=self.sog[idx],
|
|
86
|
+
cog=self.cog[idx],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class Segment:
|
|
92
|
+
"""A constant-COG/SOG linestring between two key AIS points.
|
|
93
|
+
|
|
94
|
+
Attributes
|
|
95
|
+
----------
|
|
96
|
+
mmsi : int
|
|
97
|
+
Vessel identifier.
|
|
98
|
+
t_start, t_end : float
|
|
99
|
+
Unix-second timestamps of the segment endpoints.
|
|
100
|
+
lon_start, lat_start, lon_end, lat_end : float
|
|
101
|
+
WGS84 coordinates (degrees) of the endpoints.
|
|
102
|
+
cog_mean, sog_mean : float
|
|
103
|
+
Mean course (degrees) and speed (knots) across the segment endpoints.
|
|
104
|
+
n_points : int
|
|
105
|
+
Number of *original* AIS points represented by the segment, including
|
|
106
|
+
both endpoints. ``2`` if the segment was built without a backing
|
|
107
|
+
original track; ``>=2`` after enrichment by :func:`tdkc_segments`.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
mmsi: int
|
|
111
|
+
t_start: float
|
|
112
|
+
t_end: float
|
|
113
|
+
lon_start: float
|
|
114
|
+
lat_start: float
|
|
115
|
+
lon_end: float
|
|
116
|
+
lat_end: float
|
|
117
|
+
cog_mean: float
|
|
118
|
+
sog_mean: float
|
|
119
|
+
n_points: int = 2
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def to_segments(track: Track) -> list[Segment]:
|
|
123
|
+
"""Pair consecutive points of ``track`` into ``Segment`` records.
|
|
124
|
+
|
|
125
|
+
Each segment carries ``n_points = 2`` since the source track is treated as
|
|
126
|
+
already-compressed (or already-key-point-only). Use
|
|
127
|
+
:func:`aissegments.tdkc_segments` if you need the original-point-count
|
|
128
|
+
enrichment.
|
|
129
|
+
"""
|
|
130
|
+
n = len(track)
|
|
131
|
+
if n < 2:
|
|
132
|
+
return []
|
|
133
|
+
segments: list[Segment] = []
|
|
134
|
+
for i in range(n - 1):
|
|
135
|
+
# Course wrap: take the shorter of the two arc directions when the
|
|
136
|
+
# endpoints straddle 0/360. Mean is then re-wrapped to [0, 360).
|
|
137
|
+
cog_diff = ((track.cog[i + 1] - track.cog[i] + 180.0) % 360.0) - 180.0
|
|
138
|
+
cog_mean = (track.cog[i] + cog_diff / 2.0) % 360.0
|
|
139
|
+
segments.append(
|
|
140
|
+
Segment(
|
|
141
|
+
mmsi=track.mmsi,
|
|
142
|
+
t_start=float(track.t[i]),
|
|
143
|
+
t_end=float(track.t[i + 1]),
|
|
144
|
+
lon_start=float(track.lon[i]),
|
|
145
|
+
lat_start=float(track.lat[i]),
|
|
146
|
+
lon_end=float(track.lon[i + 1]),
|
|
147
|
+
lat_end=float(track.lat[i + 1]),
|
|
148
|
+
cog_mean=float(cog_mean),
|
|
149
|
+
sog_mean=float((track.sog[i] + track.sog[i + 1]) / 2.0),
|
|
150
|
+
n_points=2,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return segments
|