Source code for idaes.core.scaling.custom_scaler_base

#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2026 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""
Base class for custom scaling routines.

Authors: Andrew Lee, Douglas Allan
"""

from __future__ import annotations  # For type hinting with unions using |
from copy import copy

from pyomo.environ import ComponentMap, units, value
from pyomo.core.base.units_container import UnitsError

from pyomo.core.base.block import Block, BlockData
from pyomo.core.base.constraint import ConstraintData
from pyomo.core.base.var import VarData, ScalarVar
from pyomo.core.base.expression import ExpressionData, ScalarExpression

from pyomo.common.collections import ComponentSet

from pyomo.core.expr import identify_components, identify_variables, replace_expressions
from pyomo.core.expr.calculus.derivatives import Modes, differentiate
from pyomo.common.deprecation import deprecation_warning
from pyomo.util.calc_var_value import calculate_variable_from_constraint

from idaes.core.scaling.scaling_base import CONFIG, ScalerBase
from idaes.core.scaling.util import NominalValueExtractionVisitor
import idaes.logger as idaeslog
from idaes.core.util.exceptions import BurntToast
from idaes.core.util.misc import StrEnum

# Set up logger
_log = idaeslog.getLogger(__name__)


def _filter_scaling_factor(sf):
    # Cast sf to float to catch obvious garbage
    sf = float(sf)
    # This comparison filters out negative numbers and infinity.
    # It also filters out NaN values because comparisons involving
    # NaN return False by default (including float("NaN") == float("NaN")).
    if not 0 < sf < float("inf"):
        raise ValueError(
            f"Scaling factors must be strictly positive and finite. Received "
            f"value of {sf} instead."
        )
    return sf


CSCONFIG = CONFIG()

DEFAULT_UNIT_SCALING = {
    # "QuantityName: (reference units, scaling factor)
    # Model developers should be careful when using these, especially when
    # dealing with differential measurements (e.g. pressure and temperature differences)
    "Temperature": (units.K, 1e-2),
    "Pressure": (units.Pa, 1e-5),
}


[docs] class ConstraintScalingScheme(StrEnum): """ Schemes available for calculating constraint scaling factors. * harmonicMean ('harmonic_mean'): sf = sum(1/abs(nominal value)) * inverseSum ('inverse_sum'): sf = 1 / sum(abs(nominal value)) * inverseRSS ('inverse_root_sum_squared'): sf = 1 / sqrt(sum(abs(nominal value)**2)) * inverseMaximum ('inverse_maximum'): sf = 1 / max(abs(nominal value) * inverseMinimum ('inverse_minimum'): sf = 1 / min(abs(nominal value) """ harmonicMean = "harmonic_mean" inverseSum = "inverse_sum" inverseRSS = "inverse_root_sum_squared" inverseMaximum = "inverse_maximum" inverseMinimum = "inverse_minimum"
class DefaultScalingRecommendation(StrEnum): """ Enum to categorize how necessary it is for a user to set a default scaling factor. * userInputRecommended: While a value cannot be set a priori, there is a method to estimate the scaling factor for this variable/expression. It's still better for the user to supply the value. * userInputRequired: The user must provide a scaling factor or an Exception is thrown. * userSetManually: A way for a user to certify that they've set scaling factors on the the appropriate variables and constraints directly using set_scaling_factor """ userInputRecommended = "User input recommended" userInputRequired = "User input required" userSetManually = "User set manually"
[docs] class CustomScalerBase(ScalerBase): """ Base class for custom scaling routines. """ CONFIG = ScalerBase.CONFIG() # Common data structures for default scaling # DEFAULT_SCALING_FACTORS = {"component_local_name": DEFAULT_SCALING} DEFAULT_SCALING_FACTORS = None # UNIT_SCALING_FACTORS = {"units": UNIT_BASED_SCALING} UNIT_SCALING_FACTORS = copy(DEFAULT_UNIT_SCALING) def __init__(self, **kwargs): super().__init__(**kwargs) if self.DEFAULT_SCALING_FACTORS is not None: self.default_scaling_factors = copy(self.DEFAULT_SCALING_FACTORS) else: self.default_scaling_factors = {} if self.UNIT_SCALING_FACTORS is not None: self.unit_scaling_factors = copy(self.UNIT_SCALING_FACTORS) else: self.unit_scaling_factors = {}
[docs] def scale_model( self, model, first_stage_fill_in: list = None, second_stage_fill_in: list = None, submodel_scalers: ComponentMap = None, ): """ Default model scaling routine. This method performs a four-step scaling routine: 1. Scale variables using variable_scaling_routine 2. Perform first-stage scaling factor fill in using user provided method(s), called in order declared 3. Scale constraints using constraint_scaling_routine 4. Perform second-stage scaling factor fill in using user provided method(s), called in order declared Args: model: model to be scaled first_stage_fill_in: list of methods to use for first-stage scaling factor fill in second_stage_fill_in: list of methods to use for second-stage scaling factor fill in submodel_scalers: ComponentMap of Scalers to use for sub-models Returns: None """ # Step 1: Call variable scaling routine self.variable_scaling_routine( model, overwrite=self.config.overwrite, submodel_scalers=submodel_scalers ) # Step 2: Call variable fill in if first_stage_fill_in is not None: for i in first_stage_fill_in: i(model) # Step 3: Call constraint scaling routine self.constraint_scaling_routine( model, overwrite=self.config.overwrite, submodel_scalers=submodel_scalers ) # Step 4: Call constraint fill in if second_stage_fill_in is not None: for i in second_stage_fill_in: i(model)
[docs] def variable_scaling_routine( self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None ): """ Routine to apply scaling factors to variables in model. Derived classes must overload this method. Args: model: model to be scaled overwrite: whether to overwrite existing scaling factors submodel_scalers: ComponentMap of Scalers to use for sub-models Returns: None """ raise NotImplementedError( "Custom Scaler has not implemented a variable_scaling_routine method." )
[docs] def constraint_scaling_routine( self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None ): """ Routine to apply scaling factors to constraints in model. Derived classes must overload this method. Args: model: model to be scaled overwrite: whether to overwrite existing scaling factors submodel_scalers: ComponentMap of Scalers to use for sub-models Returns: None """ raise NotImplementedError( "Custom Scaler has not implemented a constraint_scaling_routine method." )
[docs] def get_default_scaling_factor( self, component: VarData | ConstraintData | ExpressionData ): """ Get scaling factor for component from dict of default values. Args: component: component to get default scaling factor for Returns: default scaling factor if it exists, else None """ blk = component.parent_block() comp_default = None parent_default = None # We're not just returning self.default_scaling_factors[component.local_name] # because the component finder has additional logic to handle, e.g., spaces # separating indices for elements of an indexed component. We want the user # to be able to provide either "var[a,b]" or "var[a, b]" (with a space following # the comma) and still find the right component # If the user provides both "var[a,b]" and "var[a, b]", then whichever is # encountered first when iterating through the keys will be returned. This # behavior is not ideal. TODO dictionary validation # Iterating through every key is certainly not the most efficient way to go # about this look-up process, but it's also probably not going to be the # rate limiting step, especially considering these default dictionaries # should be relatively short. # Locking attribute creation context prevents build-on-demand properties # from getting triggered through this lookup. if hasattr(blk, "_lock_attribute_creation"): # pylint: disable=protected-access lock_attribute_creation_orig = ( blk._lock_attribute_creation # pylint: disable=protected-access ) blk._lock_attribute_creation = True # pylint: disable=protected-access for key in self.default_scaling_factors: comp2 = blk.find_component(key) if comp2 is component: comp_default = self.default_scaling_factors[key] break elif comp2 is component.parent_component(): parent_default = self.default_scaling_factors[key] if hasattr(blk, "_lock_attribute_creation"): # pylint: disable=protected-access blk._lock_attribute_creation = ( # pylint: disable=protected-access lock_attribute_creation_orig ) if comp_default is not None: return comp_default elif parent_default is not None: return parent_default else: _log.debug(f"No default scaling factor found for {component.name}") return None
# Common methods for variable scaling
[docs] def scale_variable_by_component( self, target_variable: VarData, scaling_component: VarData | ConstraintData | ExpressionData, overwrite: bool = False, ): """ Set scaling factor for target_variable equal to that of scaling_component. Args: target_variable: variable to set scaling factor for scaling_component: component to use for scaling factor overwrite: whether to overwrite existing scaling factors Returns: None """ sf = self.get_scaling_factor(scaling_component) if sf is not None: self.set_variable_scaling_factor( variable=target_variable, scaling_factor=sf, overwrite=overwrite ) else: # TODO add infrastructure to log a warning _log.debug( f"Could not set scaling factor for {target_variable.name}, " f"no scaling factor set for {scaling_component.name}" )
[docs] def scale_variable_by_bounds(self, variable: VarData, overwrite: bool = False): """ Set scaling factor for variable based on bounds. If variable has both upper and lower bounds, scaling factor will be based on the mean of the bounds. If variable has only one bound, scaling factor will be based on that bound. If variable has no bounds, scaling factor will not be set. Args: variable: variable to set scaling factor for overwrite: whether to overwrite existing scaling factors Returns: None """ if variable.lb is not None: if variable.ub is not None: # Both bounds, use mean xmag = 0.5 * (variable.ub + variable.lb) else: # Only lower bound xmag = variable.lb elif variable.ub is not None: # Only upper bound xmag = variable.ub else: # No bounds _log.debug( f"No scaling factor set for {variable.name}; variable has no bounds." ) return if xmag == 0: sf = 1 else: sf = 1 / abs(xmag) self.set_variable_scaling_factor( variable=variable, scaling_factor=sf, overwrite=overwrite )
def _scale_component_by_default( self, component: VarData | ConstraintData | ExpressionData, overwrite: bool = False, ): """ Set scaling factor for component based on default scaling factor. Args: component: Var, Constraint, or Expression to set scaling factor/hint for overwrite: whether to overwrite existing scaling factors Returns: None """ sf = self.get_default_scaling_factor(component) if sf is None or sf == DefaultScalingRecommendation.userInputRequired: # Check to see if the user manually set a scaling factor sf = self.get_scaling_factor(component) if sf is None or overwrite: # If the user told us to overwrite scaling factors, then # accepting a preexisting scaling factor is not good enough. # They need to go manually alter the default entry to # DefaultScalingRecommendation.userInputRecommended raise ValueError( "This scaler requires the user to provide a default " f"scaling factor for {component}, but no default scaling " "factor was set." ) else: # If a preexisting scaling factor exists, then we'll accept it pass elif ( sf == DefaultScalingRecommendation.userInputRecommended or sf == DefaultScalingRecommendation.userSetManually ): # Either the user has already set scaling factors or # the scaling method is going to try to estimate the # scaling factor pass else: self.set_component_scaling_factor( component=component, scaling_factor=sf, overwrite=overwrite )
[docs] def scale_variable_by_default( self, variable: VarData | ExpressionData, overwrite: bool = False ): """ Set scaling factor for variable or scaling hint for named expression based on default scaling factor. Args: variable: variable/expression to set scaling factor for overwrite: whether to overwrite existing scaling factors Returns: None """ if variable.is_indexed(): raise TypeError( f"{variable} is indexed. Call with ComponentData children instead." ) if not (isinstance(variable, VarData) or isinstance(variable, ExpressionData)): raise TypeError( f"{variable} is type {type(variable)}, but a variable or expression was expected." ) self._scale_component_by_default(component=variable, overwrite=overwrite)
[docs] def scale_variable_by_units(self, variable, overwrite: bool = False): """ Set scaling factor for variable based on units of measurement. Units of measurement for variable are compared to those stored in self.unit_scaling_factors, and if a match is found the scaling factor set using the associated value. Args: variable: variable to set scaling factor for overwrite: whether to overwrite existing scaling factors Returns: None """ uom = units.get_units(variable) sf = None # Keys in self.unit_scaling_factors are not used - only required because Pyomo # units are non-hashable and thus cannot be keys for refunits, unit_scale in self.unit_scaling_factors.values(): try: # Try convert the reference scaling factor to variable units # TODO: Have not found a more efficient way to do this, as Pyomo basically # TODO: involves a try/except anyway # Need to invert reference scaling factor, and then invert result sf = 1 / units.convert_value( 1 / unit_scale, from_units=refunits, to_units=uom ) # Break once we have a match - no need to continue break except UnitsError: pass if sf is not None: self.set_variable_scaling_factor( variable=variable, scaling_factor=sf, overwrite=overwrite, ) else: _log.debug( f"No scaling factor set for {variable.name}; no match for units {uom} found " "in self.unit_scaling_factors" )
def _build_replacement_map(self, variable, constraint, safety_mode: bool): """ Helper function for scale_variable_by_definition_constraint. Iterates over the constraint to build a map between Variables or Expressions appearing in Constraint and the scaling factor or scaling hint assigned to them. (Because constraints can contain non-local variables and expressions, getting the scaling factor and scaling hint suffixes from the parent block of the constraint is not good enough.) Args: variable: Variable we are scaling by the definition constraint "constraint". constraint: Definition constraint that we are using to scale "variable" and that is getting walked in this function. safety_mode: Flag about whether or not to screen named expressions to see if "variable" appears in them. Screening them takes longer (especially for extremely deep expression trees), but if we do not screen them we run the risk of removing "variable" from "constraint" entirely when we use replacement_map in the replace_expressions function. Returns: replacement_map: dictionary with variable or expression IDs as keys and nominal values (the inverse of the scaling factors/hints) as values. variable_in_constraint: Boolean flag whether "variable" was encountered in the body of "constraint" """ replacement_map = {} variable_in_constraint = False # Iterate over all variables and named expressions in constraint for data in identify_components( constraint.expr, {ExpressionData, ScalarExpression, VarData, ScalarVar} ): if data is variable: # We don't want to replace the value of "variable" with # its nominal value---that's what we're trying to calculate variable_in_constraint = True continue sf = self.get_scaling_factor(data) if sf is None: if isinstance(data, VarData): # We need to use some nominal value for the variable "data", # so use its current value if data.value is not None and data.value != 0: sf = 1 / data.value sf = min(sf, self.config.max_variable_scaling_factor) sf = max(sf, self.config.min_variable_scaling_factor) else: sf = 1 else: # No scaling hint for expression, so we # do not store a value continue if safety_mode and isinstance(data, ExpressionData): vars_in_expr = ComponentSet(var for var in identify_variables(data)) if variable in vars_in_expr: # If the variable we're trying to scale appears in the expression # "data", we shouldn't store a scaling hint because we don't want # to replace it. continue sf = _filter_scaling_factor(sf) replacement_map[id(data)] = 1 / sf return (replacement_map, variable_in_constraint)
[docs] def scale_variable_by_definition_constraint( self, variable, constraint, overwrite: bool = False ): """ Set scaling factor for variable via a constraint that defines it. We expect a constraint of the form variable == prod(v ** nu for v, nu in zip(other_variables, variable_exponents), and set a scaling factor for a variable based on the nominal value of the righthand side. This method may return a result even if the constraint does not have this expected form, but the resulting scaling factor may not be suitable. Args: variable: variable to set scaling factor for constraint: constraint defining this variable overwrite: whether to overwrite existing scaling factors Returns: None """ if variable.is_indexed(): raise TypeError( f"Variable {variable} is indexed. Call with VarData " "children instead." ) if not isinstance(variable, VarData): raise TypeError( f"{variable} is not a variable, but instead {type(variable)}" ) if constraint.is_indexed(): raise TypeError( f"Constraint {constraint} is indexed. Call with ConstraintData " "children instead." ) if not isinstance(constraint, ConstraintData): raise TypeError( f"{constraint} is not a constraint, but instead {type(constraint)}" ) if constraint.lb != constraint.ub: raise ValueError( f"A definition constraint is an equality constraint, but {constraint} " "is an inequality constraint. Cannot scale with this constraint." ) replacement_map, variable_in_constraint = self._build_replacement_map( variable, constraint, safety_mode=False ) if not variable_in_constraint: raise ValueError( f"Variable {variable} does not appear in constraint " f"{constraint}, cannot calculate scaling factor." ) # Replace variables other than "variable" with 1 / scaling_factor and # replace named expressions with 1 / scaling_hint (when scaling hints # exist) replaced_expr = replace_expressions( constraint.expr, substitution_map=replacement_map, descend_into_named_expressions=True, remove_named_expressions=True, ) # Make sure that we have replaced every variable var_set = ComponentSet(var for var in identify_variables(replaced_expr)) if not (len(var_set) == 1 and variable in var_set): # We know that "variable" must appear in "constraint" because we # encountered it when building replacement_map. If it doesn't appear # in "replaced_expr", then a named expression containing it # must have been replaced by 1 / scaling_hint. Therefore we build # a new replacement map, this time making sure that "variable" does # not appear in a named expression before we add it to replacement_map replacement_map, _ = self._build_replacement_map( variable, constraint, safety_mode=True ) replaced_expr = replace_expressions( constraint.expr, substitution_map=replacement_map, descend_into_named_expressions=True, remove_named_expressions=True, ) var_set = ComponentSet(var for var in identify_variables(replaced_expr)) if not (len(var_set) == 1 and variable in var_set): raise BurntToast( "An unexpected error has occurred in the variable replacement scheme. " "Please open an issue on the IDAES GitHub so that this problem " "can be addressed." ) variable_original_value = variable.value if variable_original_value is None: variable.value = 1 try: # If constraint has the form variable == prod(v ** nu(v)) # then 1 / sf = prod((1/sf(v)) ** nu(v)). Fixing all the # other variables v to their nominal values allows us to # calculate sf using calculate_variable_from_constraint. calculate_variable_from_constraint( variable=variable, constraint=replaced_expr ) nom = abs(variable.value) except (RuntimeError, ValueError) as err: # RuntimeError: # Reached the maximum number of iterations for calculate_variable_from_constraint(). # Since it converges in a single iteration if variable appears linearly in # constraint, then constraint must have a different form than we were expecting. # ValueError: # variable possibly appears in constraint with derivative 0, # so also not of the form we expected. raise RuntimeError( f"Could not calculate scaling factor from definition constraint {constraint}. " f"Does {variable} appear nonlinearly in it or have a linear coefficient " "equal to zero?" ) from err finally: # Revert values to what they were initially variable.set_value(variable_original_value) if nom == 0: raise ValueError( "Calculated nominal value of zero from definition constraint." ) self.set_variable_scaling_factor(variable, 1 / nom, overwrite=overwrite)
# Common methods for constraint scaling
[docs] def scale_constraint_by_component( self, target_constraint: ConstraintData, scaling_component: VarData | ConstraintData | ExpressionData, overwrite: bool = False, ): """ Set scaling factor for target_constraint equal to that of scaling_component. Args: target_constraint: constraint to set scaling factor for scaling_component: component to use for scaling factor overwrite: whether to overwrite existing scaling factors Returns: None """ sf = self.get_scaling_factor(scaling_component) if sf is not None: self.set_constraint_scaling_factor( constraint=target_constraint, scaling_factor=sf, overwrite=overwrite ) else: _log.debug( f"Could not set scaling factor for {target_constraint.name}, " f"no scaling factor set for {scaling_component.name}" )
[docs] def scale_constraint_by_default( self, constraint: ConstraintData, overwrite: bool = False ): """ Set scaling factor for constraint based on default scaling factor. Args: constraint: constraint to set scaling factor for overwrite: whether to overwrite existing scaling factors Returns: None """ if constraint.is_indexed(): raise TypeError( f"{constraint} is indexed. Call with ComponentData children instead." ) if not isinstance(constraint, ConstraintData): raise TypeError( f"{constraint} is type {type(constraint)}, but a constraint was expected." ) self._scale_component_by_default(component=constraint, overwrite=overwrite)
[docs] def get_expression_nominal_value(self, expression: ConstraintData | ExpressionData): """ Calculate nominal value for a Pyomo expression. The nominal value of any Var is defined as the inverse of its scaling factor (if assigned, else 1). Args: expression: Pyomo expression to obtain nominal value for Returns: float of nominal value """ # Handles the case where we have equality constraints # TODO is this the best way to handle things? if hasattr(expression, "body"): expression = expression.body return sum(self.get_sum_terms_nominal_values(expression))
[docs] def get_expression_nominal_values( self, expression: ConstraintData | ExpressionData ): """ Calculate nominal values for each additive term in a Pyomo expression. The nominal value of any Var is defined as the inverse of its scaling factor (if assigned, else 1). Args: expression: Pyomo expression to collect nominal values for Returns: list of nominal values for each additive term """ deprecation_warning( msg=("This method has been renamed 'get_sum_terms_nominal_values'."), version="2.9", remove_in="2.10", ) return self.get_sum_terms_nominal_values(expression)
[docs] def get_sum_terms_nominal_values(self, expression: ConstraintData | ExpressionData): """ Calculate nominal values for each additive term in a Pyomo expression. The nominal value of any Var is defined as the inverse of its scaling factor (if assigned, else 1). Args: expression: Pyomo expression to collect nominal values for Returns: list of nominal values for each additive term """ # For convenience, if expression is a Pyomo component with an expr attribute, # redirect to the expr attribute if hasattr(expression, "expr"): expression = expression.expr return NominalValueExtractionVisitor().walk_expression(expression)
[docs] def scale_constraint_by_nominal_value( self, constraint: ConstraintData, scheme: ConstraintScalingScheme = ConstraintScalingScheme.inverseMaximum, overwrite: bool = False, ): """ Set scaling factor for constraint based on the nominal value(s). Terms with expected magnitudes of 0 will be ignored. Args: constraint: constraint to set scaling factor for scheme: ConstraintScalingScheme Enum indicating method to apply for determining constraint scaling. overwrite: whether to overwrite existing scaling factors Returns: None """ nominal = self.get_sum_terms_nominal_values(constraint.expr) # Remove any 0 terms nominal = [j for j in nominal if j != 0] if len(nominal) == 0: # No non-zero terms... sf = 1 elif scheme == ConstraintScalingScheme.harmonicMean: sf = sum(1 / abs(i) for i in nominal) elif scheme == ConstraintScalingScheme.inverseSum: sf = 1 / sum(abs(i) for i in nominal) elif scheme == ConstraintScalingScheme.inverseRSS: sf = 1 / sum(abs(i) ** 2 for i in nominal) ** 0.5 elif scheme == ConstraintScalingScheme.inverseMaximum: sf = 1 / max(abs(i) for i in nominal) elif scheme == ConstraintScalingScheme.inverseMinimum: sf = 1 / min(abs(i) for i in nominal) else: raise ValueError( f"Invalid value for 'scheme' argument ({scheme}) in " "scale_constraint_by_nominal_value." ) self.set_constraint_scaling_factor( constraint=constraint, scaling_factor=sf, overwrite=overwrite )
[docs] def scale_constraint_by_nominal_derivative_norm( self, constraint: ConstraintData, norm: int = 2, overwrite: bool = False ): """ Scale constraint by norm of partial derivatives. Calculates partial derivatives of constraint at nominal variable values, and then scaled the constraint by the user-selected norm of these derivatives. Given perfect variable scaling, this should provide a similar result to applying scaling based on the Jacobian norm, however this approach does not require an initial solution for the problem (relying on nominal values instead). Args: constraint: constraint to be scaled. norm: type of norm to use for scaling. Must be a positive integer. overwrite: whether to overwrite existing scaling factors. Returns: None """ # Cast norm to int to make sure it is valid norm = int(norm) if norm < 1: raise ValueError(f"norm must be a positive integer (received {norm})") var_data = [] try: # Iterate over all variables in constraint for v in identify_variables(constraint.body): # Store current value for restoration ov = v.value # original value sf = self.get_scaling_factor(v) # scaling factor if sf is None: # If no scaling factor set, use nominal value of 1 sf = 1 var_data.append((v, ov, sf)) # Get partial derivatives pjac = [] for v in var_data: # Iterate over all variable and set values for w in var_data: if w is not v: # Set all other variables to their nominal magnitude # nominal_value = 1/ scaling_factor w[0].value = 1 / w[2] else: # Set derivative var to scaled value of 1 # With perfect scaling, scaling_factor * value = 1 w[0].value = 1 pjac.append( value( differentiate( expr=constraint.body, wrt=v[0], mode=Modes.reverse_symbolic ) * (1 / v[2]) # Need to divide by scaling_factor ) ) finally: # Restore all values for clean up for v in var_data: v[0].value = v[1] # Calculate norm cnorm = sum(abs(j) ** norm for j in pjac) ** (1 / norm) if cnorm != 0: sf = 1 / cnorm else: sf = 1 self.set_constraint_scaling_factor(constraint, sf, overwrite=overwrite)
# Other methods
[docs] def propagate_state_scaling( self, target_state: Block | BlockData, source_state: Block | BlockData, overwrite: bool = False, ): """ Propagate scaling of state variables from one StateBlock to another. If both source and target state are indexed, the index sets must match. If the source state block is indexed, the target state block must also be indexed. Args: target_state: StateBlock to set scaling factors on source_state: StateBlock to use as source for scaling factors overwrite: whether to overwrite existing scaling factors Returns: None """ if target_state.is_indexed() and source_state.is_indexed(): for bidx, target_data in target_state.items(): self._propagate_state_data_scaling( target_state_data=target_data, source_state_data=source_state[bidx], overwrite=overwrite, ) elif target_state.is_indexed() and not source_state.is_indexed(): for target_data in target_state.values(): self._propagate_state_data_scaling( target_state_data=target_data, source_state_data=source_state, overwrite=overwrite, ) elif not target_state.is_indexed() and not source_state.is_indexed(): self._propagate_state_data_scaling( target_state_data=target_state, source_state_data=source_state, overwrite=overwrite, ) else: raise ValueError( "Source state block is indexed but target state block is not indexed. " "It is ambiguous which index should be used." )
def _propagate_state_data_scaling( self, target_state_data: BlockData, source_state_data: BlockData, overwrite: bool = False, ): """ Propagate scaling of state variables from one StateBlockData to another. Args: target_state_data: StateBlockData to set scaling factors on source_state_data: StateBlockData to use as source for scaling factors overwrite: whether to overwrite existing scaling factors Returns: None """ target_vars = target_state_data.define_state_vars() source_vars = source_state_data.define_state_vars() for state, var in target_vars.items(): for vidx, vardata in var.items(): self.scale_variable_by_component( target_variable=vardata, scaling_component=source_vars[state][vidx], overwrite=overwrite, )
[docs] def call_submodel_scaler_method( self, submodel, method: str, submodel_scalers: ComponentMap = None, overwrite: bool = False, ): """ Call scaling method for submodel. Scaler for submodel is taken from submodel_scalers if present, otherwise the default scaler for the submodel is used. Args: submodel: submodel to be scaled submodel_scalers: user provided ComponentMap of Scalers to use for submodels method: name of method to call from submodel (as string) overwrite: whether to overwrite existing scaling factors Returns: None """ if submodel_scalers is None: submodel_scalers = {} def scale_smdata(smdata): # Get Scaler for submodel if submodel in submodel_scalers: scaler = submodel_scalers[submodel] if callable(scaler): # Check to see if Scaler is callable - this implies it is a class and not an instance # Call the class to create an instance scaler = scaler(**self.config) _log.debug(f"Using user-defined Scaler for {submodel}.") else: try: scaler = smdata.default_scaler _log.debug(f"Using default Scaler for {submodel}.") except AttributeError: _log.debug( f"No default Scaler set for {submodel}. Cannot call {method}." ) # TODO Is it possible for one index to have a scaler and another # not without user insanity? return if scaler is not None: scaler = scaler(**self.config) else: # TODO Why not return here but return above? _log.debug(f"No Scaler found for {submodel}. Cannot call {method}.") # If a Scaler is found, call desired method if scaler is not None: try: smeth = getattr(scaler, method) except AttributeError as err: raise AttributeError( f"Scaler for {submodel} does not have a method named {method}." ) from err smeth(smdata, submodel_scalers=submodel_scalers, overwrite=overwrite) if submodel.is_indexed(): # Iterate over indices of submodel for data in submodel.values(): scale_smdata(data) else: scale_smdata(submodel)