Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
# Conflicts:
#	.gitignore
  • Loading branch information
kevinloeffler committed Dec 2, 2023
2 parents c76e628 + 9b82df7 commit c78db26
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 83 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ data/
.env
map-output.png
__pycache__/
todo
trained_models/
Binary file added lib/__pycache__/map_service.cpython-312.pyc
Binary file not shown.
Binary file modified lib/__pycache__/preprocessing.cpython-312.pyc
Binary file not shown.
55 changes: 35 additions & 20 deletions lib/map_service.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
from googlemaps.maps import StaticMapPath
from googlemaps.maps import StaticMapMarker
'''
Google Maps API library from:
https://github.com/googlemaps/google-maps-services-python/blob/master/tests/test_distance_matrix.py
'''
import googlemaps
import pandas as pd
import numpy as np
from datetime import datetime
from itertools import combinations
from googlemaps.maps import StaticMapPath
from googlemaps.maps import StaticMapMarker


class MapService:
def __init__(self, key: str, sensor_data: pd.DataFrame, day: datetime, n: int, station_0: tuple, empty_if_below: float):
def __init__(self, key: str, sensor_data: pd.DataFrame, day: datetime, n_sensors: int, station_0: tuple, no_empty_if_below: float):
self.gmaps = googlemaps.Client(key=key)
self.sensor_data = sensor_data
self.day = day
self.n = n
self.n_sensors = n_sensors
self.station_0 = station_0
self.empty_if_below = empty_if_below
self.no_empty_if_below = no_empty_if_below

def get_distances(self):
distances = np.zeros((self.n+1,self.n+1))
for (node_from, node_to) in combinations(range(self.n), 2):
dist_matrix = np.zeros((self.n_sensors+1,self.n_sensors+1))
for (node_from, node_to) in combinations(range(self.n_sensors), 2):
sensor_from = self.sensor_data.iloc[node_from]
sensor_to = self.sensor_data.iloc[node_to]
origins = [
Expand All @@ -38,16 +42,11 @@ def get_distances(self):
)
distance_in_seconds = matrix["rows"][0]["elements"][0]["duration"]["value"]

# TODO: check if this makes sense:
# Set weights of edges to inverse of level given by the sensor
cost_to = (distance_in_seconds / sensor_to["level"]) if sensor_to["level"] > self.empty_if_below else np.inf
cost_from = (distance_in_seconds / sensor_from["level"]) if sensor_from["level"] > self.empty_if_below else np.inf

distances[node_from][node_to] = cost_to
distances[node_to][node_from] = cost_from
dist_matrix[node_from][node_to] = distance_in_seconds
dist_matrix[node_to][node_from] = distance_in_seconds

# Get distances to sensor_0 which
for node_to in range(self.n):
for node_to in range(self.n_sensors):
sensor_to = self.sensor_data.iloc[node_to]
origins = [
self.station_0
Expand All @@ -66,12 +65,28 @@ def get_distances(self):
)
distance_in_seconds = matrix["rows"][0]["elements"][0]["duration"]["value"]

# TODO: check if this makes sense:
# Set weights of edges to inverse of level given by the sensor
distances[-1][node_to] = distance_in_seconds / sensor_to["level"]
distances[node_to][-1] = distance_in_seconds
dist_matrix[-1][node_to] = distance_in_seconds
dist_matrix[node_to][-1] = distance_in_seconds

return distances
return dist_matrix

def get_costs(self, dist_matrix):
cost_matrix = np.zeros((self.n_sensors+1,self.n_sensors+1))
for (node_from, node_to) in combinations(range(self.n_sensors), 2):
sensor_from = self.sensor_data.iloc[node_from]
sensor_to = self.sensor_data.iloc[node_to]

# Set weights of edges to inverse of level given by the sensor
cost_matrix[node_from][node_to] = (dist_matrix[node_from][node_to] / sensor_to["level"]) if sensor_to["level"] > self.no_empty_if_below else np.inf
cost_matrix[node_to][node_from] = (dist_matrix[node_to][node_from] / sensor_from["level"]) if sensor_from["level"] > self.no_empty_if_below else np.inf

for node_to in range(self.n_sensors):
sensor_to = self.sensor_data.iloc[node_to]
cost_matrix[-1][node_to] = dist_matrix[-1][node_to] / sensor_to["level"]
cost_matrix[node_to][-1] = dist_matrix[node_to][-1]

return cost_matrix

def generate_map(self, visited_stops: list, show_min_max_markers: bool = False):
points = []
Expand Down Expand Up @@ -128,4 +143,4 @@ def add_min_max_markers():
scale=4,
path=path,
markers=markers
)
)
138 changes: 75 additions & 63 deletions lib/path_finder.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,100 @@
'''
Calculate optimal route to empty glass containers. Units is in seconds.
Google Maps API library from:
https://github.com/googlemaps/google-maps-services-python/blob/master/tests/test_distance_matrix.py
Calculate optimal route to empty glass containers. Units are in seconds.
'''
from preprocessing import read_data
from map_service import MapService
import numpy as np
import pandas as pd
from datetime import datetime
import os

create_map = True
show_min_max_markers = False
data_file = 'data/fill-level.csv'
dist_file = 'data/distances.npy'
time_per_working_day = 8 * 60 * 60 # 8 hours in seconds
time_per_emptying = 30 * 60 # 15 minutes in seconds
day = datetime(2023, 12, 4, 10, 00) # Date of prediction
station_0 = (47.4156038, 9.3325804) # Assumption: Empyting starts and ends at Kehrichtheizkraftwerk St.Gallen
empty_if_below = 0.4
map_file = 'map-output.png'

with open('.env', 'r') as fh:
vars_dict = dict(
tuple(line.replace('\n', '').split('='))
for line in fh.readlines() if not line.startswith('#')
)
class PathFinder:
time_per_working_day = 8 * 60 * 60 # 8 hours in seconds
time_per_emptying = 30 * 60 # 15 minutes in seconds

sensor_data = read_data('data/fill-level.csv', use_coordinates=True)
sensor_data = sensor_data.loc[sensor_data.groupby('sensor_id').date.idxmax()] # Get sensor_id only once
#sensor_data = sensor_data[sensor_data['level'] > empty_if_below-0.1] # Don't use very low values to get more interesting results
def __init__(self, map_service: MapService, sensor_data: pd.DataFrame, station_0: tuple, n_sensors: int):
self.sensor_data = sensor_data
self.station_0 = station_0
self.map_service = map_service
self.n_sensors = n_sensors

# distances matrix: row=from, column=to
if os.path.isfile(dist_file):
# Reuse distances to reduce api calls
self.dist_matrix = np.load(dist_file)
else:
self.dist_matrix = self.map_service.get_distances()
np.save(dist_file, self.dist_matrix)

n = sensor_data.shape[0] if sensor_data.shape[0] < 50 else 50 # Take a look at the n highest sensor levels of the next day
def find_path(self):
cost_matrix = self.map_service.get_costs(self.dist_matrix)

map_service = MapService(vars_dict["MAPS_KEY"], sensor_data, day, n, station_0, empty_if_below)
needed_time = 0
current_stop_idx = -1 #station_0 index
visited_stops = []

# distances matrix: row=from, column=to
if os.path.isfile(dist_file):
# Reuse distances to reduce api calls
distances = np.load(dist_file)
else:
distances = map_service.get_distances()
np.save(dist_file, distances)
# distances[current_stop_idx, 0] is the time needed from the station to station_0
while (needed_time < (self.time_per_working_day - cost_matrix[current_stop_idx, -1] - self.time_per_emptying)):
visited_stops.append(current_stop_idx)

levels = [sensor_data.iloc[i]["level"] for i in range(n)]
if len(visited_stops) == self.n_sensors+1:
# all stops visited
break
min_cost = np.min(np.delete(cost_matrix[current_stop_idx,:], visited_stops, axis=0)) # Min cost of unvisited stops
for idx in np.argwhere(cost_matrix[current_stop_idx,:] == min_cost).ravel():
if idx not in visited_stops:
next_stop_idx = idx

actual_travel_time = self.sensor_data.iloc[next_stop_idx]["level"]
needed_time += actual_travel_time + self.time_per_emptying
current_stop_idx = next_stop_idx

needed_time = 0
current_stop_idx = -1 #station_0 index
visited_stops = []
visited_stops.append(-1) # End at station_0
needed_time += cost_matrix[current_stop_idx, -1] # Add time to go to station_0

# distances[current_stop_idx, 0] is the time needed from the station to station_0
while (needed_time < (time_per_working_day - distances[current_stop_idx, -1] - time_per_emptying)):
visited_stops.append(current_stop_idx)
return visited_stops, needed_time

if len(visited_stops) == n+1:
# all stops visited
break
min_cost = np.min(np.delete(distances[current_stop_idx,:], visited_stops, axis=0)) # Min cost of unvisited stops
for idx in np.argwhere(distances[current_stop_idx,:] == min_cost).ravel():
if idx not in visited_stops:
next_stop_idx = idx
if __name__ == "__main__":
with open('.env', 'r') as fh:
vars_dict = dict(
tuple(line.replace('\n', '').split('='))
for line in fh.readlines() if not line.startswith('#')
)

actual_travel_time = sensor_data.iloc[next_stop_idx]["level"]
needed_time += actual_travel_time + time_per_emptying
current_stop_idx = next_stop_idx
day = datetime(2023, 12, 4, 10, 00) # Date of prediction
n_sensors = 42 # Number of sensors in St. Gallen
no_empty_if_below = 0.4
station_0 = (47.4156038, 9.3325804) # Assumption: Empyting starts and ends at Kehrichtheizkraftwerk St.Gallen
sensor_data = read_data(data_file, use_coordinates=True)
sensor_data = sensor_data.loc[sensor_data.groupby('sensor_id').date.idxmax()] # Get sensor_id only once

map_service = MapService(vars_dict["MAPS_KEY"], sensor_data, day, n_sensors, station_0, no_empty_if_below)
path_finder = PathFinder(map_service, sensor_data, station_0, n_sensors)

levels = [sensor_data.iloc[i]["level"] for i in range(n_sensors)]

visited_stops.append(-1) # End at station_0
needed_time += distances[current_stop_idx, -1] # Add time to go to station_0
visited_stops, needed_time = path_finder.find_path()

print("Needed time:")
print(needed_time)
print("Trajectory:")
for stop in visited_stops:
if stop != -1:
print(f"{stop} -> {levels[stop]}")
print("Needed time:", needed_time)
print("Path:")
for stop in visited_stops:
if stop != -1:
print(f"{stop} -> {levels[stop]}")

unvisited_stops = np.setdiff1d(range(n), visited_stops)
print("Unvisited:")
print(unvisited_stops)
print("Filling levels:")
print(levels)
unvisited_stops = np.setdiff1d(range(n_sensors), visited_stops)
print("Unvisited Sensors:")
print(unvisited_stops)

if create_map:
map_response = map_service.generate_map(visited_stops, show_min_max_markers)
f = open("map-output.png", 'wb')
for chunk in map_response:
if chunk:
f.write(chunk)
f.close()
if create_map:
map_response = map_service.generate_map(visited_stops, show_min_max_markers)
f = open(map_file, 'wb')
for chunk in map_response:
if chunk:
f.write(chunk)
f.close()

0 comments on commit c78db26

Please sign in to comment.