Skip to content

Commit 469301f

Browse files
authored
Merge pull request #195 from 52North/feature/36-speed-fixed-route
Enable speed optimisation for a fixed route
2 parents 4c5d883 + 318183a commit 469301f

8 files changed

Lines changed: 472 additions & 18 deletions

File tree

WeatherRoutingTool/algorithms/genetic/__init__.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,30 @@ def optimize(
162162

163163
# FIXME temporary consistency check
164164
def consistency_check(self, res, problem):
165+
"""
166+
Temporary consistency check to uncover memory issues.
167+
"""
165168
X = res.X
169+
res_objs = res.F
166170
i_route = 0
171+
172+
# solve shape issue in case there is only one objective
173+
if self.n_objs == 1:
174+
res_objs = np.array([res.F])
175+
X = [X]
176+
167177
for route in X:
168178
fuel_dict = problem.get_power(route[0])
169179

170-
i_obj = 0
180+
# ordering of objective values in res.F is defined by RoutingProblem.get_objectives()
171181
for obj_str in self.objectives:
172182
if obj_str == "fuel_consumption":
173-
np.testing.assert_equal(fuel_dict["fuel_sum"].value, res.F[i_route, i_obj], 5)
183+
i_obj = 1
184+
if self.n_objs == 1:
185+
i_obj = 0
186+
np.testing.assert_equal(fuel_dict["fuel_sum"].value, res_objs[i_route, i_obj], 5)
174187
else:
175-
np.testing.assert_equal(fuel_dict["time_obj"], res.F[i_route, i_obj], 5)
176-
i_obj += 1
188+
np.testing.assert_equal(fuel_dict["time_obj"], res_objs[i_route, 0], 5)
177189
i_route += 1
178190

179191
def terminate(self, res: Result, problem: RoutingProblem):
@@ -196,6 +208,7 @@ def terminate(self, res: Result, problem: RoutingProblem):
196208

197209
self.plot_running_metric(res)
198210
self.plot_population_per_generation(res, best_route)
211+
self.plot_speed_per_generation(res, best_route)
199212
self.plot_convergence(res)
200213
self.plot_coverage(res, best_route)
201214
self.plot_objective_space(res, best_index)
@@ -343,8 +356,62 @@ def plot_running_metric(self, res):
343356
plt.cla()
344357
plt.close()
345358

359+
def plot_speed_per_generation(self, res, best_route) -> None:
360+
"""Plot line diagrams of speed vs. travel distance for each individual in one generation.
361+
362+
:param res: Result of GA minimization
363+
:type res: pymoo.core.result.Result
364+
:param best_route: Optimum route
365+
:type best_route: np.ndarray
366+
"""
367+
history = res.history
368+
369+
for igen in range(len(history)):
370+
plt.clf()
371+
plt.close('all')
372+
373+
fig, ax = plt.subplots(figsize=graphics.get_standard('fig_size'))
374+
plt.rcParams['font.size'] = graphics.get_standard('font_size')
375+
376+
last_pop = history[igen].pop.get('X')
377+
objs = []
378+
for iroute in range(0, last_pop.shape[0]):
379+
hist_values = utils.get_hist_values_from_route(last_pop[iroute, 0], self.departure_time)
380+
381+
new_line = ax.plot(
382+
hist_values["bin_centres"].to(u.km).value,
383+
hist_values["bin_contents"].to(u.m / u.second).value,
384+
color="blue",
385+
alpha=0.3,
386+
linestyle='-',
387+
zorder=2
388+
)
389+
objs.append(new_line)
390+
391+
if igen == (self.n_generations - 1):
392+
hist_values_best_route = utils.get_hist_values_from_route(best_route, self.departure_time)
393+
ax.plot(
394+
hist_values_best_route["bin_centres"].to(u.km).value,
395+
hist_values_best_route["bin_contents"].to(u.m / u.second).value,
396+
color="firebrick",
397+
linewidth=3
398+
)
399+
left, right = plt.xlim()
400+
ax.set_xlim(-100, right)
401+
ax.set_ylim(0, 10)
402+
403+
plt.ylabel("speed (m/s)")
404+
plt.xlabel('travel distance (km)')
405+
plt.xticks()
406+
plt.tight_layout()
407+
ax.legend()
408+
409+
figname = f"genetic_algorithm_speed {igen:02}.png"
410+
plt.savefig(os.path.join(self.figure_path, figname))
411+
plt.close(fig)
412+
346413
def plot_population_per_generation(self, res, best_route):
347-
"""Plot figures and save them in WRT_FIGURE_PATH
414+
"""Plot routes for each individual in one generation on a map.
348415
349416
:param res: Result of GA minimization
350417
:type res: pymoo.core.result.Result
@@ -413,7 +480,6 @@ def plot_population_per_generation(self, res, best_route):
413480
cbar = fig.colorbar(route_lc, ax=ax, orientation='vertical', pad=0.15, shrink=0.7)
414481
cbar.set_label('Geschwindigkeit ($m/s$)')
415482
plt.tight_layout()
416-
417483
ax.legend()
418484

419485
figname = f"genetic_algorithm_generation {igen:02}.png"

WeatherRoutingTool/algorithms/genetic/crossover.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,55 @@ def crossover(
281281
return o1, o2
282282

283283

284+
class TwoPointCrossoverSpeed(OffspringRejectionCrossover):
285+
"""
286+
Class for two-point crossover of ship speed.
287+
288+
The ship speed of a random sequence of one chromosome is replaced by the average ship speed of a random sequence
289+
of another individual.
290+
"""
291+
292+
def __init__(self, **kw):
293+
super().__init__(**kw)
294+
295+
def crossover(self, p1, p2) -> tuple[np.ndarray, np.ndarray]:
296+
"""
297+
Crossover implementation for two-point crossover of ship speed.
298+
299+
:param p1: the first chromosome
300+
:param p2: the second chromosome
301+
:return: the two offspring chromosomes
302+
:rtype: tuple[np.ndarray, np.ndarray]
303+
"""
304+
r1 = deepcopy(p1)
305+
r2 = deepcopy(p2)
306+
307+
p1x1 = np.random.randint(1, p1.shape[0] - 4)
308+
p1x2 = p1x1 + np.random.randint(3, p1.shape[0] - p1x1 - 1)
309+
310+
p2x1 = np.random.randint(1, p2.shape[0] - 4)
311+
p2x2 = p2x1 + np.random.randint(3, p2.shape[0] - p2x1 - 1)
312+
313+
speed1 = p1[:, -1]
314+
speed2 = p2[:, -1]
315+
av_speed_seg1 = np.average(speed1[p1x1:p1x2 + 1])
316+
av_speed_seg2 = np.average(speed2[p2x1:p2x2 + 1])
317+
318+
new_speed1 = np.concatenate([
319+
speed1[:p1x1],
320+
np.full(p1x2 - p1x1, av_speed_seg2),
321+
speed1[p1x2:], ])
322+
new_speed2 = np.concatenate([
323+
speed2[:p2x1],
324+
np.full(p2x2 - p2x1, av_speed_seg1),
325+
speed2[p2x2:], ])
326+
327+
r1[:, -1] = new_speed1
328+
r2[:, -1] = new_speed2
329+
330+
return r1, r2
331+
332+
284333
# factory
285334
# ----------
286335
class CrossoverFactory:
@@ -292,12 +341,12 @@ def get_crossover(config: Config, constraints_list: ConstraintsList):
292341

293342
if config.GENETIC_CROSSOVER_TYPE == "speed":
294343
logger.debug('Setting crossover type of genetic algorithm to "speed".')
295-
return SpeedCrossover(
344+
return TwoPointCrossoverSpeed(
296345
config=config,
297346
departure_time=departure_time,
298347
constraints_list=constraints_list,
299348
prob=.5,
300-
crossover_type="Speed crossover")
349+
crossover_type="TP Crossover speed")
301350

302351
if config.GENETIC_CROSSOVER_TYPE == "waypoints":
303352
logger.debug('Setting crossover type of genetic algorithm to "random".')

WeatherRoutingTool/algorithms/genetic/mutation.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ class NoMutation(MutationBase):
147147
def _do(self, problem, X, **kw):
148148
return X
149149

150+
def print_mutation_statistics(self):
151+
print('No mutation.')
152+
150153

151154
class RandomPlateauMutation(MutationConstraintRejection):
152155
"""
@@ -574,7 +577,7 @@ class GaussianSpeedMutation(MutationConstraintRejection):
574577
n_updates: int
575578
config: Config
576579

577-
def __init__(self, n_updates: int = 10, **kw):
580+
def __init__(self, n_updates: int = 5, **kw):
578581
super().__init__(
579582
mutation_type="GaussianSpeedMutation",
580583
**kw
@@ -583,7 +586,7 @@ def __init__(self, n_updates: int = 10, **kw):
583586
# FIXME: these numbers should be carefully evaluated
584587
# ~99.7 % in interval (0, BOAT_SPEED_MAX)
585588
self.mu = 0.5 * self.config.BOAT_SPEED_BOUNDARIES[1]
586-
self.sigma = self.config.BOAT_SPEED_BOUNDARIES[1] / 6
589+
self.sigma = 1.
587590

588591
def mutate(self, problem, rt, **kw):
589592
rt_new = copy.deepcopy(rt)
@@ -600,6 +603,7 @@ def mutate(self, problem, rt, **kw):
600603
new = old_speed
601604
rt_new[i][2] = new
602605

606+
rt_new[:, 2] = utils.smoothen_speed(rt_new[:, 2], 1)
603607
return rt_new
604608

605609

WeatherRoutingTool/algorithms/genetic/population.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,53 @@ def __init__(self, config: Config, routes_dir: Path, default_route, constraints_
190190
constraints_list=constraints_list,
191191
pop_size=pop_size
192192
)
193+
self.sole_speed_mutation = config.GENETIC_MUTATION_TYPE == "speed" and config.GENETIC_CROSSOVER_TYPE == "speed"
194+
self.min_boat_speed = config.BOAT_SPEED_BOUNDARIES[0]
195+
self.max_boat_speed = config.BOAT_SPEED_BOUNDARIES[1]
193196

194197
if not routes_dir.exists() or not routes_dir.is_dir():
195198
raise FileNotFoundError("Routes directory not found")
196199
self.routes_dir = routes_dir
197200

201+
@staticmethod
202+
def spread_velocity(min_boat_speed: float, max_boat_speed: float, boat_speed: float, pop_size: float) -> np.ndarray:
203+
"""
204+
Calculate velocity spread for individuals in case of pure speed optimisation.
205+
206+
The following steps are performed to obtain values for the boat speed that are accumulated around the original
207+
boat speed:
208+
- sample a numpy array from a gaussian distribution (mean = original boat speed,
209+
sigma = 1/4 possible value range of boat speed)
210+
- cut values below the minimum velocity and above the maximum velocity
211+
- determine `pop_size` quantiles with equally spaced probabilities
212+
213+
:param min_boat_speed: Minimum boat speed
214+
:type min_boat_speed: float
215+
:param max_boat_speed: Maximum boat speed
216+
:type max_boat_speed: float
217+
:param boat_speed: Boat speed
218+
:type boat_speed: float
219+
:param pop_size: Population size
220+
:type pop_size: int
221+
:return: array of quantiles
222+
:rtype: np.ndarray
223+
"""
224+
std_dev = (max_boat_speed - min_boat_speed) / 4
225+
gaussian_sample = np.random.normal(boat_speed, std_dev, 1000)
226+
gaussian_sample[gaussian_sample < min_boat_speed] = np.nan
227+
gaussian_sample[gaussian_sample > max_boat_speed] = np.nan
228+
gaussian_sample = gaussian_sample[~np.isnan(gaussian_sample)]
229+
230+
quant_size = 100. / pop_size * 0.01
231+
232+
quantiles = np.full(pop_size, np.nan)
233+
quant_sum = 0
234+
for q in range(pop_size):
235+
quantiles[q] = np.quantile(gaussian_sample, q=quant_sum)
236+
quant_sum += quant_size
237+
238+
return quantiles
239+
198240
def generate(self, problem, n_samples, **kw):
199241
logger.debug(f"Population from geojson routes: {self.routes_dir}")
200242

@@ -205,15 +247,22 @@ def generate(self, problem, n_samples, **kw):
205247
# FIXME: add test in config.py and raise exception depending on configuration (not only speed optimization...)
206248

207249
X = np.full((n_samples, 1), None, dtype=object)
250+
quantiles = None
251+
252+
# determine quantiles for pure speed optimisation
253+
if self.sole_speed_mutation:
254+
quantiles = self.spread_velocity(self.min_boat_speed, self.max_boat_speed, self.boat_speed.value,
255+
self.pop_size)
208256

257+
# obtain list of filenames
209258
files = []
210259
for file in os.listdir(self.routes_dir):
211260
if match(r"route_[0-9]+\.(json|geojson)$", file.lower()):
212261
files.append(file)
213-
214262
if len(files) == 0:
215263
raise ValueError(f"Couldn't find any route in {self.routes_dir} for the initial population.")
216264

265+
# read route(s) from file
217266
for i, file in enumerate(files):
218267
path = os.path.join(self.routes_dir, file)
219268
if not os.path.exists(path):
@@ -227,6 +276,12 @@ def generate(self, problem, n_samples, **kw):
227276
X[added_routes, 0] = np.copy(X[0, 0])
228277
added_routes += 1
229278

279+
# mutate velocity in case of pure speed optimisation
280+
if self.sole_speed_mutation:
281+
for i, (rt,) in enumerate(X):
282+
rt[:, -1] = quantiles[i]
283+
rt[-1, -1] = -99.
284+
230285
return X
231286

232287

0 commit comments

Comments
 (0)