Skip to content

Commit

Permalink
Merge pull request #56 from tomtom-international/TTIRI-611_extend_dec…
Browse files Browse the repository at this point in the history
…oder_observer

Ttiri-611 extend decoder observer
  • Loading branch information
ClaudiaDieckmann-TomTom authored Nov 30, 2022
2 parents 3318332 + 7fbd8e8 commit 641a08d
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 32 deletions.
49 changes: 35 additions & 14 deletions openlr_dereferencer/decoding/candidate_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from .configuration import Config


def make_candidates(
lrp: LocationReferencePoint, line: Line, config: Config, is_last_lrp: bool
) -> Iterable[Candidate]:
"Yields zero or more LRP candidates based on the given line"
def make_candidate(
lrp: LocationReferencePoint, line: Line, config: Config, observer: Optional[DecoderObserver], is_last_lrp: bool
) -> Candidate:
"Returns one or none LRP candidates based on the given line"
# When the line is of length zero, we expect that also the adjacent lines are considered as candidates, hence
# we don't need to project on the point that is the degenerated line.
if line.geometry.length == 0:
Expand Down Expand Up @@ -56,25 +56,47 @@ def make_candidates(
bearing = compute_bearing(lrp, candidate, is_last_lrp, config.bear_dist)
bear_diff = angle_difference(bearing, lrp.bear)
if abs(bear_diff) > config.max_bear_deviation:
if observer is not None:
observer.on_candidate_rejected(
lrp, candidate,
f"Bearing difference = {bear_diff} greater than max. bearing deviation = {config.max_bear_deviation}",
)
debug(
f"Not considering {candidate} because the bearing difference is {bear_diff} °.",
f"bear: {bearing}. lrp bear: {lrp.bear}",
)
return
candidate.score = score_lrp_candidate(lrp, candidate, config, is_last_lrp)
if candidate.score >= config.min_score:
yield candidate
if candidate.score < config.min_score:
if observer is not None:
observer.on_candidate_rejected(
lrp, candidate,
f"Candidate score = {candidate.score} lower than min. score = {config.min_score}",
)
debug(
f"Not considering {candidate}",
f"Candidate score = {candidate.score} < min. score = {config.min_score}",
)
return
if observer is not None:
observer.on_candidate_found(
lrp, candidate,
)
return candidate


def nominate_candidates(
lrp: LocationReferencePoint, reader: MapReader, config: Config, is_last_lrp: bool
lrp: LocationReferencePoint, reader: MapReader, config: Config,
observer: Optional[DecoderObserver], is_last_lrp: bool
) -> Iterable[Candidate]:
"Yields candidate lines for the LRP along with their score."
debug(
f"Finding candidates for LRP {lrp} at {coords(lrp)} in radius {config.search_radius}"
)
for line in reader.find_lines_close_to(coords(lrp), config.search_radius):
yield from make_candidates(lrp, line, config, is_last_lrp)
candidate = make_candidate(lrp, line, config, observer, is_last_lrp)
if candidate:
yield candidate


def get_candidate_route(
Expand Down Expand Up @@ -164,10 +186,7 @@ def match_tail(

# Generate all pairs of candidates for the first two lrps
next_lrp = tail[0]
next_candidates = list(nominate_candidates(next_lrp, reader, config, last_lrp))

if observer is not None:
observer.on_candidates_found(next_lrp, next_candidates)
next_candidates = list(nominate_candidates(next_lrp, reader, config, observer, last_lrp))

pairs = list(product(candidates, next_candidates))
# Sort by line scores
Expand All @@ -191,7 +210,7 @@ def match_tail(
continue

if observer is not None:
observer.on_matching_fail(current, next_lrp, candidates, next_candidates)
observer.on_matching_fail(current, next_lrp, candidates, next_candidates, "No candidate pair matches")
raise LRDecodeError("Decoding was unsuccessful: No candidates left or available.")


Expand Down Expand Up @@ -231,7 +250,7 @@ def handleCandidatePair(
if not route:
debug("No path for candidate found")
if observer is not None:
observer.on_route_fail(current, next_lrp, source, dest)
observer.on_route_fail(current, next_lrp, source, dest, "No path for candidate found")
return None

length = route.length()
Expand All @@ -243,6 +262,8 @@ def handleCandidatePair(
# If the path does not match DNP, continue with the next candidate pair
if length < minlen or length > maxlen:
debug("Shortest path deviation from DNP is too large")
if observer is not None:
observer.on_route_fail(current, next_lrp, source, dest, "Shortest path deviation from DNP is too large")
return None

debug(f"Taking route {route}.")
Expand Down
5 changes: 1 addition & 4 deletions openlr_dereferencer/decoding/line_decoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ def dereference_path(
) -> List[Route]:
"Decode the location reference path, without considering any offsets"
first_lrp = lrps[0]
first_candidates = list(nominate_candidates(first_lrp, reader, config, False))

if observer is not None:
observer.on_candidates_found(first_lrp, first_candidates)
first_candidates = list(nominate_candidates(first_lrp, reader, config, observer, False))

linelocationpath = match_tail(first_lrp, first_candidates, lrps[1:], reader, config, observer)
return linelocationpath
Expand Down
12 changes: 8 additions & 4 deletions openlr_dereferencer/observer/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ class DecoderObserver:
"Abstract class representing an observer to the OpenLR decoding process"

@abstractmethod
def on_candidates_found(self, lrp: LocationReferencePoint, candidates: Sequence[Candidate]):
"Called by the decoder when it finds a list of candidates for a location reference point"
def on_candidate_found(self, lrp: LocationReferencePoint, candidate: Candidate):
"Called by the decoder when it finds a candidate for a location reference point"

@abstractmethod
def on_candidate_rejected(self, lrp: LocationReferencePoint, candidate: Candidate, reason: str):
"Called by the decoder when a candidate for a location reference point is rejected"

@abstractmethod
def on_route_success(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
Expand All @@ -23,12 +27,12 @@ def on_route_success(self, from_lrp: LocationReferencePoint, to_lrp: LocationRef

@abstractmethod
def on_route_fail(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
from_line: Line, to_line: Line):
from_line: Line, to_line: Line, reason: str):
"""Called after the decoder fails to find a route between two candidate
lines for successive location reference points"""

def on_matching_fail(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
from_candidates: Sequence[Candidate], to_candidates: Sequence[Candidate]):
from_candidates: Sequence[Candidate], to_candidates: Sequence[Candidate], reason: str):
"""Called after none of the candidate pairs for two LRPs were matching.
The only way of recovering is to go back and discard the last bit of
Expand Down
25 changes: 18 additions & 7 deletions openlr_dereferencer/observer/simple_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ class AttemptedRoute(NamedTuple):
to_line: Line
success: bool
path: Optional[Sequence[Line]]
reason: Optional[str]

class AttemptedMatch(NamedTuple):
"An attempted try to resolve a pair of two LRPs"
from_lrp: LocationReferencePoint
to_lrp: LocationReferencePoint
from_candidate: Sequence[Candidate]
to_candidate: Sequence[Candidate]
reason: Optional[str]


class SimpleObserver(DecoderObserver):
Expand All @@ -29,26 +31,35 @@ class SimpleObserver(DecoderObserver):

def __init__(self):
self.candidates = {}
self.failed_candidates = []
self.attempted_routes = []
self.failed_matches = []

def on_candidates_found(self, lrp: LocationReferencePoint, candidates: Sequence[Candidate]):
self.candidates[lrp] = candidates
def on_candidate_found(self, lrp: LocationReferencePoint, candidate: Candidate):
if lrp not in self.candidates:
self.candidates[lrp] = [candidate]
else:
self.candidates[lrp].append(candidate)

def on_candidate_rejected(self, lrp: LocationReferencePoint, candidate: Candidate, reason: str):
self.failed_candidates.append(
(lrp, candidate, reason)
)

def on_route_fail(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
from_line: Line, to_line: Line):
from_line: Line, to_line: Line, reason: str):
self.attempted_routes.append(
AttemptedRoute(from_lrp, to_lrp, from_line, to_line, False, None)
AttemptedRoute(from_lrp, to_lrp, from_line, to_line, False, None, reason)
)

def on_route_success(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
from_line: Line, to_line: Line, path: Sequence[Line]):
self.attempted_routes.append(
AttemptedRoute(from_lrp, to_lrp, from_line, to_line, True, path)
AttemptedRoute(from_lrp, to_lrp, from_line, to_line, True, path, None)
)

def on_matching_fail(self, from_lrp: LocationReferencePoint, to_lrp: LocationReferencePoint,
from_candidates: Sequence[Candidate], to_candidates: Sequence[Candidate]):
from_candidates: Sequence[Candidate], to_candidates: Sequence[Candidate], reason: str):
self.failed_matches.append(
AttemptedMatch(from_lrp, to_lrp, from_candidates, to_candidates)
AttemptedMatch(from_lrp, to_lrp, from_candidates, to_candidates, reason)
)
29 changes: 26 additions & 3 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from openlr_dereferencer import decode, Config
from openlr_dereferencer.decoding import PointAlongLine, LineLocation, LRDecodeError, PoiWithAccessPoint
from openlr_dereferencer.decoding.candidate_functions import nominate_candidates, make_candidates
from openlr_dereferencer.decoding.candidate_functions import nominate_candidates, make_candidate
from openlr_dereferencer.decoding.scoring import score_geolocation, score_frc, \
score_bearing, score_angle_difference
from openlr_dereferencer.decoding.routes import PointOnLine, Route
Expand Down Expand Up @@ -167,7 +167,7 @@ def test_make_candidates_with_zero_length_line(self):
node1 = DummyNode(Coordinates(0.0, 0.0))
node2 = DummyNode(Coordinates(0.0, 0.0))
line = DummyLine(0, node1, node2)
self.assertListEqual(list(make_candidates(lrp, line, self.config, False)), [])
self.assertEqual(make_candidate(lrp, line, self.config, None, False), None)

def test_geoscore_1(self):
"Test scoring an excactly matching LRP candidate line"
Expand Down Expand Up @@ -288,7 +288,7 @@ def test_anglescore_2(self):
def test_generate_candidates_1(self):
"Generate candidates and pick the best"
reference = get_test_linelocation_1()
candidates = list(nominate_candidates(reference.points[0], self.reader, self.config, False))
candidates = list(nominate_candidates(reference.points[0], self.reader, self.config, None, False))
# Sort by score
candidates.sort(key=lambda candidate: candidate.score, reverse=True)
# Get only the line ids
Expand Down Expand Up @@ -383,6 +383,29 @@ def test_observer_decode_3_lrps(self):
self.assertTrue(observer.candidates)
self.assertListEqual([route.success for route in observer.attempted_routes], [True, True])

def test_observer_generate_candidates_strict_bearing_threshold(self):
"Try to generate candidates with a strict bearing threshold"
reference = get_test_linelocation_1()
observer = SimpleObserver()
candidates = list(
nominate_candidates(reference.points[0], self.reader, Config(max_bear_deviation=0), observer, False)
)

self.assertListEqual(candidates, [])
self.assertDictEqual(observer.candidates, {})
self.assertTrue(observer.failed_candidates)

def test_observer_generate_candidates_score_threshold(self):
"Try to generate candidates with a high min score"
reference = get_test_linelocation_1()
observer = SimpleObserver()
candidates = list(
nominate_candidates(reference.points[0], self.reader, Config(min_score=0.8), observer, False)
)

self.assertEqual(len(candidates), 1)
self.assertTrue(observer.failed_candidates)

def test_observer_decode_pointalongline(self):
"Add a simple observer for decoding a valid point along line location"
reference = get_test_pointalongline()
Expand Down

0 comments on commit 641a08d

Please sign in to comment.