diff --git a/tests/router_diagnostics_test.py b/tests/router_diagnostics_test.py new file mode 100644 index 0000000..a48d9eb --- /dev/null +++ b/tests/router_diagnostics_test.py @@ -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 diff --git a/tests/router_state_test.py b/tests/router_state_test.py new file mode 100644 index 0000000..4ae2c8d --- /dev/null +++ b/tests/router_state_test.py @@ -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 diff --git a/weatherrouting/routers/linearbestisorouter.py b/weatherrouting/routers/linearbestisorouter.py index ff4cf56..b2e3cc9 100644 --- a/weatherrouting/routers/linearbestisorouter.py +++ b/weatherrouting/routers/linearbestisorouter.py @@ -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 diff --git a/weatherrouting/routers/router.py b/weatherrouting/routers/router.py index 7817a9f..d10f57e 100644 --- a/weatherrouting/routers/router.py +++ b/weatherrouting/routers/router.py @@ -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 @@ -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: @@ -164,6 +177,8 @@ 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 @@ -171,14 +186,14 @@ def __init__( 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); @@ -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 @@ -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: [ @@ -262,6 +290,9 @@ 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 @@ -269,9 +300,21 @@ def _calculate_isochrones( # noqa: C901 ): """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 = [] @@ -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: @@ -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]), @@ -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 = {} @@ -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) diff --git a/weatherrouting/routing.py b/weatherrouting/routing.py index d36c7e2..7fb54b2 100644 --- a/weatherrouting/routing.py +++ b/weatherrouting/routing.py @@ -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)