From 2564f4d995bb633366040e92cef5034c08338cb0 Mon Sep 17 00:00:00 2001 From: Matteo Tortora Date: Thu, 15 Sep 2022 10:59:30 +0200 Subject: [PATCH] Update v2.0.4 --- docs/source/conf.py | 2 +- pytrack/analytics/visualization.py | 7 +++---- pytrack/graph/download.py | 27 ++++++++++++++++++++----- pytrack/graph/graph.py | 4 ++-- pytrack/matching/__init__.py | 5 +++++ pytrack/matching/candidate.py | 6 ++++++ pytrack/matching/mpmatching.py | 12 +++++++---- pytrack/matching/mpmatching_utils.py | 30 +++++++++++++++++++++++----- setup.py | 2 +- 9 files changed, 73 insertions(+), 22 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index be7e260..72ff40e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,7 +24,7 @@ author = 'Matteo Tortora' # The full version, including alpha/beta/rc tags -release = '2.0.3' +release = '2.0.4' # -- General configuration --------------------------------------------------- diff --git a/pytrack/analytics/visualization.py b/pytrack/analytics/visualization.py index b8f50fe..aaab362 100644 --- a/pytrack/analytics/visualization.py +++ b/pytrack/analytics/visualization.py @@ -211,11 +211,10 @@ def draw_candidates(self, candidates, radius): popup = f"coord: {cand} \n edge_osmid: {label}" if cand_type: folium.Circle(location=cand, popup=popup, radius=2, color="yellow", fill=True, - fill_opacity=1).add_to( - fg_cands) + fill_opacity=1).add_to(fg_cands) else: - folium.Circle(location=cand, popup=popup, radius=1, color="red", fill=True, fill_opacity=1).add_to( - fg_cands) + folium.Circle(location=cand, popup=popup, radius=1, color="orange", fill=True, + fill_opacity=1).add_to(fg_cands) del self._children[next(k for k in self._children.keys() if k.startswith('layer_control'))] self.add_child(folium.LayerControl()) diff --git a/pytrack/graph/download.py b/pytrack/graph/download.py index 420c863..d6f6524 100644 --- a/pytrack/graph/download.py +++ b/pytrack/graph/download.py @@ -20,27 +20,44 @@ def get_filters(network_type='drive'): 'elevator|escalator|footway|path|pedestrian|planned|platform|proposed|raceway|steps|track"]' '["service"!~"emergency_access|parking|parking_aisle|private"]') + osm_filters['bicycle'] = ('["highway"]["area"!~"yes"]["access"!~"private"]' + '["highway"!~"abandoned|bridleway|footway|bus_guideway|construction|corridor|elevator|' + 'escalator|planned|platform|proposed|raceway|steps|footway"]' + '["service"!~"private"]["bicycle"!~"no"])') + + osm_filters['service'] = ('["highway"]["area"!~"yes"]["access"!~"private"]["highway"!~"abandoned|bridleway|' + 'construction|corridor|platform|cycleway|elevator|escalator|footway|path|planned|' + 'proposed|raceway|steps|track"]["service"!~"emergency_access|parking|' + 'parking_aisle|private"]["psv"!~"no"]["footway"!~"yes"]') + return osm_filters[network_type] -def osm_download(bbox, network_type=None): +def osm_download(bbox, network_type='drive', custom_filter=None): """ Get the OpenStreetMap response. Parameters ---------- bbox: tuple bounding box within N, S, E, W coordinates. - network_type: str or None, optional, default: None + network_type: str, optional, default: 'drive' Type of street network to obtain. + custom_filter: str or None, optional, default: None + Custom filter to be used instead of the predefined ones to query the Overpass API. + An example of a custom filter is the following '[highway][!"footway"]'. + For more information visit https://overpass-turbo.eu and https://taginfo.openstreetmap.org. + Returns ------- response: json Response of the OpenStreetMao API service. """ - # TODO: add network_type statement north, south, west, east = bbox - osm_filters = get_filters(network_type='drive') + if custom_filter is not None: + osm_filter = custom_filter + else: + osm_filter = get_filters(network_type=network_type) url_endpoint = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter' @@ -49,7 +66,7 @@ def osm_download(bbox, network_type=None): overpass_settings = f'[out:{out_resp}][timeout:{timeout}]' - query_str = f'{overpass_settings};(way{osm_filters}({south}, {west}, {north}, {east});>;);out;' + query_str = f'{overpass_settings};(way{osm_filter}({south}, {west}, {north}, {east});>;);out;' response_json = requests.post(url_endpoint, data={'data': query_str}) diff --git a/pytrack/graph/graph.py b/pytrack/graph/graph.py index d362c27..7058528 100644 --- a/pytrack/graph/graph.py +++ b/pytrack/graph/graph.py @@ -102,7 +102,7 @@ def _simplification(G, response_json): junction = [False, False] if is_oneway: - junction[1:1] = [True if G.degree[node] > 3 else False for node in nodes[1:-1]] + junction[1:1] = [True if G.degree[node] > 2 else False for node in nodes[1:-1]] # Changed >3 to >2 V2.0.4 else: junction[1:1] = [True if G.degree[node] > 4 else False for node in nodes[1:-1]] @@ -259,7 +259,7 @@ def _oneway_path_values(path): ret: dict Indicates whether an OSM path is oneway. """ - return {path[key] for key in path.keys() if key.startswith("oneway") and path[key] == "no"} + return {path[key] for key in path.keys() if key.startswith("oneway")} # Removed 'and path[key] == "no"' v2.0.4 def _is_oneway(path, bidirectional): diff --git a/pytrack/matching/__init__.py b/pytrack/matching/__init__.py index e69de29..33c4b5c 100644 --- a/pytrack/matching/__init__.py +++ b/pytrack/matching/__init__.py @@ -0,0 +1,5 @@ +# This lets you use package.module.Class as package.Class in your code. +from .candidate import Candidate + +# This lets Sphinx know you want to document package.module.Class as package.Class. +__all__ = ['Candidate'] \ No newline at end of file diff --git a/pytrack/matching/candidate.py b/pytrack/matching/candidate.py index 2854fd0..bb0caa1 100644 --- a/pytrack/matching/candidate.py +++ b/pytrack/matching/candidate.py @@ -105,6 +105,12 @@ def get_candidates(G, points, interp_dist=1, closest=True, radius=10): "candidate_type": np.full(len(nodes.index[idx]), False), "dists": list(dist)} for i, (point, idx, dist) in enumerate(zip(points, idxs, dists))} + no_cands = [node_id for node_id, cand in results.items() if not cand["candidates"]] + + if no_cands: + for cand in no_cands: + del results[cand] + print(f"A total of {len(no_cands)} points has no candidates: {*no_cands,}") return G, results diff --git a/pytrack/matching/mpmatching.py b/pytrack/matching/mpmatching.py index 26c0deb..847c125 100644 --- a/pytrack/matching/mpmatching.py +++ b/pytrack/matching/mpmatching.py @@ -1,4 +1,5 @@ from collections import deque +import math from . import mpmatching_utils @@ -40,12 +41,12 @@ def viterbi_search(G, trellis, start="start", target="target", beta=mpmatching_u # Initialize joint probability for each node joint_prob = {} for u_name in trellis.nodes(): - joint_prob[u_name] = 0 + joint_prob[u_name] = -float('inf') predecessor = {} queue = deque() queue.append(start) - joint_prob[start] = mpmatching_utils.emission_prob(trellis.nodes[start]["candidate"], sigma) + joint_prob[start] = math.log10(mpmatching_utils.emission_prob(trellis.nodes[start]["candidate"], sigma)) predecessor[start] = None while queue: @@ -58,8 +59,11 @@ def viterbi_search(G, trellis, start="start", target="target", beta=mpmatching_u for v_name in trellis.successors(u_name): v = trellis.nodes[v_name]["candidate"] - new_prob = joint_prob[u_name] * mpmatching_utils.transition_prob(G, u, v, beta) * \ - mpmatching_utils.emission_prob(v, sigma) + try: + new_prob = joint_prob[u_name] + math.log10(mpmatching_utils.transition_prob(G, u, v, beta)) + \ + math.log10(mpmatching_utils.emission_prob(v, sigma)) + except Exception as e: + print(e) if joint_prob[v_name] < new_prob: joint_prob[v_name] = new_prob diff --git a/pytrack/matching/mpmatching_utils.py b/pytrack/matching/mpmatching_utils.py index 9762c26..7793472 100644 --- a/pytrack/matching/mpmatching_utils.py +++ b/pytrack/matching/mpmatching_utils.py @@ -8,7 +8,27 @@ from pytrack.matching import candidate SIGMA_Z = 4.07 -BETA = 20 +BETA = 3 + + +def _emission_prob(dist, sigma=SIGMA_Z): + """ Compute emission probability of a node + + Parameters + ---------- + dist: float + Distance between a real GPS point and a candidate node. + sigma: float, optional, default: SIGMA_Z + It is an estimate of the magnitude of the GPS error. See https://www.ismll.uni-hildesheim.de/lehre/semSpatial-10s/script/6.pdf + for a more detailed description of its calculation. + + Returns + ------- + ret: float + Emission probability of a node. + """ + c = 1 / (sigma * math.sqrt(2 * math.pi)) + return c * math.exp(-(dist / sigma) ** 2) # A gaussian distribution @@ -17,9 +37,9 @@ def emission_prob(u, sigma=SIGMA_Z): Parameters ---------- - u: dict + u: pytrack.matching.Candidate Node of the graph. - sigma: float + sigma: float, optional, default: SIGMA_Z It is an estimate of the magnitude of the GPS error. See https://www.ismll.uni-hildesheim.de/lehre/semSpatial-10s/script/6.pdf for a more detailed description of its calculation. @@ -56,13 +76,13 @@ def transition_prob(G, u, v, beta=BETA): ret: float Transition probability between node u and v. """ - c = 1 / BETA + c = 1 / beta if u.great_dist and v.great_dist: delta = abs( nx.shortest_path_length(G, u.node_id, v.node_id, weight="length", method='dijkstra') - distance.haversine_dist(*u.coord, *v.coord)) - return c * math.exp(-delta / BETA) * 1e5 + return c * math.exp(-delta / beta) else: return 1 diff --git a/setup.py b/setup.py index 5ae1a91..0866b93 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def reqs(*f): setuptools.setup( name='PyTrack-lib', - version='2.0.3', + version='2.0.4', packages=setuptools.find_packages(), # namespace_packages=['pytrack'], url='https://github.com/cosbidev/PyTrack',