From 58fc5c72ffa845ee1a39750473c9217f05183060 Mon Sep 17 00:00:00 2001 From: Ritesh Thakur Date: Fri, 30 Jan 2026 18:58:09 +0530 Subject: [PATCH 1/2] Fix #131: Implement optional dependency groups for reduced installation footprint --- INSTALLATION.md | 179 ++++++++++++++++++++++++++++++++++++++++ README.md | 63 ++++++++++++++ pyproject.toml | 64 +++++++++++++- requirements.core.txt | 9 ++ test_minimal_install.py | 119 ++++++++++++++++++++++++++ 5 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 INSTALLATION.md create mode 100644 requirements.core.txt create mode 100644 test_minimal_install.py diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..2931b4d --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,179 @@ +# Installation Guide + +## Dependency Management + +WeatherRoutingTool uses optional dependency groups to provide a flexible installation experience. This allows users to install only the dependencies they need, reducing installation time and potential conflicts. + +## Installation Options + +### 1. Minimal Installation (Recommended for beginners) +```bash +pip install WeatherRoutingTool +``` + +**What you get:** +- Core weather routing functionality +- Basic data processing capabilities +- Configuration management +- Essential scientific computing tools + +**Use case:** Learning weather routing, basic route optimization, or when you don't need visualization. + +### 2. With Visualization +```bash +pip install WeatherRoutingTool[viz] +``` + +**What you get:** +- Everything from minimal installation +- Route visualization and plotting +- Map generation +- Statistical plots + +**Use case:** When you need to visualize routes, weather conditions, or optimization results. + +### 3. With Geospatial Features +```bash +pip install WeatherRoutingTool[geospatial] +``` + +**What you get:** +- Everything from minimal installation +- Advanced geographic calculations +- Land mask support +- Geospatial data analysis + +**Use case:** When working with complex geographic constraints or advanced route planning. + +### 4. With Genetic Algorithm +```bash +pip install WeatherRoutingTool[genetic] +``` + +**What you get:** +- Everything from minimal installation +- Genetic algorithm optimization +- Multi-objective optimization capabilities + +**Use case:** When you need advanced optimization algorithms beyond basic routing. + +### 5. With Data Processing +```bash +pip install WeatherRoutingTool[data] +``` + +**What you get:** +- Everything from minimal installation +- Large dataset processing +- Parallel computing capabilities +- Advanced data access + +**Use case:** When working with large weather datasets or need performance optimization. + +### 6. With External Data Download +```bash +pip install WeatherRoutingTool[download] +``` + +**What you get:** +- Everything from minimal installation +- Automatic weather data downloading +- Access to external data sources + +**Use case:** When you need to fetch weather data from external sources automatically. + +### 7. Full Installation +```bash +pip install WeatherRoutingTool[all] +``` + +**What you get:** All features and dependencies. + +**Use case:** When you need all features or are unsure which groups you need. + +### 8. Development Installation +```bash +pip install WeatherRoutingTool[dev] +``` + +**What you get:** +- All features +- Development tools (flake8, pytest) +- Testing framework + +**Use case:** For contributors and developers working on the codebase. + +## Combining Groups + +You can combine multiple groups: +```bash +pip install WeatherRoutingTool[viz,genetic] +``` + +## Upgrading Installations + +To add features to an existing installation: +```bash +pip install WeatherRoutingTool[viz] # Adds visualization to existing installation +``` + +## Troubleshooting + +### Installation Issues + +1. **Cartopy installation fails on Windows:** + ```bash + pip install WeatherRoutingTool[viz] --no-binary cartopy + ``` + +2. **Memory errors during installation:** + Use minimal installation and add groups as needed: + ```bash + pip install WeatherRoutingTool + pip install WeatherRoutingTool[viz] + ``` + +3. **Conflicts with existing packages:** + Use a virtual environment: + ```bash + python -m venv wrt_env + source wrt_env/bin/activate # On Windows: wrt_env\Scripts\activate + pip install WeatherRoutingTool[all] + ``` + +### Dependency Conflicts + +If you encounter dependency conflicts, try: +1. Using a fresh virtual environment +2. Installing groups individually to identify the conflicting package +3. Using `pip install --upgrade` to update conflicting packages + +## Performance Considerations + +- **Minimal installation** has the fastest installation time and smallest footprint +- **Full installation** provides all features but takes longer to install +- **Selective installation** reduces the chance of dependency conflicts +- **Virtual environments** are recommended for all installations + +## Feature Requirements + +| Feature | Required Dependency Group | +|---------|---------------------------| +| Basic routing | core (included in all installations) | +| Route plotting | viz | +| Map generation | viz | +| Genetic algorithm | genetic | +| Land constraints | geospatial | +| Large datasets | data | +| Auto-download weather | download | +| Image processing | image | + +## Migration from Previous Versions + +If you're upgrading from a version before dependency groups: +1. Your existing installation will continue to work +2. To use the new modular approach, create a fresh installation: + ```bash + pip uninstall WeatherRoutingTool + pip install WeatherRoutingTool[all] # Or your preferred groups + ``` diff --git a/README.md b/README.md index 72b7f47..006c4af 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,69 @@ A tool to perform optimization of ship routes based on fuel consumption in different weather conditions. +## Installation + +### Minimal Installation (Core Features) +For basic weather routing functionality without visualization or advanced features: + +```bash +pip install WeatherRoutingTool +``` + +### Installation with Optional Features + +#### With Visualization Support +For plotting and visualization capabilities: +```bash +pip install WeatherRoutingTool[viz] +``` + +#### With Geospatial Features +For advanced geospatial analysis: +```bash +pip install WeatherRoutingTool[geospatial] +``` + +#### With Genetic Algorithm +For optimization using genetic algorithms: +```bash +pip install WeatherRoutingTool[genetic] +``` + +#### With Data Processing +For large dataset processing: +```bash +pip install WeatherRoutingTool[data] +``` + +#### With External Data Download +For downloading weather data: +```bash +pip install WeatherRoutingTool[download] +``` + +#### Full Installation +For all features (equivalent to current installation): +```bash +pip install WeatherRoutingTool[all] +``` + +#### Development Installation +For contributors and developers: +```bash +pip install WeatherRoutingTool[dev] +``` + +### Dependency Groups + +- **Core**: `numpy`, `pandas`, `xarray`, `scipy`, `pydantic`, `astropy` - Essential for basic functionality +- **Visualization**: `matplotlib`, `seaborn`, `cartopy` - Plotting and mapping +- **Geospatial**: `geopandas`, `shapely`, `geographiclib`, `geovectorslib`, `global_land_mask` - Advanced geographic features +- **Data Processing**: `dask`, `datacube`, `netcdf4` - Large dataset handling +- **Download**: `maridatadownloader` - External weather data downloading +- **Genetic**: `pymoo` - Genetic algorithm optimization +- **Image**: `scikit-image`, `Pillow` - Image processing + 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/pyproject.toml b/pyproject.toml index d676a3f..9a5cbe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,70 @@ classifiers = [ ] dynamic = ["dependencies"] +[project.optional-dependencies] +# Core dependencies for basic weather routing functionality +core = [ + "numpy", + "pandas", + "xarray", + "scipy>=1.10.0", + "pydantic", + "astropy" +] + +# Visualization and plotting capabilities +viz = [ + "matplotlib", + "seaborn", + "cartopy>0.20.0" +] + +# Advanced geospatial features +geospatial = [ + "geopandas", + "shapely", + "geographiclib", + "geovectorslib", + "global_land_mask" +] + +# Large dataset processing and data access +data = [ + "dask", + "datacube", + "netcdf4" +] + +# External data downloading capabilities +download = [ + "maridatadownloader@git+https://github.com/52North/maridatadownloader" +] + +# Genetic algorithm optimization +genetic = [ + "pymoo>=0.6.1" +] + +# Image processing features +image = [ + "scikit-image", + "Pillow" +] + +# All optional dependencies (full installation) +all = [ + "WeatherRoutingTool[core,viz,geospatial,data,download,genetic,image]" +] + +# Development dependencies +dev = [ + "WeatherRoutingTool[all]", + "flake8", + "pytest" +] + [tool.setuptools.dynamic] -dependencies = { file = ["requirements.txt"] } +dependencies = { file = ["requirements.core.txt"] } [tool.setuptools.packages.find] include = ["WeatherRoutingTool*"] diff --git a/requirements.core.txt b/requirements.core.txt new file mode 100644 index 0000000..0415220 --- /dev/null +++ b/requirements.core.txt @@ -0,0 +1,9 @@ +# Core dependencies for minimal WeatherRoutingTool installation +# These are essential for basic weather routing functionality + +numpy +pandas +xarray +scipy>=1.10.0 +pydantic +astropy diff --git a/test_minimal_install.py b/test_minimal_install.py new file mode 100644 index 0000000..09ec8bf --- /dev/null +++ b/test_minimal_install.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Test script to verify minimal installation works correctly. +This script tests core functionality without optional dependencies. +""" + +import sys +import traceback + +def test_core_imports(): + """Test that core modules can be imported with minimal installation.""" + print("Testing core imports...") + + try: + # Core dependencies + import numpy as np + import pandas as pd + import xarray as xr + from scipy import interpolate + import astropy.units as u + from pydantic import BaseModel + print("✓ Core dependencies imported successfully") + except ImportError as e: + print(f"✗ Core dependency import failed: {e}") + return False + + try: + # WRT core modules + import WeatherRoutingTool.config as config + import WeatherRoutingTool.routeparams as routeparams + import WeatherRoutingTool.weather as weather + print("✓ WRT core modules imported successfully") + except ImportError as e: + print(f"✗ WRT core module import failed: {e}") + traceback.print_exc() + return False + + return True + +def test_basic_functionality(): + """Test basic WRT functionality.""" + print("\nTesting basic functionality...") + + try: + from WeatherRoutingTool.config import Config + from WeatherRoutingTool.weather import WeatherCond + from datetime import datetime, timedelta + + # Test basic configuration + print("✓ Config class accessible") + + # Test weather condition creation + weather_cond = WeatherCond( + time=datetime.now(), + hours=24, + time_res=3 + ) + print("✓ WeatherCond creation successful") + + return True + except Exception as e: + print(f"✗ Basic functionality test failed: {e}") + traceback.print_exc() + return False + +def test_optional_imports_fail(): + """Test that optional dependencies are not available in minimal install.""" + print("\nTesting that optional dependencies are not available...") + + optional_deps = [ + ('matplotlib', 'matplotlib.pyplot'), + ('cartopy', 'cartopy.crs'), + ('geopandas', 'geopandas'), + ('pymoo', 'pymoo'), + ('dask', 'dask'), + ('netcdf4', 'netCDF4'), + ] + + for dep_name, import_path in optional_deps: + try: + __import__(import_path) + print(f"⚠ Optional dependency {dep_name} is available (unexpected)") + except ImportError: + print(f"✓ Optional dependency {dep_name} not available (expected)") + + return True + +def main(): + """Run all tests.""" + print("WeatherRoutingTool Minimal Installation Test") + print("=" * 50) + + tests = [ + test_core_imports, + test_basic_functionality, + test_optional_imports_fail, + ] + + results = [] + for test in tests: + try: + result = test() + results.append(result) + except Exception as e: + print(f"✗ Test {test.__name__} failed with exception: {e}") + results.append(False) + + print("\n" + "=" * 50) + print(f"Results: {sum(results)}/{len(results)} tests passed") + + if all(results): + print("🎉 All tests passed! Minimal installation works correctly.") + return 0 + else: + print("❌ Some tests failed. Check the output above.") + return 1 + +if __name__ == "__main__": + sys.exit(main()) From 2c41f2772c405d66f934eb377dce85c763a2f3a9 Mon Sep 17 00:00:00 2001 From: Ritesh Thakur Date: Fri, 30 Jan 2026 19:15:29 +0530 Subject: [PATCH 2/2] Fix #104: Improve initial population diversity in IsoFuel algorithm --- .../algorithms/genetic/population.py | 96 +++++++- test_perturbation_simple.py | 205 ++++++++++++++++ test_population_diversity.py | 227 ++++++++++++++++++ 3 files changed, 524 insertions(+), 4 deletions(-) create mode 100644 test_perturbation_simple.py create mode 100644 test_population_diversity.py diff --git a/WeatherRoutingTool/algorithms/genetic/population.py b/WeatherRoutingTool/algorithms/genetic/population.py index 1796a48..60b686f 100644 --- a/WeatherRoutingTool/algorithms/genetic/population.py +++ b/WeatherRoutingTool/algorithms/genetic/population.py @@ -270,11 +270,55 @@ def generate(self, problem, n_samples, **kw): X[i, 0] = rt - # fallback: fill all other individuals with the same population as the last one + # fallback: fill remaining individuals with perturbed copies to maintain diversity for j in range(i + 1, n_samples): - X[j, 0] = np.copy(X[j - 1, 0]) + X[j, 0] = self._perturb_route(np.copy(X[j - 1, 0]), j - i) return X + def _perturb_route(self, route: np.ndarray, perturbation_factor: int) -> np.ndarray: + """Apply controlled perturbation to a route to maintain population diversity. + + Creates small random variations in intermediate waypoints while preserving + start and destination points. Perturbation magnitude increases with perturbation_factor + to ensure diversity across multiple copied routes. + + :param route: Original route to perturb as array of [lat, lon, speed] waypoints + :type route: np.ndarray + :param perturbation_factor: Factor to control perturbation magnitude (higher = more variation) + :type perturbation_factor: int + :return: Perturbed route with same structure as input + :rtype: np.ndarray + """ + if len(route) <= 2: + # Route has only start and end points, cannot perturb + return route + + # Create perturbed copy + perturbed_route = np.copy(route) + + # Calculate perturbation magnitude (0.1 to 1.0 degrees based on factor) + # Cap at reasonable maximum to maintain route coherence + max_perturbation = min(0.1 + (perturbation_factor * 0.1), 1.0) + + # Perturb intermediate waypoints (exclude start [0] and end [-1]) + for i in range(1, len(route) - 1): + # Apply random perturbation to latitude and longitude + lat_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + lon_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + + # Apply perturbations + perturbed_route[i, 0] += lat_perturbation # latitude + perturbed_route[i, 1] += lon_perturbation # longitude + + # Ensure coordinates remain within valid ranges + perturbed_route[i, 0] = np.clip(perturbed_route[i, 0], -90, 90) # latitude bounds + perturbed_route[i, 1] = np.clip(perturbed_route[i, 1], -180, 180) # longitude bounds + + # Log perturbation for debugging + logger.debug(f"Applied perturbation factor {perturbation_factor} with max magnitude {max_perturbation:.3f} degrees") + + return perturbed_route + class GcrSliderPopulation(Population): @@ -345,11 +389,55 @@ def generate(self, problem, n_samples, **kw): rt = self.recalculate_speed_for_route(rt) X[i, 0] = rt - # fallback: fill all other individuals with the same population as the last one + # fallback: fill remaining individuals with perturbed copies to maintain diversity for j in range(i + 1, n_samples): - X[j, 0] = np.copy(X[j - 1, 0]) + X[j, 0] = self._perturb_route(np.copy(X[j - 1, 0]), j - i) return X + def _perturb_route(self, route: np.ndarray, perturbation_factor: int) -> np.ndarray: + """Apply controlled perturbation to a route to maintain population diversity. + + Creates small random variations in intermediate waypoints while preserving + start and destination points. Perturbation magnitude increases with perturbation_factor + to ensure diversity across multiple copied routes. + + :param route: Original route to perturb as array of [lat, lon, speed] waypoints + :type route: np.ndarray + :param perturbation_factor: Factor to control perturbation magnitude (higher = more variation) + :type perturbation_factor: int + :return: Perturbed route with same structure as input + :rtype: np.ndarray + """ + if len(route) <= 2: + # Route has only start and end points, cannot perturb + return route + + # Create perturbed copy + perturbed_route = np.copy(route) + + # Calculate perturbation magnitude (0.1 to 1.0 degrees based on factor) + # Cap at reasonable maximum to maintain route coherence + max_perturbation = min(0.1 + (perturbation_factor * 0.1), 1.0) + + # Perturb intermediate waypoints (exclude start [0] and end [-1]) + for i in range(1, len(route) - 1): + # Apply random perturbation to latitude and longitude + lat_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + lon_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + + # Apply perturbations + perturbed_route[i, 0] += lat_perturbation # latitude + perturbed_route[i, 1] += lon_perturbation # longitude + + # Ensure coordinates remain within valid ranges + perturbed_route[i, 0] = np.clip(perturbed_route[i, 0], -90, 90) # latitude bounds + perturbed_route[i, 1] = np.clip(perturbed_route[i, 1], -180, 180) # longitude bounds + + # Log perturbation for debugging + logger.debug(f"Applied perturbation factor {perturbation_factor} with max magnitude {max_perturbation:.3f} degrees") + + return perturbed_route + def create_route(self, lat: float = None, lon: float = None, speed: float = None): """ :param lat: latitude of the waypoint diff --git a/test_perturbation_simple.py b/test_perturbation_simple.py new file mode 100644 index 0000000..72cf69c --- /dev/null +++ b/test_perturbation_simple.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +Simple test to verify the perturbation logic without requiring full module imports. +This tests the core perturbation algorithm directly. +""" + +import numpy as np +import sys +import os + +def test_perturbation_logic(): + """Test the perturbation logic directly.""" + print("Testing perturbation logic...") + + def perturb_route(route: np.ndarray, perturbation_factor: int) -> np.ndarray: + """Apply controlled perturbation to a route to maintain population diversity.""" + if len(route) <= 2: + # Route has only start and end points, cannot perturb + return route + + # Create perturbed copy + perturbed_route = np.copy(route) + + # Calculate perturbation magnitude (0.1 to 1.0 degrees based on factor) + # Cap at reasonable maximum to maintain route coherence + max_perturbation = min(0.1 + (perturbation_factor * 0.1), 1.0) + + # Perturb intermediate waypoints (exclude start [0] and end [-1]) + for i in range(1, len(route) - 1): + # Apply random perturbation to latitude and longitude + lat_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + lon_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + + # Apply perturbations + perturbed_route[i, 0] += lat_perturbation # latitude + perturbed_route[i, 1] += lon_perturbation # longitude + + # Ensure coordinates remain within valid ranges + perturbed_route[i, 0] = np.clip(perturbed_route[i, 0], -90, 90) # latitude bounds + perturbed_route[i, 1] = np.clip(perturbed_route[i, 1], -180, 180) # longitude bounds + + return perturbed_route + + try: + # Set seed for reproducible tests + np.random.seed(42) + + # Create test route + original_route = np.array([ + [0.0, 0.0, 10.0], # start + [0.5, 0.5, 10.0], # intermediate 1 + [0.7, 0.7, 10.0], # intermediate 2 + [1.0, 1.0, 10.0] # end + ]) + + # Apply perturbations with different factors + perturbed1 = perturb_route(np.copy(original_route), 1) + perturbed2 = perturb_route(np.copy(original_route), 2) + perturbed3 = perturb_route(np.copy(original_route), 3) + + # Check that routes are different + assert not np.array_equal(perturbed1, original_route), "Perturbed route 1 should be different" + assert not np.array_equal(perturbed2, original_route), "Perturbed route 2 should be different" + assert not np.array_equal(perturbed3, original_route), "Perturbed route 3 should be different" + + # Check that perturbed routes are different from each other + assert not np.array_equal(perturbed1, perturbed2), "Perturbed routes should be different" + assert not np.array_equal(perturbed2, perturbed3), "Perturbed routes should be different" + + # Verify start and end points are preserved + np.testing.assert_array_equal(perturbed1[0], original_route[0], "Start point should be preserved") + np.testing.assert_array_equal(perturbed1[-1], original_route[-1], "End point should be preserved") + + # Verify coordinates are within valid bounds + assert np.all(perturbed1[:, 0] >= -90) and np.all(perturbed1[:, 0] <= 90), "Latitude out of bounds" + assert np.all(perturbed1[:, 1] >= -180) and np.all(perturbed1[:, 1] <= 180), "Longitude out of bounds" + + # Test edge case: route with only start and end points + short_route = np.array([ + [0.0, 0.0, 10.0], # start + [1.0, 1.0, 10.0] # end + ]) + + perturbed_short = perturb_route(np.copy(short_route), 1) + np.testing.assert_array_equal(perturbed_short, short_route, "Short route should remain unchanged") + + print("✓ Perturbation logic test passed!") + return True + + except Exception as e: + print(f"✗ Perturbation logic test failed: {e}") + import traceback + traceback.print_exc() + return False + +def test_diversity_simulation(): + """Simulate the diversity improvement scenario.""" + print("\nTesting diversity simulation...") + + def perturb_route(route: np.ndarray, perturbation_factor: int) -> np.ndarray: + """Apply controlled perturbation to a route to maintain population diversity.""" + if len(route) <= 2: + return route + + perturbed_route = np.copy(route) + max_perturbation = min(0.1 + (perturbation_factor * 0.1), 1.0) + + for i in range(1, len(route) - 1): + lat_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + lon_perturbation = np.random.uniform(-max_perturbation, max_perturbation) + perturbed_route[i, 0] += lat_perturbation + perturbed_route[i, 1] += lon_perturbation + perturbed_route[i, 0] = np.clip(perturbed_route[i, 0], -90, 90) + perturbed_route[i, 1] = np.clip(perturbed_route[i, 1], -180, 180) + + return perturbed_route + + try: + # Set seed for reproducible tests + np.random.seed(42) + + # Simulate the scenario: IsoFuel generates only 3 routes, but we need 10 + n_samples = 10 + generated_routes = 3 + + # Create mock routes + base_routes = [] + for i in range(generated_routes): + route = np.array([ + [0.0, 0.0, 10.0], # start + [0.5 + i*0.1, 0.5 + i*0.1, 10.0], # intermediate + [1.0, 1.0, 10.0] # end + ]) + base_routes.append(route) + + # OLD approach: exact copying + old_population = [] + for i in range(n_samples): + if i < generated_routes: + old_population.append(base_routes[i]) + else: + old_population.append(np.copy(base_routes[-1])) # Exact copy + + # NEW approach: perturbation + new_population = [] + for i in range(n_samples): + if i < generated_routes: + new_population.append(base_routes[i]) + else: + perturbed = perturb_route(np.copy(base_routes[-1]), i - generated_routes + 1) + new_population.append(perturbed) + + # Count unique routes in old approach + old_unique = len(set(tuple(map(tuple, route)) for route in old_population)) + new_unique = len(set(tuple(map(tuple, route)) for route in new_population)) + + print(f"Old approach: {old_unique} unique routes out of {n_samples}") + print(f"New approach: {new_unique} unique routes out of {n_samples}") + + # Old approach should have only 3 unique routes (the generated ones) + assert old_unique == generated_routes, f"Old approach should have {generated_routes} unique routes" + + # New approach should have more unique routes + assert new_unique > generated_routes, f"New approach should have more than {generated_routes} unique routes" + + print("✓ Diversity simulation test passed!") + return True + + except Exception as e: + print(f"✗ Diversity simulation test failed: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Run all tests.""" + print("Population Diversity Improvement Test (Simple)") + print("=" * 50) + + tests = [ + test_perturbation_logic, + test_diversity_simulation, + ] + + results = [] + for test in tests: + try: + result = test() + results.append(result) + except Exception as e: + print(f"✗ Test {test.__name__} failed with exception: {e}") + results.append(False) + + print("\n" + "=" * 50) + print(f"Results: {sum(results)}/{len(results)} tests passed") + + if all(results): + print("🎉 All tests passed! Population diversity improvement works correctly.") + return 0 + else: + print("❌ Some tests failed. Check the output above.") + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test_population_diversity.py b/test_population_diversity.py new file mode 100644 index 0000000..4963aaa --- /dev/null +++ b/test_population_diversity.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Test script to verify the population diversity improvement in genetic algorithms. +This script tests that the perturbation mechanism creates diverse routes instead of identical clones. +""" + +import sys +import numpy as np +from unittest.mock import Mock, patch + +# Add the WeatherRoutingTool to the path +sys.path.insert(0, '/Users/riteshthakur/Developer/open source/WeatherRoutingTool') + +def test_isofuel_population_diversity(): + """Test that IsoFuelPopulation creates diverse routes when fallback is used.""" + print("Testing IsoFuelPopulation diversity...") + + try: + from WeatherRoutingTool.algorithms.genetic.population import IsoFuelPopulation + from WeatherRoutingTool.config import Config + from WeatherRoutingTool.constraints.constraints import ConstraintsList + + # Create mock config + mock_config = Mock(spec=Config) + mock_config.DEPARTURE_TIME = None + mock_config.ARRIVAL_TIME = None + mock_config.BOAT_SPEED = 10.0 + mock_config.DEFAULT_ROUTE = [0.0, 0.0, 1.0, 1.0, 1.0] + + # Create mock constraints + mock_constraints = Mock(spec=ConstraintsList) + + # Create population instance + pop = IsoFuelPopulation( + config=mock_config, + default_route=[0.0, 0.0, 1.0, 1.0, 1.0], + constraints_list=mock_constraints, + pop_size=10 + ) + + # Mock the patcher to return only 2 routes instead of 10 + mock_route1 = np.array([ + [0.0, 0.0, 10.0], # start + [0.5, 0.5, 10.0], # intermediate + [1.0, 1.0, 10.0] # end + ]) + + mock_route2 = np.array([ + [0.0, 0.0, 10.0], # start + [0.6, 0.4, 10.0], # intermediate + [1.0, 1.0, 10.0] # end + ]) + + pop.patcher = Mock() + pop.patcher.patch.return_value = [mock_route1, mock_route2] + + # Generate population + problem = Mock() + X = pop.generate(problem, n_samples=10) + + # Verify we have 10 routes + assert X.shape[0] == 10, f"Expected 10 routes, got {X.shape[0]}" + + # Check diversity: routes should not be identical + unique_routes = [] + for i in range(10): + route = X[i, 0] + # Convert to tuple for comparison + route_tuple = tuple(map(tuple, route)) + unique_routes.append(route_tuple) + + # Count unique routes + unique_count = len(set(unique_routes)) + print(f"Generated {unique_count} unique routes out of 10") + + # With perturbation, we should have more than 2 unique routes + assert unique_count > 2, f"Expected more than 2 unique routes, got {unique_count}" + + # Verify start and end points are preserved + for i in range(10): + route = X[i, 0] + np.testing.assert_array_equal(route[0], [0.0, 0.0, 10.0], + err_msg=f"Route {i} start point modified") + np.testing.assert_array_equal(route[-1], [1.0, 1.0, 10.0], + err_msg=f"Route {i} end point modified") + + print("✓ IsoFuelPopulation diversity test passed!") + return True + + except Exception as e: + print(f"✗ IsoFuelPopulation diversity test failed: {e}") + import traceback + traceback.print_exc() + return False + +def test_perturbation_mechanism(): + """Test the _perturb_route method directly.""" + print("\nTesting perturbation mechanism...") + + try: + from WeatherRoutingTool.algorithms.genetic.population import IsoFuelPopulation + from WeatherRoutingTool.config import Config + from WeatherRoutingTool.constraints.constraints import ConstraintsList + + # Create population instance + mock_config = Mock(spec=Config) + mock_constraints = Mock(spec=ConstraintsList) + + pop = IsoFuelPopulation( + config=mock_config, + default_route=[0.0, 0.0, 1.0, 1.0, 1.0], + constraints_list=mock_constraints, + pop_size=10 + ) + + # Create test route + original_route = np.array([ + [0.0, 0.0, 10.0], # start + [0.5, 0.5, 10.0], # intermediate 1 + [0.7, 0.7, 10.0], # intermediate 2 + [1.0, 1.0, 10.0] # end + ]) + + # Apply perturbations with different factors + perturbed1 = pop._perturb_route(np.copy(original_route), 1) + perturbed2 = pop._perturb_route(np.copy(original_route), 2) + perturbed3 = pop._perturb_route(np.copy(original_route), 3) + + # Check that routes are different + assert not np.array_equal(perturbed1, original_route), "Perturbed route 1 should be different" + assert not np.array_equal(perturbed2, original_route), "Perturbed route 2 should be different" + assert not np.array_equal(perturbed3, original_route), "Perturbed route 3 should be different" + + # Check that perturbed routes are different from each other + assert not np.array_equal(perturbed1, perturbed2), "Perturbed routes should be different" + assert not np.array_equal(perturbed2, perturbed3), "Perturbed routes should be different" + + # Verify start and end points are preserved + np.testing.assert_array_equal(perturbed1[0], original_route[0], "Start point should be preserved") + np.testing.assert_array_equal(perturbed1[-1], original_route[-1], "End point should be preserved") + + # Verify coordinates are within valid bounds + assert np.all(perturbed1[:, 0] >= -90) and np.all(perturbed1[:, 0] <= 90), "Latitude out of bounds" + assert np.all(perturbed1[:, 1] >= -180) and np.all(perturbed1[:, 1] <= 180), "Longitude out of bounds" + + print("✓ Perturbation mechanism test passed!") + return True + + except Exception as e: + print(f"✗ Perturbation mechanism test failed: {e}") + import traceback + traceback.print_exc() + return False + +def test_edge_cases(): + """Test edge cases for perturbation.""" + print("\nTesting edge cases...") + + try: + from WeatherRoutingTool.algorithms.genetic.population import IsoFuelPopulation + from WeatherRoutingTool.config import Config + from WeatherRoutingTool.constraints.constraints import ConstraintsList + + # Create population instance + mock_config = Mock(spec=Config) + mock_constraints = Mock(spec=ConstraintsList) + + pop = IsoFuelPopulation( + config=mock_config, + default_route=[0.0, 0.0, 1.0, 1.0, 1.0], + constraints_list=mock_constraints, + pop_size=10 + ) + + # Test with route that has only start and end points + short_route = np.array([ + [0.0, 0.0, 10.0], # start + [1.0, 1.0, 10.0] # end + ]) + + perturbed_short = pop._perturb_route(np.copy(short_route), 1) + + # Should return unchanged since there are no intermediate waypoints + np.testing.assert_array_equal(perturbed_short, short_route, + "Short route should remain unchanged") + + print("✓ Edge cases test passed!") + return True + + except Exception as e: + print(f"✗ Edge cases test failed: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Run all tests.""" + print("Population Diversity Improvement Test") + print("=" * 50) + + tests = [ + test_isofuel_population_diversity, + test_perturbation_mechanism, + test_edge_cases, + ] + + results = [] + for test in tests: + try: + result = test() + results.append(result) + except Exception as e: + print(f"✗ Test {test.__name__} failed with exception: {e}") + results.append(False) + + print("\n" + "=" * 50) + print(f"Results: {sum(results)}/{len(results)} tests passed") + + if all(results): + print("🎉 All tests passed! Population diversity improvement works correctly.") + return 0 + else: + print("❌ Some tests failed. Check the output above.") + return 1 + +if __name__ == "__main__": + sys.exit(main())