-
Notifications
You must be signed in to change notification settings - Fork 115
Improve weather data handling #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,33 +8,108 @@ | |
| import WeatherRoutingTool.utils.formatting as form | ||
| from WeatherRoutingTool.ship.shipparams import ShipParams | ||
| from WeatherRoutingTool.ship.ship_config import ShipConfig | ||
| from collections import deque | ||
| from typing import Tuple, Optional, Sequence, Any | ||
|
|
||
| logger = logging.getLogger('WRT.ship') | ||
|
|
||
| logger = logging.getLogger("WRT.ship") | ||
|
|
||
|
|
||
| # Boat: Main class for boats. Classes 'Tanker' and 'SailingBoat' derive from it | ||
| # Tanker: implements interface to mariPower package which is used for power estimation. | ||
|
|
||
|
|
||
| class Cache: | ||
| """ | ||
| keys are arbitrary hashable objects | ||
| stores arbitrary values | ||
| evicts oldest entries when max_entries exceeded | ||
| """ | ||
|
|
||
| def __init__(self, max_entries: int = 10_000): | ||
| if max_entries <= 0: | ||
| raise ValueError("max_entries must be > 0") | ||
| self.max_entries = int(max_entries) | ||
| self._cache = dict() | ||
| self._order = deque() # oldest keys on left | ||
|
|
||
| def __len__(self) -> int: | ||
| return len(self._cache) | ||
|
|
||
| def contains(self, key: Any) -> bool: | ||
| return key in self._cache | ||
|
|
||
| def get(self, key: Any, default: Any = None) -> Any: | ||
| return self._cache.get(key, default) | ||
|
|
||
| def set(self, key: Any, value: Any) -> None: | ||
| if key in self._cache: | ||
| # update value but do not change order | ||
| self._cache[key] = value | ||
| return | ||
| self._cache[key] = value | ||
| self._order.append(key) | ||
| # evict oldest if necessary | ||
| while len(self._order) > self.max_entries: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please drop the while, as every time the function is called only one entry needs to be deleted. |
||
| old = self._order.popleft() | ||
| # guard: old might already been removed | ||
| self._cache.pop(old, None) | ||
|
|
||
| def pop_oldest(self) -> Optional[Tuple[Any, Any]]: | ||
| if not self._order: | ||
| return None | ||
| oldest = self._order.popleft() | ||
| val = self._cache.pop(oldest, None) | ||
| return (oldest, val) | ||
|
|
||
|
|
||
| class Boat: | ||
| weather_path: str # path to netCDF containing weather data | ||
|
|
||
| def __init__(self, ship_config: ShipConfig): | ||
| self.counter = 0 | ||
| self.under_keel_clearance = ship_config.BOAT_UNDER_KEEL_CLEARANCE * u.meter | ||
| self.draught_aft = ship_config.BOAT_DRAUGHT_AFT * u.meter | ||
| self.draught_fore = ship_config.BOAT_DRAUGHT_FORE * u.meter | ||
| self._weather_data = None | ||
| self._weather_access_counter = None | ||
| self._weather_access_dims = ("time", "latitude", "longitude") | ||
| self._lat_min = None | ||
| self._lon_min = None | ||
| self._lat_width = 1.0 / 12.0 | ||
| self._lon_width = 1.0 / 12.0 | ||
| self._weather_cache = Cache(max_entries=10000) | ||
|
|
||
| def _get_weather_data(self): | ||
| if self._weather_data is not None: | ||
| return self._weather_data | ||
|
|
||
| # Keep the dataset lazy so only requested slices are read into memory. | ||
| self._weather_data = xr.open_dataset(self.weather_path) | ||
| return self._weather_data | ||
|
|
||
| def close(self): | ||
| if self._weather_data is not None: | ||
| self._weather_data.close() | ||
| self._weather_data = None | ||
|
|
||
| def get_required_water_depth(self): | ||
| needs_water_depth = max(self.draught_aft, self.draught_fore) + self.under_keel_clearance | ||
| needs_water_depth = ( | ||
| max(self.draught_aft, self.draught_fore) + self.under_keel_clearance | ||
| ) | ||
| return needs_water_depth.value | ||
|
|
||
| def get_ship_parameters(self, courses, lats, lons, time, speed, unique_coords=False): | ||
| def get_ship_parameters( | ||
| self, courses, lats, lons, time, speed, unique_coords=False | ||
| ): | ||
| pass | ||
|
|
||
| def print_init(self): | ||
| pass | ||
|
|
||
| def evaluate_weather(self, ship_params, lats, lons, time): | ||
| weather_data = xr.open_dataset(self.weather_path) | ||
| weather_data = self._get_weather_data() | ||
| # weather_data= xr.open_dataset(self.weather_path) | ||
| n_coords = len(lats) | ||
|
|
||
| wave_height = [] | ||
|
|
@@ -49,53 +124,180 @@ def evaluate_weather(self, ship_params, lats, lons, time): | |
| salinity = [] | ||
| water_temperature = [] | ||
|
|
||
| def cached_lookup(var_key, da, lat, lon, t, height=None, depth=None): | ||
| key = ( | ||
| var_key, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please convert the latitude, longitude and time values into indices and use these as keys to refer to the data in the lookup table. |
||
| float(lat), | ||
| float(lon), | ||
| np.datetime64(t) if np.issubdtype(type(t), np.datetime64) else t, | ||
| height, | ||
| depth, | ||
| ) | ||
| val = self._weather_cache.get(key) | ||
| if val is not None: | ||
| return val | ||
| v = self.approx_weather(da, lat, lon, t, height, depth) | ||
| self._weather_cache.set(key, v) | ||
| return v | ||
|
|
||
| for i_coord in range(0, n_coords): | ||
| wave_direction.append( | ||
| self.approx_weather(weather_data['VMDR'], lats[i_coord], lons[i_coord], time[i_coord])) | ||
| wave_period.append(self.approx_weather(weather_data['VTPK'], lats[i_coord], lons[i_coord], time[i_coord])) | ||
| wave_height.append(self.approx_weather(weather_data['VHM0'], lats[i_coord], lons[i_coord], time[i_coord])) | ||
| cached_lookup( | ||
| "VMDR", | ||
| weather_data["VMDR"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| ) | ||
| ) | ||
| wave_period.append( | ||
| cached_lookup( | ||
| "VTPK", | ||
| weather_data["VTPK"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| ) | ||
| ) | ||
| wave_height.append( | ||
| cached_lookup( | ||
| "VHM0", | ||
| weather_data["VHM0"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| ) | ||
| ) | ||
| v_currents.append( | ||
| self.approx_weather(weather_data['vtotal'], lats[i_coord], lons[i_coord], time[i_coord], None, 0.5)) | ||
| cached_lookup( | ||
| "vtotal", | ||
| weather_data["vtotal"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| None, | ||
| 0.5, | ||
| ) | ||
| ) | ||
| u_currents.append( | ||
| self.approx_weather(weather_data['utotal'], lats[i_coord], lons[i_coord], time[i_coord], None, 0.5)) | ||
| cached_lookup( | ||
| "utotal", | ||
| weather_data["utotal"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| None, | ||
| 0.5, | ||
| ) | ||
| ) | ||
| pressure.append( | ||
| self.approx_weather(weather_data['Pressure_reduced_to_MSL_msl'], lats[i_coord], lons[i_coord], | ||
| time[i_coord])) | ||
| cached_lookup( | ||
| "Pressure_reduced_to_MSL_msl", | ||
| weather_data["Pressure_reduced_to_MSL_msl"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| ) | ||
| ) | ||
| water_temperature.append( | ||
| self.approx_weather(weather_data['thetao'], lats[i_coord], lons[i_coord], time[i_coord], None, 0.5)) | ||
| cached_lookup( | ||
| "thetao", | ||
| weather_data["thetao"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| None, | ||
| 0.5, | ||
| ) | ||
| ) | ||
| salinity.append( | ||
| self.approx_weather(weather_data['so'], lats[i_coord], lons[i_coord], time[i_coord], None, 0.5)) | ||
| cached_lookup( | ||
| "so", | ||
| weather_data["so"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| None, | ||
| 0.5, | ||
| ) | ||
| ) | ||
| air_temperature.append( | ||
| self.approx_weather(weather_data['Temperature_surface'], lats[i_coord], lons[i_coord], time[i_coord])) | ||
| cached_lookup( | ||
| "Temperature_surface", | ||
| weather_data["Temperature_surface"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| ) | ||
| ) | ||
| u_wind_speed.append( | ||
| self.approx_weather(weather_data['u-component_of_wind_height_above_ground'], lats[i_coord], | ||
| lons[i_coord], time[i_coord], 10)) | ||
| cached_lookup( | ||
| "u-component_of_wind_height_above_ground", | ||
| weather_data["u-component_of_wind_height_above_ground"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| 10, | ||
| ) | ||
| ) | ||
| v_wind_speed.append( | ||
| self.approx_weather(weather_data['v-component_of_wind_height_above_ground'], lats[i_coord], | ||
| lons[i_coord], time[i_coord], 10)) | ||
|
|
||
| ship_params.wave_direction = np.array(wave_direction, dtype='float32') * u.radian | ||
| ship_params.wave_period = np.array(wave_period, dtype='float32') * u.second | ||
| ship_params.wave_height = np.array(wave_height, dtype='float32') * u.meter | ||
| ship_params.u_wind_speed = np.array(u_wind_speed, dtype='float32') * u.meter / u.second | ||
| ship_params.v_wind_speed = np.array(v_wind_speed, dtype='float32') * u.meter / u.second | ||
| ship_params.v_currents = np.array(v_currents, dtype='float32') * u.meter / u.second | ||
| ship_params.u_currents = np.array(u_currents, dtype='float32') * u.meter / u.second | ||
| ship_params.pressure = np.array(pressure, dtype='float32') * u.kg / (u.meter * u.second ** 2) | ||
| ship_params.air_temperature = np.array(air_temperature, dtype='float32') * u.Kelvin | ||
| ship_params.air_temperature = ship_params.air_temperature.to(u.deg_C, equivalencies=u.temperature()) | ||
| ship_params.salinity = np.array(salinity, dtype='float32') * 0.001 * u.dimensionless_unscaled | ||
| ship_params.water_temperature = np.array(water_temperature, dtype='float32') * u.deg_C | ||
| cached_lookup( | ||
| "v-component_of_wind_height_above_ground", | ||
| weather_data["v-component_of_wind_height_above_ground"], | ||
| lats[i_coord], | ||
| lons[i_coord], | ||
| time[i_coord], | ||
| 10, | ||
| ) | ||
| ) | ||
|
|
||
| ship_params.wave_direction = ( | ||
| np.array(wave_direction, dtype="float32") * u.radian | ||
| ) | ||
| ship_params.wave_period = np.array(wave_period, dtype="float32") * u.second | ||
| ship_params.wave_height = np.array(wave_height, dtype="float32") * u.meter | ||
| ship_params.u_wind_speed = ( | ||
| np.array(u_wind_speed, dtype="float32") * u.meter / u.second | ||
| ) | ||
| ship_params.v_wind_speed = ( | ||
| np.array(v_wind_speed, dtype="float32") * u.meter / u.second | ||
| ) | ||
| ship_params.v_currents = ( | ||
| np.array(v_currents, dtype="float32") * u.meter / u.second | ||
| ) | ||
| ship_params.u_currents = ( | ||
| np.array(u_currents, dtype="float32") * u.meter / u.second | ||
| ) | ||
| ship_params.pressure = ( | ||
| np.array(pressure, dtype="float32") * u.kg / (u.meter * u.second**2) | ||
| ) | ||
| ship_params.air_temperature = ( | ||
| np.array(air_temperature, dtype="float32") * u.Kelvin | ||
| ) | ||
| ship_params.air_temperature = ship_params.air_temperature.to( | ||
| u.deg_C, equivalencies=u.temperature() | ||
| ) | ||
| ship_params.salinity = ( | ||
| np.array(salinity, dtype="float32") * 0.001 * u.dimensionless_unscaled | ||
| ) | ||
| ship_params.water_temperature = ( | ||
| np.array(water_temperature, dtype="float32") * u.deg_C | ||
| ) | ||
|
|
||
| return ship_params | ||
|
|
||
| def approx_weather(self, var, lats, lons, time, height=None, depth=None): | ||
| ship_var = var.sel(latitude=lats, longitude=lons, time=time, method='nearest', drop=False) | ||
| ship_var = var.sel( | ||
| latitude=lats, longitude=lons, time=time, method="nearest", drop=False | ||
| ) | ||
| if height: | ||
| ship_var = ship_var.sel(height_above_ground=height, method='nearest', drop=False) | ||
| ship_var = ship_var.sel( | ||
| height_above_ground=height, method="nearest", drop=False | ||
| ) | ||
| if depth: | ||
| ship_var = ship_var.sel(depth=depth, method='nearest', drop=False) | ||
| ship_var = ship_var.sel(depth=depth, method="nearest", drop=False) | ||
| ship_var = ship_var.fillna(0).to_numpy() | ||
| self.counter = +1 | ||
|
|
||
| return ship_var | ||
|
|
||
|
|
@@ -120,10 +322,12 @@ def __init__(self, ship_config: ShipConfig): | |
| self.fuel_rate = ship_config.BOAT_FUEL_RATE * u.kg / u.second | ||
|
|
||
| def print_init(self): | ||
| logger.info(form.get_log_step('boat fuel rate' + str(self.fuel_rate), 1)) | ||
| logger.info(form.get_log_step("boat fuel rate" + str(self.fuel_rate), 1)) | ||
| form.print_line() | ||
|
|
||
| def get_ship_parameters(self, courses, lats, lons, time, speed, unique_coords=False): | ||
| def get_ship_parameters( | ||
| self, courses, lats, lons, time, speed, unique_coords=False | ||
| ): | ||
| debug = False | ||
| n_requests = len(courses) | ||
|
|
||
|
|
@@ -147,16 +351,16 @@ def get_ship_parameters(self, courses, lats, lons, time, speed, unique_coords=Fa | |
| v_currents=dummy_array * u.meter / u.second, | ||
| u_wind_speed=dummy_array * u.meter / u.second, | ||
| v_wind_speed=dummy_array * u.meter / u.second, | ||
| pressure=dummy_array * u.kg / u.meter / u.second ** 2, | ||
| pressure=dummy_array * u.kg / u.meter / u.second**2, | ||
| air_temperature=dummy_array * u.deg_C, | ||
| salinity=dummy_array * u.dimensionless_unscaled, | ||
| water_temperature=dummy_array * u.deg_C, | ||
| status=dummy_array, | ||
| message=np.full(n_requests, "") | ||
| message=np.full(n_requests, ""), | ||
| ) | ||
|
|
||
| if (debug): | ||
| if debug: | ||
| ship_params.print() | ||
| form.print_step('fuel result' + str(ship_params.get_fuel_rate())) | ||
| form.print_step("fuel result" + str(ship_params.get_fuel_rate())) | ||
|
|
||
| return ship_params | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check whether the deletion of self._order and the replacement of self._cache by an OrderedDict increases the run-time performance. You can then use the command 'popitem(last=False)' to delete the first entry of _order.