Skip to content

Commit d51f9fa

Browse files
committed
feature: add constraint StayInTime
Add constraint that assures that Boat stays in the time coverage of the weather data. Only to be used for testing purposes.
1 parent 5f0f51b commit d51f9fa

8 files changed

Lines changed: 246 additions & 35 deletions

File tree

WeatherRoutingTool/algorithms/genetic/patcher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def _setup_components(self) -> tuple[WeatherCond, Boat, WaterDepth, ConstraintsL
241241
time_forecast,
242242
time_resolution,
243243
default_map, )
244+
time_frame = (wt.time_start, wt.time_end)
244245

245246
# *******************************************
246247
# initialise boat
@@ -265,7 +266,8 @@ def _setup_components(self) -> tuple[WeatherCond, Boat, WaterDepth, ConstraintsL
265266
map_size=default_map,
266267
depthfile=depthfile,
267268
waypoints=config.INTERMEDIATE_WAYPOINTS,
268-
courses_path=config.COURSES_FILE, )
269+
courses_path=config.COURSES_FILE,
270+
time_frame=time_frame)
269271

270272
return wt, boat, water_depth, constraints_list
271273

WeatherRoutingTool/algorithms/genetic/problem.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def _evaluate(self, x: np.ndarray, out: dict, *args, **kwargs) -> None:
100100

101101
# logger.debug(f"RoutingProblem._evaluate: type(x)={type(x)}, x.shape={x.shape}, x={x}")
102102
obj_dict = self.get_power(x[0])
103-
constraints = utils.get_constraints(x[0], self.constraint_list)
103+
constraints = utils.get_constraints(x[0], self.constraint_list, obj_dict["start_times"])
104104
out['F'] = self.get_objectives(obj_dict)
105105
out['G'] = np.column_stack([constraints])
106106

@@ -184,4 +184,5 @@ def get_power(self, route: np.array) -> dict:
184184
print('time_diff: ', time_diff)
185185
print('time obj.: ', time_obj)
186186

187-
return {"fuel_sum": fuel_obj, "shipparams": shipparams, "time_obj": time_obj}
187+
return {"fuel_sum": fuel_obj, "shipparams": shipparams, "time_obj": time_obj,
188+
"start_times": route_dict["start_times"]}

WeatherRoutingTool/algorithms/genetic/utils.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pymoo.core.duplicate import ElementwiseDuplicateElimination
1010

1111
import WeatherRoutingTool.utils.graphics as graphics
12+
from WeatherRoutingTool.constraints.constraints import ConstraintsList
1213
from WeatherRoutingTool.routeparams import RouteParams
1314

1415
logger = logging.getLogger("WRT.genetic")
@@ -84,13 +85,22 @@ def geojson_from_route(
8485
return geojson
8586

8687

87-
def get_constraints_array(route: np.ndarray, constraint_list) -> np.ndarray:
88-
"""Return constraint violation per waypoint in route
88+
def get_constraints_array(route: np.ndarray, constraint_list: ConstraintsList,
89+
start_times: np.ndarray = None) -> np.ndarray:
90+
"""Return constraint violation per waypoint in a route.
91+
92+
This function calls the constraint evaluation method of the constraint module for every waypoint of a route.
8993
9094
:param route: Candidate array of waypoints
9195
:type route: np.ndarray
96+
:param constraint_list: List of constraints that need to be evaluated
97+
:type constraint_list: ConstraintsList
98+
:param start_times: array of start times from every waypoint
99+
:type start_times: np.ndarray
92100
:return: Array of constraint violations
101+
:rtype: np.ndarray
93102
"""
103+
94104
lat = route[:, 0]
95105
lon = route[:, 1]
96106
is_constrained = [False for i in range(0, lat.shape[0] - 1)]
@@ -100,21 +110,40 @@ def get_constraints_array(route: np.ndarray, constraint_list) -> np.ndarray:
100110
lon_start = lon[:-1]
101111
lon_end = lon[1:]
102112

103-
is_constrained = constraint_list.safe_crossing(lat_start, lon_start, lat_end, lon_end, None, is_constrained)
113+
is_constrained = constraint_list.safe_crossing(
114+
lat_start=lat_start,
115+
lon_start=lon_start,
116+
lat_end=lat_end,
117+
lon_end=lon_end,
118+
start_times=start_times,
119+
is_constrained=is_constrained
120+
)
104121
return is_constrained
105122

106123

107-
def get_constraints(route, constraint_list):
108-
"""Get sum of constraint violations of all waypoints of the provided route
124+
def get_constraints(route: np.ndarray, constraint_list: ConstraintsList, start_times: np.ndarray = None) -> np.ndarray:
125+
"""Get sum of constraint violations of all waypoints of the provided route.
126+
127+
Currently, the start times from a waypoint are only passed by RoutingProblem._evaluate. This means that restrictions
128+
with respect to arrival time are only enforced in the evaluation process and not in the Mutation, Crossover or
129+
Population classes.
130+
# TODO enable support of constraints with a time dependence also in Mutation, Crossover and Population classes
109131
110132
:param route: List of waypoints
111133
:type route: np.ndarray
112-
:param constraints_list: List of constraints configured by the config
113-
:type constraints_list: ConstraintsList
134+
:param constraint_list: List of constraints that need to be evaluated
135+
:type constraint_list: ConstraintsList
136+
:param start_times: array of start times from every waypoint
137+
:type start_times: np.ndarray
138+
:return: Array of constraint violations
139+
:rtype: np.ndarray
114140
"""
115141

116-
# ToDo: what about time?
117-
constraints = np.sum(get_constraints_array(route, constraint_list))
142+
constraints = np.sum(get_constraints_array(
143+
route=route,
144+
constraint_list=constraint_list,
145+
start_times=start_times)
146+
)
118147
return constraints
119148

120149

WeatherRoutingTool/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ class Config(BaseModel):
6464
# minimum and maximum possible boat speed [m/s]
6565
CONSTRAINTS_LIST: List[Literal[
6666
'land_crossing_global_land_mask', 'land_crossing_polygons', 'seamarks',
67-
'water_depth', 'on_map', 'via_waypoints', 'status_error'
67+
'water_depth', 'on_map', 'in_time', 'via_waypoints', 'status_error'
6868
]]
6969
# options: 'land_crossing_global_land_mask', 'land_crossing_polygons',
70-
# 'seamarks','water_depth', 'on_map', 'via_waypoints', 'status_error'
70+
# 'seamarks','water_depth', 'on_map', 'in_time', 'via_waypoints', 'status_error'
7171

7272
_DATA_MODE_DEPTH: str = PrivateAttr('from_file') # options: 'automatic', 'from_file', 'odc', 'skip'
7373
_DATA_MODE_WEATHER: str = PrivateAttr('from_file') # options: 'automatic', 'from_file', 'odc', 'skip'

WeatherRoutingTool/constraints/constraints.py

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import logging
3+
from datetime import datetime
34

45
import cartopy.crs as ccrs
56
import cartopy.feature as cf
@@ -28,7 +29,8 @@ class Constraint:
2829
"""
2930
Main class for handling of constraints. Constraints implemented so far:
3031
LandCrossing (prohibit land crossing), WaterDepth (prohibit crossing of areas with too low water depth),
31-
StayOnMap (prohibit leaving the area for which the weather data has been obtained)
32+
StayOnMap (prohibit leaving the area for which the weather data has been obtained),
33+
StayInTime (prohibit leaving the timeframe for which the weather data has been obtained)
3234
"""
3335

3436
name: str
@@ -52,6 +54,9 @@ def print_constraint_message(self):
5254
def constraint_on_point(self, lat, lon, time):
5355
pass
5456

57+
def check_crossing(self, lat_start, lon_start, lat_end, lon_end, time_start=None):
58+
pass
59+
5560
def print_debug(self, message):
5661
logger.debug(self.name + str(": ") + str(message))
5762

@@ -191,6 +196,17 @@ def get_constraints_list(constraints_string_list, **kwargs):
191196
constraints_list.add_neg_constraint(on_map)
192197
is_stay_on_map = True
193198

199+
if 'in_time' in constraints_string_list:
200+
if 'time_frame' not in kwargs:
201+
raise ValueError('To use the in-time constraint module, you need to provide the timeframe '
202+
'of the weather data.')
203+
time = kwargs.get('time_frame')
204+
time_start = time[0]
205+
time_end = time[1]
206+
in_time = StayInTime()
207+
in_time.set_time(time_start, time_end)
208+
constraints_list.add_neg_constraint(in_time, 'continuous')
209+
194210
if 'seamarks' in constraints_string_list:
195211
if is_stay_on_map:
196212
seamarks = SeamarkCrossing(is_stay_on_map, map_size)
@@ -339,21 +355,53 @@ def safe_endpoint(self, lat, lon, current_time, is_constrained):
339355
is_constrained += is_constrained_temp
340356
return is_constrained
341357

342-
def safe_crossing(self, lat_start, lon_start, lat_end, lon_end, current_time, is_constrained):
343-
is_constrained_discrete = is_constrained
344-
is_constrained_continuous = is_constrained
345-
is_constrained_discrete = self.safe_crossing_discrete(lat_start, lon_start, lat_end, lon_end, current_time,
346-
is_constrained)
347-
is_constrained_continuous = self.safe_crossing_continuous(lat_start, lon_start, lat_end, lon_end,
348-
is_constrained)
358+
def safe_crossing(self, lat_start: np.ndarray, lon_start: np.ndarray, lat_end: np.ndarray, lon_end: np.ndarray,
359+
start_times: np.ndarray, is_constrained: list[bool]):
360+
"""
361+
Check whether there is a constraint on the way from a starting point (lat_start, lon_start) to the destination
362+
(lat_end, lon_end).
363+
364+
:param lat_start: Latitude(s) of start point(s) of section(s) to check
365+
:type lat_start: numpy.ndarray or float
366+
:param lon_start: Longitude(s) of start point(s) of section(s) to check
367+
:type lon_start: numpy.ndarray or float
368+
:param lat_end: Latitude(s) of end point(s) of section(s) to check
369+
:type lat_end: numpy.ndarray or float
370+
:param lon_end: Longitude(s) of end point(s) of section(s) to check
371+
:type lon_end: numpy.ndarray or float
372+
:param current_time: Time(s) at the start point(s) of the section(s); may be None
373+
:type current_time: numpy.ndarray or None
374+
:param is_constrained: Booleans for every section stating if it is already constrained
375+
:type is_constrained: list[bool]
376+
:return: is_constrained
377+
:rtype: list[bool]
378+
"""
379+
380+
is_constrained_discrete = self.safe_crossing_discrete(
381+
lat_start=lat_start,
382+
lon_start=lon_start,
383+
lat_end=lat_end,
384+
lon_end=lon_end,
385+
current_time=start_times,
386+
is_constrained=is_constrained
387+
)
388+
is_constrained_continuous = self.safe_crossing_continuous(
389+
lat_start=lat_start,
390+
lon_start=lon_start,
391+
lat_end=lat_end,
392+
lon_end=lon_end,
393+
time_start=start_times,
394+
is_constrained=is_constrained
395+
)
349396

350397
# TO BE UPDATED
351398
is_constrained_array = (np.array(is_constrained) | np.array(is_constrained_discrete)
352399
| np.array(is_constrained_continuous))
353400
is_constrained = is_constrained_array.tolist()
354401
return is_constrained
355402

356-
def safe_crossing_continuous(self, lat_start, lon_start, lat_end, lon_end, is_constrained):
403+
def safe_crossing_continuous(self, lat_start: np.ndarray, lon_start: np.ndarray, lat_end: np.ndarray,
404+
lon_end: np.ndarray, time_start: np.ndarray, is_constrained: list[bool]):
357405
"""TODO: add description
358406
_summary_
359407
@@ -367,6 +415,8 @@ def safe_crossing_continuous(self, lat_start, lon_start, lat_end, lon_end, is_co
367415
:type lon_end: numpy.ndarray or float
368416
:param is_constrained: List of booleans for every constraint stating if the section is constraint by it
369417
:type is_constrained: list[bool]
418+
:param time_start: Time(s) at start point(s) of section(s) to check
419+
:type time_start: numpy.ndarray or None
370420
:return: is_constrained.tolist()
371421
:rtype: list[bool]
372422
"""
@@ -377,12 +427,19 @@ def safe_crossing_continuous(self, lat_start, lon_start, lat_end, lon_end, is_co
377427
# logger.debug('Length of latitudes: ' + str(len(lat_start)))
378428

379429
for constr in self.negative_constraints_continuous:
380-
is_constrained_temp = constr.check_crossing(lat_start, lon_start, lat_end, lon_end)
430+
is_constrained_temp = constr.check_crossing(
431+
lat_start=lat_start,
432+
lon_start=lon_start,
433+
lat_end=lat_end,
434+
lon_end=lon_end,
435+
time_start=time_start
436+
)
381437
is_constrained = np.array(is_constrained) | np.array(is_constrained_temp)
382438

383439
return is_constrained.tolist()
384440

385-
def safe_crossing_discrete(self, lat_start, lon_start, lat_end, lon_end, current_time, is_constrained):
441+
def safe_crossing_discrete(self, lat_start: np.ndarray, lon_start: np.ndarray, lat_end: np.ndarray,
442+
lon_end: np.ndarray, current_time: np.ndarray, is_constrained: list[bool]):
386443
"""
387444
Check whether there is a constraint on the way from a starting point (lat_start, lon_start) to the destination
388445
(lat_end, lon_end).
@@ -511,7 +568,7 @@ def load_data_from_file(self, courses_path):
511568
routeData.close()
512569
return status, lats, lons
513570

514-
def check_crossing(self, lat_start=None, lon_start=None, lat_end=None, lon_end=None, current_time=None):
571+
def check_crossing(self, lat_start=None, lon_start=None, lat_end=None, lon_end=None, time_start=None):
515572
status, lats_netcdf, lon_netcdf = self.load_data_from_file(self.courses_path)
516573
# Double-check coordinates
517574
assert (lats_netcdf == lat_start).all()
@@ -799,6 +856,56 @@ def set_map(self, lat1, lon1, lat2, lon2):
799856
self.lon2 = lon2
800857

801858

859+
class StayInTime(NegativeContraint):
860+
"""
861+
Constraint such that the boat can't leave the timeframe that has weather data available
862+
"""
863+
864+
time_start: datetime
865+
time_end: datetime
866+
867+
def __init__(self):
868+
NegativeContraint.__init__(self, "StayInTime")
869+
self.message += "leaving wheather time frame!" # self.resource_type = 0
870+
871+
def check_crossing(self, lat_start: np.ndarray, lon_start: np.ndarray, lat_end: np.ndarray, lon_end: np.ndarray,
872+
time_start: np.ndarray = None):
873+
"""
874+
Check whether array of start times from route waypoints is covered by weather data.
875+
876+
time_start can be None. In this case, all waypoints are not constrained.
877+
:param lat_start: Start latitude
878+
:type lat_start: np.ndarray
879+
:param lon_start: Start longitude
880+
:type lon_start: np.ndarray
881+
:param lat_end: End latitude
882+
:type lat_end: np.ndarray
883+
:param lon_end: End longitude
884+
:type lon_end: np.ndarray
885+
:param time_start: Start time
886+
:type time_start: np.ndarray or None
887+
:return is_out_of_time: list constraints
888+
:rtype is_out_of_time: list[bool]
889+
890+
"""
891+
892+
if time_start is None:
893+
return np.full(np.shape(lat_start), False)
894+
is_out_of_time = ((time_start > self.time_end) + (time_start < self.time_start))
895+
# if is_out_of_time.any():
896+
# print('checking time: ' + str(time_start))
897+
# print('is_out_of_time: ' + str(is_out_of_time))
898+
# print(f'start_time={self.time_start}, end_time={self.time_end}')
899+
return is_out_of_time
900+
901+
def print_info(self):
902+
logger.info(form.get_log_step("stay in wheather time frame", 1))
903+
904+
def set_time(self, time_start, time_end):
905+
self.time_start = time_start
906+
self.time_end = time_end
907+
908+
802909
class ContinuousCheck(NegativeContraint):
803910
"""
804911
Contains various functions to test data connection, gathering and use for obtaining spatial relations
@@ -876,7 +983,7 @@ def print_info(self):
876983
def connect_database(self):
877984
pass
878985

879-
def check_crossing(self, lat_start, lon_start, lat_end, lon_end, time=None):
986+
def check_crossing(self, lat_start, lon_start, lat_end, lon_end, time_start=None):
880987
result_length = len(lat_start)
881988
res = []
882989

@@ -1024,7 +1131,7 @@ def concat_nodes_ways(self, db_engine, query):
10241131
else:
10251132
return "false query passed"
10261133

1027-
def check_crossing(self, lat_start, lon_start, lat_end, lon_end):
1134+
def check_crossing(self, lat_start, lon_start, lat_end, lon_end, time_start=None):
10281135
"""
10291136
Check if certain route crosses specified seamark objects
10301137
@@ -1106,7 +1213,7 @@ def query_land_polygons(self, db_engine, query):
11061213
gdf = gdf[gdf["geom"] != None]
11071214
return gdf
11081215

1109-
def check_crossing(self, lat_start, lon_start, lat_end, lon_end):
1216+
def check_crossing(self, lat_start, lon_start, lat_end, lon_end, time_start=None):
11101217
"""
11111218
Check if certain route crosses specified seamark objects
11121219

WeatherRoutingTool/execute_routing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def execute_routing(config, ship_config):
4242
# initialise weather
4343
wt = WeatherFactory.get_weather(config._DATA_MODE_WEATHER, windfile, departure_time, time_forecast, time_resolution,
4444
default_map)
45+
time_frame = (wt.time_start, wt.time_end)
4546

4647
# *******************************************
4748
# initialise boat
@@ -55,7 +56,7 @@ def execute_routing(config, ship_config):
5556
constraints_string_list=config.CONSTRAINTS_LIST, data_mode=config._DATA_MODE_DEPTH,
5657
min_depth=boat.get_required_water_depth(),
5758
map_size=default_map, depthfile=depthfile, waypoints=config.INTERMEDIATE_WAYPOINTS,
58-
courses_path=config.COURSES_FILE)
59+
courses_path=config.COURSES_FILE, time_frame=time_frame)
5960

6061
# *******************************************
6162
# initialise route

WeatherRoutingTool/ship/ship.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def check_value_in_range(self, lats, lons, time):
123123

124124
def approx_weather(self, var, lats, lons, time, height=None, depth=None):
125125

126-
self.check_value_in_range(lats, lons, time)
126+
# self.check_value_in_range(lats, lons, time)
127127

128128
ship_var = var.sel(latitude=lats, longitude=lons, time=time, method='nearest', drop=False)
129129
if height:

0 commit comments

Comments
 (0)