Source code for foxes_opt.problems.layout.regular_layout

from copy import deepcopy
from typing import Any

import foxes.constants as FC
import foxes.variables as FV
import numpy as np
from foxes.config import config
from foxes.core import Algorithm, FData, MData
from foxes.models.turbine_models import Calculator

from foxes_opt.core import FarmOptProblem, FarmVarsProblem


def _calc_func(
    valid: np.ndarray,
    P: np.ndarray,
    ct: np.ndarray,
    algo: Algorithm,
    mdata: MData,
    fdata: FData,
    st_sel: Any,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """helper function for Calculator turbine model"""
    return (valid, P * valid, ct * valid)


[docs] class RegularLayoutOptProblem(FarmVarsProblem): """ Places turbines on a regular grid and optimizes its parameters. """ SPACING_X = "spacing_x" SPACING_Y = "spacing_y" OFFSET_X = "offset_x" OFFSET_Y = "offset_y" ANGLE = "angle" def __init__( self, name: str, algo: Algorithm, min_spacing: float, initial_values: dict[str, float] | None = None, **kwargs: Any, ) -> None: """ Parameters ---------- name The problem's name algo The algorithm min_spacing The minimal turbine spacing initial_values Initial values for opt variables, key: spacing_x, spacing_y, offset_x, offset_y, angle kwargs Additional parameters for `FarmVarsProblem` """ super().__init__(name, algo, **kwargs) self.min_spacing = min_spacing self.initial_values = initial_values
[docs] def initialize( self, verbosity: int = 1, model_vars: dict[str, list[str]] | list[str] | None = None, **kwargs: Any, ) -> None: """ Initialize the object. Parameters ---------- verbosity The verbosity level, 0 = silent kwargs Additional parameters for super class init """ self._mname = self.name + "_calc" for t in self.algo.farm.turbines: if self._mname not in t.models: t.add_model(self._mname) self._turbine = deepcopy(self.farm.turbines[-1]) self.algo.mbook.turbine_models[self._mname] = Calculator( in_vars=[FC.VALID, FV.P, FV.CT], out_vars=[FC.VALID, FV.P, FV.CT], func=_calc_func, ) b = self.farm.boundary assert b is not None, f"Problem '{self.name}': Missing wind farm boundary." pmax = b.p_max() pmin = b.p_min() self._pmin = pmin self._xy0 = pmin self._halfspan = (pmax - pmin) / 2 self._halflen = np.linalg.norm(self._halfspan) self.max_spacing = 2 * (self._halflen + self.min_spacing) self._halfn = int(self._halflen / self.min_spacing) if self._halfn * self.min_spacing < self._halflen: self._halfn += 1 self._nrow = 2 * self._halfn + 1 self._nturb = self._nrow**2 if verbosity > 0: print(f"Problem '{self.name}':") print(f" xy0 = {self._xy0}") print(f" span = {np.linalg.norm(self._halfspan * 2):.2f}") print(f" min spacing = {self.min_spacing:.2f}") print(f" max spacing = {self.max_spacing:.2f}") print(f" n row turbns = {self._nrow}") print(f" n turbines = {self._nturb}") print(f" turbine mdls = {self._turbine.models}") iniv = self.initial_values self.initial_values = { self.SPACING_X: self.min_spacing, self.SPACING_Y: self.min_spacing, self.OFFSET_X: 0.0, self.OFFSET_Y: 0.0, self.ANGLE: 0.0, } if iniv is not None: mins = { v: m for v, m in zip(self.var_names_float(), self.min_values_float()) } maxs = { v: m for v, m in zip(self.var_names_float(), self.max_values_float()) } for k, v in iniv.items(): assert k in self.initial_values, ( f"Invalid initial value key: '{k}', expected one of {list(self.initial_values.keys())}." ) assert v >= mins[k], ( f"Initial value for '{k}' too small: {v} < {mins[k]}." ) assert v <= maxs[k], ( f"Initial value for '{k}' too large: {v} > {maxs[k]}." ) self.initial_values[k] = v if verbosity > 0: print(" initial values:") for k, v in self.initial_values.items(): print(f" {k:12s} = {v}") super().initialize( model_vars=[FV.X, FV.Y, FC.VALID], verbosity=verbosity, **kwargs, ) if self.farm.n_turbines < self._nturb: new_turbines = self.farm.turbines for i in range(self._nturb - self.farm.n_turbines): ti = len(new_turbines) new_turbines.append(deepcopy(self._turbine)) new_turbines[-1].index = ti new_turbines[-1].name = f"T{ti}" self.farm.reset_turbines(self.algo, new_turbines) elif self.farm.n_turbines > self._nturb: new_turbines = self.farm.turbines[: self._nturb] self.farm.reset_turbines(self.algo, new_turbines)
[docs] def var_names_float(self) -> list[str]: """ The names of float variables. Returns ------- names The names of the float variables """ return [ self.SPACING_X, self.SPACING_Y, self.OFFSET_X, self.OFFSET_Y, self.ANGLE, ]
[docs] def initial_values_float(self) -> list[float]: """ The initial values of the float variables. Returns ------- values Initial float values, shape: (n_vars_float,) """ assert self.initial_values is not None return list(self.initial_values.values())
[docs] def min_values_float(self) -> np.ndarray: """ The minimal values of the float variables. Use -numpy.inf for unbounded. Returns ------- values Minimal float values, shape: (n_vars_float,) """ return np.array( [ self.min_spacing, self.min_spacing, 0.0, 0.0, 0.0, ], dtype=config.dtype_double, )
[docs] def max_values_float(self) -> np.ndarray: """ The maximal values of the float variables. Use numpy.inf for unbounded. Returns ------- values Maximal float values, shape: (n_vars_float,) """ return np.array( [ self.max_spacing, self.max_spacing, 1.0, 1.0, 90.0, ], dtype=config.dtype_double, )
[docs] def opt2farm_vars_individual( self, vars_int: np.ndarray, vars_float: np.ndarray ) -> dict[str, np.ndarray]: """ Translates optimization variables to farm variables Parameters ---------- vars_int The integer optimization variable values, shape vars_float The float optimization variable values, shape Returns ------- farm_vars The foxes farm variables. Key: var name, value (n_states, n_sel_turbines) """ dx, dy, ox, oy, a = vars_float n_states = self.algo.n_states nx = self._nrow ny = self._nrow a = np.deg2rad(a) nax = np.array([np.cos(a), np.sin(a), 0.0], dtype=config.dtype_double) nay = np.cross(np.array([0.0, 0.0, 1.0], dtype=config.dtype_double), nax) pts = np.zeros((nx, ny, 2), dtype=config.dtype_double) pts[:] = ( self._xy0[None, None, :] + (ox + np.arange(nx)[:, None, None]) * dx * nax[None, None, :2] + (oy + np.arange(ny)[None, :, None]) * dy * nay[None, None, :2] ) pts = pts.reshape(nx * ny, 2) valid = self.farm.boundary.points_inside(pts) farm_vars = {} for v, d in zip([FV.X, FV.Y, FC.VALID], [pts[:, 0], pts[:, 1], valid]): a = np.zeros((n_states, nx * ny), dtype=config.dtype_double) a[:] = d[None, :] farm_vars[v] = a return farm_vars
[docs] def opt2farm_vars_population( self, vars_int: np.ndarray, vars_float: np.ndarray, n_states: int ) -> dict[str, np.ndarray]: """ Translates optimization variables to farm variables Parameters ---------- vars_int The integer optimization variable values, shape vars_float The float optimization variable values, shape n_states The number of original (non-pop) states Returns ------- farm_vars The foxes farm variables. Key: var name, value (n_pop, n_states, n_sel_turbines) """ n_pop = len(vars_float) n_turbines = self.farm.n_turbines dx = vars_float[:, 0] dy = vars_float[:, 1] ox = vars_float[:, 2] oy = vars_float[:, 3] nx = self._nrow ny = self._nrow a = vars_float[:, 4] N = self._nturb a = np.deg2rad(a) nax = np.stack([np.cos(a), np.sin(a), np.zeros_like(a)], axis=-1) naz = np.zeros_like(nax) naz[..., 2] = 1 nay = np.cross(naz, nax) pts = np.zeros((n_pop, nx, ny, 2), dtype=config.dtype_double) pts[:] = ( self._xy0[None, None, None, :] + (ox[:, None, None, None] + np.arange(nx)[None, :, None, None]) * dx[:, None, None, None] * nax[:, None, None, :2] + (oy[:, None, None, None] + np.arange(ny)[None, None, :, None]) * dy[:, None, None, None] * nay[:, None, None, :2] ) qts = np.zeros((n_pop, n_turbines, 2)) qts[:, :N] = pts.reshape(n_pop, N, 2) del pts valid = self.farm.boundary.points_inside( qts.reshape(n_pop * n_turbines, 2) ).reshape(n_pop, n_turbines) farm_vars = {} for v, d in zip([FV.X, FV.Y, FC.VALID], [qts[:, :, 0], qts[:, :, 1], valid]): a = np.zeros((n_pop, n_states, n_turbines), dtype=config.dtype_double) a[:] = d[:, None, :] farm_vars[v] = a return farm_vars
[docs] def finalize_individual( self, vars_int: np.ndarray, vars_float: np.ndarray, verbosity: int = 1 ) -> Any: """ Finalization, given the champion data. Parameters ---------- vars_int The optimal integer variable values, shape: (n_vars_int,) vars_float The optimal float variable values, shape: (n_vars_float,) verbosity The verbosity level, 0 = silent Returns ------- problem_results The results of the variable application to the problem objs The objective function values, shape: (n_objectives,) cons The constraints values, shape: (n_constraints,) """ farm_vars = self.opt2farm_vars_individual(vars_int, vars_float) sel = np.where(farm_vars[FC.VALID][0])[0] x = farm_vars[FV.X][0, sel] y = farm_vars[FV.Y][0, sel] turbines = [t for i, t in enumerate(self.farm.turbines) if i in sel] for i, t in enumerate(turbines): t.xy = np.array([x[i], y[i]], dtype=config.dtype_double) t.models = [m for m in t.models if m not in [self.name, self._mname]] t.index = i t.name = f"T{i}" self.farm.reset_turbines(self.algo, turbines) return FarmOptProblem.finalize_individual( self, vars_int, vars_float, verbosity=1 )