File size: 11,174 Bytes
b010f1b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
"""
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
# Decision variables: [x1, y1, width1, height1, orientation1, ..., xN, yN, widthN, heightN, orientationN]
# 5 variables per plot: x, y position (normalized), width, height (meters), orientation (0-360)
n_var = n_plots * 5
# Variable bounds
xl = np.array([0, 0, 20, 20, 0] * n_plots) # Lower bounds
xu = np.array([1, 1, 200, 200, 360] * n_plots) # Upper bounds
super().__init__(
n_var=n_var,
n_obj=4, # 4 objectives
n_constr=0, # Constraints handled via penalties
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]
# Initialize objective arrays
f1_sellable = np.zeros(n_individuals) # Maximize (will negate)
f2_green = np.zeros(n_individuals) # Maximize (will negate)
f3_road_length = np.zeros(n_individuals) # Minimize
f4_compliance = np.zeros(n_individuals) # Maximize (will negate)
for i in range(n_individuals):
layout = self._decode_solution(X[i])
# Calculate objectives
metrics = layout.calculate_metrics()
# F1: Maximize sellable area (negate for minimization)
f1_sellable[i] = -metrics.sellable_area_sqm
# F2: Maximize green space (negate for minimization)
f2_green[i] = -metrics.green_space_area_sqm
# F3: Minimize road network length
if layout.road_network:
f3_road_length[i] = layout.road_network.total_length_m
else:
f3_road_length[i] = 1e6 # Penalty for no road
# F4: Regulatory compliance score (0-1, higher is better)
compliance_score = self._calculate_compliance_score(layout)
f4_compliance[i] = -compliance_score # Negate for minimization
# Set objectives
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)
# Get site bounds for denormalization
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
# Denormalize position
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]
# Create simple rectangular plot
plot_geom = box(x_pos, y_pos, x_pos + width, y_pos + height)
# Check if plot is within buildable area
if not self.site_boundary.geometry.contains(plot_geom):
continue # Skip invalid plots
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)
# Add green space (simplified: use remaining area)
# In practice, this would be more sophisticated
green_area_target = self.site_boundary.buildable_area_sqm * 0.15 # 15% minimum
layout.plots = plots
layout.road_network = RoadNetwork() # Simplified
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
# Check green space requirement
min_green = self.regulations.get('green_space', {}).get('minimum_percentage', 0.15)
if metrics.green_space_ratio < min_green:
penalties += 0.3
# Check FAR
max_far = self.regulations.get('far', {}).get('maximum', 0.7)
if metrics.far_value > max_far:
penalties += 0.3
# Check plot sizes
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}")
# Define problem
problem = IndustrialEstateProblem(
site_boundary=site_boundary,
regulations=self.regulations,
n_plots=n_plots
)
# Define algorithm
algorithm = NSGA2(
pop_size=population_size,
sampling=FloatRandomSampling(),
crossover=SBX(prob=0.9, eta=15),
mutation=PM(eta=20),
eliminate_duplicates=True
)
# Run optimization
result = minimize(
problem,
algorithm,
('n_gen', n_generations),
seed=42,
verbose=True
)
# Extract Pareto front
pareto_front = ParetoFront()
if result.X is not None:
# result.X can be 1D or 2D depending on number of solutions
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()
# Store fitness scores
if len(result.F.shape) == 1:
f = result.F
else:
f = result.F[i]
layout.fitness_scores = {
'sellable_area': -f[0], # Negate back
'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
# Example usage
if __name__ == "__main__":
# Example: Create a simple rectangular site
from shapely.geometry import box
site_geom = box(0, 0, 500, 500) # 500m x 500m site
site = SiteBoundary(
geometry=site_geom,
area_sqm=site_geom.area
)
site.buildable_area_sqm = site.area_sqm
# Run optimization
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")
# Show best 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²")
|