first commit

main
yfe404 2023-06-29 06:52:38 +02:00
commit 1c6724b7d0
11 changed files with 14152 additions and 0 deletions

10
.gitignore vendored 100644
View File

@ -0,0 +1,10 @@
*~
venv/
__pycache__/
protos/*.py
route_service_pb2.py
route_service_pb2_grpc.py
*.o
*.so
*.cpp

46
Dockerfile 100644
View File

@ -0,0 +1,46 @@
FROM python:3.9-bullseye
MAINTAINER Yann Feunteun <yfe@protonmail>
RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.bz2
RUN apt update && apt install -y python3.9-dev
RUN tar --bzip2 -xf boost_1_77_0.tar.bz2
RUN cd boost_1_77_0 && ./bootstrap.sh --with-python-version=3.9 && ./b2
#apt-get install -y --no-install-recommends libboost-all-dev
RUN cp /boost_1_77_0/stage/lib/libboost_python39.so.1.77.0 /usr/lib
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY generate_graph.py generate_graph.py
COPY generator.py generator.py
COPY access.py access.py
COPY export_clean_splitted.json data.json
RUN python generate_graph.py
#COPY test.cc test.cc
RUN g++ -Ofast -c -fPIC -I/boost_1_77_0/ -I/usr/include/python3.9/ foot.cpp -o c_foot.o
RUN g++ -Ofast -c -fPIC -I/boost_1_77_0/ -I/usr/include/python3.9/ manual_wheelchair.cpp -o c_manual_wheelchair.o
RUN g++ -Ofast -c -fPIC -I/boost_1_77_0/ -I/usr/include/python3.9/ electric_wheelchair.cpp -o c_electric_wheelchair.o
RUN g++ -Ofast -L/boost_1_77_0/stage/lib -shared -Wl,-soname,test.so -o c_foot.so c_foot.o -lboost_python39 -lpython3.9
RUN g++ -Ofast -L/boost_1_77_0/stage/lib -shared -Wl,-soname,test.so -o c_manual_wheelchair.so c_manual_wheelchair.o -lboost_python39 -lpython3.9
RUN g++ -Ofast -L/boost_1_77_0/stage/lib -shared -Wl,-soname,test.so -o c_electric_wheelchair.so c_electric_wheelchair.o -lboost_python39 -lpython3.9
COPY protos protos
RUN python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/route_service.proto
COPY server.py /
EXPOSE 50051
ENTRYPOINT python server.py

16
README.org 100644
View File

@ -0,0 +1,16 @@
* grpc-routing-microservice
** Build with Docker
#+begin_src sh
docker build . -t routing
#+end_src
** Run with Docker
#+begin_src sh
docker run -it routing
#+end_src
** Benchmark (from within the container)
#+begin_src sh
python benchmark.py N_RUNS
#+end_src
=N_RUNS= denotes the number of time a random routing request will be
made. Runtimes are then averaged and returned by the script.

97
access.py 100644
View File

@ -0,0 +1,97 @@
from andyamo import types
def is_accessible(feature, profile: types.Profile):
feature_type = feature["feature"]["properties"]["type"]
feature_attributes = feature["attr"]
# |-------------- foot / crosswalk, sidewalk, stairs
if profile.value == "foot":
return True
elif profile.value == "manual_wheelchair":
# |-------------- manual_wheelchair / crosswalk
if feature_type == "passage_pieton":
predicates = [
feature_attributes["Hauteur"] == "Non abaissé",
feature_attributes["Marquage"] == "Non"
and feature_attributes["Feu tricolore"] == "Non",
feature_attributes["Feu tricolore"] == "Non"
and feature_attributes["Croisement"]
== "Piste cyclable bidirectionnelle",
]
not_accessible = sum(predicates) > 0
if not_accessible:
return False
# |-------------- manual_wheelchair / sidewalk
elif feature_type == "segment":
predicates = [
feature_attributes["Largeur"] == "Passe pas",
feature_attributes["Largeur"] == "Passe seul"
and feature_attributes["Obstacle"] == "Oui qui bloque le passage",
feature_attributes["Type de revêtement"] == "Impraticable",
feature_attributes["Type de revêtement"] == "Carrossable",
feature_attributes["Etat"] == "Très abimé",
feature_attributes["Inclinaison"] == "Très pentu",
feature_attributes["Dévers"] == "Très pentu",
feature_attributes["A vérifier"] == "En travaux",
]
not_accessible = sum(predicates) > 0
if not_accessible:
return False
# |-------------- manual_wheelchair / stairs
elif feature_type == "escalier":
return False
# |-------------- manual_wheelchair / catch all
else:
raise Exception(f"Unknown feature: {feature_type}")
return True
elif profile.value == "electric_wheelchair":
# |-------------- electric_wheelchair / crosswalk
if feature_type == "passage_pieton":
predicates = [
feature_attributes["Hauteur"] == "Non abaissé",
feature_attributes["Marquage"] == "Non"
and feature_attributes["Feu tricolore"] == "Non",
feature_attributes["Feu tricolore"] == "Non"
and feature_attributes["Croisement"]
== "Piste cyclable bidirectionnelle",
]
not_accessible = sum(predicates) > 0
if not_accessible:
return False
# |-------------- electric_wheelchair / sidewalk
elif feature_type == "segment":
predicates = [
feature_attributes["Largeur"] == "Passe pas",
feature_attributes["Largeur"] == "Passe seul"
and feature_attributes["Obstacle"] == "Oui qui bloque le passage",
feature_attributes["Type de revêtement"] == "Impraticable",
feature_attributes["Etat"] == "Très abimé",
feature_attributes["Inclinaison"] == "Très pentu",
feature_attributes["Dévers"] == "Très pentu",
feature_attributes["A vérifier"] == "En travaux",
]
not_accessible = sum(predicates) > 0
if not_accessible:
return False
# |-------------- electric_wheelchair / stairs
elif feature_type == "escalier":
return False
# |-------------- electric_wheelchair / catch all
else:
raise Exception(f"Unknown feature: {feature_type}")
return True

File diff suppressed because it is too large Load Diff

57
generate_graph.py 100644
View File

@ -0,0 +1,57 @@
import json
import random
from copy import deepcopy
from collections import namedtuple
import access
from andyamo import types
from generator import generate_graph
from geopy import distance as geo_distance
Edge = namedtuple("Edge", ["start_id", "end_id", "distance"])
with open("data.json", "r") as f:
data = [json.loads(x.strip()) for x in f.readlines()]
def get_graph(profile: types.Profile):
node_set = set()
edges = list()
for feature in data:
try:
if access.is_accessible(feature, profile):
nodes = feature["feature"]["properties"]["nodes"]
if len(nodes) == 2:
coordinates = feature["feature"]["geometry"]["coordinates"]
for node in nodes:
node_set.add(f"id_{node}")
distance = 0
for i in range(len(coordinates)-1):
distance += geo_distance.distance(
coordinates[i], coordinates[i+1]
).m
edges.append(Edge(f"id_{nodes[0]}", f"id_{nodes[1]}", distance))
except Exception as e :
print(feature)
raise(e)
node_ids = list(node_set)
return node_ids, edges
node_ids, edges = get_graph(types.Profile("manual_wheelchair"))
generate_graph("manual_wheelchair.cpp", "c_manual_wheelchair", node_ids, edges)
node_ids, edges = get_graph(types.Profile("electric_wheelchair"))
generate_graph("electric_wheelchair.cpp", "c_electric_wheelchair", node_ids, edges)
node_ids, edges = get_graph(types.Profile("foot"))
generate_graph("foot.cpp", "c_foot", node_ids, edges)

170
generator.py 100644
View File

@ -0,0 +1,170 @@
def generate_graph(output_name, module_name, node_ids, edges):
with open(output_name, "w") as out:
out.write("""
#include <boost/config.hpp>
#include <boost/python.hpp>
#include <iostream>
#include <fstream>
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/dijkstra_shortest_paths.hpp>
#include <boost/property_map/property_map.hpp>
using namespace boost;
// create a typedef for the Graph type
typedef adjacency_list< vecS, vecS, undirectedS, no_property,
property< edge_weight_t, int > > graph_t;
typedef graph_traits< graph_t >::vertex_descriptor vertex_descriptor;
typedef std::pair< int, int > Edge;
int getIndex(std::vector<std::string>, std::string);
graph_t buildGraph();
""")
out.write(f"const int num_nodes = {len(node_ids)};\n\n")
out.write("enum nodes {\n")
for node_id in node_ids:
out.write(f" {node_id},\n")
out.write("};\n\n")
out.write('std::string _name[] = {')
for node_id in node_ids:
if node_id != node_ids[-1]:
out.write(f'"{str(node_id)}",')
else:
out.write(f'"{str(node_id)}"')
out.write('};\n')
out.write("std::vector<std::string> name(_name, _name + sizeof(_name)/sizeof(std::string));\n\n")
out.write("Edge edge_array[] = {\n")
for edge in edges:
if edge != edges[-1]:
out.write(f" Edge({edge.start_id}, {edge.end_id}),\n")
else:
out.write(f" Edge({edge.start_id}, {edge.end_id})\n")
out.write("};\n\n")
out.write("float weights[] = {")
for edge in edges:
if edge != edges[-1]:
out.write(f"{edge.distance}, ")
else:
out.write(f"{edge.distance}")
out.write("};\n")
out.write("""
std::string get_route(std::string start, std::string end) {
graph_t graph = buildGraph();
property_map< graph_t, edge_weight_t >::type weightmap = get(edge_weight, graph);
std::vector< vertex_descriptor > p(num_vertices(graph));
std::vector< int > d(num_vertices(graph));
int pos_start = getIndex(name, start);
int pos_end = getIndex(name, end);
if (pos_start == -1) {
std::string msg("Invalid id (not found) for start position");
throw std::invalid_argument(msg);
} else if (pos_end == -1) {
std::string msg("Invalid id (not found) for end position");
throw std::invalid_argument(msg);
}
vertex_descriptor s = vertex(nodes(pos_start), graph);
vertex_descriptor e = vertex(nodes(pos_end), graph);
dijkstra_shortest_paths(graph, s,
predecessor_map(boost::make_iterator_property_map(
p.begin(), get(boost::vertex_index, graph)))
.distance_map(boost::make_iterator_property_map(
d.begin(), get(boost::vertex_index, graph))));
//find path from start to end vertex
std::list<vertex_descriptor> pathVertices;
int distance = d[e];
while (e != s)
{
pathVertices.push_front(e);
if(name[e] == name[p[e]]) // see https://stackoverflow.com/questions/48367649/boost-dijkstra-shortest-paths-cant-extract-or-find-the-path-path-contains
break;
e = p[e];
}
pathVertices.push_front(s); // add starting point to path
std::string path;
for (vertex_descriptor n : pathVertices) {
path += name[n];
path += " ";
}
path += ":";
path += std::to_string(distance);
return path;
}
graph_t buildGraph() {
int num_arcs = sizeof(edge_array) / sizeof(Edge);
graph_t g(edge_array, edge_array + num_arcs, weights, num_nodes);
return g;
}
int getIndex(std::vector<std::string> v, std::string element)
{
auto it = find(v.begin(), v.end(), element);
// If element was found
if (it != v.end())
{
// calculating the index
// of element
int index = it - v.begin();
//std::cout << index << std::endl;
return index;
}
else {
// If the element is not
// present in the vector
return -1;
}
}
""")
out.write(f"BOOST_PYTHON_MODULE({module_name}) // module name must match the name of the .so compiled")
out.write("""
{
using namespace boost::python;
def("get_route", get_route);
}
""")

View File

@ -0,0 +1,35 @@
syntax = "proto3";
// Interface exported by the server.
service RouteService {
rpc ShortestPath(RouteRequest) returns (RouteResponse) {}
rpc ListNodes(Profile) returns (stream Node) {}
}
message RouteRequest {
string start = 1;
string end = 2;
Profile profile = 3;
}
message Profile {
enum Profile {
FOOT = 0;
MANUAL_WHEELCHAIR = 1;
ELECTRIC_WHEELCHAIR = 2;
}
Profile profile = 1;
}
message RouteResponse {
repeated string route = 1;
int32 distance = 2;
}
message Node {
string index = 1;
float latitude = 2;
float longitude = 3;
}

5
requirements.txt 100644
View File

@ -0,0 +1,5 @@
-i https://pypi.andyamo.fr/simple
andyamo==0.1.2
grpcio-tools==1.44.0
geographiclib==1.52
geopy==2.2.0

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: route_service.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13route_service.proto\"E\n\x0cRouteRequest\x12\r\n\x05start\x18\x01 \x01(\t\x12\x0b\n\x03\x65nd\x18\x02 \x01(\t\x12\x19\n\x07profile\x18\x03 \x01(\x0b\x32\x08.Profile\"q\n\x07Profile\x12!\n\x07profile\x18\x01 \x01(\x0e\x32\x10.Profile.Profile\"C\n\x07Profile\x12\x08\n\x04\x46OOT\x10\x00\x12\x15\n\x11MANUAL_WHEELCHAIR\x10\x01\x12\x17\n\x13\x45LECTRIC_WHEELCHAIR\x10\x02\"0\n\rRouteResponse\x12\r\n\x05route\x18\x01 \x03(\t\x12\x10\n\x08\x64istance\x18\x02 \x01(\x05\":\n\x04Node\x12\r\n\x05index\x18\x01 \x01(\t\x12\x10\n\x08latitude\x18\x02 \x01(\x02\x12\x11\n\tlongitude\x18\x03 \x01(\x02\x32\x61\n\x0cRouteService\x12/\n\x0cShortestPath\x12\r.RouteRequest\x1a\x0e.RouteResponse\"\x00\x12 \n\tListNodes\x12\x08.Profile\x1a\x05.Node\"\x00\x30\x01\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'route_service_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_ROUTEREQUEST._serialized_start=23
_ROUTEREQUEST._serialized_end=92
_PROFILE._serialized_start=94
_PROFILE._serialized_end=207
_PROFILE_PROFILE._serialized_start=140
_PROFILE_PROFILE._serialized_end=207
_ROUTERESPONSE._serialized_start=209
_ROUTERESPONSE._serialized_end=257
_NODE._serialized_start=259
_NODE._serialized_end=317
_ROUTESERVICE._serialized_start=319
_ROUTESERVICE._serialized_end=416
# @@protoc_insertion_point(module_scope)

82
server.py 100644
View File

@ -0,0 +1,82 @@
import sys
import timeit
from concurrent import futures
import logging
import math
import time
import json
import grpc
import route_service_pb2
import route_service_pb2_grpc
from andyamo import types
import c_foot
import c_manual_wheelchair
import c_electric_wheelchair
import access
class RouteServiceServicer(route_service_pb2_grpc.RouteService):
"""Provides methods that implement functionality of routing server."""
def __init__(self):
pass
def ShortestPath(self, request, context):
_profile = request.profile.profile
profile_as_str = route_service_pb2._PROFILE_PROFILE.values_by_number[_profile].name
profile = types.Profile[profile_as_str]
if profile == types.Profile.FOOT:
res = c_foot.get_route(request.start, request.end)
elif profile == types.Profile.MANUAL_WHEELCHAIR:
res = c_manual_wheelchair.get_route(request.start, request.end)
elif profile == types.Profile.ELECTRIC_WHEELCHAIR:
res = c_electric_wheelchair.get_route(request.start, request.end)
else:
raise NotImplementedError
route_str, distance_str = res.split(":")
route = route_str.strip().split(" ")
distance = int(distance_str.strip())
return route_service_pb2.RouteResponse(route=route, distance=distance)
def ListNodes(self, request, context):
_profile = request.profile
profile_as_str = route_service_pb2._PROFILE_PROFILE.values_by_number[_profile].name
print(profile_as_str)
profile = types.Profile[profile_as_str]
with open("data.json", "r") as f:
for line in f.readlines():
feature = json.loads(line.strip())
if access.is_accessible(feature, profile):
coords = feature["feature"]["geometry"]["coordinates"]
indexes = feature["feature"]["properties"]["nodes"]
for coord, index in zip(coords, indexes):
yield route_service_pb2.Node(index=f"id_{index}", latitude=coord[1], longitude=coord[0])
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_service_pb2_grpc.add_RouteServiceServicer_to_server(
RouteServiceServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
logging.basicConfig()
serve()