| | """ |
| | NSGA-II Optimizer - Module A: The Architect |
| | Multi-objective genetic algorithm for industrial estate layout optimization |
| | """ |
| | import numpy as np |
| | from pymoo.algorithms.moo.nsga2 import NSGA2 |
| | from pymoo.core.problem import Problem |
| | from pymoo.optimize import minimize |
| | from pymoo.operators.crossover.sbx import SBX |
| | from pymoo.operators.mutation.pm import PM |
| | from pymoo.operators.sampling.rnd import FloatRandomSampling |
| | from typing import List, Tuple |
| | import yaml |
| | from pathlib import Path |
| |
|
| | from src.models.domain import Layout, SiteBoundary, Plot, PlotType, ParetoFront, RoadNetwork |
| | from shapely.geometry import Polygon, box |
| | import logging |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | class IndustrialEstateProblem(Problem): |
| | """ |
| | Multi-objective optimization problem for industrial estate layout |
| | |
| | Objectives: |
| | 1. Maximize sellable area |
| | 2. Maximize green space |
| | 3. Minimize road network length |
| | 4. Maximize regulatory compliance score |
| | """ |
| | |
| | def __init__(self, site_boundary: SiteBoundary, regulations: dict, n_plots: int = 20): |
| | """ |
| | Initialize optimization problem |
| | |
| | Args: |
| | site_boundary: Site boundary with constraints |
| | regulations: Regulatory requirements from YAML |
| | n_plots: Target number of industrial plots |
| | """ |
| | self.site_boundary = site_boundary |
| | self.regulations = regulations |
| | self.n_plots = n_plots |
| | |
| | |
| | |
| | n_var = n_plots * 5 |
| | |
| | |
| | xl = np.array([0, 0, 20, 20, 0] * n_plots) |
| | xu = np.array([1, 1, 200, 200, 360] * n_plots) |
| | |
| | super().__init__( |
| | n_var=n_var, |
| | n_obj=4, |
| | n_constr=0, |
| | xl=xl, |
| | xu=xu |
| | ) |
| | |
| | def _evaluate(self, X, out, *args, **kwargs): |
| | """ |
| | Evaluate population |
| | |
| | Args: |
| | X: Population matrix (n_individuals x n_variables) |
| | out: Output dictionary |
| | """ |
| | n_individuals = X.shape[0] |
| | |
| | |
| | f1_sellable = np.zeros(n_individuals) |
| | f2_green = np.zeros(n_individuals) |
| | f3_road_length = np.zeros(n_individuals) |
| | f4_compliance = np.zeros(n_individuals) |
| | |
| | for i in range(n_individuals): |
| | layout = self._decode_solution(X[i]) |
| | |
| | |
| | metrics = layout.calculate_metrics() |
| | |
| | |
| | f1_sellable[i] = -metrics.sellable_area_sqm |
| | |
| | |
| | f2_green[i] = -metrics.green_space_area_sqm |
| | |
| | |
| | if layout.road_network: |
| | f3_road_length[i] = layout.road_network.total_length_m |
| | else: |
| | f3_road_length[i] = 1e6 |
| | |
| | |
| | compliance_score = self._calculate_compliance_score(layout) |
| | f4_compliance[i] = -compliance_score |
| | |
| | |
| | out["F"] = np.column_stack([f1_sellable, f2_green, f3_road_length, f4_compliance]) |
| | |
| | def _decode_solution(self, x: np.ndarray) -> Layout: |
| | """ |
| | Decode decision variables into a Layout |
| | |
| | Args: |
| | x: Decision variables array |
| | |
| | Returns: |
| | Layout object |
| | """ |
| | layout = Layout(site_boundary=self.site_boundary) |
| | |
| | |
| | minx, miny, maxx, maxy = self.site_boundary.geometry.bounds |
| | site_width = maxx - minx |
| | site_height = maxy - miny |
| | |
| | plots = [] |
| | for i in range(self.n_plots): |
| | idx = i * 5 |
| | |
| | |
| | x_norm, y_norm = x[idx], x[idx + 1] |
| | x_pos = minx + x_norm * site_width |
| | y_pos = miny + y_norm * site_height |
| | |
| | width, height = x[idx + 2], x[idx + 3] |
| | orientation = x[idx + 4] |
| | |
| | |
| | plot_geom = box(x_pos, y_pos, x_pos + width, y_pos + height) |
| | |
| | |
| | if not self.site_boundary.geometry.contains(plot_geom): |
| | continue |
| | |
| | plot = Plot( |
| | geometry=plot_geom, |
| | area_sqm=plot_geom.area, |
| | type=PlotType.INDUSTRIAL, |
| | width_m=width, |
| | depth_m=height, |
| | orientation_degrees=orientation |
| | ) |
| | plots.append(plot) |
| | |
| | |
| | |
| | green_area_target = self.site_boundary.buildable_area_sqm * 0.15 |
| | |
| | layout.plots = plots |
| | layout.road_network = RoadNetwork() |
| | |
| | return layout |
| | |
| | def _calculate_compliance_score(self, layout: Layout) -> float: |
| | """ |
| | Calculate regulatory compliance score (0-1) |
| | |
| | Args: |
| | layout: Layout to evaluate |
| | |
| | Returns: |
| | Compliance score (1.0 = fully compliant) |
| | """ |
| | score = 1.0 |
| | penalties = 0 |
| | |
| | metrics = layout.metrics |
| | |
| | |
| | min_green = self.regulations.get('green_space', {}).get('minimum_percentage', 0.15) |
| | if metrics.green_space_ratio < min_green: |
| | penalties += 0.3 |
| | |
| | |
| | max_far = self.regulations.get('far', {}).get('maximum', 0.7) |
| | if metrics.far_value > max_far: |
| | penalties += 0.3 |
| | |
| | |
| | min_plot_size = self.regulations.get('plot', {}).get('minimum_area_sqm', 1000) |
| | for plot in layout.plots: |
| | if plot.type == PlotType.INDUSTRIAL and plot.area_sqm < min_plot_size: |
| | penalties += 0.1 |
| | break |
| | |
| | score = max(0.0, score - penalties) |
| | return score |
| |
|
| |
|
| | class NSGA2Optimizer: |
| | """ |
| | NSGA-II based multi-objective optimizer for industrial estate layouts |
| | """ |
| | |
| | def __init__(self, config_path: str = "config/regulations.yaml"): |
| | """ |
| | Initialize optimizer |
| | |
| | Args: |
| | config_path: Path to regulations YAML file |
| | """ |
| | self.config_path = Path(config_path) |
| | self.regulations = self._load_regulations() |
| | self.logger = logging.getLogger(__name__) |
| | |
| | def _load_regulations(self) -> dict: |
| | """Load regulations from YAML file""" |
| | if not self.config_path.exists(): |
| | self.logger.warning(f"Regulations file not found: {self.config_path}") |
| | return {} |
| | |
| | with open(self.config_path, 'r', encoding='utf-8') as f: |
| | return yaml.safe_load(f) |
| | |
| | def optimize( |
| | self, |
| | site_boundary: SiteBoundary, |
| | population_size: int = 100, |
| | n_generations: int = 200, |
| | n_plots: int = 20 |
| | ) -> ParetoFront: |
| | """ |
| | Run NSGA-II optimization |
| | |
| | Args: |
| | site_boundary: Site boundary with constraints |
| | population_size: NSGA-II population size |
| | n_generations: Number of generations |
| | n_plots: Target number of plots |
| | |
| | Returns: |
| | ParetoFront with optimal solutions |
| | """ |
| | import time |
| | start_time = time.time() |
| | |
| | self.logger.info(f"Starting NSGA-II optimization: pop={population_size}, gen={n_generations}") |
| | |
| | |
| | problem = IndustrialEstateProblem( |
| | site_boundary=site_boundary, |
| | regulations=self.regulations, |
| | n_plots=n_plots |
| | ) |
| | |
| | |
| | algorithm = NSGA2( |
| | pop_size=population_size, |
| | sampling=FloatRandomSampling(), |
| | crossover=SBX(prob=0.9, eta=15), |
| | mutation=PM(eta=20), |
| | eliminate_duplicates=True |
| | ) |
| | |
| | |
| | result = minimize( |
| | problem, |
| | algorithm, |
| | ('n_gen', n_generations), |
| | seed=42, |
| | verbose=True |
| | ) |
| | |
| | |
| | pareto_front = ParetoFront() |
| | |
| | if result.X is not None: |
| | |
| | if len(result.X.shape) == 1: |
| | solutions = [result.X] |
| | else: |
| | solutions = result.X |
| | |
| | for i, x in enumerate(solutions): |
| | layout = problem._decode_solution(x) |
| | layout.pareto_rank = i |
| | layout.calculate_metrics() |
| | |
| | |
| | if len(result.F.shape) == 1: |
| | f = result.F |
| | else: |
| | f = result.F[i] |
| | |
| | layout.fitness_scores = { |
| | 'sellable_area': -f[0], |
| | 'green_space': -f[1], |
| | 'road_length': f[2], |
| | 'compliance': -f[3] |
| | } |
| | |
| | pareto_front.layouts.append(layout) |
| | |
| | pareto_front.generation_time_seconds = time.time() - start_time |
| | |
| | self.logger.info(f"Optimization complete: {len(pareto_front.layouts)} solutions in {pareto_front.generation_time_seconds:.2f}s") |
| | |
| | return pareto_front |
| |
|
| |
|
| | |
| | if __name__ == "__main__": |
| | |
| | from shapely.geometry import box |
| | |
| | site_geom = box(0, 0, 500, 500) |
| | site = SiteBoundary( |
| | geometry=site_geom, |
| | area_sqm=site_geom.area |
| | ) |
| | site.buildable_area_sqm = site.area_sqm |
| | |
| | |
| | optimizer = NSGA2Optimizer() |
| | pareto_front = optimizer.optimize( |
| | site_boundary=site, |
| | population_size=50, |
| | n_generations=100, |
| | n_plots=15 |
| | ) |
| | |
| | print(f"Generated {len(pareto_front.layouts)} Pareto-optimal layouts") |
| | |
| | |
| | max_sellable = pareto_front.get_max_sellable_layout() |
| | if max_sellable: |
| | print(f"Max sellable area: {max_sellable.metrics.sellable_area_sqm:.2f} m²") |
| | |
| | max_green = pareto_front.get_max_green_layout() |
| | if max_green: |
| | print(f"Max green space: {max_green.metrics.green_space_area_sqm:.2f} m²") |
| |
|