Source code for foxes_opt.problems.layout.regular_layout

import numpy as np
from copy import deepcopy
from typing import Any

from foxes_opt.core import FarmVarsProblem, FarmOptProblem
from foxes.models.turbine_models import Calculator
from foxes.core import Algorithm, MData, FData
from foxes.config import config
import foxes.variables as FV
import foxes.constants as FC


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_states, n_pop, 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_states, n_pop, 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 )