Skip to content

Conversation

@dallan-keylogic
Copy link
Contributor

Summary/Motivation:

Add additional scaling for various unit models, mostly involving RO. Depends on #1695.

Also, the unit test harness is slightly modified. The check for "badly scaled variables" can be bypassed, and an optional check for the model condition number is added instead.

Legal Acknowledgement

By contributing to this software project, I agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the license terms described in the LICENSE.txt file at the top level of this directory.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.


def calculate_scaling_factors(self):
if iscale.get_scaling_factor(self.area) is None:
iscale.set_scaling_factor(self.area, 100)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also display a warning to the user that we are doing this since so scaling factor was provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do the old scaling tools really need additional warnings to spam the user's logs?

In the new scaling tools, I'm going to raise an exception instead, so they can no longer ignore it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dallan-keylogic in the new scaling tools, could you think about perhaps a setting where the user could choose to have exceptions raised or bypass (with warnings if you prefer)? The default would be to raise exceptions, but it'd be nice to have an escape route for devs (for whatever exploratory reasons someone might have).

if iscale.get_scaling_factor(v) is None:
iscale.set_scaling_factor(v, 1)

# TODO why did this file have zero scaling factors for constraints
Copy link
Contributor

Choose a reason for hiding this comment

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

@bknueven can probably give a more informative response, but the line of thinking was that scaling all variables should suffice in most cases without the need to scale constraints. However, we know that this isn't always the case, as we've experienced particularly when dealing with getting "complex" models to converge.

warnings, next_steps = dt._collect_numerical_warnings()
assert len(warnings) == 3
warnings, _ = dt._collect_numerical_warnings()
assert len(warnings) == 1
Copy link
Contributor

Choose a reason for hiding this comment

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

nice


initializer = BlockTriangularizationInitializer()
initializer = BlockTriangularizationInitializer(
calculate_variable_options={"eps": 2e-8}, skip_final_solve=True
Copy link
Contributor

Choose a reason for hiding this comment

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

how is eps applied through calculate_variable_options?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is the tolerance used to check whether the constraint residual is small enough to consider the constraint "satisfied". When solving the flowsheet, I encountered a problem where it couldn't reduce the residual of some enthalpy constraint (i.e., a constraint that needs a scaling factor <<1) to less than 1e-8. Due to Pyomo/pyomo#3785 , it does not take the constraint's scaling factor into account when calculating this residual. Using a constraint_scaling_transformation would allow scaling to be taken into account, but we wanted to move away from that in the new scaling tools.

Comment on lines +560 to +561
# TODO is it intentional that we're setting scaling factors for some constraints
# but doing constraint scaling transforms for others?
Copy link
Contributor

Choose a reason for hiding this comment

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

Not intentional--traditionally we applied constraint scaling transforms.

Copy link
Contributor

Choose a reason for hiding this comment

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

this appears to contradict @MarcusHolly in #1695

Comment on lines +1410 to +1411
b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j]
== b.flow_mol_phase_comp[p, j]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why opt for this form?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is changed due to how the WaterTap scaling utilities work. Because the constraint is named eq_flow_mol_phase_comp, it is automatically scaled using the scaling factor for flow_mol_phase_comp. However, as originally written, it ought to be scaled instead like flow_mass_phase_comp. This slight modification of the constraint is easier than messing with that workflow.

Comment on lines -3101 to +3113
] == pytest.approx(12120000, rel=1e-3)
] == pytest.approx(4.309e07, rel=1e-3)
assert m.fs.stream[0].scaling_factor[
m.fs.stream[0].eq_elec_mobility_phase_comp["Liq", "Na_+"]
] == pytest.approx(19320000, rel=1e-3)
] == pytest.approx(8.617e07, rel=1e-3)
assert m.fs.stream[0].scaling_factor[
m.fs.stream[0].eq_elec_mobility_phase_comp["Liq", "Cl_-"]
] == pytest.approx(12660000, rel=1e-3)
] == pytest.approx(8.617e07, rel=1e-3)
assert m.fs.stream[0].scaling_factor[
m.fs.stream[0].eq_elec_mobility_phase_comp["Liq", "Mg_2+"]
] == pytest.approx(18200000, rel=1e-3)
] == pytest.approx(4.309e07, rel=1e-3)
Copy link
Contributor

Choose a reason for hiding this comment

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

Unclear why we are getting relatively significant changes in these test values

Copy link
Contributor

Choose a reason for hiding this comment

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

It's clearer now.

Comment on lines 1105 to 1152
] == pytest.approx(0.01866, rel=1e-3)
] == pytest.approx(0.1, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Ca_2+"]
] == pytest.approx(104.7, rel=1e-3)
] == pytest.approx(1e3, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "SO4_2-"]
] == pytest.approx(44.94, rel=1e-3)
] == pytest.approx(1e2, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Mg_2+"]
] == pytest.approx(1.722, rel=1e-3)
] == pytest.approx(10, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Na_+"]
] == pytest.approx(2.068, rel=1e-3)
] == pytest.approx(10, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Cl_-"]
] == pytest.approx(0.6174, rel=1e-3)
] == pytest.approx(1, rel=1e-3)

assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "H2O"]
] == pytest.approx(0.0233193, rel=1e-5)
] == pytest.approx(0.1, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Ca_2+"]
] == pytest.approx(0.004, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "SO4_2-"]
] == pytest.approx(0.00960, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Mg_2+"]
] == pytest.approx(0.00240, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Na_+"]
] == pytest.approx(0.0023, rel=1e-3)

assert (
mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.permeate_electronegativity[0.0]
]
== 1
)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.retentate_pressure_balance[0.0]
] == pytest.approx(0.0000025, rel=1e-3)
] == pytest.approx(1e-5, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.retentate_temperature_equality[0.0]
] == pytest.approx(0.003354, rel=1e-3)
] == pytest.approx(1e-2, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.permeate_temperature_equality[0.0]
] == pytest.approx(0.003354, rel=1e-3)
] == pytest.approx(1e-2, rel=1e-3)
Copy link
Contributor

Choose a reason for hiding this comment

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

These changes seem significant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These changes weren't a result of this PR, but instead #1695. They're due to a change in how the nominal value expression walker works. Previously it used 1/value(var) if var was fixed, but now it uses 1/scaling_factor[var] regardless of whether or not var is fixed.

Comment on lines +157 to +158
self.skip_badly_scaled_vars = True
self.condition_number = 3.9140627e8 # Was previously 4.8157167e15
Copy link
Contributor

Choose a reason for hiding this comment

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

these seem like great improvements!

Comment on lines +1410 to +1411
b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j]
== b.flow_mol_phase_comp[p, j]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is changed due to how the WaterTap scaling utilities work. Because the constraint is named eq_flow_mol_phase_comp, it is automatically scaled using the scaling factor for flow_mol_phase_comp. However, as originally written, it ought to be scaled instead like flow_mass_phase_comp. This slight modification of the constraint is easier than messing with that workflow.


def calculate_scaling_factors(self):
if iscale.get_scaling_factor(self.area) is None:
iscale.set_scaling_factor(self.area, 100)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do the old scaling tools really need additional warnings to spam the user's logs?

In the new scaling tools, I'm going to raise an exception instead, so they can no longer ignore it.


initializer = BlockTriangularizationInitializer()
initializer = BlockTriangularizationInitializer(
calculate_variable_options={"eps": 2e-8}, skip_final_solve=True
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is the tolerance used to check whether the constraint residual is small enough to consider the constraint "satisfied". When solving the flowsheet, I encountered a problem where it couldn't reduce the residual of some enthalpy constraint (i.e., a constraint that needs a scaling factor <<1) to less than 1e-8. Due to Pyomo/pyomo#3785 , it does not take the constraint's scaling factor into account when calculating this residual. Using a constraint_scaling_transformation would allow scaling to be taken into account, but we wanted to move away from that in the new scaling tools.

Comment on lines +1410 to +1411
b.flow_mass_phase_comp[p, j] / b.params.mw_comp[j]
== b.flow_mol_phase_comp[p, j]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is changed due to how the WaterTap scaling utilities work. Because the constraint is named eq_flow_mol_phase_comp, it is automatically scaled using the scaling factor for flow_mol_phase_comp. However, as originally written, it ought to be scaled instead like flow_mass_phase_comp. This slight modification of the constraint is easier than messing with that workflow.

Comment on lines 1105 to 1152
] == pytest.approx(0.01866, rel=1e-3)
] == pytest.approx(0.1, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Ca_2+"]
] == pytest.approx(104.7, rel=1e-3)
] == pytest.approx(1e3, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "SO4_2-"]
] == pytest.approx(44.94, rel=1e-3)
] == pytest.approx(1e2, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Mg_2+"]
] == pytest.approx(1.722, rel=1e-3)
] == pytest.approx(10, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Na_+"]
] == pytest.approx(2.068, rel=1e-3)
] == pytest.approx(10, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.material_balances[0.0, "Cl_-"]
] == pytest.approx(0.6174, rel=1e-3)
] == pytest.approx(1, rel=1e-3)

assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "H2O"]
] == pytest.approx(0.0233193, rel=1e-5)
] == pytest.approx(0.1, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Ca_2+"]
] == pytest.approx(0.004, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "SO4_2-"]
] == pytest.approx(0.00960, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Mg_2+"]
] == pytest.approx(0.00240, rel=1e-5)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.separation_constraint[0.0, "Liq", "Na_+"]
] == pytest.approx(0.0023, rel=1e-3)

assert (
mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.permeate_electronegativity[0.0]
]
== 1
)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.retentate_pressure_balance[0.0]
] == pytest.approx(0.0000025, rel=1e-3)
] == pytest.approx(1e-5, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.retentate_temperature_equality[0.0]
] == pytest.approx(0.003354, rel=1e-3)
] == pytest.approx(1e-2, rel=1e-3)
assert mcas_case.fs.unit.scaling_factor[
mcas_case.fs.unit.permeate_temperature_equality[0.0]
] == pytest.approx(0.003354, rel=1e-3)
] == pytest.approx(1e-2, rel=1e-3)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These changes weren't a result of this PR, but instead #1695. They're due to a change in how the nominal value expression walker works. Previously it used 1/value(var) if var was fixed, but now it uses 1/scaling_factor[var] regardless of whether or not var is fixed.

@ksbeattie ksbeattie added the Priority:Normal Normal Priority Issue or PR label Dec 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority:Normal Normal Priority Issue or PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants