Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions tests/router_diagnostics_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import datetime
import os

import weatherrouting
from weatherrouting.routers.linearbestisorouter import LinearBestIsoRouter

from .mock_grib import MockGrib

polar_bavaria38 = weatherrouting.Polar(
os.path.join(os.path.dirname(__file__), "data/bavaria38.pol")
)

def test_routing_result_contain_diagnostics():
track = [(1, 1), (1.5, 1.5)]

routing_obj = weatherrouting.Routing(
LinearBestIsoRouter,
polar_bavaria38,
track,
MockGrib(10, 10, 0),
datetime.datetime.fromisoformat("2026-01-01T12:00:00")
)

res = routing_obj.step()

assert isinstance(res.diagnostics, dict)
assert "parents" in res.diagnostics
assert "generated" in res.diagnostics
assert "before_pruning" in res.diagnostics
assert "after_pruning" in res.diagnostics
assert "frontier_size" in res.diagnostics

assert res.diagnostics["parents"] >= 1
assert res.diagnostics["generated"] >= 0
assert res.diagnostics["before_pruning"] >= res.diagnostics["after_pruning"]

def test_point_validity_rejections_are_reported():
def reject_all_points(lat, lon):
return False

track = [(1, 1), (1.5, 1.5)]

routing_obj = weatherrouting.Routing(
LinearBestIsoRouter,
polar_bavaria38,
track,
MockGrib(10, 10, 0),
datetime.datetime.fromisoformat("2026-01-01T12:00:00"),
point_validity=reject_all_points,
)

res = routing_obj.step()

assert res.diagnostics["after_pruning"] > 0
assert res.diagnostics["rejected_point_validity"] == res.diagnostics["after_pruning"]
assert res.diagnostics["frontier_size"] == 0
28 changes: 28 additions & 0 deletions tests/router_state_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import datetime

from weatherrouting.routers.router import RoutingResult
from weatherrouting.routers.linearbestisorouter import LinearBestIsoRouter
from .mock_grib import MockGrib

def test_routing_result_defaults_are_not_shared():
t = datetime.datetime.fromisoformat("2026-01-01T12:00:00")

r1 = RoutingResult(time=t)
r2 = RoutingResult(time=t)

r1.path.append("x")
r1.isochrones.append(["iso"])

assert r2.path == []
assert r2.isochrones == []

def test_router_params_are_instance_local():
grib = MockGrib(10, 10, 0)

r1 = LinearBestIsoRouter(None, grib)
r2 = LinearBestIsoRouter(None, grib)

r1.set_param_value("subdiv", 5)

assert r1.get_param_value("subdiv") == 5
assert r2.get_param_value("subdiv") == 1
1 change: 1 addition & 0 deletions weatherrouting/routers/linearbestisorouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def generate_path(p):
path=path,
position=position,
isochrones=isoc,
diagnostics=self.last_diagnostics,
)

def get_current_best_path(self, lastlog, end) -> List: # noqa: C901
Expand Down
76 changes: 62 additions & 14 deletions weatherrouting/routers/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
from copy import deepcopy

from .. import utils

Expand Down Expand Up @@ -63,18 +64,30 @@ class RoutingNoWindError(Exception):


class RoutingResult:
def __init__(self, time, path=[], isochrones=[], position=None, progress=0):
def __init__(
self,
time = None,
path=None,
isochrones=None,
position=None,
progress=0,
diagnostics=None,
):
self.time = time
self.path = path
self.isochrones = isochrones
self.path = [] if path is None else path
self.isochrones = [] if isochrones is None else isochrones
self.position = position
self.progress = progress
self.diagnostics = {} if diagnostics is None else diagnostics

def __str__(self):
sp = list(map(lambda x: x.to_list(True), self.path))
return f"RoutingResult(time={self.time}, path={sp}, progress={self.progress})"
# position=%s, self.position,

return (
f"RoutingResult(time={self.time}, "
f"path={sp}, "
f"progress={self.progress}), "
f"diagnostics={self.diagnostics})"
)

@dataclass
class IsoPoint:
Expand Down Expand Up @@ -164,21 +177,23 @@ def __init__(
self.line_validity = line_validity
self.points_validity = points_validity
self.lines_validity = lines_validity
self.params = deepcopy(self.PARAMS)
self.last_diagnostics = {}

if self.points_validity:
self.point_validity = None
if self.lines_validity:
self.line_validity = None

def set_param_value(self, code, value):
if code not in self.PARAMS:
if code not in self.params:
raise Exception(f"Invalid param: {code}")
self.PARAMS[code].value = value
self.params[code].value = value

def get_param_value(self, code):
if code not in self.PARAMS:
if code not in self.params:
raise Exception(f"Invalid param: {code}")
return self.PARAMS[code].value
return self.params[code].value

def calculate_shortest_path_isochrones(self, fixed_speed, t, dt, isocrone, nextwp):
"""Calculates isochrones based on shortest path at fixed speed in knots (motoring);
Expand Down Expand Up @@ -218,7 +233,7 @@ def point_f(p, tws, twa, dt, brg):
t, dt, isocrone, nextwp, point_f, self.get_param_value("subdiv")
)

def _filter_validity(self, isonew, last): # noqa: C901
def _filter_validity(self, isonew, last, diagnostics=None): # noqa: C901
def valid_point(a):
if not self.point_validity(a.pos[0], a.pos[1]):
return False
Expand All @@ -232,18 +247,31 @@ def valid_line(a):
return True

if self.point_validity:
before = len(isonew)
isonew = list(filter(valid_point, isonew))
if diagnostics is not None:
diagnostics["rejected_point_validity"] += before - len(isonew)
if self.line_validity:
before = len(isonew)
isonew = list(filter(valid_line, isonew))
if diagnostics is not None:
diagnostics["rejected_line_validity"] += before - len(isonew)
if self.points_validity:
before = len(isonew)
pp = list(map(lambda a: a.pos, isonew))
pv = self.points_validity(pp)

for x in range(len(isonew)):
if not pv[x]:
isonew[x] = None

isonew = list(filter(lambda a: a is not None, isonew))
if diagnostics is not None:
diagnostics["rejected_points_validity"] += before - len(isonew)

if self.lines_validity:
before = len(isonew)

pp = list(
map(
lambda a: [
Expand All @@ -262,16 +290,31 @@ def valid_line(a):
isonew[x] = None
isonew = list(filter(lambda a: a is not None, isonew))

if diagnostics is not None:
diagnostics["rejected_lines_validity"] += before - len(isonew)

return isonew

def _calculate_isochrones( # noqa: C901
self, t, dt, isocrone, nextwp, point_f, subdiv
):
"""Calcuates isochrones based on pointF next point calculation"""
last = isocrone[-1]

newisopoints = []

diagnostics = {
"parents": len(last),
"generated": 0,
"rejected_no_progress": 0,
"before_pruning": 0,
"after_pruning": 0,
"rejected_point_validity": 0,
"rejected_line_validity": 0,
"rejected_points_validity": 0,
"rejected_lines_validity": 0,
"frontier_size": 0,
}

def _calculate_iso_points(i):
last = isocrone[-1]
cisos = []
Expand All @@ -298,6 +341,7 @@ def _calculate_iso_points(i):
startwplos = isocrone[0][0].lossodromic((ptoiso[0], ptoiso[1]))

if nextwpdist > p.next_wp_dist:
diagnostics["rejected_no_progress"] += 1
continue

# if self.point_validity:
Expand All @@ -306,7 +350,7 @@ def _calculate_iso_points(i):
# if self.line_validity:
# if not self.line_validity (ptoiso[0], ptoiso[1], p.pos[0], p.pos[1]):
# continue

diagnostics["generated"] += 1
cisos.append(
IsoPoint(
(ptoiso[0], ptoiso[1]),
Expand Down Expand Up @@ -336,6 +380,7 @@ def _calculate_iso_points(i):
newisopoints += _calculate_iso_points(i)

newisopoints = sorted(newisopoints, key=(lambda a: a.start_wp_los[1]))
diagnostics["before_pruning"] = len(newisopoints)

# Remove slow isopoints inside
bearing = {}
Expand All @@ -348,7 +393,10 @@ def _calculate_iso_points(i):
else:
bearing[k] = x

isonew = self._filter_validity(list(bearing.values()), last)
diagnostics["after_pruning"] = len(bearing)
isonew = self._filter_validity(list(bearing.values()), last, diagnostics)
diagnostics["frontier_size"] = len(isonew)
self.last_diagnostics = diagnostics
isonew = sorted(isonew, key=(lambda a: a.start_wp_los[1]))
isocrone.append(isonew)

Expand Down
6 changes: 5 additions & 1 deletion weatherrouting/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ def step(self, timedelta=1) -> RoutingResult:
self.path = np
self.time = res.time
nlog = RoutingResult(
progress=progress, time=res.time, path=self.path, isochrones=res.isochrones
progress=progress,
time=res.time,
path=self.path,
isochrones=res.isochrones,
diagnostics=res.diagnostics,
)

self.log.append(nlog)
Expand Down
Loading