colregs-planner 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- colregs_planner-0.1.0/LICENSE +21 -0
- colregs_planner-0.1.0/PKG-INFO +471 -0
- colregs_planner-0.1.0/README.md +425 -0
- colregs_planner-0.1.0/colregs_planner/__init__.py +22 -0
- colregs_planner-0.1.0/colregs_planner/ais/__init__.py +0 -0
- colregs_planner-0.1.0/colregs_planner/ais/cleaning.py +166 -0
- colregs_planner-0.1.0/colregs_planner/ais/windows.py +52 -0
- colregs_planner-0.1.0/colregs_planner/colregs_graph/__init__.py +0 -0
- colregs_planner-0.1.0/colregs_planner/colregs_graph/colregs_classifier.py +418 -0
- colregs_planner-0.1.0/colregs_planner/colregs_graph/colregs_graph.py +115 -0
- colregs_planner-0.1.0/colregs_planner/motion_models/__init__.py +1 -0
- colregs_planner-0.1.0/colregs_planner/motion_models/adapters.py +386 -0
- colregs_planner-0.1.0/colregs_planner/motion_models/ct_model.py +191 -0
- colregs_planner-0.1.0/colregs_planner/motion_models/milliampere_model.py +344 -0
- colregs_planner-0.1.0/colregs_planner/motion_models/vikenes_model.py +404 -0
- colregs_planner-0.1.0/colregs_planner/nominal/__init__.py +19 -0
- colregs_planner-0.1.0/colregs_planner/nominal/indexer.py +166 -0
- colregs_planner-0.1.0/colregs_planner/nominal/library.py +144 -0
- colregs_planner-0.1.0/colregs_planner/nominal/retriever.py +216 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/__init__.py +40 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/dp_planner.py +320 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/manoeuvre_search.py +519 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/manoeuvres.py +48 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/schema.py +208 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/service.py +687 -0
- colregs_planner-0.1.0/colregs_planner/planner_api/tools.py +218 -0
- colregs_planner-0.1.0/colregs_planner/policies/__init__.py +1 -0
- colregs_planner-0.1.0/colregs_planner/policies/constant.py +70 -0
- colregs_planner-0.1.0/colregs_planner/policies/cto.py +452 -0
- colregs_planner-0.1.0/colregs_planner/policies/debug_types.py +79 -0
- colregs_planner-0.1.0/colregs_planner/policies/predictor_dp.py +52 -0
- colregs_planner-0.1.0/colregs_planner/policies/search_dp.py +93 -0
- colregs_planner-0.1.0/colregs_planner/policies/vo_analytic.py +777 -0
- colregs_planner-0.1.0/colregs_planner/policies/vo_common.py +387 -0
- colregs_planner-0.1.0/colregs_planner/policies/vo_kuwata.py +553 -0
- colregs_planner-0.1.0/colregs_planner/prediction/__init__.py +1 -0
- colregs_planner-0.1.0/colregs_planner/prediction/parallel.py +203 -0
- colregs_planner-0.1.0/colregs_planner/prediction/predictor.py +693 -0
- colregs_planner-0.1.0/colregs_planner/prediction/responding_filter.py +217 -0
- colregs_planner-0.1.0/colregs_planner/prediction/vo_rollout.py +621 -0
- colregs_planner-0.1.0/colregs_planner/scenario_config.py +165 -0
- colregs_planner-0.1.0/colregs_planner/seachart/__init__.py +0 -0
- colregs_planner-0.1.0/colregs_planner/seachart/loader.py +292 -0
- colregs_planner-0.1.0/colregs_planner/vessel/__init__.py +2 -0
- colregs_planner-0.1.0/colregs_planner/vessel/base.py +85 -0
- colregs_planner-0.1.0/colregs_planner/vessel/encounter.py +225 -0
- colregs_planner-0.1.0/colregs_planner/vessel/state.py +170 -0
- colregs_planner-0.1.0/colregs_planner/visualization/__init__.py +1 -0
- colregs_planner-0.1.0/colregs_planner/visualization/live_viz.py +2088 -0
- colregs_planner-0.1.0/colregs_planner/visualization/map_background.py +131 -0
- colregs_planner-0.1.0/colregs_planner/visualization/multi_panel.py +540 -0
- colregs_planner-0.1.0/colregs_planner.egg-info/PKG-INFO +471 -0
- colregs_planner-0.1.0/colregs_planner.egg-info/SOURCES.txt +63 -0
- colregs_planner-0.1.0/colregs_planner.egg-info/dependency_links.txt +1 -0
- colregs_planner-0.1.0/colregs_planner.egg-info/requires.txt +32 -0
- colregs_planner-0.1.0/colregs_planner.egg-info/top_level.txt +1 -0
- colregs_planner-0.1.0/pyproject.toml +56 -0
- colregs_planner-0.1.0/setup.cfg +4 -0
- colregs_planner-0.1.0/tests/test_colregs_classifier.py +240 -0
- colregs_planner-0.1.0/tests/test_dp_planner.py +95 -0
- colregs_planner-0.1.0/tests/test_manoeuvre_search.py +601 -0
- colregs_planner-0.1.0/tests/test_milliampere.py +441 -0
- colregs_planner-0.1.0/tests/test_nominal_indexer.py +64 -0
- colregs_planner-0.1.0/tests/test_parallel.py +152 -0
- colregs_planner-0.1.0/tests/test_vo_solver.py +115 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes Robert Skarø and Mikkel Bergstrand
|
|
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,471 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: colregs-planner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: COLREGs-aware multi-vessel trajectory prediction and own-ship manoeuvre planning, with a dynamic-positioning reference as output
|
|
5
|
+
Author: Johannes Robert Skarø, Mikkel Bergstrand
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: COLREGs,collision avoidance,maritime,autonomous ships,trajectory prediction,velocity obstacle,motion planning
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: scipy>=1.11
|
|
22
|
+
Requires-Dist: PyYAML>=6.0
|
|
23
|
+
Provides-Extra: nominal
|
|
24
|
+
Requires-Dist: h5py>=3.10; extra == "nominal"
|
|
25
|
+
Requires-Dist: pyproj>=3.6; extra == "nominal"
|
|
26
|
+
Provides-Extra: ais
|
|
27
|
+
Requires-Dist: pandas>=3.0; extra == "ais"
|
|
28
|
+
Requires-Dist: pyarrow>=14; extra == "ais"
|
|
29
|
+
Requires-Dist: pyproj>=3.6; extra == "ais"
|
|
30
|
+
Requires-Dist: h5py>=3.10; extra == "ais"
|
|
31
|
+
Provides-Extra: sim
|
|
32
|
+
Requires-Dist: matplotlib>=3.8; extra == "sim"
|
|
33
|
+
Provides-Extra: seachart
|
|
34
|
+
Requires-Dist: pyogrio>=0.7; extra == "seachart"
|
|
35
|
+
Requires-Dist: shapely>=2.0; extra == "seachart"
|
|
36
|
+
Requires-Dist: pyproj>=3.6; extra == "seachart"
|
|
37
|
+
Provides-Extra: plots
|
|
38
|
+
Requires-Dist: matplotlib>=3.8; extra == "plots"
|
|
39
|
+
Requires-Dist: contextily>=1.5; extra == "plots"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: colregs-planner[ais,nominal,plots,seachart,sim]; extra == "all"
|
|
42
|
+
Provides-Extra: test
|
|
43
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
44
|
+
Requires-Dist: colregs-planner[ais,nominal,sim]; extra == "test"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# colregs-planner
|
|
48
|
+
|
|
49
|
+
COLREGs-aware multi-vessel trajectory prediction and own-ship manoeuvre
|
|
50
|
+
planning. Code for **"COLREGs-Aware Multi-Vessel Trajectory Prediction via Interaction
|
|
51
|
+
Hypothesis Inference and Velocity-Obstacle Rollout."**
|
|
52
|
+
|
|
53
|
+
Each vessel's trajectory is predicted in two layers. A *nominal* layer predicts
|
|
54
|
+
where the vessel would go without other traffic. An *interaction-aware* layer
|
|
55
|
+
assigns COLREGs roles between vessel pairs, estimates which vessels actually
|
|
56
|
+
respond to their obligations, and predicts the resulting maneuvers with a
|
|
57
|
+
velocity-obstacle (VO) rollout.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Python 3.11 or newer. From PyPI:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install colregs-planner # prediction, planner, ColregsDPPlanner
|
|
67
|
+
pip install "colregs-planner[all]" # plus AIS processing, nominal library, plots, sea charts
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Extras: `nominal` (track library), `ais` (AIS processing and library
|
|
71
|
+
building), `sim` (simulation configs and plots), `seachart`, `plots`.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from colregs_planner import ColregsDPPlanner, Route # see "DP reference for an own ship"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The simulation and AIS scripts live in the repository, not in the package. To
|
|
78
|
+
run them, install from source:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/johannesskaro/multi-vessel-prediction.git
|
|
82
|
+
cd multi-vessel-prediction
|
|
83
|
+
python -m venv venv
|
|
84
|
+
venv\Scripts\activate # Windows; source venv/bin/activate on Linux/macOS
|
|
85
|
+
python --version # must be 3.11 or newer
|
|
86
|
+
pip install -e ".[all,test]"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If `python --version` shows an older version, another Python comes first on your
|
|
90
|
+
PATH. Delete the `venv` folder and create it with a specific version instead:
|
|
91
|
+
`py -3.13 -m venv venv` on Windows (`py --list` shows the installed versions), or
|
|
92
|
+
`python3.13 -m venv venv` on Linux/macOS.
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
The simulations need no data:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python scripts/run_simulation.py --config scenarios/simulation/multi_vessel.yaml
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This is the four-vessel scenario from the paper (Fig. 2): V2 does not respond,
|
|
103
|
+
and the others adapt once the filter detects it. Controls in the plot window:
|
|
104
|
+
**Space** pause/resume, **S** save the frame to `outputs/frames/`, **Q** quit.
|
|
105
|
+
Useful flags: `--save` writes a GIF to `outputs/`, `--stop-time 20.5` pauses and
|
|
106
|
+
saves a frame at that time, `--record` saves a video.
|
|
107
|
+
|
|
108
|
+
## Simulation scenarios
|
|
109
|
+
|
|
110
|
+
| Config | Scenario |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `head_on.yaml` | two vessels head-on, both responsive |
|
|
113
|
+
| `crossing.yaml` | two vessels crossing, the target does not respond |
|
|
114
|
+
| `overtaking.yaml` | two vessels overtaking, the target does not respond |
|
|
115
|
+
| `overtaking_ahead.yaml` | ego overtakes a slower vessel dead ahead; the pass is far beyond a short planning horizon |
|
|
116
|
+
| `head_on_crossing.yaml` | head-on plus a non-responsive crossing vessel |
|
|
117
|
+
| `multi_crossing.yaml` | ego and three crossing vessels; ego is non-responsive, the others must avoid it |
|
|
118
|
+
| `multi_crossing_give_way.yaml` | the same with a responsive ego: give-way to two starboard crossers while stand-on to a port crosser |
|
|
119
|
+
| `multi_vessel.yaml` | four vessels, one non-responsive (paper Fig. 2) |
|
|
120
|
+
| `give_way_stop.yaml` | ego gives way to a close crosser from starboard while a vessel alongside blocks a starboard turn; stopping is the clean solution |
|
|
121
|
+
|
|
122
|
+
### Changing a scenario
|
|
123
|
+
|
|
124
|
+
Copy a config in `scenarios/simulation/` and edit it. The vessels are listed
|
|
125
|
+
under `scenario.vessels`. The vessel named `ego` starts at the origin; every other
|
|
126
|
+
vessel is placed relative to it:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
scenario:
|
|
130
|
+
dt: 0.5 # time step [s]
|
|
131
|
+
t_horizon: 75.0 # simulation length [s]
|
|
132
|
+
vessels:
|
|
133
|
+
ego:
|
|
134
|
+
speed: 5.0 # [m/s]
|
|
135
|
+
heading: 0.0 # [deg], 0 = north, 90 = east
|
|
136
|
+
policy: vo_analytic
|
|
137
|
+
sim_model: ct_decoupled_ar
|
|
138
|
+
intent: responsive
|
|
139
|
+
d_pass: 75.0 # desired passing distance [m]
|
|
140
|
+
|
|
141
|
+
target_1: # add vessels by adding blocks like this one
|
|
142
|
+
encounter: head_on # where it starts, relative to ego (see below)
|
|
143
|
+
range: 400.0 # distance from ego [m]
|
|
144
|
+
speed: 5.0
|
|
145
|
+
policy: vo_analytic
|
|
146
|
+
sim_model: ct_decoupled_ar
|
|
147
|
+
intent: non_responsive # holds its course and ignores all traffic
|
|
148
|
+
d_pass: 75.0
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **Start position.** `encounter` and `range` place a vessel relative to ego:
|
|
152
|
+
|
|
153
|
+
| `encounter` | Start position and course |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `head_on` | ahead, on the reciprocal course; `lateral_offset_m` shifts it sideways |
|
|
156
|
+
| `crossing_starboard` | on ego's starboard side, crossing to port |
|
|
157
|
+
| `crossing_port` | on ego's port side, crossing to starboard |
|
|
158
|
+
| `overtaking_ahead` | ahead, same course (ego overtakes), 50 m to starboard (`lateral_offset_m` overrides) |
|
|
159
|
+
| `overtaking_astern` | astern, same course (it overtakes ego), 50 m to starboard (`lateral_offset_m` overrides) |
|
|
160
|
+
|
|
161
|
+
Crossing vessels are timed to reach the crossing point together with ego. The
|
|
162
|
+
COLREGs roles are not taken from `encounter`: they are classified from the
|
|
163
|
+
vessel states at every step, as in the paper.
|
|
164
|
+
- **Responsiveness.** `intent: responsive` vessels follow their COLREGs
|
|
165
|
+
obligations. `intent: non_responsive` vessels keep their course whatever
|
|
166
|
+
happens. The filter has to discover this from their motion. Play around by setting different vessels as non_responsive to see how that changes the predictions.
|
|
167
|
+
A vessel planned by `predictor_dp` or `search_dp` is the own ship under our
|
|
168
|
+
control and always follows COLREGs, so it cannot be `non_responsive`; give a
|
|
169
|
+
non-responsive `ego` a plain policy such as `vo_analytic` (as in
|
|
170
|
+
`multi_crossing.yaml`).
|
|
171
|
+
- **Behavior.** Any VO parameter (see [Parameters](#parameters)) can be set per
|
|
172
|
+
vessel, e.g. `d_pass` or `course_budget_deg`. Names are checked, so a typo
|
|
173
|
+
gives an error.
|
|
174
|
+
- **Motion model.** `sim_model` sets how a vessel's motion is simulated:
|
|
175
|
+
`ct_decoupled_ar` (default in the configs), `ct_coupled_ar`, `viknes_ar`, `cv`,
|
|
176
|
+
or `milliampere` (see below).
|
|
177
|
+
|
|
178
|
+
### milliAmpere own ship
|
|
179
|
+
|
|
180
|
+
`sim_model: milliampere` simulates a vessel with the 3DOF maneuvering model of
|
|
181
|
+
the milliAmpere autonomous ferry and its controllers (ported from liteAmpere
|
|
182
|
+
into `colregs_planner/motion_models/milliampere_model.py`). How the hull is driven depends
|
|
183
|
+
on the vessel's `policy`:
|
|
184
|
+
|
|
185
|
+
- `predictor_dp`: dynamic positioning on a moving reference. Every step the
|
|
186
|
+
predictor (top-1 hypothesis, the dashed line in the plot) is rolled out one
|
|
187
|
+
step from the current states, and the reference moves by the vessel's
|
|
188
|
+
predicted step and turns to its predicted course. The vessel is the own
|
|
189
|
+
ship under our control, so it is always responsive, not estimated. The DP law is
|
|
190
|
+
liteAmpere's with its damping moved onto the velocity error and the hull's
|
|
191
|
+
drag at the reference velocity fed forward, so the hull stays within
|
|
192
|
+
~0.1 m of the reference instead of trailing it by 10–17 m.
|
|
193
|
+
- any VO policy (e.g. `vo_analytic`): the policy's desired velocity becomes
|
|
194
|
+
heading and speed commands for liteAmpere's course and speed controllers.
|
|
195
|
+
- `search_dp`: dynamic positioning along a plan chosen by search. Every
|
|
196
|
+
`replan_s` (default 5 s) the own ship scores every sequence of two 7.5 s
|
|
197
|
+
manoeuvres from {nominal, starboard 30°, starboard 60°, port 30°, half speed,
|
|
198
|
+
stop},
|
|
199
|
+
each rolled out with the prediction model while the other vessels react to
|
|
200
|
+
it, under the top-2 responsiveness hypotheses. The cost weighs passing
|
|
201
|
+
distance (with `bow_factor` × `d_pass`, default 2, required when crossing
|
|
202
|
+
ahead of a vessel; expected, and worst case over the hypotheses with at least 10 %
|
|
203
|
+
probability, so a stand-on own ship holds course until the filter actually
|
|
204
|
+
doubts the give-way vessel), COLREGs (starboard
|
|
205
|
+
in head-on and act early when giving way, both while the route course would
|
|
206
|
+
still pass inside `d_pass`, hold when standing on unless bound
|
|
207
|
+
to keep clear of another vessel, no port turns
|
|
208
|
+
toward a give-way encounter, alter course early when overtaking), deviation
|
|
209
|
+
from the route, manoeuvre effort,
|
|
210
|
+
time lost along the route, and switching away from a manoeuvre less than
|
|
211
|
+
one edge old. Code in
|
|
212
|
+
`colregs_planner/planner_api/manoeuvre_search.py`; tune with a per-vessel
|
|
213
|
+
`search: {depth, edge_s, plan_dt, horizon_s, top_k}` block.
|
|
214
|
+
A vessel being overtaken only costs a collision until the TCPA on the
|
|
215
|
+
route course is below the predictor's `tcpa_threshold`. From then on, the
|
|
216
|
+
passing distance also counts the straight-line CPA after the horizon.
|
|
217
|
+
While the route course would also pass inside the overtaking `d_pass`, a
|
|
218
|
+
plan must alter course away from the vessel on its first edge, not slow
|
|
219
|
+
down behind it. No edge may turn back toward it until the own ship is past.
|
|
220
|
+
The plot draws the own ship along its committed plan, with the others
|
|
221
|
+
reacting to it, and the console lists each plan made.
|
|
222
|
+
|
|
223
|
+
`--ego-policy` overrides ego's policy from the command line, for comparisons:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python scripts/run_simulation.py --config scenarios/simulation/milliampere/crossing.yaml --ego-policy search_dp
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The model is identified for speeds up to about 2 m/s, so the ship-scale
|
|
230
|
+
scenarios are too fast for it. `scenarios/simulation/milliampere/` holds the
|
|
231
|
+
same scenarios with every length and speed scaled by 0.3 (ego at 1.5 m/s) and
|
|
232
|
+
ego on the milliAmpere model with `predictor_dp`:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
python scripts/run_simulation.py --config scenarios/simulation/milliampere/multi_vessel.yaml
|
|
236
|
+
python scripts/tools/scale_scenarios.py # regenerate them after editing the originals
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Because times are unchanged, horizons and TCPA thresholds carry over as they
|
|
240
|
+
are. Lengths that live outside the vessel blocks are scaled too: the COLREGs
|
|
241
|
+
classifier distances (`scenario.classifier: {d_u, dcpa_max, d_a}`, defaults
|
|
242
|
+
150 m, 1500 m, 10 km) and the overtaking start offset (`lateral_offset_m`,
|
|
243
|
+
default 50 m for overtaking encounters).
|
|
244
|
+
|
|
245
|
+
The `prediction` block sets what is predicted and shown:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
prediction:
|
|
249
|
+
enabled: true
|
|
250
|
+
horizon: 120.0 # prediction horizon [s]
|
|
251
|
+
use_responding_filter: true # estimate which vessels respond (2^N hypotheses)
|
|
252
|
+
top_k_hypotheses: 2 # number of most likely hypotheses drawn (1 = only the best)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Plot options go under `viz`. `scenarios/viz_reference.yaml` lists all of them
|
|
256
|
+
with their defaults.
|
|
257
|
+
|
|
258
|
+
## Real AIS data
|
|
259
|
+
|
|
260
|
+
The real-data experiments use AIS data from the Oslofjord (Moss–Horten). The data
|
|
261
|
+
is not in the repository; it is shared separately as a `handover` folder. Put it
|
|
262
|
+
in the repository root:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
handover/
|
|
266
|
+
Moss Horten Feb 2026 AIS/processed/ais_01.pkl, ais_04.pkl # encounter windows (test data)
|
|
267
|
+
nominal_library_jan_26.h5 # nominal track library
|
|
268
|
+
seacharts/moss_horten.gpkg # sea chart (optional)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Run the three test windows from the paper:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
python scripts/run_AIS.py --config scenarios/real/window_079.yaml # live playback
|
|
275
|
+
python scripts/run_AIS.py --config scenarios/real/window_098.yaml
|
|
276
|
+
python scripts/run_AIS.py --config scenarios/real/window_054.yaml
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The sea-chart background needs `pip install -e ".[seachart]"`; without it the
|
|
280
|
+
plots have a plain background. `moss_horten.gpkg` is the Moss–Horten part of
|
|
281
|
+
Kartverket's depth data. For another area, download the full chart
|
|
282
|
+
(`Basisdata_0000_Norge_25833_Dybdedata_FGDB.gdb`) from Kartverket into
|
|
283
|
+
`seacharts/`, which is used when present, or crop it with
|
|
284
|
+
`scripts/tools/crop_seachart.py --bbox LAT_MIN LAT_MAX LON_MIN LON_MAX`. `scenarios/README.md` explains all config blocks.
|
|
285
|
+
|
|
286
|
+
To build the data yourself from the daily AIS files at
|
|
287
|
+
[Kystverket](https://ais.kystverket.no/):
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
python scripts/process_raw_ais.py --data-dir "data/<test period>" --out "data/<test period>/processed/ais_01.pkl"
|
|
291
|
+
python scripts/build_nominal_library.py --data-dir "data/<library period>" --out data/nominal_library.h5
|
|
292
|
+
python scripts/tools/browse_encounters.py --data "data/<test period>/processed/ais_01.pkl" # find encounter windows
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
and point `data.pkl_path` and `prediction.nominal.library_path` in the config to
|
|
296
|
+
the new files.
|
|
297
|
+
|
|
298
|
+
To use the predictor in your own code:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from colregs_planner.prediction.predictor import VesselPredictor
|
|
302
|
+
from colregs_planner.prediction.vo_rollout import VOAgentConfig
|
|
303
|
+
|
|
304
|
+
predictor = VesselPredictor(dt=2.0, H=300, use_responding_filter=True,
|
|
305
|
+
default_agent_config=VOAgentConfig(d_pass=100.0))
|
|
306
|
+
for states in stream: # dict: vessel id -> VesselState (NED)
|
|
307
|
+
p_responsive = predictor.update(states)
|
|
308
|
+
trajectories = predictor.predict()[0][0] # most likely hypothesis
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The full-horizon rollouts can run in parallel worker processes
|
|
312
|
+
(`colregs_planner/prediction/parallel.py`), with identical results:
|
|
313
|
+
`prediction.interaction.parallel_workers: N` in the real-data configs,
|
|
314
|
+
`prediction.parallel_workers: N` in the simulation configs, or
|
|
315
|
+
`VesselPredictor(..., parallel_workers=N)`. Live playback gains only when
|
|
316
|
+
`top_k_hypotheses > 1` (one worker per hypothesis); `--metrics` also runs the
|
|
317
|
+
rollouts of different frames in parallel, so it gains even with
|
|
318
|
+
`top_k_hypotheses: 1`. `parallel_workers: auto` picks this for you: one worker
|
|
319
|
+
per hypothesis live, all CPU cores but one for `--metrics`; it is the default
|
|
320
|
+
in both config types. Use 0 (serial) when debugging the rollout.
|
|
321
|
+
|
|
322
|
+
The planners use the same mechanism. A `search_dp` own ship in the simulation
|
|
323
|
+
gets a pool sized by `prediction.parallel_workers` (`auto`: one worker per
|
|
324
|
+
plan rollout, at most CPU cores − 1), and `PredictionService.evaluate_manoeuvres`
|
|
325
|
+
runs on the predictor's pool when the service config sets
|
|
326
|
+
`interaction.parallel_workers` (default 0 there). Results are the same either
|
|
327
|
+
way.
|
|
328
|
+
|
|
329
|
+
## Planner API
|
|
330
|
+
|
|
331
|
+
`colregs_planner/planner_api` wraps the predictor for use as an information source inside
|
|
332
|
+
a larger system, e.g. an AI planner. `PredictionService.step()` runs the full
|
|
333
|
+
predictor cycle once per observation; the `get_*` queries then read the cached
|
|
334
|
+
result and return JSON-serializable dicts, so they can be called as often as
|
|
335
|
+
needed without re-running the prediction or disturbing the responding filters.
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from colregs_planner.planner_api import PredictionService, TOOLS, call_tool
|
|
339
|
+
|
|
340
|
+
service = PredictionService.from_config("scenarios/real/window_079.yaml",
|
|
341
|
+
dt=2.0, ref_lat=REF_LAT, ref_lon=REF_LON)
|
|
342
|
+
for t, obs in stream: # {vessel_id: VesselState or {"north", "east", "speed", "course_deg"}}
|
|
343
|
+
service.step(obs, t=t)
|
|
344
|
+
|
|
345
|
+
service.get_encounters("ego") # roles, encounter type, range, DCPA/TCPA, from ego's view
|
|
346
|
+
service.get_responsiveness() # P(vessel follows its COLREGs obligations)
|
|
347
|
+
service.get_hypotheses() # responsiveness hypotheses + re-assigned roles
|
|
348
|
+
service.get_predicted_cpa("ego") # closest approach along the predicted trajectories
|
|
349
|
+
service.get_predicted_trajectory("257123000", hypothesis=0, resolution_s=30)
|
|
350
|
+
service.get_predicted_state("257123000", t=120)
|
|
351
|
+
service.get_snapshot() # everything at once
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`TOOLS` holds a JSON Schema definition for every query (Anthropic `tools=`
|
|
355
|
+
format; `tool_specs("parameters")` for frameworks that use that key), and
|
|
356
|
+
`call_tool(service, name, arguments)` dispatches a tool call, returning
|
|
357
|
+
`{"result": ...}` or `{"error": ...}`. Units: NED metres from the session
|
|
358
|
+
origin, degrees with 0 = north clockwise, m/s, seconds.
|
|
359
|
+
|
|
360
|
+
`python scripts/planner_api_demo.py` runs the API on a synthetic encounter
|
|
361
|
+
(no data needed); `--tools` prints the tool definitions.
|
|
362
|
+
|
|
363
|
+
Pass `own_id` for the own ship: its responsiveness is then fixed (it follows
|
|
364
|
+
COLREGs) instead of estimated, and the hypotheses only vary over the other
|
|
365
|
+
vessels. `service.evaluate_manoeuvre(own_id, course_change_deg, speed_factor,
|
|
366
|
+
duration_s)` predicts the closest approaches if the own ship starts that
|
|
367
|
+
manoeuvre now, with every other vessel reacting to it, under each hypothesis.
|
|
368
|
+
|
|
369
|
+
### DP reference for an own ship
|
|
370
|
+
|
|
371
|
+
`ColregsDPPlanner` (`colregs_planner/planner_api/dp_planner.py`) is the interface for
|
|
372
|
+
running the planner in another vessel system: vessel states in, dynamic
|
|
373
|
+
positioning (DP) reference out. It runs the same pipeline as the simulator's
|
|
374
|
+
`search_dp` own ship: responsiveness estimated from the observations, COLREGs
|
|
375
|
+
roles from the classifier, a manoeuvre search every `replan_s`, and the own
|
|
376
|
+
ship's track along the chosen plan as the DP reference.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from colregs_planner.planner_api import ColregsDPPlanner, Route
|
|
380
|
+
|
|
381
|
+
planner = ColregsDPPlanner(
|
|
382
|
+
own_id = "ego",
|
|
383
|
+
route = Route(north=0.0, east=0.0, course_deg=0.0, speed=1.5), # straight leg
|
|
384
|
+
dt = 0.5, # interval between update() calls [s]
|
|
385
|
+
config = {"interaction": {"use_responding_filter": True, "top_k_hypotheses": 2,
|
|
386
|
+
"agent": {"d_pass": 30.0, "tcpa_threshold": 100.0}}},
|
|
387
|
+
classifier = {"d_u": 45.0, "dcpa_max": 450.0, "d_a": 3000.0},
|
|
388
|
+
parallel_workers = "auto", # worker processes for the rollouts
|
|
389
|
+
)
|
|
390
|
+
ref = planner.update(t, {"ego": own_state, "257123000": other_state, ...})
|
|
391
|
+
ref.north, ref.east, ref.heading_deg, ref.vn, ref.ve # DP reference at t
|
|
392
|
+
planner.reference(t_now) # sample the committed plan at any rate
|
|
393
|
+
planner.set_route(next_leg) # onto the next leg of the route
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
States are `VesselState`s or dicts with `north`, `east`, `speed` and one of
|
|
397
|
+
`course_deg` / `heading_deg` / `psi`, in local NED metres. Call `update()` at a
|
|
398
|
+
fixed `dt` with every observed vessel, own ship included. Vessels slower than
|
|
399
|
+
0.5 m/s (e.g. at anchor) are planned around as holding position. A replan
|
|
400
|
+
takes about 0.05–0.2 s on one process. `python scripts/dp_planner_demo.py` runs it
|
|
401
|
+
on a synthetic head-on encounter.
|
|
402
|
+
|
|
403
|
+
## Parameters
|
|
404
|
+
|
|
405
|
+
The VO parameters and their defaults are the fields of `VOAgentConfig`
|
|
406
|
+
(`colregs_planner/prediction/vo_rollout.py`). They are set per vessel in the simulation
|
|
407
|
+
configs and under `prediction.interaction.agent` in the real-data configs.
|
|
408
|
+
|
|
409
|
+
| Parameter | Paper | Default | Meaning |
|
|
410
|
+
|---|---|---|---|
|
|
411
|
+
| `d_pass` | `D` | 100 m | desired passing distance |
|
|
412
|
+
| `los_lookahead` | `Δ` | 100 m | LOS lookahead back to the nominal track |
|
|
413
|
+
| `continuity_weight` | `λ` | 0.9 | penalty on changing the selected heading (Eq. 19) |
|
|
414
|
+
| `course_budget_deg` | | 70° | largest course change from the nominal course |
|
|
415
|
+
| `k_psi` | `k_ψ` | 0.1 s⁻¹ | turn-rate gain of the rollout motion model (Eq. 20) |
|
|
416
|
+
| `head_on_rvo_weight` | | 0.5 | share of the head-on maneuver each vessel takes (0.5 = RVO) |
|
|
417
|
+
|
|
418
|
+
The responding filter (`colregs_planner/prediction/responding_filter.py`) uses
|
|
419
|
+
`p(r₀=R) = 0.99` and `σ_ψ = 5°`. The COLREGs classifier
|
|
420
|
+
(`colregs_planner/colregs_graph/colregs_classifier.py`) uses `DCPA_max = 1500 m`. The nominal
|
|
421
|
+
predictor's `β`, search radius and history length are set under
|
|
422
|
+
`prediction.nominal` in the real-data configs.
|
|
423
|
+
|
|
424
|
+
## Differences from the paper
|
|
425
|
+
|
|
426
|
+
- **VO solver.** In the paper, a vessel must keep the full passing distance `D`
|
|
427
|
+
from every vessel. When that is impossible, e.g. vessels already closer than
|
|
428
|
+
`D` or several vessels blocking each other, the paper's solver gives extreme
|
|
429
|
+
turns or sudden course switches. This solver instead keeps the largest passing
|
|
430
|
+
distance that can be reached within `course_budget_deg`. When several vessels
|
|
431
|
+
conflict, it reduces all passing distances together, the most for encounters
|
|
432
|
+
furthest ahead in time. Speed is never changed, so every maneuver is a course
|
|
433
|
+
change. See `solve_vo_achievable` in `colregs_planner/policies/vo_analytic.py`.
|
|
434
|
+
- **STAND-ON.** A responsive stand-on vessel still avoids a collision at the last
|
|
435
|
+
moment (COLREGs Rule 17(b)), with a small passing distance `d_coll` =
|
|
436
|
+
0.1·`d_pass`. In the paper, STAND-ON has no constraint. Non-responsive vessels
|
|
437
|
+
ignore all traffic, as in the paper.
|
|
438
|
+
|
|
439
|
+
## Repository layout
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
scripts/
|
|
443
|
+
run_simulation.py simulations (scenarios/simulation/)
|
|
444
|
+
run_AIS.py real AIS playback, figures and metrics (scenarios/real/)
|
|
445
|
+
planner_api_demo.py planner API on a synthetic encounter
|
|
446
|
+
dp_planner_demo.py own-ship DP reference from vessel states (ColregsDPPlanner)
|
|
447
|
+
process_raw_ais.py raw AIS -> encounter windows
|
|
448
|
+
build_nominal_library.py
|
|
449
|
+
tools/ encounter browsing, sea-chart cropping, deviation analysis, profiling
|
|
450
|
+
colregs_planner/ the package
|
|
451
|
+
nominal/ nominal layer: track library and nearest-neighbor retrieval (Sec. III)
|
|
452
|
+
colregs_graph/ COLREGs classifier and interaction graph (Sec. IV-A, IV-B)
|
|
453
|
+
prediction/ responding filter, VO rollout, VesselPredictor (Sec. IV-C, V)
|
|
454
|
+
planner_api/ PredictionService queries and tool definitions for planners
|
|
455
|
+
policies/ VO solvers (vo_analytic: this work, vo_kuwata: Kuwata et al.), CTO
|
|
456
|
+
motion_models/ kinematic models used in simulation and rollout
|
|
457
|
+
vessel/, ais/, visualization/, seachart/
|
|
458
|
+
scenarios/ run configurations
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Development
|
|
462
|
+
|
|
463
|
+
`main` holds the code used for the paper. Make changes on your own branch and
|
|
464
|
+
open a pull request to merge them, instead of pushing to `main`:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
git switch main && git pull
|
|
468
|
+
git switch -c <your-name>/<topic> # e.g. anna/new-scenario
|
|
469
|
+
# ... edit, then commit
|
|
470
|
+
git push -u origin <your-name>/<topic>
|
|
471
|
+
```
|