diff --git a/README.md b/README.md index 8be7e3a..1bb88fe 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Optional variables (default values provided and don't need to be changed normall - `GENETIC_NUMBER_GENERATIONS`: number of generations for genetic algorithm - `GENETIC_NUMBER_OFFSPRINGS`: number of offsprings for genetic algorithm - `GENETIC_POPULATION_SIZE`: population size for genetic algorithm -- `GENETIC_POPULATION_TYPE`: type for initial population (options: 'grid_based') +- `GENETIC_POPULATION_TYPE`: type for initial population (options: 'grid_based', 'from_geojson') - `INTERMEDIATE_WAYPOINTS`: [[lat_one,lon_one], [lat_two,lon_two] ... ] - `ISOCHRONE_MAX_ROUTING_STEPS`: maximum number of routing steps. Applies also if more than one route is searched! - `ISOCHRONE_MINIMISATION_CRITERION`: options: 'dist', 'squareddist_over_disttodest' diff --git a/WeatherRoutingTool/algorithms/genetic.py b/WeatherRoutingTool/algorithms/genetic.py index 76be7a9..dba9f16 100644 --- a/WeatherRoutingTool/algorithms/genetic.py +++ b/WeatherRoutingTool/algorithms/genetic.py @@ -28,20 +28,20 @@ class Genetic(RoutingAlg): fig: matplotlib.figure - route_ensemble: list - route: np.array # temp pop_size: int n_offsprings: int default_map: Map weather_path: str + path_to_route_folder: str def __init__(self, config) -> None: super().__init__(config) self.default_map = config.DEFAULT_MAP self.weather_path = config.WEATHER_DATA + self.path_to_route_folder = config.ROUTE_PATH self.ncount = config.GENETIC_NUMBER_GENERATIONS # ToDo: use better name than ncount? self.count = 0 @@ -61,6 +61,7 @@ def execute_routing(self, boat: Boat, wt: WeatherCond, constraints_list: Constra wave_height = wave_height[::lat_int, ::lon_int] problem = RoutingProblem(departure_time=self.departure_time, boat=boat, constraint_list=constraints_list) initial_population = PopulationFactory.get_population(self.population_type, self.start, self.finish, + path_to_route_folder=self.path_to_route_folder, grid=wave_height) mutation = MutationFactory.get_mutation(self.mutation_type, grid=wave_height) crossover = CrossoverFactory.get_crossover() @@ -107,13 +108,13 @@ def terminate(self, **kwargs): waypoint_coors = RouteParams.get_per_waypoint_coords(lons, lats, self.departure_time, speed) dists = waypoint_coors['dist'] - bearings = waypoint_coors['courses'] + courses = waypoint_coors['courses'] start_times = waypoint_coors['start_times'] travel_times = waypoint_coors['travel_times'] arrival_time = start_times[-1] + timedelta(seconds=dists[-1]/speed) dists = np.append(dists, -99) - bearings = np.append(bearings, -99) + courses = np.append(courses, -99) start_times = np.append(start_times, arrival_time) travel_times = np.append(travel_times, -99) @@ -125,7 +126,7 @@ def terminate(self, **kwargs): time=travel_times, lats_per_step=lats, lons_per_step=lons, - azimuths_per_step=bearings, + course_per_step=courses, dists_per_step=dists, starttime_per_step=start_times, ship_params_per_step=self.ship_params) diff --git a/WeatherRoutingTool/algorithms/genetic_utils.py b/WeatherRoutingTool/algorithms/genetic_utils.py index ea55422..27a7975 100644 --- a/WeatherRoutingTool/algorithms/genetic_utils.py +++ b/WeatherRoutingTool/algorithms/genetic_utils.py @@ -1,11 +1,11 @@ +import json import logging import os import random +from math import ceil -import cartopy.crs as ccrs -import cartopy.feature as cf import numpy as np -from matplotlib import pyplot as plt +from geographiclib.geodesic import Geodesic from pymoo.core.crossover import Crossover from pymoo.core.duplicate import ElementwiseDuplicateElimination from pymoo.core.mutation import Mutation @@ -13,9 +13,9 @@ from pymoo.core.sampling import Sampling from skimage.graph import route_through_array -import WeatherRoutingTool.utils.graphics as graphics from WeatherRoutingTool.algorithms.data_utils import GridMixin from WeatherRoutingTool.routeparams import RouteParams +from WeatherRoutingTool.utils.graphics import plot_genetic_algorithm_initial_population logger = logging.getLogger('WRT.Genetic') @@ -47,28 +47,88 @@ def _do(self, problem, n_samples, **kwargs): _, _, route = self.index_to_coords(route) routes[i][0] = np.array(route) - figure_path = graphics.get_figure_path() - if figure_path is not None: - plt.rcParams['font.size'] = graphics.get_standard('font_size') - fig, ax = plt.subplots(figsize=graphics.get_standard('fig_size')) - ax.remove() - fig, ax = graphics.generate_basemap(fig, None, self.src, self.dest, '', False) - for i in range(0, n_samples): - ax.plot(routes[i, 0][:, 1], routes[i, 0][:, 0], color="firebrick") - plt.savefig(os.path.join(figure_path, 'genetic_algorithm_initial_population.png')) + plot_genetic_algorithm_initial_population(self.src, self.dest, routes) + self.X = routes + return self.X + + +class FromGeojsonPopulation(Sampling): + """ + Make initial population for genetic algorithm based on the isofuel algorithm with a ConstantFuelBoat + """ + def __init__(self, src, dest, path_to_route_folder, var_type=np.float64): + super().__init__() + self.var_type = var_type + self.src = src + self.dest = dest + self.path_to_route_folder = path_to_route_folder + def _do(self, problem, n_samples, **kwargs): + routes = np.full((n_samples, 1), None, dtype=object) + # Routes have to be named route_1.json, route_2.json, etc. + # See method find_routes_reaching_destination_in_current_step in isobased.py + for i in range(n_samples): + route_file = os.path.join(self.path_to_route_folder, f'route_{i+1}.json') + try: + route = self.read_route_from_file(route_file) + routes[i][0] = np.array(route) + except FileNotFoundError: + logger.warning(f"File '{route_file}' couldn't be found. Use great circle route instead.") + route = self.get_great_circle_route() + routes[i][0] = np.array(route) + + plot_genetic_algorithm_initial_population(self.src, self.dest, routes) self.X = routes return self.X + def get_great_circle_route(self, distance=100000): + """ + Get equidistant route along great circle in the form [[lat1, lon1], [lat12, lon2], ...] + :param distance: distance in m + :return: route as list of lat/lon points + """ + geod = Geodesic.WGS84 + line = geod.InverseLine(self.src[0], self.src[1], self.dest[0], self.dest[1]) + n = int(ceil(line.s13 / distance)) + route = [] + for i in range(n+1): + s = min(distance * i, line.s13) + g = line.Position(s, Geodesic.STANDARD | Geodesic.LONG_UNROLL) + route.append([g['lat2'], g['lon2']]) + return route + + def read_route_from_file(self, route_absolute_path): + """ + Read route from geojson file and return the coordinates in the form [[lat1, lon1], [lat12, lon2], ...] + :param route_absolute_path: absolute path to geojson file + :return: route as list of lat/lon points + """ + with open(route_absolute_path) as file: + rp_dict = json.load(file) + route = [[feature['geometry']['coordinates'][1], feature['geometry']['coordinates'][0]] + for feature in rp_dict['features']] + return route + class PopulationFactory: def __init__(self): pass @staticmethod - def get_population(population_type, src, dest, grid=None): + def get_population(population_type, src, dest, path_to_route_folder=None, grid=None): if population_type == 'grid_based': + if grid is None: + msg = f"For population type '{population_type}', a grid has to be provided!" + logger.error(msg) + raise ValueError(msg) population = GridBasedPopulation(src, dest, grid) + elif population_type == 'from_geojson': + if (not path_to_route_folder or not os.path.isdir(path_to_route_folder) or + not os.access(path_to_route_folder, os.R_OK)): + msg = f"For population type '{population_type}', a valid route path has to be provided!" + logger.error(msg) + raise ValueError(msg) + population = FromGeojsonPopulation(src, dest, path_to_route_folder) else: msg = f"Population type '{population_type}' is invalid!" logger.error(msg) @@ -223,10 +283,10 @@ def get_constraints(self, route): def get_power(self, route): route_dict = RouteParams.get_per_waypoint_coords(route[:, 1], route[:, 0], self.departure_time, - self.boat.boat_speed_function()) + self.boat.get_boat_speed()) - shipparams = self.boat.get_fuel_per_time_netCDF(route_dict['courses'], route_dict['start_lats'], - route_dict['start_lons'], route_dict['start_times']) + shipparams = self.boat.get_ship_parameters(route_dict['courses'], route_dict['start_lats'], + route_dict['start_lons'], route_dict['start_times'], []) fuel = shipparams.get_fuel() fuel = (fuel / 3600) * route_dict['travel_times'] return np.sum(fuel), shipparams diff --git a/WeatherRoutingTool/config.py b/WeatherRoutingTool/config.py index 439db5c..d14d47f 100644 --- a/WeatherRoutingTool/config.py +++ b/WeatherRoutingTool/config.py @@ -67,7 +67,7 @@ def __init__(self, init_mode='from_json', file_name=None, config_dict=None): self.GENETIC_NUMBER_GENERATIONS = None # number of generations for genetic algorithm self.GENETIC_NUMBER_OFFSPRINGS = None # number of offsprings for genetic algorithm self.GENETIC_POPULATION_SIZE = None # population size for genetic algorithm - self.GENETIC_POPULATION_TYPE = None # type for initial population (options: 'grid_based') + self.GENETIC_POPULATION_TYPE = None # type for initial population (options: 'grid_based', 'from_geojson') self.INTERMEDIATE_WAYPOINTS = None # [[lat_one,lon_one], [lat_two,lon_two] ... ] self.ISOCHRONE_MAX_ROUTING_STEPS = None # maximum number of routing steps self.ISOCHRONE_MINIMISATION_CRITERION = None # options: 'dist', 'squareddist_over_disttodest' diff --git a/WeatherRoutingTool/utils/graphics.py b/WeatherRoutingTool/utils/graphics.py index 18735d9..59bc46e 100644 --- a/WeatherRoutingTool/utils/graphics.py +++ b/WeatherRoutingTool/utils/graphics.py @@ -292,3 +292,15 @@ def generate_basemap(fig, depth, start=None, finish=None, title='', show_depth=T plt.title(title) return fig, ax + + +def plot_genetic_algorithm_initial_population(src, dest, routes): + figure_path = get_figure_path() + if figure_path is not None: + plt.rcParams['font.size'] = get_standard('font_size') + fig, ax = plt.subplots(figsize=get_standard('fig_size')) + ax.remove() + fig, ax = generate_basemap(fig, None, src, dest, '', False) + for i in range(0, len(routes)): + ax.plot(routes[i, 0][:, 1], routes[i, 0][:, 0], color="firebrick") + plt.savefig(os.path.join(figure_path, 'genetic_algorithm_initial_population.png'))