Source code for iwopy.core.function

import numpy as np
import fnmatch
from abc import ABCMeta, abstractmethod

from .base import Base


[docs]class OptFunction(Base, metaclass=ABCMeta): """ Abstract base class for functions that calculate scalars based on a problem. Attributes ---------- problem: iwopy.Problem The underlying optimization problem :group: core """
[docs] def __init__( self, problem, name, n_vars_int=None, n_vars_float=None, vnames_int=None, vnames_float=None, cnames=None, ): """ Constructor Parameters ---------- problem: iwopy.Problem The underlying optimization problem name: str The function name n_vars_int: int, optional The number of integer variables. If not specified it is assumed that the function depends on all problem int variables n_vars_float: int, optional The number of float variables. If not specified it is assumed that the function depends on all problem float variables vnames_int: list of str, optional The integer variable names. Useful for mapping function variables to problem variables, otherwise map by integer or default name vnames_float: list of str, optional The float variable names. Useful for mapping function variables to problem variables, otherwise map by integer or default name cnames: list of str, optional The names of the components """ super().__init__(name) self.problem = problem self._vnamesi = vnames_int self._vnamesf = vnames_float self._cnames = cnames if n_vars_int is not None: if vnames_int is not None: if len(vnames_int) != n_vars_int: raise ValueError( f"Problem '{self.name}': Mismatch between n_vars_int = {n_vars_int} and vnames_int = {vnames_int}, length {len(vnames_int)}" ) else: self._vnamesi = [f"{name}_n{i}" for i in range(n_vars_int)] if n_vars_float is not None: if vnames_float is not None: if len(vnames_float) != n_vars_float: raise ValueError( f"Problem '{self.name}': Mismatch between n_vars_float = {n_vars_float} and vnames_float = {vnames_float}, length {len(vnames_float)}" ) else: self._vnamesf = [f"{name}_x{i}" for i in range(n_vars_float)]
[docs] @abstractmethod def n_components(self): """ Returns the number of components of the function. Returns ------- int: The number of components. """ pass
[docs] def initialize(self, verbosity=0): """ Initialize the object. Parameters ---------- verbosity: int The verbosity level, 0 = silent """ if self._cnames is None: if self.n_components() > 1: self._cnames = [ f"{self.name}_{ci}" for ci in range(self.n_components()) ] else: self._cnames = [self.name] if self._vnamesi is None: self._vnamesi = list(self.problem.var_names_int()) if self._vnamesf is None: self._vnamesf = list(self.problem.var_names_float()) super().initialize(verbosity)
@property def component_names(self): """ The names of the components Returns ------- names: list of str The component names """ return self._cnames @property def var_names_int(self): """ The names of the integer variables Returns ------- names: list of str The integer variable names """ return self._vnamesi @property def n_vars_int(self): """ The number of int variables Returns ------- n: int The number of int variables """ return len(self.var_names_int) @property def var_names_float(self): """ The names of the float variables Returns ------- names: list of str The float variable names """ return self._vnamesf @property def n_vars_float(self): """ The number of float variables Returns ------- n: int The number of float variables """ return len(self.var_names_float)
[docs] def vardeps_int(self): """ Gets the dependencies of all components on the function int variables Returns ------- deps: numpy.ndarray of bool The dependencies of components on function variables, shape: (n_components, n_vars_int) """ return np.ones((self.n_components(), self.n_vars_int), dtype=bool)
[docs] def vardeps_float(self): """ Gets the dependencies of all components on the function float variables Returns ------- deps: numpy.ndarray of bool The dependencies of components on function variables, shape: (n_components, n_vars_float) """ return np.ones((self.n_components(), self.n_vars_float), dtype=bool)
def _rename_vars(self, varmap, target, vtype): """ Helper function for variable renaming """ for ov, nv in varmap.items(): if isinstance(ov, str): ovl = fnmatch.filter(target, ov) if not len(ovl): raise KeyError( f"Function '{self.name}': Cannot apply renaming '{ov} --> {nv}', since '{ov}' not found in {vtype} variables {target}" ) elif len(ovl) > 1: raise KeyError( f"Function '{self.name}': Cannot apply renaming '{ov} --> {nv}', since more than one match found in {vtype} variables: {ovl}" ) oi = target.index(ovl[0]) elif isinstance(ov, int): oi = ov if oi < 0 or oi >= len(target): raise ValueError( f"Function '{self.name}': Renaming rule '{ov} --> {nv}' cannot be applied for {len(target)} {vtype} variables {target}" ) else: raise TypeError( f"Function '{self.name}': Unacceptable source type '{type(ov)}' in renaming rule '{ov} --> {nv}', expecting str or int" ) if not isinstance(nv, str): raise TypeError( f"Function '{self.name}': Unacceptable target type '{type(nv)}' in renaming rule '{ov} --> {nv}', expecting str" ) target[oi] = nv
[docs] def rename_vars_int(self, varmap): """ Rename integer variables. Parameters ---------- varmap: dict The name mapping. Key: old name str, Value: new name str """ self._rename_vars(varmap, self._vnamesi, "int")
[docs] def rename_vars_float(self, varmap): """ Rename float variables. Parameters ---------- varmap: dict The name mapping. Key: old name str, Value: new name str """ self._rename_vars(varmap, self._vnamesf, "float")
[docs] def calc_individual(self, vars_int, vars_float, problem_results, components=None): """ Calculate values for a single individual of the underlying problem. Parameters ---------- vars_int: np.array The integer variable values, shape: (n_vars_int,) vars_float: np.array The float variable values, shape: (n_vars_float,) problem_results: Any The results of the variable application to the problem components: list of int, optional The selected components or None for all Returns ------- values: np.array The component values, shape: (n_sel_components,) """ raise NotImplementedError(f"Not implemented for class {type(self).__name__}")
[docs] def calc_population(self, vars_int, vars_float, problem_results, components=None): """ Calculate values for all individuals of a population. Parameters ---------- vars_int: np.array The integer variable values, shape: (n_pop, n_vars_int) vars_float: np.array The float variable values, shape: (n_pop, n_vars_float) problem_results: Any The results of the variable application to the problem components: list of int, optional The selected components or None for all Returns ------- values: np.array The component values, shape: (n_pop, n_sel_components) """ if problem_results is not None: raise NotImplementedError( f"Not implemented for class {type(self).__name__}, results type {type(problem_results).__name__}" ) # prepare: n_pop = ( vars_float.shape[0] if vars_float is not None and len(vars_float.shape) else vars_int.shape[0] ) vals = np.full((n_pop, self.n_components()), np.nan, dtype=np.float64) # loop over individuals: for i in range(n_pop): vals[i] = self.calc_individual(vars_int[i], vars_float[i], None) return vals
[docs] def finalize_individual(self, vars_int, vars_float, problem_results, verbosity=1): """ Finalization, given the champion data. Parameters ---------- vars_int: np.array The optimal integer variable values, shape: (n_vars_int,) vars_float: np.array The optimal float variable values, shape: (n_vars_float,) problem_results: Any The results of the variable application to the problem verbosity: int The verbosity level, 0 = silent Returns ------- values: np.array The component values, shape: (n_components,) """ return self.calc_individual(vars_int, vars_float, problem_results)
[docs] def finalize_population(self, vars_int, vars_float, problem_results, verbosity=1): """ Finalization, given the final population data. Parameters ---------- vars_int: np.array The integer variable values of the final generation, shape: (n_pop, n_vars_int) vars_float: np.array The float variable values of the final generation, shape: (n_pop, n_vars_float) problem_results: Any The results of the variable application to the problem verbosity: int The verbosity level, 0 = silent Returns ------- values: np.array The component values, shape: (n_pop, n_components) """ return self.calc_population(vars_int, vars_float, problem_results)
[docs] def ana_deriv(self, vars_int, vars_float, var, components=None): """ Calculates the analytic derivative, if possible. Use `numpy.nan` if analytic derivatives cannot be calculated. Parameters ---------- vars_int: np.array The integer variable values, shape: (n_vars_int,) vars_float: np.array The float variable values, shape: (n_vars_float,) var: int The index of the differentiation float variable components: list of int The selected components, or None for all Returns ------- deriv: numpy.ndarray The derivative values, shape: (n_sel_components,) """ n_cmpnts = len(components) if components is not None else self.n_components() return np.full(n_cmpnts, np.nan, dtype=np.float64)