Skip to content

CSTR Re-Scaling #1542

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

Merged
merged 22 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
88cca29
Update test files
MarcusHolly Dec 19, 2024
c0ec701
Merge branch 'main' into cstr_scaling
MarcusHolly Dec 19, 2024
cd5c8fa
Address pylint issue
MarcusHolly Dec 19, 2024
69dde0f
Create aeratoin tank scaler and tests
MarcusHolly Jan 2, 2025
3387020
Address pylint issues
MarcusHolly Jan 2, 2025
3e8836d
Add initial Scaler Profiler testing
MarcusHolly Jan 9, 2025
755456c
Skip ScalingProfiler tests on Mac
MarcusHolly Jan 9, 2025
14d8f9b
Perturb injection for ScalingProfiler tests
MarcusHolly Jan 10, 2025
938f6e3
Merge branch 'main' into cstr_scaling
MarcusHolly Apr 2, 2025
46b1205
Modify scaling routines
MarcusHolly Apr 4, 2025
3b3b511
Merge branch 'main' into cstr_scaling
MarcusHolly Apr 14, 2025
85f3665
Add scaling factor for injection to cstr_injection
MarcusHolly Apr 15, 2025
32f76f1
Add injection scaling to aeration tank
MarcusHolly Apr 15, 2025
64dd1a2
Remove redundant constraint scaling
MarcusHolly Apr 15, 2025
67b0449
Adjust constraint scaling
MarcusHolly Apr 24, 2025
2fcf9b5
Merge branch 'main' into cstr_scaling
MarcusHolly Apr 24, 2025
bc479e4
Trim tests by removing asserts for individual scaling factors
MarcusHolly Apr 25, 2025
f681f11
Remove unnecessary TODO note
MarcusHolly Apr 25, 2025
c78aaed
Merge branch 'main' into cstr_scaling
MarcusHolly May 15, 2025
0182600
Add additional checks for condition numbers
MarcusHolly May 15, 2025
494763d
Merge branch 'main' into cstr_scaling
ksbeattie May 22, 2025
5a520f6
Merge branch 'main' into cstr_scaling
adam-a-a Jun 16, 2025
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
137 changes: 137 additions & 0 deletions watertap/unit_models/aeration_tank.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

# Import Pyomo libraries
from pyomo.common.config import In
from pyomo.environ import Constraint

# Import IDAES cores
from idaes.core import (
declare_process_block_class,
)
from idaes.core.scaling import CustomScalerBase, ConstraintScalingScheme


from watertap.unit_models.cstr_injection import (
Expand All @@ -30,12 +32,147 @@
__author__ = "Adam Atia"


class AerationTankScaler(CustomScalerBase):
"""
Default modular scaler for the aeration tank unit model.

This Scaler relies on the associated property and reaction packages,
either through user provided options (submodel_scalers argument) or by default
Scalers assigned to the packages.
"""

DEFAULT_SCALING_FACTORS = {
"volume": 1e-3,
"hydraulic_retention_time": 1e-3,
"KLa": 1e-1,
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason why we wouldn't use the inverse of the variable value (assuming it was fixed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So basically why don't we use the autoscaling functions for these variables? In general, the autoscaling tools haven't been working great for me when applying them in a wide-sweeping sense like this. They're also just used not used very much in general. I haven't seen any examples of the new Scaler Objects using the autoscaling tools

Choose a reason for hiding this comment

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

any reason why we wouldn't use the inverse of the variable value (assuming it was fixed)?

Fixed variables are not visible to IPOPT at all. The only reason you might need a scaling factor for them is if some other quantity depended on it.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's true...forgot about that. What I really meant was "assuming the inverse of the variable value would be tolerable".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we're aiming for "perfect scaling". I'm rusty on the mathematical reasons why we'd want to avoid it, but I know it was discussed in one of Andrew's scaling documents. Does anybody happen to have/know where that up-to-date scaling document is?

Copy link
Contributor

Choose a reason for hiding this comment

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

Unsure, but my only concern is that volume would be tied to define flowrate, which can be whatever the user wants to specify. So for a very large system, volumes would be larger, and the scaling factor might not be small enough (for example).

Copy link
Contributor

Choose a reason for hiding this comment

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

Additionally, volume divided by volumetric flowrate equals HRT, so it would make more sense to set a scaling factor for volume that is based on the scaling factors for HRT and volumetric flowrate (for example).

Actually, since volumetric flowrate is a state variable in ASM/ADM models, the user should always provide scaling for that. Now, I think we can safely assume some default for HRT, based on typical HRT values. From that, we can compute the scaling factor for volume as scaling_factor_of_HRT * scaling_factor_of_volumetric_flowrate

Copy link
Contributor Author

@MarcusHolly MarcusHolly Jun 6, 2025

Choose a reason for hiding this comment

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

Thsoe are valid concerns... the user would just have to update these scaling factors manually for a large system. To give some background on why I chose these values, 1e-3 was a good scaling factor for HRT and volume for the CSTRs in the BSM models. This implies that the magnitude of the flow would be on the scale of 1 (equivalent to no scaling factor), which checks out after looking at 2 CSTRs in the BSM2 flowsheet. For the sake of transparency, I could add a scaling factor of 1 to the list of default scaling factors for volumetric flow.

TLDR: I'm not opposed to what you're suggesting, but I'm not sure how I would implement it, especially given Doug's comment

Choose a reason for hiding this comment

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

I don't think we're aiming for "perfect scaling".

What Andrew meant by "perfect scaling" was scaling every variable using a scaling factor of 1/abs(value(var)). We don't want to do that because it typically screws up the Jacobian scaling. The best example is something like enthalpy, where the variable value can pass through zero. If you scale based on an initial condition for which the enthalpy just happens to be a number close to zero, that causes major problems.

Variables that pass through zero aren't the only case where naive scaling like that causes problems, but they're the most obvious.

Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's document these default scaling factors an issue in case they ever become suspects in a "failure to converge" crime scene. I won't hold this up anymore!

"mass_transfer_term": 1e2,
}

def variable_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
"""
Routine to apply scaling factors to variables in model.

Args:
model: model to be scaled
overwrite: whether to overwrite existing scaling factors
submodel_scalers: dict of Scalers to use for sub-models, keyed by submodel local name

Returns:
None
"""
# Call scaling methods for sub-models
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_in,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.propagate_state_scaling(
target_state=model.control_volume.properties_out,
source_state=model.control_volume.properties_in,
overwrite=overwrite,
)

self.call_submodel_scaler_method(
submodel=model.control_volume.properties_out,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.reactions,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)

# Scaling control volume variables
self.scale_variable_by_default(
model.control_volume.volume[0], overwrite=overwrite
)
self.scale_variable_by_default(
model.hydraulic_retention_time[0], overwrite=overwrite
)
self.scale_variable_by_default(model.KLa, overwrite=overwrite)
if model.config.has_aeration:
if "S_O" and "S_O2" in model.config.property_package.component_list:
self.scale_variable_by_default(
model.control_volume.mass_transfer_term[0, "Liq", "S_O"],
overwrite=overwrite,
)
self.scale_variable_by_default(
model.control_volume.mass_transfer_term[0, "Liq", "S_O2"],
overwrite=overwrite,
)
elif "S_O" in model.config.property_package.component_list:
self.scale_variable_by_default(
model.control_volume.mass_transfer_term[0, "Liq", "S_O"],
overwrite=overwrite,
)
elif "S_O2" in model.config.property_package.component_list:
self.scale_variable_by_default(
model.control_volume.mass_transfer_term[0, "Liq", "S_O2"],
overwrite=overwrite,
)
else:
pass

def constraint_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
"""
Routine to apply scaling factors to constraints in model.

Submodel Scalers are called for the property and reaction blocks. All other constraints
are scaled using the inverse maximum scheme.

Args:
model: model to be scaled
overwrite: whether to overwrite existing scaling factors
submodel_scalers: dict of Scalers to use for sub-models, keyed by submodel local name

Returns:
None
"""
# Call scaling methods for sub-models
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_in,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_out,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.reactions,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)

# Scale unit level constraints
for c in model.component_data_objects(Constraint, descend_into=True):
self.scale_constraint_by_nominal_value(
c,
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=overwrite,
)


@declare_process_block_class("AerationTank")
class AerationTankData(CSTR_InjectionData):
"""
CSTR Unit Model with Injection Class
"""

default_scaler = AerationTankScaler

CONFIG = CSTR_InjectionData.CONFIG()
CONFIG.electricity_consumption = ElectricityConsumption.calculated
CONFIG.get("has_aeration")._domain = In([True])
Expand Down
111 changes: 111 additions & 0 deletions watertap/unit_models/cstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from idaes.models.unit_models.cstr import CSTRData as CSTRIDAESData

import idaes.logger as idaeslog
from idaes.core.scaling import CustomScalerBase, ConstraintScalingScheme

from pyomo.environ import (
Constraint,
Expand All @@ -38,12 +39,122 @@
_log = idaeslog.getLogger(__name__)


class CSTRScaler(CustomScalerBase):
"""
Default modular scaler for CSTR.

This Scaler relies on the associated property and reaction packages,
either through user provided options (submodel_scalers argument) or by default
Scalers assigned to the packages.
"""

DEFAULT_SCALING_FACTORS = {
"volume": 1e-3,
"hydraulic_retention_time": 1e-3,
}

def variable_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
"""
Routine to apply scaling factors to variables in model.

Args:
model: model to be scaled
overwrite: whether to overwrite existing scaling factors
submodel_scalers: dict of Scalers to use for sub-models, keyed by submodel local name

Returns:
None
"""
# Call scaling methods for sub-models
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_in,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.propagate_state_scaling(
target_state=model.control_volume.properties_out,
source_state=model.control_volume.properties_in,
overwrite=overwrite,
)

self.call_submodel_scaler_method(
submodel=model.control_volume.properties_out,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.reactions,
method="variable_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)

# Scaling control volume variables
self.scale_variable_by_default(
model.control_volume.volume[0], overwrite=overwrite
)
self.scale_variable_by_default(
model.hydraulic_retention_time[0], overwrite=overwrite
)

def constraint_scaling_routine(
self, model, overwrite: bool = False, submodel_scalers: dict = None
):
"""
Routine to apply scaling factors to constraints in model.

Submodel Scalers are called for the property and reaction blocks. All other constraints
are scaled using the inverse maximum scheme.

Args:
model: model to be scaled
overwrite: whether to overwrite existing scaling factors
submodel_scalers: dict of Scalers to use for sub-models, keyed by submodel local name

Returns:
None
"""
# Call scaling methods for sub-models
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_in,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.properties_out,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)
self.call_submodel_scaler_method(
submodel=model.control_volume.reactions,
method="constraint_scaling_routine",
submodel_scalers=submodel_scalers,
overwrite=overwrite,
)

# Scale unit level constraints
for c in model.component_data_objects(Constraint, descend_into=True):
self.scale_constraint_by_nominal_value(
c,
scheme=ConstraintScalingScheme.inverseMaximum,
overwrite=overwrite,
)


@declare_process_block_class("CSTR")
class CSTRData(CSTRIDAESData):
"""
CSTR unit block for BSM2
"""

default_scaler = CSTRScaler

CONFIG = CSTRIDAESData.CONFIG()

def build(self):
Expand Down
Loading
Loading