Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scaler for equilibrium reactor and saponification properties #1500

Open
wants to merge 84 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
528a4f7
Adding infrastructure to support ipopt_v2
andrewlee94 Jun 14, 2024
0faabba
Moving core/util to ipopt_v2
andrewlee94 Jun 14, 2024
47d9d73
Moving MH initializer to ipopt_v2
andrewlee94 Jun 15, 2024
47beac6
Fixing pint version issue
andrewlee94 Jun 17, 2024
c39f256
Set TSA to use old IPOPT interface
andrewlee94 Jun 17, 2024
04f1463
Fixing conflict
andrewlee94 Jun 17, 2024
3ce08dd
Trying to resolve Windows failures
andrewlee94 Jun 17, 2024
4aa1405
Working on platofrm dependent failure
andrewlee94 Jun 17, 2024
7353472
BTInitializer with presolve
andrewlee94 Jun 20, 2024
e88bd83
Merge branch 'main' of https://github.com/IDAES/idaes-pse into presolve
andrewlee94 Jun 20, 2024
d5135f4
Moving last bits of core code to ipopt_v2
andrewlee94 Jun 20, 2024
9ccc8b4
Starting on idaes/models
andrewlee94 Jun 20, 2024
9423501
Removing ma57_automatic_scaling default and updating idaes/models/con…
andrewlee94 Jun 20, 2024
6754964
idaes/model/properties part 1
andrewlee94 Jun 20, 2024
f6079ca
Remaining parts of idaes/models/proeprties
andrewlee94 Jun 20, 2024
ad475fd
Fixing typo
andrewlee94 Jun 20, 2024
dda16b3
Switching idaes/models/unit_models to ipopt_v2
andrewlee94 Jun 21, 2024
bc8e411
Attempt to work around HXLC issues for now
andrewlee94 Jun 21, 2024
4273e24
Some clean up
andrewlee94 Jun 21, 2024
542914a
Switching modular properties initializer to solver indexed blocks
andrewlee94 Jun 21, 2024
6578d01
Merge branch 'modular_properties_init' into presolve
andrewlee94 Jun 21, 2024
84e9d69
Addressing comments
andrewlee94 Jun 21, 2024
c3615a1
Recovering from previous branch
andrewlee94 Jun 21, 2024
6a7750c
Merge branch 'presolve' into autoscaling_rebase
andrewlee94 Jun 21, 2024
8be4d5a
Some clean up
andrewlee94 Jun 21, 2024
dbdbdd4
Adding ScalerBase class and tests
andrewlee94 Jun 24, 2024
adc2809
Working on CustomScalerBase
andrewlee94 Jun 24, 2024
d9f1904
Nominal value constraint scaling
andrewlee94 Jun 25, 2024
5f4fe46
Adding some initial integration tests for scaling
andrewlee94 Jun 25, 2024
72a183d
Some more nominal magnitude scaling approaches
andrewlee94 Jun 25, 2024
a6ef9cb
Merge pull request #21 from andrewlee94/custom_scalers_proto
andrewlee94 Jun 25, 2024
a12cb95
Prototyping pseudojacobian scaler
andrewlee94 Jun 26, 2024
b3aafb0
Merge branch 'main' into autoscaling
andrewlee94 Jun 26, 2024
c51672f
Trying to debug pseudojacobian
andrewlee94 Jun 26, 2024
861ef58
Removing unnecessary import
andrewlee94 Jun 26, 2024
3805792
Merge branch 'main' of https://github.com/IDAES/idaes-pse
andrewlee94 Jun 26, 2024
fbedaf4
Merge branch 'main' into autoscaling
andrewlee94 Jun 26, 2024
76bec99
Addressing pylint issues
andrewlee94 Jun 26, 2024
eec789e
Merge branch 'autoscaling' into custom_scalers_proto
andrewlee94 Jun 26, 2024
d80eed6
Cleaning up nominal jacobian
andrewlee94 Jun 26, 2024
c88879e
More methods for CustomScalerBase
andrewlee94 Jun 27, 2024
5a29597
Prototyping Gibbs reactor scaler
andrewlee94 Jun 27, 2024
154a622
Gibbs reactor constraint scaling
andrewlee94 Jun 27, 2024
a6d61fa
Working on testing and profiling
andrewlee94 Jun 28, 2024
86faf86
Refining Gibbs scaler
andrewlee94 Jun 30, 2024
ac2b639
Refining nominal value walker
andrewlee94 Jun 30, 2024
f3f6330
Fixing walker tests
andrewlee94 Jul 1, 2024
ce8c202
Testing GibbsScaler with initialization
andrewlee94 Jul 1, 2024
05537fd
Fixing auto norm scaling on indexed blocks
andrewlee94 Jul 1, 2024
3c302e6
Testing scaling profiler
andrewlee94 Jul 2, 2024
937e596
Fixing typos
andrewlee94 Jul 2, 2024
ef9c5e9
Fixing pylint issue
andrewlee94 Jul 2, 2024
6662fc7
Imrpoving some doc strings
andrewlee94 Jul 5, 2024
0308f1e
Merge branch 'main' into autoscaling
andrewlee94 Jul 22, 2024
ec49414
Apply suggestions from code review
andrewlee94 Aug 6, 2024
f7eda46
Merge branch 'main' into autoscaling
andrewlee94 Aug 15, 2024
e3b169d
Fixing issue with autoscaling vars with value None
andrewlee94 Aug 15, 2024
1370bbe
Merge branch 'autoscaling' of https://github.com/andrewlee94/idaes-ps…
andrewlee94 Aug 15, 2024
a5de0b7
Adding profiler to __init__
andrewlee94 Aug 15, 2024
0964c7a
Fixing name for RSS method
andrewlee94 Aug 15, 2024
0af66e1
Merge branch 'main' of https://github.com/IDAES/idaes-pse into autosc…
andrewlee94 Aug 26, 2024
0f6d262
Fixing import of pyomo.environ
andrewlee94 Aug 26, 2024
bd1b6aa
Allowing default scaling for indexed components
andrewlee94 Aug 26, 2024
0cad4ed
Adding catch for critical solver failure in profiler
andrewlee94 Aug 27, 2024
3b6ae69
Merge branch 'main' into autoscaling
andrewlee94 Sep 30, 2024
f3940a1
Merge branch 'autoscaling' of https://github.com/andrewlee94/idaes-ps…
andrewlee94 Sep 30, 2024
c71fbd1
Starting on docs
andrewlee94 Sep 30, 2024
10fbf3d
Finishing docs and unifiying method names
andrewlee94 Oct 1, 2024
794f1ed
Profiler report methods and docs
andrewlee94 Oct 2, 2024
21ef587
Fixing typos
andrewlee94 Oct 2, 2024
2d1dd54
Pylint: fix unnecessary f-string
andrewlee94 Oct 2, 2024
19212fa
Working on equilibrium reactor scaler
andrewlee94 Oct 4, 2024
8f56b88
Finishing tests for equilibrium reactor scaler
andrewlee94 Oct 4, 2024
1e3163f
Fixing conflicts
andrewlee94 Oct 11, 2024
1d55e06
Updaing requil scaler
andrewlee94 Oct 11, 2024
fb5d2a9
Fixing typo
andrewlee94 Oct 11, 2024
1f200db
Addressing pylint issue
andrewlee94 Oct 11, 2024
c8e7408
Improving guess for heat duty scaling
andrewlee94 Oct 11, 2024
148c2f3
Updating test value for heat duty scaling
andrewlee94 Oct 11, 2024
c18f464
Minor fix
andrewlee94 Oct 18, 2024
2aa07a0
Merge branch 'main' into requil_scaling
andrewlee94 Oct 19, 2024
2759010
Adding Enum to __init__
andrewlee94 Oct 19, 2024
7f32d73
Adding doc strings for saponification scalers
andrewlee94 Oct 20, 2024
cd82ad8
Fixing typo
andrewlee94 Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/explanations/scaling_toolbox/scaling_theory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Types of Scaling
* Constraint residual scaling refers to the magnitude of the residual of all constraints in the model. This is important as this is required to determine whether or not a model has converged, thus it is important that a residual equal to the solver tolerance does not significantly alter the solution to the model.

* E.g. consider a constraint A=B. If the magnitude of A and B are 1e6 and the solver tolerance is 1e-6, this means that A and B need to be solved to 12 significant figures of precision to converge the constraint (which may be unnecessarily onerous). Similarly, if A and B were of order 1e-6, then they will be considered equal if A=1e-6 and B=0; i.e. ±100% error.

* Jacobian scaling refers to the overall scaling and conditioning of the problem Jacobian. This is important as this determines the ability of the solver to find a path from the initial state to the final solution. It is important to ensure that the Jacobian is well-conditioned both in order to get good numerical behavior from the solver and also to ensure that floating point round-off error does not result in large changes to the variables at the solution.

These aspects are not always complimentary, and there are often cases where improving one aspect of the model scaling can negatively affect another aspect (e.g., focusing too much on Jacobian condition number can often result in poor constraint residual tolerances). Additionally, each aspect affects different parts of the solver routine; Jacobian scaling is important for determining the step direction and size at each iteration, whilst constraint residual scaling is important for determining if and when a solution is found. The relative importance of each of these depends on the solver being used (e.g., for IPOPT constraint residual scaling is more important than Jacobian scaling as IPOPT does its own internal scaling of the Jacobian), however in general all of these will have an impact upon the solver behavior and users should endeavor to have good scaling for all aspects of the model.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ EquilibriumReactorData Class

.. autoclass:: EquilibriumReactorData
:members:

EquilibriumReactorScaler Class
------------------------------

.. autoclass:: EquilibriumReactorScaler
:members:
8 changes: 8 additions & 0 deletions idaes/core/base/property_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,14 @@ def include_inherent_reactions(self):
# pylint: disable-next=protected-access
return self.parent_component()._include_inherent_reactions()

@property
def default_initializer(self):
return self.parent_component().default_initializer

@property
def default_scaler(self):
return self.parent_component().default_scaler

def build(self):
"""
General build method for StateBlockDatas.
Expand Down
8 changes: 8 additions & 0 deletions idaes/core/base/reaction_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ def phase_list(self):
def phase_component_set(self):
return self.state_ref.phase_component_set

@property
def default_initializer(self):
return self.parent_component().default_initializer

@property
def default_scaler(self):
return self.parent_component().default_scaler

def lock_attribute_creation_context(self):
"""Returns a context manager that does not allow attributes to be created
while in the context and allows attributes to be created normally outside
Expand Down
2 changes: 1 addition & 1 deletion idaes/core/scaling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# for full copyright and license information.
#################################################################################
from .autoscaling import AutoScaler
from .custom_scaler_base import CustomScalerBase
from .custom_scaler_base import CustomScalerBase, ConstraintScalingScheme
from .scaler_profiling import ScalingProfiler
from .util import (
scaling_factors_from_json_file,
Expand Down
1 change: 1 addition & 0 deletions idaes/core/scaling/custom_scaler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def call_submodel_scaler_method(
submodel: local name of submodel to be scaled as str
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
Expand Down
16 changes: 10 additions & 6 deletions idaes/core/scaling/scaler_profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
from pyomo.common.tempfiles import TempfileManager

from idaes.core.util.scaling import jacobian_cond
from idaes.core.scaling import AutoScaler, CustomScalerBase
from idaes.core.scaling.autoscaling import AutoScaler
from idaes.core.scaling.custom_scaler_base import (
CustomScalerBase,
ConstraintScalingScheme,
)
from idaes.core.solvers import get_solver


Expand Down Expand Up @@ -83,23 +87,23 @@ def __init__(
"Vars Only": (None, {}),
"Harmonic": (
cscaler.scale_constraint_by_nominal_value,
{"scheme": "harmonic_mean"},
{"scheme": ConstraintScalingScheme.harmonicMean},
),
"Inverse Sum": (
cscaler.scale_constraint_by_nominal_value,
{"scheme": "inverse_sum"},
{"scheme": ConstraintScalingScheme.inverseSum},
),
"Inverse Root Sum Squares": (
cscaler.scale_constraint_by_nominal_value,
{"scheme": "inverse_root_sum_squared"},
{"scheme": ConstraintScalingScheme.inverseRSS},
),
"Inverse Maximum": (
cscaler.scale_constraint_by_nominal_value,
{"scheme": "inverse_maximum"},
{"scheme": ConstraintScalingScheme.inverseMaximum},
),
"Inverse Minimum": (
cscaler.scale_constraint_by_nominal_value,
{"scheme": "inverse_minimum"},
{"scheme": ConstraintScalingScheme.inverseMinimum},
),
"Nominal L1 Norm": (
cscaler.scale_constraint_by_nominal_derivative_norm,
Expand Down
2 changes: 0 additions & 2 deletions idaes/core/scaling/tests/test_custom_scaling_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
from idaes.models.properties.activity_coeff_models.methane_combustion_ideal import (
MethaneParameterBlock as MethaneCombustionParameterBlock,
)
from idaes.core.util.testing import PhysicalParameterTestBlock, initialization_tester
from idaes.core.solvers import get_solver
from idaes.core.util import to_json, from_json, StoreSpec
from idaes.core.util.scaling import jacobian_cond
from idaes.core.scaling import AutoScaler, CustomScalerBase, set_scaling_factor
Expand Down
74 changes: 74 additions & 0 deletions idaes/models/properties/examples/saponification_reactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
from idaes.core.util.misc import add_object_reference
from idaes.core.util.constants import Constants as const
import idaes.logger as idaeslog
from idaes.core.scaling import CustomScalerBase


# Some more information about this module
__author__ = "Andrew Lee"

from idaes.core.util.scaling import get_scaling_factor

# Set up logger
_log = idaeslog.getLogger(__name__)
Expand Down Expand Up @@ -111,12 +113,84 @@ def define_metadata(cls, obj):
)


class SaponificationReactionScaler(CustomScalerBase):
"""
Scaler for saponification reaction package.

Variables are scaled by nominal order of magnitude, and constraints
using the inverse maximum scheme.
"""

DEFAULT_SCALING_FACTORS = {"reaction_rate": 1e2}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the scaling framework know to use this value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you tell it to scale_variable_by_default or scale_constraint_by_default.


def variable_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
if model.is_property_constructed("k_rxn"):
# First check to see if k_rxn is already scaled
sf = get_scaling_factor(model.k_rxn)

if sf is not None and not overwrite:
# k_rxn already has a scaling factor and we are not set to overwrite - move on
pass
else:
# Hopefully temperature has been scaled, so we can get the nominal value of k_rxn
# by walking the expression in the constraint.
nominals = self.get_expression_nominal_values(model.arrhenius_eqn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saponification has a rather tight range of temperatures. This is probably overkill.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this specific case probably, but this also stands as an example of how to do it for a more general case.


# We should get two values, k_rxn (LHS) and the Arrhenius equation (RHS)
# As of 10/3/2024, the LHS will be the 0-th element of the list, and the RHS the 1st
# However, we cannot assume this will always be the case

# If LHS has been scaled, nominal will be 1/sf, otherwise it will be k_rxn.value
# Find the value which does NOT match this - guess that this is the 1st element
if nominals[1] != model.k_rxn.value and sf is None:
# This is the most likely case, so check it first
nominal = nominals[1]
elif sf is not None and nominals[1] != 1 / sf:
# Next, check for case where k_rxn was already scaled
nominal = nominals[1]
else:
# Otherwise we have the case where something changed in Pyomo since 10/3/2024
nominal = nominals[0]

self.set_variable_scaling_factor(
model.k_rxn,
1 / nominal,
overwrite=overwrite,
)
Comment on lines +157 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order of magnitude scaling is probably not appropriate here. Based on heuristic analysis, a fixed scaling factor of 1/3 would work.


if model.is_property_constructed("reaction_rate"):
for j in model.reaction_rate.values():
self.scale_variable_by_default(j, overwrite=overwrite)
Comment on lines +163 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user provides a default, we should do that, but we can get a decent scaling factor by multiplying the maximum scaling factor among the reactants provided by some period of time (I used 3600 s). (Equivalent to dividing the minimum default value by that period of time).


def constraint_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
if model.is_property_constructed("arrhenius_eqn"):
self.scale_constraint_by_nominal_value(
model.arrhenius_eqn,
scheme="inverse_maximum",
overwrite=overwrite,
)

if model.is_property_constructed("rate_expression"):
for j in model.rate_expression.values():
self.scale_constraint_by_nominal_value(
j,
scheme="inverse_maximum",
overwrite=overwrite,
)


class _ReactionBlock(ReactionBlockBase):
"""
This Class contains methods which should be applied to Reaction Blocks as a
whole, rather than individual elements of indexed Reaction Blocks.
"""

default_scaler = SaponificationReactionScaler

def initialize(blk, outlvl=idaeslog.NOTSET, **kwargs):
"""
Initialization routine for reaction package.
Expand Down
46 changes: 46 additions & 0 deletions idaes/models/properties/examples/saponification_thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from idaes.core.util.model_statistics import degrees_of_freedom
from idaes.core.util.initialization import fix_state_vars, revert_state_vars
import idaes.logger as idaeslog
from idaes.core.scaling import CustomScalerBase

# Some more information about this module
__author__ = "Andrew Lee"
Expand Down Expand Up @@ -135,12 +136,57 @@ def define_metadata(cls, obj):
)


class SaponificationPropertiesScaler(CustomScalerBase):
"""
Scaler for saponification properties package.

Flow and concentration are scaled by default value (if no user input provided),
pressure is scaled assuming order of magnitude of 1e5 Pa, and temperature is
scaled using the average of the bounds. Constraints using the inverse maximum
scheme.
"""

UNIT_SCALING_FACTORS = {
# "QuantityName: (reference units, scaling factor)
"Pressure": (units.Pa, 1e-5),
}

DEFAULT_SCALING_FACTORS = {
"flow_vol": 1e2,
"conc_mol_comp": 1e-2,
}

def variable_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
self.scale_variable_by_default(model.flow_vol, overwrite=overwrite)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for the user to provide a default value, rather than just using whatever happens to be the default default value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, although it might not be the best way to do it. The default values are in a class attribute (dict) which users can interact with. That said, that is probably not the most obvious API for it (we could have a method to update the defaults).

self.scale_variable_by_units(model.pressure, overwrite=overwrite)
self.scale_variable_by_bounds(model.temperature, overwrite=overwrite)
for k, v in model.conc_mol_comp.items():
if k == "H2O":
self.set_variable_scaling_factor(v, 1e-4, overwrite=overwrite)
else:
self.scale_variable_by_default(v, overwrite=overwrite)

def constraint_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
if model.is_property_constructed("conc_water_eqn"):
self.set_constraint_scaling_factor(
model.conc_water_eqn,
1e-4,
overwrite=overwrite,
)


class _StateBlock(StateBlock):
"""
This Class contains methods which should be applied to Property Blocks as a
whole, rather than individual elements of indexed Property Blocks.
"""

default_scaler = SaponificationPropertiesScaler

def fix_initialization_states(self):
"""
Fixes state variables for state blocks.
Expand Down
Loading
Loading