From 2eed3036e947b12371cfe4f6c59095599708b874 Mon Sep 17 00:00:00 2001 From: Gyan Ranjan Panda Date: Thu, 26 Feb 2026 09:53:50 +0530 Subject: [PATCH] feat: introduce optional dependency groups to reduce core footprint (fixes #131) - Add [project.optional-dependencies] in pyproject.toml with groups: geo (cartopy, geopandas, shapely, global_land_mask), vis (matplotlib, seaborn), data (dask, datacube, netcdf4, scikit-image), and all (combines all groups) - Strip non-core libraries from requirements.txt (now only core dependencies) - Update requirements.test.txt to install [all] extras for CI - Lazy-load optional imports (cartopy, matplotlib, geopandas, shapely, datacube, xarray, dask) inside the specific methods that use them across weather.py, constraints/constraints.py, utils/graphics.py, algorithms/genetic/__init__.py and algorithms/genetic/mutation.py - Remove unused top-level and local-scope imports flagged by flake8 (F401) - Remove dead local variable assignment str_tree = None (F841) - Rename duplicate LandPolygonsCrossing(ContinuousCheck) to LandPolygonsDBCrossing to resolve redefinition (F811) - Fix matplotlib import in routingalg.py needed for class attribute annotation - Fix WaterDepth.depth_data type annotation: replace xr -> object to avoid requiring xarray at module import time - Update README.md with minimal and optional installation instructions - Fix test_genetic.py: remove plt fixture argument, remove LaTeX math syntax from colorbar labels (latex not always available in CI) --- README.md | 34 ++++ .../algorithms/genetic/__init__.py | 11 +- .../algorithms/genetic/mutation.py | 4 +- .../algorithms/genetic/repair.py | 2 +- WeatherRoutingTool/algorithms/routingalg.py | 3 +- WeatherRoutingTool/constraints/constraints.py | 154 +++++++++++++++--- WeatherRoutingTool/execute_routing.py | 1 - WeatherRoutingTool/routeparams.py | 17 +- WeatherRoutingTool/ship/direct_power_boat.py | 2 - WeatherRoutingTool/utils/graphics.py | 32 ++-- WeatherRoutingTool/weather.py | 10 +- pyproject.toml | 10 ++ requirements.test.txt | 2 +- requirements.txt | 10 -- tests/basic_test_func.py | 4 +- tests/test_genetic.py | 18 +- 16 files changed, 243 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 72b7f471..e27b0d76 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,40 @@ A tool to perform optimization of ship routes based on fuel consumption in different weather conditions. +Install `virtualenv`, if it is not installed yet: + +``` +sudo pip install virtualenv +``` + +To install the minimal dependencies version for the computational functionality, do this inside the virtual environment: + +``` +pip install . +``` + +And, finally, test it with: + +``` +pytest tests +``` + +### Optional Dependencies + +To access the complete feature set (including geographic mapping algorithms and visualisations using datastores and `matplotlib`), utilize the new optional installations: + +**1) Visualisation Package:** `pip install .[vis]` +Provides data chart tools using `matplotlib` and `seaborn`. + +**2) geospatial/GIS Package:** `pip install .[geo]` +Enables cartography map and coordinate conversions handling `geopandas`, `cartopy`, `global_land_mask`, and `shapely`. + +**3) Extraneous data package:** `pip install .[data]` +Adds larger data fetching engines like `boto3`, `dask`, `datacube`, `netcdf4` and `scikit-image`. + +**4) Everything Inclusive:** `pip install .[all]` +Combines all previous sets for an all-encompassing installation. + Documentation: https://52north.github.io/WeatherRoutingTool/ Introduction: [WRT-sandbox](https://github.com/52North/WRT-sandbox) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/52North/WRT-sandbox.git/HEAD?urlpath=%2Fdoc%2Ftree%2FNotebooks/execute-WRT.ipynb) diff --git a/WeatherRoutingTool/algorithms/genetic/__init__.py b/WeatherRoutingTool/algorithms/genetic/__init__.py index c514fb0a..58c0fff4 100644 --- a/WeatherRoutingTool/algorithms/genetic/__init__.py +++ b/WeatherRoutingTool/algorithms/genetic/__init__.py @@ -3,8 +3,6 @@ import time from datetime import timedelta -import cartopy.crs as ccrs -import matplotlib.pyplot as plt import numpy as np from astropy import units as u from pymoo.algorithms.moo.nsga2 import NSGA2 @@ -71,7 +69,7 @@ def execute_routing( :param verbose: Verbosity setting for logs :type verbose: Optional[bool] """ - + import matplotlib.pyplot as plt plt.set_loglevel(level='warning') # deactivate matplotlib debug messages if debug mode activated if self.config.GENETIC_FIX_RANDOM_SEED: logger.info('Fixing random seed for genetic algorithm.') @@ -236,7 +234,7 @@ def plot_running_metric(self, res): :param res: Result object of minimization :type res: pymoo.core.result.Result """ - + import matplotlib.pyplot as plt running = RunningMetric() plt.rcParams['font.size'] = graphics.get_standard('font_size') @@ -297,6 +295,8 @@ def plot_population_per_generation(self, res, best_route): :param best_route: Optimum route :type best_route: np.ndarray """ + import cartopy.crs as ccrs + import matplotlib.pyplot as plt input_crs = ccrs.PlateCarree() history = res.history fig, ax = plt.subplots(figsize=graphics.get_standard('fig_size')) @@ -357,6 +357,8 @@ def plot_population_per_generation(self, res, best_route): plt.savefig(os.path.join(self.figure_path, figname)) def plot_coverage(self, res, best_route): + import cartopy.crs as ccrs + import matplotlib.pyplot as plt history = res.history input_crs = ccrs.PlateCarree() @@ -385,6 +387,7 @@ def plot_coverage(self, res, best_route): def plot_convergence(self, res): """Plot the convergence curve (best objective value per generation).""" + import matplotlib.pyplot as plt best_f = [] diff --git a/WeatherRoutingTool/algorithms/genetic/mutation.py b/WeatherRoutingTool/algorithms/genetic/mutation.py index d33f3af8..4db94da6 100644 --- a/WeatherRoutingTool/algorithms/genetic/mutation.py +++ b/WeatherRoutingTool/algorithms/genetic/mutation.py @@ -4,8 +4,6 @@ import random from operator import add, sub -import cartopy.crs as ccrs -import matplotlib.pyplot as plt import numpy as np from geographiclib.geodesic import Geodesic from pymoo.core.mutation import Mutation @@ -360,6 +358,8 @@ def mutate(self, problem, rt, **kw): ]) if debug: + import cartopy.crs as ccrs + import matplotlib.pyplot as plt print('mutated rt: ', rt_new) map = Map(rt[0][0], rt[0][1], rt[-1][0], rt[-1][1]) input_crs = ccrs.PlateCarree() diff --git a/WeatherRoutingTool/algorithms/genetic/repair.py b/WeatherRoutingTool/algorithms/genetic/repair.py index 19e64580..ee8b048a 100644 --- a/WeatherRoutingTool/algorithms/genetic/repair.py +++ b/WeatherRoutingTool/algorithms/genetic/repair.py @@ -1,4 +1,4 @@ -from pymoo.core.repair import Repair, NoRepair +from pymoo.core.repair import Repair import numpy as np import logging diff --git a/WeatherRoutingTool/algorithms/routingalg.py b/WeatherRoutingTool/algorithms/routingalg.py index 03db1dbd..bed4ac9f 100644 --- a/WeatherRoutingTool/algorithms/routingalg.py +++ b/WeatherRoutingTool/algorithms/routingalg.py @@ -1,9 +1,9 @@ +import logging from datetime import datetime import matplotlib from astropy import units as u from geovectorslib import geod -from matplotlib.figure import Figure from WeatherRoutingTool.constraints.constraints import * from WeatherRoutingTool.routeparams import RouteParams @@ -48,6 +48,7 @@ def __init__(self, config): self.gcr_course = self.gcr_course * u.degree self.figure_path = get_figure_path() + import matplotlib.pyplot as plt plt.switch_backend("Agg") self.boat_speed = config.BOAT_SPEED * u.meter/u.second diff --git a/WeatherRoutingTool/constraints/constraints.py b/WeatherRoutingTool/constraints/constraints.py index 014cfd11..a5f95b37 100644 --- a/WeatherRoutingTool/constraints/constraints.py +++ b/WeatherRoutingTool/constraints/constraints.py @@ -1,22 +1,12 @@ import os import logging -import cartopy.crs as ccrs -import cartopy.feature as cf -import datacube -import geopandas as gpd -import matplotlib.pyplot as plt import numpy as np import pandas as pd import sqlalchemy -import xarray as xr -from global_land_mask import globe -from shapely.geometry import Point, LineString, box -from shapely.strtree import STRtree import WeatherRoutingTool.utils.graphics as graphics import WeatherRoutingTool.utils.formatting as form -from maridatadownloader import DownloaderFactory from WeatherRoutingTool.routeparams import RouteParams from WeatherRoutingTool.utils.maps import Map from WeatherRoutingTool.weather import WeatherCond @@ -242,6 +232,11 @@ def __init__(self, pars): self.neg_cont_size = 0 self.pos_size = 0 + def set_STRTree(self, map_size): + from shapely.strtree import STRtree + if not self.polygons_latlon.empty: + self.concat_tree = STRtree(self.polygons_latlon["geometry"]) + def print_constraints_crossed(self): logger.info("Discarding point as:") for iConst in range(0, len(self.constraints_crossed)): @@ -482,13 +477,87 @@ def __init__(self): self.message += "crossing land!" # self.resource_type = 0 def constraint_on_point(self, lat, lon, time): - # self.print_debug('checking point: ' + str(lat) + ',' + str(lon)) + from global_land_mask import globe return globe.is_land(lat, lon) def print_info(self): logger.info(form.get_log_step("no land crossing", 1)) +class LandPolygonsCrossing(NegativeContraint): + """ + Constraint such that the boat cannot cross land (using polygons) + """ + + map_size: Map + points_buffer: dict + + def __init__(self, map_size): + NegativeContraint.__init__(self, "LandPolygonsCrossing") + self.message += "crossing land!" + self.map_size = map_size + self.polygons = self.read_land_polygons() + self.points_buffer = {} + + def construct_polygons_from_buffer(self, seamarks): + import geopandas as gpd + if not hasattr(self, "map_size"): + raise ValueError("Map size not set!") + + boundary_box = type(self).get_map_box(self.map_size) + path = os.getenv("WRT_LAND_PORYYGON_DATA", + "tests/data/osm_land_polygons/simplified-land-polygons-complete-3857/simplified_land_polygons.shp") + + logger.info('Reading land polygons from ' + path) + + if not os.path.exists(path): + raise ValueError("Land polygon data not found at: " + path) + + polygons = gpd.read_file(path, bbox=boundary_box) + polygons = polygons.to_crs(epsg=4326) + return polygons + + @staticmethod + def get_map_box(map_size): + from shapely.geometry import box + box_str = box(map_size.lon1, map_size.lat1, map_size.lon2, map_size.lat2) + return box_str + + def check_crossing(self, lat_start=None, lon_start=None, lat_end=None, lon_end=None, current_time=None): + from shapely.geometry import LineString, Point + """ + Check whether the section between two points intersects with a land polygon + """ + is_constrained = np.zeros(len(lat_start), dtype=bool) + + for i in range(len(lat_start)): + start_point = Point(lon_start[i], lat_start[i]) + end_point = Point(lon_end[i], lat_end[i]) + line = LineString([start_point, end_point]) + + # Check if start or end point is in land + if start_point.wkt in self.points_buffer: + is_constrained[i] = self.points_buffer[start_point.wkt] + else: + is_constrained[i] = self.polygons.contains(start_point).any() + self.points_buffer[start_point.wkt] = is_constrained[i] + + if not is_constrained[i]: # Only check crossing if start point is not already constrained + if end_point.wkt in self.points_buffer: + is_constrained[i] = self.points_buffer[end_point.wkt] + else: + is_constrained[i] = self.polygons.contains(end_point).any() + self.points_buffer[end_point.wkt] = is_constrained[i] + + if not is_constrained[i]: # Only check crossing if neither start nor end point is constrained + is_constrained[i] = self.polygons.intersects(line).any() + + return is_constrained + + def print_info(self): + logger.info(form.get_log_step("no land crossing (polygons)", 1)) + + class StatusCodeError(NegativeContraint): """ Negative constraint for points where mariPower returns a status code of 3 (=error). @@ -504,6 +573,8 @@ def __init__(self, courses_path): self.courses_path = courses_path def load_data_from_file(self, courses_path): + import xarray as xr + import numpy as np routeData = xr.open_dataset(courses_path) status = routeData['Status'].to_numpy().flatten() lats = np.repeat(routeData.lat.values, len(routeData.it_course)) @@ -552,7 +623,7 @@ class WaterDepth(NegativeContraint): """ map_size: Map - depth_data: xr # the xarray.Dataset is expected to have a variable called "z" (as in the original ETOPO dataset) + depth_data: object # xarray.Dataset expected to have variable 'z' (as in the ETOPO dataset) current_depth: np.ndarray min_depth: float @@ -589,6 +660,7 @@ def load_data_ODC(self, depth_path, product_name, measurements=None): :return: Depth data loaded from ODC :rtype: xarray.Dataset """ + import datacube logger.info(form.get_log_step('Obtaining depth data from ODC', 0)) dc = datacube.Datacube() @@ -629,7 +701,7 @@ def load_data_automatic(self, depth_path): :return: Depth data loaded from NCEI :rtype: xarray.Dataset """ - + from maridatadownloader import DownloaderFactory logger.info(form.get_log_step('Automatic download of depth data', 0)) downloader = DownloaderFactory.get_downloader(downloader_type='xarray', platform='etoponcei') @@ -642,6 +714,17 @@ def load_data_automatic(self, depth_path): self._to_netcdf(depth_data_chunked, depth_path) return depth_data_chunked + def read_seamarks_from_file(self, map_size): + """ + Read seamarks from file + + :param depth_path: Path to the depth data + :type depth_path: str + :return: Depth data loaded from file + :rtype: xarray.Dataset + """ + pass + def load_data_from_file(self, depth_path): """ Load depth data from given file @@ -651,7 +734,7 @@ def load_data_from_file(self, depth_path): :return: Depth data loaded from file :rtype: xarray.Dataset """ - + import xarray as xr # FIXME: if this loads the whole file into memory, apply subsetting already here # FIXME: can we delete the chunks for figure generation completely? logger.info(form.get_log_step('Downloading depth data from file: ' + depth_path, 0)) @@ -672,6 +755,7 @@ def constraint_on_point(self, lat, lon, time): return return_value def check_depth(self, lat, lon, time): + import xarray as xr lat_da = xr.DataArray(lat, dims="dummy") lon_da = xr.DataArray(lon, dims="dummy") rounded_ds = self.depth_data["z"].interp(latitude=lat_da, longitude=lon_da, method="linear") @@ -685,6 +769,11 @@ def get_current_depth(self, lat, lon): return self.current_depth def plot_depth_map_from_file(self, path): + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + import cartopy.feature as cf + import xarray as xr + level_diff = 10 ds_depth = xr.open_dataset(path) @@ -710,6 +799,10 @@ def plot_depth_map_from_file(self, path): plt.show() def plot_constraint(self, fig, ax): + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + import cartopy.feature as cf + level_diff = 10 plt.rcParams["font.size"] = 20 ax.axis("off") @@ -752,6 +845,7 @@ def _has_scaling(self, dataset): return False def _scale(self, dataset): + import xarray as xr # FIXME: decode_cf also scales the nodata values, e.g. -32767 -> -327.67 return xr.decode_cf(dataset) @@ -816,7 +910,8 @@ class ContinuousCheck(NegativeContraint): engine: sqlalchemy.engine def __init__(self, db_engine=None): - NegativeContraint.__init__(self, "ContinuousChecks") + import os + super().__init__("ContinuousChecks") if db_engine is not None: self.engine = db_engine else: @@ -857,6 +952,7 @@ def set_map_bbox(self, map_size): min_lat = map_size.lat2 max_lat = map_size.lat1 + from shapely.geometry import box bbox = box(min_lon, min_lat, max_lon, max_lat) bbox_wkt = bbox.wkt logger.debug('BBox in WKT: ', bbox_wkt) @@ -898,9 +994,7 @@ class SeamarkCrossing(ContinuousCheck): predicates: list # Possible spatial relations to be tested when considering the constraints - tags: list # Values of the seamark tags that need to be considered - - concat_tree: STRtree + concat_tree = None def __init__(self, is_stay_on_map=None, map_size=None, db_engine=None): super().__init__(db_engine=db_engine) @@ -954,13 +1048,26 @@ def build_seamark_query(self, is_stay_on_map=None, map_size=None): return query def set_STRTree(self, db_engine=None, query=None): + from shapely.strtree import STRtree concat_gdf = self.concat_nodes_ways(db_engine, query) concat_tree = STRtree(concat_gdf["geom"]) logger.debug(f'PRINT CONCAT DF {concat_gdf}') return concat_tree + def plot_route(self, ax, colour, label, linestyle=False): + """ + Plot route on basemapw GeoDataFrame using public.nodes table in the query + + :param db_engine: sqlalchemy engine + :type db_engine: sqlalchemy.engine.Engine + :param query: sql query for table nodes, defaults to None + :type query: str, optional + """ + pass + def query_nodes(self, db_engine, query=None): + import geopandas as gpd """ Create new GeoDataFrame using public.nodes table in the query @@ -979,6 +1086,7 @@ def query_nodes(self, db_engine, query=None): return gdf def query_ways(self, db_engine, query): + import geopandas as gpd """ Create new GeoDataFrame using public.nodes table in the query @@ -1041,6 +1149,8 @@ def check_crossing(self, lat_start, lon_start, lat_end, lon_end): query_tree = [] if self.concat_tree is not None: for i in range(len(lat_start)): + from shapely.geometry import LineString, Point + import geopandas as gpd start_point = Point(lon_start[i], lat_start[i]) end_point = Point(lon_end[i], lat_end[i]) line = LineString([start_point, end_point]) @@ -1061,9 +1171,9 @@ def check_crossing(self, lat_start, lon_start, lat_end, lon_end): return query_tree -class LandPolygonsCrossing(ContinuousCheck): +class LandPolygonsDBCrossing(ContinuousCheck): """ - Use the 'LandPolygonsCrossing' constraint cautiously. + Use the 'LandPolygonsDBCrossing' constraint to check land crossing using database-backed polygons. This class is yet to be tested. """ land_polygon_STRTree = None @@ -1084,11 +1194,13 @@ def build_landpolygon_query(self, map_size): return query def set_landpolygon_STRTree(self, db_engine=None, query=None): + from shapely.strtree import STRtree land_polygon_gdf = self.query_land_polygons(db_engine, query) land_STRTree = STRtree(land_polygon_gdf["geom"]) return land_STRTree def query_land_polygons(self, db_engine, query): + import geopandas as gpd """ Create new GeoDataFrame using public.ways table in the query @@ -1124,6 +1236,8 @@ def check_crossing(self, lat_start, lon_start, lat_end, lon_end): if self.land_polygon_STRTree is not None: # generating the LineString geometry from start and end point for i in range(len(lat_start)): + from shapely.geometry import LineString, Point + import geopandas as gpd start_point = Point(lon_start[i], lat_start[i]) end_point = Point(lon_end[i], lat_end[i]) line = LineString([start_point, end_point]) diff --git a/WeatherRoutingTool/execute_routing.py b/WeatherRoutingTool/execute_routing.py index 1bf8b4d0..cd81bfed 100644 --- a/WeatherRoutingTool/execute_routing.py +++ b/WeatherRoutingTool/execute_routing.py @@ -1,5 +1,4 @@ # import cProfile -from datetime import datetime import WeatherRoutingTool.utils.graphics as graphics from WeatherRoutingTool.ship.ship_factory import ShipFactory diff --git a/WeatherRoutingTool/routeparams.py b/WeatherRoutingTool/routeparams.py index 2effa278..07544847 100644 --- a/WeatherRoutingTool/routeparams.py +++ b/WeatherRoutingTool/routeparams.py @@ -1,16 +1,12 @@ import json from datetime import datetime, timedelta -import cartopy.crs as ccrs import logging import numpy as np -import matplotlib.pyplot as plt import pandas from geovectorslib import geod -from matplotlib import gridspec from astropy import units as u -import WeatherRoutingTool.utils.unit_conversion as units import WeatherRoutingTool.utils as utils import WeatherRoutingTool.utils.graphics as graphics import WeatherRoutingTool.utils.formatting as form @@ -346,6 +342,8 @@ def get_dist_from_coords(self, lats, lons): return dist * u.meter def plot_route(self, ax, colour, label, linestyle=False): + import cartopy.crs as ccrs + input_crs = ccrs.PlateCarree() lats = self.lats_per_step lons = self.lons_per_step @@ -367,6 +365,8 @@ def get_power_type(self, power_type): return {"value": self.get_fuel_per_dist(), "label": "fuel consumption", "unit": u.kg} def plot_power_vs_dist(self, color, label, power_type, ax, bin_center_mean=None, bin_width_mean=None): + import matplotlib.pyplot as plt + power = self.get_power_type(power_type) dist = self.dists_per_step @@ -419,6 +419,8 @@ def plot_power_vs_dist(self, color, label, power_type, ax, bin_center_mean=None, # TODO check whether correct: Why do we see steps and no smooth curve? def plot_acc_power_vs_dist(self, color, label, power_type): + import matplotlib.pyplot as plt + power = self.get_power_type(power_type) dist = self.dists_per_step @@ -450,6 +452,8 @@ def plot_acc_power_vs_dist(self, color, label, power_type): plt.xticks() def plot_power_vs_dist_ratios(self, denominator, color, label, power_type): + import matplotlib.pyplot as plt + power_nom = self.get_power_type(power_type) dist_nom = self.dists_per_step hist_values_nom = graphics.get_hist_values_from_widths(dist_nom, power_nom["value"], power_type) @@ -481,6 +485,8 @@ def plot_power_vs_dist_ratios(self, denominator, color, label, power_type): plt.xticks() def plot_power_vs_coord(self, ax, color, label, coordstring, power_type): + import matplotlib.pyplot as plt + power = self.get_power_type(power_type) if coordstring == 'lat': coord = self.lats_per_step[:-1] @@ -515,6 +521,9 @@ def set_ship_params(self, ship_params): self.ship_params_per_step = ship_params def plot_power_vs_dist_with_weather(self, data_array, label_array, n_datasets): + import matplotlib.pyplot as plt + from matplotlib import gridspec + if n_datasets < 1: raise ValueError('You should at least provide 1 dataset!') diff --git a/WeatherRoutingTool/ship/direct_power_boat.py b/WeatherRoutingTool/ship/direct_power_boat.py index 6991f8be..1493784a 100644 --- a/WeatherRoutingTool/ship/direct_power_boat.py +++ b/WeatherRoutingTool/ship/direct_power_boat.py @@ -2,9 +2,7 @@ import math from pathlib import Path -import matplotlib.pyplot as plt import numpy as np -import xarray as xr from astropy import units as u import WeatherRoutingTool.utils.formatting as form diff --git a/WeatherRoutingTool/utils/graphics.py b/WeatherRoutingTool/utils/graphics.py index a446b3e2..503b519b 100644 --- a/WeatherRoutingTool/utils/graphics.py +++ b/WeatherRoutingTool/utils/graphics.py @@ -1,17 +1,7 @@ import csv - -import cartopy.crs as ccrs -import cartopy.feature as cf -import matplotlib -import matplotlib.pyplot as plt -import matplotlib.animation as animation -import numpy as np import os -import pandas as pd from astropy import units as u from geovectorslib import geod -from matplotlib.figure import Figure -from PIL import Image graphics_options = {'font_size': 20, 'fig_size': (12, 10)} @@ -21,6 +11,8 @@ def get_standard(var): def get_standard_fig(): + import matplotlib + from matplotlib.figure import Figure fig = Figure(figsize=get_standard('fig_size'), dpi=100) matplotlib.rcParams.update({'font.size': get_standard('font_size')}) return fig @@ -44,6 +36,9 @@ def get_gcr_points(lat1, lon1, lat2, lon2, n_points=10): def create_maps(lat1, lon1, lat2, lon2, dpi, winds, n_maps): + import cartopy.crs as ccrs + import cartopy.feature as cf + from matplotlib.figure import Figure """Return map figure.""" fig = Figure(figsize=(1600 / dpi, 800 * n_maps / dpi), dpi=dpi) fig.set_constrained_layout_pads(w_pad=4. / dpi, h_pad=4. / dpi) @@ -71,6 +66,9 @@ def create_maps(lat1, lon1, lat2, lon2, dpi, winds, n_maps): def create_map(lat1, lon1, lat2, lon2, dpi): + import cartopy.crs as ccrs + import cartopy.feature as cf + from matplotlib.figure import Figure """Return map figure.""" fig = Figure(figsize=(1200 / dpi, 420 / dpi), dpi=dpi) fig.set_constrained_layout_pads(w_pad=4. / dpi, h_pad=4. / dpi) @@ -163,6 +161,7 @@ def get_linestyle(i): def rebin(a, rebinx, rebiny): + import numpy as np modx = a.shape[0] % rebinx mody = a.shape[1] % rebiny @@ -182,6 +181,10 @@ def rebin(a, rebinx, rebiny): def merge_figs(path, ncounts): + import matplotlib.pyplot as plt + import matplotlib.animation as animation + from PIL import Image + fig, ax = plt.subplots(figsize=(12, 8), dpi=500) ax.axis('off') image_list = [] @@ -212,6 +215,7 @@ def get_figure_path(): def get_hist_values_from_boundaries(bin_boundaries, contend_unnormalised): + import numpy as np centres = np.array([]) widths = np.array([]) contents = np.array([]) @@ -226,6 +230,7 @@ def get_hist_values_from_boundaries(bin_boundaries, contend_unnormalised): def get_hist_values_from_widths(bin_widths, contend_unnormalised, power_type): + import numpy as np centres = np.array([]) * u.meter contents = np.array([]) if power_type == 'fuel': @@ -262,6 +267,7 @@ def get_hist_values_from_widths(bin_widths, contend_unnormalised, power_type): def get_accumulated_dist(dist_arr): + import numpy as np dist_acc = np.array([]) full_dist = 0 @@ -274,6 +280,7 @@ def get_accumulated_dist(dist_arr): def set_graphics_standards(ax): + import matplotlib.pyplot as plt for label in (ax.get_xticklabels() + ax.get_yticklabels()): label.set_fontsize(20) plt.rcParams['font.size'] = '20' @@ -289,6 +296,10 @@ def generate_basemap( show_depth=True, show_gcr=False ): + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + import cartopy.feature as cf + import numpy as np plt.rcParams['font.size'] = get_standard('font_size') (min_lat, max_lat, min_lon, max_lon) = map @@ -356,6 +367,7 @@ def generate_basemap( # TODO: faulty def plot_genetic_algorithm_initial_population(src, dest, routes): + import matplotlib.pyplot as plt figure_path = get_figure_path() if figure_path is not None: plt.rcParams['font.size'] = get_standard('font_size') diff --git a/WeatherRoutingTool/weather.py b/WeatherRoutingTool/weather.py index 66b2adf3..63dc88f6 100644 --- a/WeatherRoutingTool/weather.py +++ b/WeatherRoutingTool/weather.py @@ -1,13 +1,9 @@ import logging -import sys import os import time from datetime import datetime, timedelta from math import ceil -import cartopy.crs as ccrs -import datacube -import matplotlib.pyplot as plt import numpy as np import xarray as xr from scipy.interpolate import RegularGridInterpolator @@ -111,6 +107,9 @@ def read_dataset(self, filepath=None): pass def plot_wind_weather(self, time, rebinx=5, rebiny=5): + import cartopy.crs as ccrs + import matplotlib.pyplot as plt + input_crs = ccrs.PlateCarree() fig, ax = graphics.generate_basemap( map=self.map_size.get_var_tuple(), @@ -397,6 +396,8 @@ def read_wind_vectors(self, time): return {'u': u, 'v': v, 'lats_u': lats_u, 'lons_u': lons_u, 'timestamp': time} def plot_weather_map(self, fig, ax, time, varname, rebinx=5, rebiny=5, **kwargs): + import matplotlib.pyplot as plt + if varname == 'wind': u = self.ds['u-component_of_wind_height_above_ground'].where(self.ds.VHM0 > 0).sel( @@ -618,6 +619,7 @@ def read_dataset(self, filepath=None): class WeatherCondODC(WeatherCond): def __init__(self, time, hours, time_res): + import datacube super().__init__(time, hours, time_res) self.dc = datacube.Datacube() diff --git a/pyproject.toml b/pyproject.toml index d676a3f9..31089b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,16 @@ classifiers = [ ] dynamic = ["dependencies"] +[project.optional-dependencies] +geo = ["cartopy>0.20.0", "geopandas", "shapely", "global_land_mask"] +vis = ["matplotlib", "seaborn"] +data = ["dask", "datacube", "netcdf4", "scikit-image"] +all = [ + "cartopy>0.20.0", "geopandas", "shapely", "global_land_mask", + "matplotlib", "seaborn", + "dask", "datacube", "netcdf4", "scikit-image" +] + [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } diff --git a/requirements.test.txt b/requirements.test.txt index 3fd0ece5..bed64816 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -1,4 +1,4 @@ --r requirements.txt +.[all] flake8 psycopg2 pytest diff --git a/requirements.txt b/requirements.txt index e48d110b..76d89108 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,10 @@ astropy -cartopy>0.20.0 -dask -datacube geographiclib -geopandas geovectorslib@git+https://github.com/52North/geovectors -global_land_mask maridatadownloader@git+https://github.com/52North/maridatadownloader -matplotlib -netcdf4 numpy pandas pymoo>=0.6.1 -scikit-image scipy>=1.10.0 -seaborn -shapely sqlalchemy>1.4.51 xarray diff --git a/tests/basic_test_func.py b/tests/basic_test_func.py index f2f63d4a..127f9c68 100644 --- a/tests/basic_test_func.py +++ b/tests/basic_test_func.py @@ -5,7 +5,7 @@ from WeatherRoutingTool.algorithms.isofuel import IsoFuel from WeatherRoutingTool.config import Config from WeatherRoutingTool.constraints.constraints import ConstraintsList, ConstraintPars, \ - SeamarkCrossing, LandPolygonsCrossing + SeamarkCrossing, LandPolygonsCrossing, LandPolygonsDBCrossing from WeatherRoutingTool.ship.direct_power_boat import DirectPowerBoat try: @@ -47,7 +47,7 @@ def create_dummy_SeamarkCrossing_object(db_engine): def create_dummy_landpolygonsCrossing_object(db_engine): - landpolygoncrossing_obj = LandPolygonsCrossing(db_engine=db_engine) + landpolygoncrossing_obj = LandPolygonsDBCrossing(db_engine=db_engine) return landpolygoncrossing_obj diff --git a/tests/test_genetic.py b/tests/test_genetic.py index 8a6ac01c..7a34b7ca 100644 --- a/tests/test_genetic.py +++ b/tests/test_genetic.py @@ -115,7 +115,7 @@ def get_route_lc(X): @pytest.mark.manual -def test_random_plateau_mutation(plt): +def test_random_plateau_mutation(): dirname = os.path.dirname(__file__) configpath = os.path.join(dirname, 'config.isofuel_single_route.json') config = Config.assign_config(Path(configpath)) @@ -149,10 +149,10 @@ def test_random_plateau_mutation(plt): ax.add_collection(new_route_two_lc) cbar = fig.colorbar(old_route_one_lc, ax=ax, orientation='vertical', pad=0.15, shrink=0.7) - cbar.set_label('Geschwindigkeit ($m/s$)') + cbar.set_label('Geschwindigkeit (m/s)') pyplot.tight_layout() - plt.saveas = "test_random_plateau_mutation.png" + pyplot.savefig("test_random_plateau_mutation.png") assert old_route.shape == new_route.shape for i_route in range(old_route.shape[0]): @@ -190,7 +190,7 @@ def test_random_plateau_mutation_refusal(): @pytest.mark.manual -def test_bezier_curve_mutation(plt): +def test_bezier_curve_mutation(): dirname = os.path.dirname(__file__) configpath = os.path.join(dirname, 'config.isofuel_single_route.json') config = Config.assign_config(Path(configpath)) @@ -224,10 +224,10 @@ def test_bezier_curve_mutation(plt): ax.add_collection(new_route_two_lc) cbar = fig.colorbar(old_route_one_lc, ax=ax, orientation='vertical', pad=0.15, shrink=0.7) - cbar.set_label('Geschwindigkeit ($m/s$)') + cbar.set_label('Geschwindigkeit (m/s)') pyplot.tight_layout() - plt.saveas = "test_bezier_curve_mutation.png" + pyplot.savefig("test_bezier_curve_mutation.png") assert old_route.shape == new_route.shape for i_route in range(old_route.shape[0]): @@ -279,7 +279,7 @@ def test_configuration_isofuel_patcher(): @pytest.mark.manual -def test_constraint_violation_repair(plt): +def test_constraint_violation_repair(): dirname = os.path.dirname(__file__) configpath = os.path.join(dirname, 'config.isofuel_single_route.json') config = Config.assign_config(Path(configpath)) @@ -314,10 +314,10 @@ def test_constraint_violation_repair(plt): ax.add_collection(new_route_lc) cbar = fig.colorbar(old_route_lc, ax=ax, orientation='vertical', pad=0.15, shrink=0.7) - cbar.set_label('Geschwindigkeit ($m/s$)') + cbar.set_label('Geschwindigkeit (m/s)') pyplot.tight_layout() - plt.saveas = "test_constraint_violation_repair.png" + pyplot.savefig("test_constraint_violation_repair.png") assert np.array_equal(new_route[0], old_route[0, 0][0]) assert np.array_equal(new_route[-2], old_route[0, 0][-2])