From ed3eb3a699f21c98d589650ac45006a74fbf8561 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 14:09:20 -0700 Subject: [PATCH 01/21] update imports of native_types and pyomo_constant_types (which is deprecated) --- idaes/core/util/scaling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/core/util/scaling.py b/idaes/core/util/scaling.py index d4bd8256aa..d3b8609c70 100644 --- a/idaes/core/util/scaling.py +++ b/idaes/core/util/scaling.py @@ -47,7 +47,7 @@ from pyomo.dae.flatten import slice_component_along_sets from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.core import expr as EXPR -from pyomo.core.expr.numvalue import native_types, pyomo_constant_types +from pyomo.common.numeric_types import native_types, pyomo_constant_types from pyomo.core.base.units_container import _PyomoUnit import idaes.logger as idaeslog From 842650be8caf097ac480d994d8e305e5ec4a5738 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 8 Mar 2024 16:05:53 -0500 Subject: [PATCH 02/21] Adding next batch of diagnostics tests --- idaes/models/unit_models/tests/test_cstr.py | 27 ++++---- .../tests/test_equilibrium_reactor.py | 27 ++++---- idaes/models/unit_models/tests/test_feed.py | 29 ++++----- .../unit_models/tests/test_feed_flash.py | 56 ++++++++++++----- idaes/models/unit_models/tests/test_flash.py | 61 +++++++++++++------ 5 files changed, 125 insertions(+), 75 deletions(-) diff --git a/idaes/models/unit_models/tests/test_cstr.py b/idaes/models/unit_models/tests/test_cstr.py index ed1f8b636e..0701c68e32 100644 --- a/idaes/models/unit_models/tests/test_cstr.py +++ b/idaes/models/unit_models/tests/test_cstr.py @@ -18,7 +18,6 @@ import pytest from pyomo.environ import check_optimal_termination, ConcreteModel, units, value -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, @@ -34,7 +33,6 @@ SaponificationReactionParameterBlock, ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -50,6 +48,8 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox + # ----------------------------------------------------------------------------- # Get default solver for testing @@ -109,8 +109,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -149,15 +149,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - assert_units_equivalent(sapon.fs.unit.volume[0], units.m**3) - assert_units_equivalent(sapon.fs.unit.heat_duty[0], units.W) - assert_units_equivalent(sapon.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -248,6 +242,13 @@ def test_conservation(self, sapon): <= 1e-3 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): diff --git a/idaes/models/unit_models/tests/test_equilibrium_reactor.py b/idaes/models/unit_models/tests/test_equilibrium_reactor.py index dd1b07047d..8dc8c9c199 100644 --- a/idaes/models/unit_models/tests/test_equilibrium_reactor.py +++ b/idaes/models/unit_models/tests/test_equilibrium_reactor.py @@ -33,7 +33,6 @@ SaponificationReactionParameterBlock, ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -44,14 +43,12 @@ initialization_tester, ) from idaes.core.solvers import get_solver -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent -from idaes.core.util.exceptions import InitializationError - from idaes.core.initialization import ( BlockTriangularizationInitializer, SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- # Get default solver for testing @@ -116,8 +113,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -153,14 +150,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - assert_units_equivalent(sapon.fs.unit.heat_duty[0], units.W) - assert_units_equivalent(sapon.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -241,6 +233,13 @@ def test_conservation(self, sapon): <= 1e-1 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): diff --git a/idaes/models/unit_models/tests/test_feed.py b/idaes/models/unit_models/tests/test_feed.py index 262adad514..f299334bb3 100644 --- a/idaes/models/unit_models/tests/test_feed.py +++ b/idaes/models/unit_models/tests/test_feed.py @@ -18,7 +18,6 @@ import pytest from pyomo.environ import ConcreteModel, value, units as pyunits -from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock from idaes.models.unit_models.feed import Feed, FeedInitializer @@ -27,7 +26,6 @@ ) from idaes.models.properties import iapws95 from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -39,6 +37,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -81,8 +80,8 @@ def sapon(self): m.fs.unit.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.temperature.fix(303.15) m.fs.unit.pressure.fix(101325.0) @@ -110,12 +109,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 8 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -195,6 +191,8 @@ def test_solution(self, sapon): sapon.fs.unit.outlet.conc_mol_comp[0, "SodiumAcetate"] ) + # No numerical test as there are no equations to solve + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -235,12 +233,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 3 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -302,6 +297,8 @@ def test_solution(self, iapws): iapws.fs.unit.properties[0].phase_frac["Liq"] ) + # No numerical test as there are no equations to solve + class TestInitializers: @pytest.fixture diff --git a/idaes/models/unit_models/tests/test_feed_flash.py b/idaes/models/unit_models/tests/test_feed_flash.py index 56e8ce6a12..f521b2002c 100644 --- a/idaes/models/unit_models/tests/test_feed_flash.py +++ b/idaes/models/unit_models/tests/test_feed_flash.py @@ -23,7 +23,6 @@ value, units as pyunits, ) -from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock, MaterialBalanceType from idaes.models.unit_models.feed_flash import FeedFlash, FlashType @@ -32,7 +31,6 @@ BTXParameterBlock, ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -45,6 +43,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -93,6 +92,27 @@ def btx(self): m.fs.unit.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.mole_frac_comp[0, "toluene"].fix(0.5) + # Legacy property package, does not bound many variables which triggers + # warnings for potential evaluation errors. + # Fixing property package is out of scope for now. + m.fs.unit.control_volume.properties_in[0.0].temperature_bubble.setlb(300) + m.fs.unit.control_volume.properties_in[0.0].temperature_bubble.setub(550) + m.fs.unit.control_volume.properties_in[0.0].temperature_dew.setlb(300) + m.fs.unit.control_volume.properties_in[0.0].temperature_dew.setub(550) + m.fs.unit.control_volume.properties_in[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.control_volume.properties_in[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.control_volume.properties_in[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.control_volume.properties_in[0.0].pressure_sat_comp.setub(5e6) + + m.fs.unit.control_volume.properties_out[0.0].temperature_bubble.setlb(300) + m.fs.unit.control_volume.properties_out[0.0].temperature_bubble.setub(550) + m.fs.unit.control_volume.properties_out[0.0].temperature_dew.setlb(300) + m.fs.unit.control_volume.properties_out[0.0].temperature_dew.setub(550) + m.fs.unit.control_volume.properties_out[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.control_volume.properties_out[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.control_volume.properties_out[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.control_volume.properties_out[0.0].pressure_sat_comp.setub(5e6) + return m @pytest.mark.build @@ -117,12 +137,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -187,6 +204,13 @@ def test_solution(self, btx): btx.fs.unit.control_volume.properties_out[0].flow_mol_phase["Vap"] ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -230,12 +254,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 0 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -302,6 +323,13 @@ def test_solution(self, iapws): iapws.fs.unit.control_volume.properties_out[0].phase_frac["Liq"] ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + class TestInitializersIAWPS: @pytest.fixture diff --git a/idaes/models/unit_models/tests/test_flash.py b/idaes/models/unit_models/tests/test_flash.py index 66abf5ad05..d78715db78 100644 --- a/idaes/models/unit_models/tests/test_flash.py +++ b/idaes/models/unit_models/tests/test_flash.py @@ -23,7 +23,6 @@ units, units as pyunits, ) -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, @@ -37,7 +36,6 @@ ) from idaes.models.properties import iapws95 from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -50,6 +48,7 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -114,6 +113,27 @@ def btx(self): m.fs.unit.heat_duty.fix(0) m.fs.unit.deltaP.fix(0) + # Legacy property package, does not bound many variables which triggers + # warnings for potential evaluation errors. + # Fixing property package is out of scope for now. + m.fs.unit.control_volume.properties_in[0.0].temperature_bubble.setlb(300) + m.fs.unit.control_volume.properties_in[0.0].temperature_bubble.setub(550) + m.fs.unit.control_volume.properties_in[0.0].temperature_dew.setlb(300) + m.fs.unit.control_volume.properties_in[0.0].temperature_dew.setub(550) + m.fs.unit.control_volume.properties_in[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.control_volume.properties_in[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.control_volume.properties_in[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.control_volume.properties_in[0.0].pressure_sat_comp.setub(5e6) + + m.fs.unit.control_volume.properties_out[0.0].temperature_bubble.setlb(300) + m.fs.unit.control_volume.properties_out[0.0].temperature_bubble.setub(550) + m.fs.unit.control_volume.properties_out[0.0].temperature_dew.setlb(300) + m.fs.unit.control_volume.properties_out[0.0].temperature_dew.setub(550) + m.fs.unit.control_volume.properties_out[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.control_volume.properties_out[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.control_volume.properties_out[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.control_volume.properties_out[0.0].pressure_sat_comp.setub(5e6) + return m @pytest.mark.build @@ -147,14 +167,10 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - assert_units_equivalent(btx.fs.unit.heat_duty[0], units.W) - assert_units_equivalent(btx.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.display_potential_evaluation_errors() + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -272,6 +288,13 @@ def test_conservation(self, btx): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -330,14 +353,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 0 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - assert_units_equivalent(iapws.fs.unit.heat_duty[0], units.W) - assert_units_equivalent(iapws.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -475,6 +493,13 @@ def test_conservation(self, iapws): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + class TestInitializersIAPWS: @pytest.fixture From 6d09b5c527024670565f2e194c6b925c6d7da56b Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 10 Mar 2024 19:33:40 -0400 Subject: [PATCH 03/21] Next batch of daignsotics tests --- .../examples/saponification_thermo.py | 4 +- idaes/models/unit_models/tests/test_flash.py | 1 - idaes/models/unit_models/tests/test_heater.py | 107 ++++++++++------- idaes/models/unit_models/tests/test_mixer.py | 59 ++++++---- idaes/models/unit_models/tests/test_pfr.py | 28 ++--- .../tests/test_pressure_changer.py | 61 +++++----- .../models/unit_models/tests/test_product.py | 45 ++++--- idaes/models/unit_models/tests/test_rstoic.py | 29 ++--- .../unit_models/tests/test_separator.py | 111 ++++++++++++++---- 9 files changed, 271 insertions(+), 174 deletions(-) diff --git a/idaes/models/properties/examples/saponification_thermo.py b/idaes/models/properties/examples/saponification_thermo.py index 05bc13d8d1..3436e67c76 100644 --- a/idaes/models/properties/examples/saponification_thermo.py +++ b/idaes/models/properties/examples/saponification_thermo.py @@ -83,7 +83,7 @@ def build(self): # Heat capacity of water self.cp_mol = Param( - mutable=False, + mutable=True, initialize=75.327, doc="Molar heat capacity of water [J/mol.K]", units=units.J / units.mol / units.K, @@ -91,7 +91,7 @@ def build(self): # Density of water self.dens_mol = Param( - mutable=False, + mutable=True, initialize=55388.0, doc="Molar density of water [mol/m^3]", units=units.mol / units.m**3, diff --git a/idaes/models/unit_models/tests/test_flash.py b/idaes/models/unit_models/tests/test_flash.py index d78715db78..7e4b459939 100644 --- a/idaes/models/unit_models/tests/test_flash.py +++ b/idaes/models/unit_models/tests/test_flash.py @@ -169,7 +169,6 @@ def test_build(self, btx): @pytest.mark.component def test_structural_issues(self, btx): dt = DiagnosticsToolbox(btx) - dt.display_potential_evaluation_errors() dt.assert_no_structural_warnings() @pytest.mark.ui diff --git a/idaes/models/unit_models/tests/test_heater.py b/idaes/models/unit_models/tests/test_heater.py index 1f8e0c7a31..7d06e61c39 100644 --- a/idaes/models/unit_models/tests/test_heater.py +++ b/idaes/models/unit_models/tests/test_heater.py @@ -59,6 +59,7 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- # Get default solver for testing @@ -133,16 +134,10 @@ def test_build(self, btx): assert number_total_constraints(btx) == 17 assert number_unused_variables(btx) == 0 - @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.control_volume.heat, pyunits.J / pyunits.s) - assert_units_equivalent(btx.fs.unit.heat_duty[0], pyunits.J / pyunits.s) - assert_units_equivalent(btx.fs.unit.deltaP[0], pyunits.Pa) - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + @pytest.mark.component + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -191,6 +186,13 @@ def test_conservation(self, btx): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btx): @@ -240,18 +242,10 @@ def test_build(self, iapws): assert number_total_constraints(iapws) == 3 assert number_unused_variables(iapws) == 0 - @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent( - iapws.fs.unit.control_volume.heat, pyunits.J / pyunits.s - ) - assert_units_equivalent(iapws.fs.unit.heat_duty[0], pyunits.J / pyunits.s) - assert_units_equivalent(iapws.fs.unit.deltaP[0], pyunits.Pa) - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + @pytest.mark.component + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -303,6 +297,13 @@ def test_conservation(self, iapws): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, iapws): @@ -371,8 +372,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.heat_duty.fix(1000) @@ -399,17 +400,10 @@ def test_build(self, sapon): assert number_total_constraints(sapon) == 8 assert number_unused_variables(sapon) == 0 - @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent( - sapon.fs.unit.control_volume.heat, pyunits.J / pyunits.s - ) - assert_units_equivalent(sapon.fs.unit.heat_duty[0], pyunits.J / pyunits.s) - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + @pytest.mark.component + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -432,11 +426,21 @@ def test_solve(self, sapon): def test_solution(self, sapon): assert pytest.approx(1e-3, abs=1e-6) == value(sapon.fs.unit.outlet.flow_vol[0]) - assert 55388.0 == value(sapon.fs.unit.inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value(sapon.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"]) - assert 0.0 == value(sapon.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"]) - assert 0.0 == value(sapon.fs.unit.inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"] + ) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"] + ) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.inlet.conc_mol_comp[0, "Ethanol"] + ) assert pytest.approx(320.2, abs=1e-1) == value( sapon.fs.unit.outlet.temperature[0] @@ -473,6 +477,13 @@ def test_conservation(self, sapon): <= 1e-3 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): @@ -529,6 +540,14 @@ def test_build(self, btg): # Unused vars are density parameters assert number_unused_variables(btg) == 10 + # TODO: Modular properties results in many potential evaluation errors + # TODO: Comment out until fixed + # @pytest.mark.component + # def test_structural_issues(self, btg): + # dt = DiagnosticsToolbox(btg) + # dt.assert_no_structural_warnings() + + # TODO: Remove once diagnostics issues fixed @pytest.mark.integration def test_units(self, btg): assert_units_equivalent(btg.fs.unit.control_volume.heat, pyunits.J / pyunits.s) @@ -536,6 +555,7 @@ def test_units(self, btg): assert_units_equivalent(btg.fs.unit.deltaP[0], pyunits.Pa) assert_units_consistent(btg) + # TODO: Remove once diagnostics issues fixed @pytest.mark.unit def test_dof(self, btg): assert degrees_of_freedom(btg) == 0 @@ -587,6 +607,13 @@ def test_conservation(self, btg): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btg): + dt = DiagnosticsToolbox(btg) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btg): diff --git a/idaes/models/unit_models/tests/test_mixer.py b/idaes/models/unit_models/tests/test_mixer.py index c3e25e81d8..393a14eb25 100644 --- a/idaes/models/unit_models/tests/test_mixer.py +++ b/idaes/models/unit_models/tests/test_mixer.py @@ -89,9 +89,10 @@ from idaes.core.util.initialization import ( fix_state_vars, ) +from idaes.core.util import DiagnosticsToolbox -# TODO: Should have a test for this that does not requrie models_extra +# TODO: Should have a test for this that does not require models_extra from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop import idaes.core.util.scaling as iscale from idaes.core.solvers import get_solver @@ -833,12 +834,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -935,6 +933,13 @@ def test_conservation(self, btx): ) ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- # Tests for Mixer in cases where properties do not support pressure @@ -1128,12 +1133,9 @@ def test_build(self, iapws): assert hasattr(iapws.fs.unit.outlet, "pressure") @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1233,6 +1235,13 @@ def test_conservation(self, iapws): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestSaponification(object): @@ -1251,8 +1260,8 @@ def sapon(self): m.fs.unit.inlet_1.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet_1.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet_1.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet_1.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet_1.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet_1.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet_1.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet_2.flow_vol[0].fix(1e-3) m.fs.unit.inlet_2.temperature[0].fix(300) @@ -1260,8 +1269,8 @@ def sapon(self): m.fs.unit.inlet_2.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet_2.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet_2.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet_2.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet_2.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet_2.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet_2.conc_mol_comp[0, "Ethanol"].fix(1e-8) return m @@ -1291,12 +1300,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1452,6 +1458,13 @@ def test_conservation(self, sapon): <= 1e-3 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @pytest.mark.component diff --git a/idaes/models/unit_models/tests/test_pfr.py b/idaes/models/unit_models/tests/test_pfr.py index 631471770e..b639ba0650 100644 --- a/idaes/models/unit_models/tests/test_pfr.py +++ b/idaes/models/unit_models/tests/test_pfr.py @@ -18,7 +18,6 @@ import pytest from pyomo.environ import check_optimal_termination, ConcreteModel, value, Var, units -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, MaterialBalanceType, @@ -34,7 +33,6 @@ SaponificationReactionParameterBlock, ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -51,6 +49,7 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -115,8 +114,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -160,17 +159,9 @@ def test_build(self, sapon): assert number_derivative_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - assert_units_equivalent(sapon.fs.unit.volume, units.m**3) - assert_units_equivalent(sapon.fs.unit.length, units.m) - assert_units_equivalent(sapon.fs.unit.area, units.m**2) - assert_units_equivalent(sapon.fs.unit.heat_duty[0, 0], units.W / units.m) - assert_units_equivalent(sapon.fs.unit.deltaP[0, 0], units.Pa / units.m) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -267,6 +258,13 @@ def test_conservation(self, sapon): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): diff --git a/idaes/models/unit_models/tests/test_pressure_changer.py b/idaes/models/unit_models/tests/test_pressure_changer.py index ad2ad665ab..0d2ab161d6 100644 --- a/idaes/models/unit_models/tests/test_pressure_changer.py +++ b/idaes/models/unit_models/tests/test_pressure_changer.py @@ -25,7 +25,7 @@ value, Var, ) -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent +from pyomo.util.check_units import assert_units_consistent from pyomo.core.expr.calculus.derivatives import differentiate from idaes.core import ( @@ -68,6 +68,7 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -278,14 +279,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - assert_units_equivalent(btx.fs.unit.work_mechanical[0], units.W) - assert_units_equivalent(btx.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -337,6 +333,13 @@ def test_conservation(self, btx): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btx): @@ -427,14 +430,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 0 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - assert_units_equivalent(iapws.fs.unit.work_mechanical[0], units.W) - assert_units_equivalent(iapws.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -569,6 +567,13 @@ def test_verify(self, iapws_turb): assert value(prop_out.temperature) == Tout assert value(prop_out.vapor_frac) == xout + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, iapws): @@ -612,8 +617,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.deltaP.fix(-20000) m.fs.unit.efficiency_pump.fix(0.9) @@ -651,14 +656,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - assert_units_equivalent(sapon.fs.unit.work_mechanical[0], units.W) - assert_units_equivalent(sapon.fs.unit.deltaP[0], units.Pa) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -729,6 +729,13 @@ def test_conservation(self, sapon): <= 1e-4 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): diff --git a/idaes/models/unit_models/tests/test_product.py b/idaes/models/unit_models/tests/test_product.py index 9d7707c0dc..72d23aaa2b 100644 --- a/idaes/models/unit_models/tests/test_product.py +++ b/idaes/models/unit_models/tests/test_product.py @@ -24,7 +24,6 @@ value, units as pyunits, ) -from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock from idaes.models.unit_models.product import Product, ProductInitializer @@ -38,7 +37,6 @@ ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -49,6 +47,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -91,8 +90,8 @@ def sapon(self): m.fs.unit.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.temperature.fix(303.15) m.fs.unit.pressure.fix(101325.0) @@ -120,12 +119,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 8 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -184,7 +180,7 @@ def test_get_stream_table_contents(self, sapon): def test_initialize(self, sapon): initialization_tester(sapon) - # No solve tests, as Product block has nothing to solve + # No solve or numerical tests, as Product block has nothing to solve # ----------------------------------------------------------------------------- @@ -226,12 +222,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 2 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -299,6 +292,13 @@ def test_solution(self, btx): btx.fs.unit.properties[0].mole_frac_phase_comp["Liq", "toluene"] ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -336,12 +336,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 3 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -384,7 +381,7 @@ def test_get_stream_table_contents(self, iapws): def test_initialize(self, iapws): initialization_tester(iapws) - # No solve as there is nothing to solve for + # No solve or numerical tests as there is nothing to solve for class TestInitializers: diff --git a/idaes/models/unit_models/tests/test_rstoic.py b/idaes/models/unit_models/tests/test_rstoic.py index 0a18e232fe..3663a294de 100644 --- a/idaes/models/unit_models/tests/test_rstoic.py +++ b/idaes/models/unit_models/tests/test_rstoic.py @@ -18,7 +18,6 @@ import pytest from pyomo.environ import check_optimal_termination, ConcreteModel, value, units -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, @@ -38,7 +37,6 @@ from idaes.core.solvers import get_solver from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -53,6 +51,7 @@ SingleControlVolumeUnitInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -110,8 +109,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -147,18 +146,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - assert_units_equivalent(sapon.fs.unit.heat_duty[0], units.W) - assert_units_equivalent(sapon.fs.unit.deltaP[0], units.Pa) - assert_units_equivalent( - sapon.fs.unit.rate_reaction_extent[0, "R1"], units.mol / units.s - ) - - @pytest.mark.unit - def test_dof(self, sapon): - - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -249,6 +239,13 @@ def test_conservation(self, sapon): <= 1e-3 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, sapon): diff --git a/idaes/models/unit_models/tests/test_separator.py b/idaes/models/unit_models/tests/test_separator.py index 9ae945dc2d..d1defdb133 100644 --- a/idaes/models/unit_models/tests/test_separator.py +++ b/idaes/models/unit_models/tests/test_separator.py @@ -86,6 +86,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -803,8 +804,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -853,12 +854,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 0 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1111,6 +1109,13 @@ def test_conservation(self, sapon): <= 1e-3 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestBTXIdeal(object): @@ -1140,6 +1145,36 @@ def btx(self): m.fs.unit.split_fraction[0, "outlet_1", "Vap"].fix(0.8) m.fs.unit.split_fraction[0, "outlet_2", "Liq"].fix(0.8) + # Legacy property package, does not bound many variables which triggers + # warnings for potential evaluation errors. + # Fixing property package is out of scope for now. + m.fs.unit.mixed_state[0.0].temperature_bubble.setlb(300) + m.fs.unit.mixed_state[0.0].temperature_bubble.setub(550) + m.fs.unit.mixed_state[0.0].temperature_dew.setlb(300) + m.fs.unit.mixed_state[0.0].temperature_dew.setub(550) + m.fs.unit.mixed_state[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.mixed_state[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.mixed_state[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.mixed_state[0.0].pressure_sat_comp.setub(5e6) + + m.fs.unit.outlet_1_state[0.0].temperature_bubble.setlb(300) + m.fs.unit.outlet_1_state[0.0].temperature_bubble.setub(550) + m.fs.unit.outlet_1_state[0.0].temperature_dew.setlb(300) + m.fs.unit.outlet_1_state[0.0].temperature_dew.setub(550) + m.fs.unit.outlet_1_state[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.outlet_1_state[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.outlet_1_state[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.outlet_1_state[0.0].pressure_sat_comp.setub(5e6) + + m.fs.unit.outlet_2_state[0.0].temperature_bubble.setlb(300) + m.fs.unit.outlet_2_state[0.0].temperature_bubble.setub(550) + m.fs.unit.outlet_2_state[0.0].temperature_dew.setlb(300) + m.fs.unit.outlet_2_state[0.0].temperature_dew.setub(550) + m.fs.unit.outlet_2_state[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.outlet_2_state[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.outlet_2_state[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.outlet_2_state[0.0].pressure_sat_comp.setub(5e6) + return m @pytest.mark.build @@ -1173,12 +1208,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1383,6 +1415,13 @@ def test_conservation(self, btx): <= 1e-1 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -1446,12 +1485,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 0 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1592,6 +1628,13 @@ def test_conservation(self, iapws): <= 1e-2 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- # Define some generic Property Block classes for testing ideal separations @@ -2992,6 +3035,18 @@ def btx(self): m.fs.unit.inlet.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.inlet.mole_frac_comp[0, "toluene"].fix(0.5) + # Legacy property package, does not bound many variables which triggers + # warnings for potential evaluation errors. + # Fixing property package is out of scope for now. + m.fs.unit.mixed_state[0.0].temperature_bubble.setlb(300) + m.fs.unit.mixed_state[0.0].temperature_bubble.setub(550) + m.fs.unit.mixed_state[0.0].temperature_dew.setlb(300) + m.fs.unit.mixed_state[0.0].temperature_dew.setub(550) + m.fs.unit.mixed_state[0.0]._temperature_equilibrium.setlb(300) + m.fs.unit.mixed_state[0.0]._temperature_equilibrium.setub(550) + m.fs.unit.mixed_state[0.0].pressure_sat_comp.setlb(1e4) + m.fs.unit.mixed_state[0.0].pressure_sat_comp.setub(5e6) + return m @pytest.mark.build @@ -3023,12 +3078,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 0 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -3197,6 +3249,13 @@ def test_conservation(self, btx): # Assume energy conservation is covered by control volume tests + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.unit def test_initialization_error(): From 01e010c59809634f82e0f993475b6a3d155db0a9 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 14 Mar 2024 11:12:01 -0400 Subject: [PATCH 04/21] Work around for ASL issue --- idaes/core/util/model_diagnostics.py | 63 +++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 49907f64e3..b2fb42700f 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -18,6 +18,7 @@ __author__ = "Alexander Dowling, Douglas Allan, Andrew Lee, Robby Parker, Ben Knueven" from operator import itemgetter +import os import sys from inspect import signature from math import log, isclose, inf, isfinite @@ -76,6 +77,7 @@ from pyomo.common.deprecation import deprecation_warning from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.common.fileutils import find_library from idaes.core.util.model_statistics import ( activated_blocks_set, @@ -439,6 +441,24 @@ def __init__(self, model: _BlockData, **kwargs): self._model = model self.config = CONFIG(kwargs) + # There appears to be a bug in the ASL which causes terminal failures + # if you try to create multiple ASL structs with different external + # functions in the same process. This causes pytest to crash during testing. + # To avoid this, register all known external functions before we call + # PyNumero. + ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] + library_set = set() + libraries = [] + + for f in ext_funcs: + library = find_library(f) + if library not in library_set: + library_set.add(library) + libraries.append(library) + + lib_str = "\n".join(libraries) + os.environ["AMPLFUNC"] = lib_str + @property def model(self): """ @@ -997,16 +1017,25 @@ def display_near_parallel_variables(self, stream=None): # TODO: Block triangularization analysis # Number and size of blocks, polynomial degree of 1x1 blocks, simple pivot check of moderate sized sub-blocks? - def _collect_structural_warnings(self): + def _collect_structural_warnings( + self, ignore_evaluation_errors=False, ignore_unit_consistency=False + ): """ Runs checks for structural warnings and returns two lists. + Args: + ignore_evaluation_errors - ignore potential evaluation error warnings + ignore_unit_consistency - ignore unit consistency warnings + Returns: warnings - list of warning messages from structural analysis next_steps - list of suggested next steps to further investigate warnings """ - uc = identify_inconsistent_units(self._model) + if not ignore_unit_consistency: + uc = identify_inconsistent_units(self._model) + else: + uc = [] uc_var, uc_con, oc_var, oc_con = self.get_dulmage_mendelsohn_partition() # Collect warnings @@ -1040,12 +1069,15 @@ def _collect_structural_warnings(self): if any(len(x) > 0 for x in [oc_var, oc_con]): next_steps.append(self.display_overconstrained_set.__name__ + "()") - eval_warnings = self._collect_potential_eval_errors() - if len(eval_warnings) > 0: - warnings.append( - f"WARNING: Found {len(eval_warnings)} potential evaluation errors." - ) - next_steps.append(self.display_potential_evaluation_errors.__name__ + "()") + if not ignore_evaluation_errors: + eval_warnings = self._collect_potential_eval_errors() + if len(eval_warnings) > 0: + warnings.append( + f"WARNING: Found {len(eval_warnings)} potential evaluation errors." + ) + next_steps.append( + self.display_potential_evaluation_errors.__name__ + "()" + ) return warnings, next_steps @@ -1289,16 +1321,27 @@ def _collect_numerical_cautions(self, jac=None, nlp=None): return cautions - def assert_no_structural_warnings(self): + def assert_no_structural_warnings( + self, + ignore_evaluation_errors: bool = False, + ignore_unit_consistency: bool = False, + ): """ Checks for structural warnings in the model and raises an AssertionError if any are found. + Args: + ignore_evaluation_errors - ignore potential evaluation error warnings + ignore_unit_consistency - ignore unit consistency warnings + Raises: AssertionError if any warnings are identified by structural analysis. """ - warnings, _ = self._collect_structural_warnings() + warnings, _ = self._collect_structural_warnings( + ignore_evaluation_errors=ignore_evaluation_errors, + ignore_unit_consistency=ignore_unit_consistency, + ) if len(warnings) > 0: raise AssertionError(f"Structural issues found ({len(warnings)}).") From cc9c240be100fd6b3921cd2d5ebb448964d2a427 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 14 Mar 2024 11:12:31 -0400 Subject: [PATCH 05/21] Adding more diagnostics checks --- idaes/models/unit_models/heat_exchanger.py | 4 +- .../unit_models/tests/test_heat_exchanger.py | 179 ++++++------- .../tests/test_heat_exchanger_1D.py | 238 +++++++++--------- .../tests/test_heat_exchanger_lc.py | 34 +-- idaes/models/unit_models/tests/test_heater.py | 25 +- .../unit_models/tests/test_statejunction.py | 45 ++-- .../unit_models/tests/test_translator.py | 1 - idaes/models/unit_models/tests/test_valve.py | 18 +- 8 files changed, 270 insertions(+), 274 deletions(-) diff --git a/idaes/models/unit_models/heat_exchanger.py b/idaes/models/unit_models/heat_exchanger.py index b9ba8ba33f..dd0535c3d7 100644 --- a/idaes/models/unit_models/heat_exchanger.py +++ b/idaes/models/unit_models/heat_exchanger.py @@ -814,11 +814,11 @@ def _get_performance_contents(self, time_point=0): var_dict["Heat Duty"] = self.heat_duty[time_point] if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: var_dict["Crossflow Factor"] = self.crossflow_factor[time_point] + var_dict["Delta T In"] = self.delta_temperature_in[time_point] + var_dict["Delta T Out"] = self.delta_temperature_out[time_point] expr_dict = {} expr_dict["Delta T Driving"] = self.delta_temperature[time_point] - expr_dict["Delta T In"] = self.delta_temperature_in[time_point] - expr_dict["Delta T Out"] = self.delta_temperature_out[time_point] return {"vars": var_dict, "exprs": expr_dict} diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index 679d564414..62801e33b8 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -71,6 +71,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # Imports to assemble BT-PR with different units @@ -501,6 +502,13 @@ def btx(self): m.fs.unit.area.fix(1) m.fs.unit.overall_heat_transfer_coefficient.fix(100) + # Bound temperature differences to avoid division by zero + m.fs.unit.delta_temperature_in[0.0].setlb(40) + m.fs.unit.delta_temperature_out[0.0].setlb(0.1) + + m.fs.unit.delta_temperature_in[0.0].setub(80) + m.fs.unit.delta_temperature_out[0.0].setub(35) + return m @pytest.mark.build @@ -548,21 +556,11 @@ def test_build(self, btx): assert number_total_constraints(btx) == 38 assert number_unused_variables(btx) == 0 - @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent( - btx.fs.unit.overall_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.delta_temperature_in, pyunits.K) - assert_units_equivalent(btx.fs.unit.delta_temperature_out, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + @pytest.mark.component + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.display_potential_evaluation_errors() + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -574,11 +572,11 @@ def test_get_performance_contents(self, btx): "HX Area": btx.fs.unit.area, "Heat Duty": btx.fs.unit.heat_duty[0], "HX Coefficient": btx.fs.unit.overall_heat_transfer_coefficient[0], + "Delta T In": btx.fs.unit.delta_temperature_in[0], + "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": btx.fs.unit.delta_temperature[0], - "Delta T In": btx.fs.unit.delta_temperature_in[0], - "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, } @@ -711,6 +709,13 @@ def test_conservation(self, btx): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestBTX_cocurrent_alt_name(object): @@ -744,6 +749,13 @@ def btx(self): m.fs.unit.area.fix(1) m.fs.unit.overall_heat_transfer_coefficient.fix(100) + # Bound temperature differences to avoid division by zero + m.fs.unit.delta_temperature_in[0.0].setlb(40) + m.fs.unit.delta_temperature_out[0.0].setlb(0.1) + + m.fs.unit.delta_temperature_in[0.0].setub(80) + m.fs.unit.delta_temperature_out[0.0].setub(35) + return m @pytest.mark.build @@ -791,21 +803,10 @@ def test_build(self, btx): assert number_total_constraints(btx) == 38 assert number_unused_variables(btx) == 0 - @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent( - btx.fs.unit.overall_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.delta_temperature_in, pyunits.K) - assert_units_equivalent(btx.fs.unit.delta_temperature_out, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + @pytest.mark.component + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -817,11 +818,11 @@ def test_get_performance_contents(self, btx): "HX Area": btx.fs.unit.area, "Heat Duty": btx.fs.unit.heat_duty[0], "HX Coefficient": btx.fs.unit.overall_heat_transfer_coefficient[0], + "Delta T In": btx.fs.unit.delta_temperature_in[0], + "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": btx.fs.unit.delta_temperature[0], - "Delta T In": btx.fs.unit.delta_temperature_in[0], - "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, } @@ -950,6 +951,13 @@ def test_conservation(self, btx): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -1047,21 +1055,11 @@ def test_build(self, iapws): assert number_total_constraints(iapws) == 10 assert number_unused_variables(iapws) == 0 - @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent( - iapws.fs.unit.overall_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(iapws.fs.unit.area, pyunits.m**2) - assert_units_equivalent(iapws.fs.unit.delta_temperature_in, pyunits.K) - assert_units_equivalent(iapws.fs.unit.delta_temperature_out, pyunits.K) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + @pytest.mark.component + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + # Delta T calculations cause potential evaluation errors that are hard to bound + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) @pytest.mark.unit def test_dof_alt_name1(self, iapws): @@ -1088,11 +1086,11 @@ def test_get_performance_contents(self, iapws): "HX Area": iapws.fs.unit.area, "Heat Duty": iapws.fs.unit.heat_duty[0], "HX Coefficient": iapws.fs.unit.overall_heat_transfer_coefficient[0], + "Delta T In": iapws.fs.unit.delta_temperature_in[0], + "Delta T Out": iapws.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": iapws.fs.unit.delta_temperature[0], - "Delta T In": iapws.fs.unit.delta_temperature_in[0], - "Delta T Out": iapws.fs.unit.delta_temperature_out[0], }, } @@ -1242,6 +1240,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestSaponification_crossflow(object): @@ -1264,8 +1269,8 @@ def sapon(self): m.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-12) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-12) m.fs.unit.cold_side_inlet.flow_vol[0].fix(1e-3) m.fs.unit.cold_side_inlet.temperature[0].fix(300) @@ -1273,8 +1278,8 @@ def sapon(self): m.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-12) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-12) m.fs.unit.area.fix(1000) m.fs.unit.overall_heat_transfer_coefficient.fix(100) @@ -1323,21 +1328,11 @@ def test_build(self, sapon): assert number_total_constraints(sapon) == 20 assert number_unused_variables(sapon) == 0 - @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent( - sapon.fs.unit.overall_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(sapon.fs.unit.area, pyunits.m**2) - assert_units_equivalent(sapon.fs.unit.delta_temperature_in, pyunits.K) - assert_units_equivalent(sapon.fs.unit.delta_temperature_out, pyunits.K) - - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + @pytest.mark.component + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + # Delta T calculations cause potential evaluation errors that are hard to bound + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) @pytest.mark.ui @pytest.mark.unit @@ -1350,11 +1345,11 @@ def test_get_performance_contents(self, sapon): "Heat Duty": sapon.fs.unit.heat_duty[0], "HX Coefficient": sapon.fs.unit.overall_heat_transfer_coefficient[0], "Crossflow Factor": sapon.fs.unit.crossflow_factor[0], + "Delta T In": sapon.fs.unit.delta_temperature_in[0], + "Delta T Out": sapon.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": sapon.fs.unit.delta_temperature[0], - "Delta T In": sapon.fs.unit.delta_temperature_in[0], - "Delta T Out": sapon.fs.unit.delta_temperature_out[0], }, } @@ -1528,6 +1523,15 @@ def test_conservation(self, sapon): ) assert abs(hot_side + cold_side) <= 1e0 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + # Heat transfer constraint has a a residual of ~1e-3 + # Model could be better scaled + dt = DiagnosticsToolbox(sapon, constraint_residual_tolerance=1e-2) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @@ -1726,21 +1730,11 @@ def test_build(self, btx): assert number_total_constraints(btx) == 118 assert number_unused_variables(btx) == 20 - @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent( - btx.fs.unit.overall_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.delta_temperature_in, pyunits.K) - assert_units_equivalent(btx.fs.unit.delta_temperature_out, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + @pytest.mark.component + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + # Delta T calculations cause potential evaluation errors that are hard to bound + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) @pytest.mark.ui @pytest.mark.unit @@ -1752,11 +1746,11 @@ def test_get_performance_contents(self, btx): "HX Area": btx.fs.unit.area, "Heat Duty": btx.fs.unit.heat_duty[0], "HX Coefficient": btx.fs.unit.overall_heat_transfer_coefficient[0], + "Delta T In": btx.fs.unit.delta_temperature_in[0], + "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": btx.fs.unit.delta_temperature[0], - "Delta T In": btx.fs.unit.delta_temperature_in[0], - "Delta T Out": btx.fs.unit.delta_temperature_out[0], }, } @@ -1895,6 +1889,13 @@ def test_conservation(self, btx): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.component def test_initialization_error(self, btx): btx.fs.unit.hot_side_outlet.flow_mol[0].fix(20) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py index eea2293e18..8c376eefc0 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py @@ -68,6 +68,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # Imports to assemble BT-PR with different units from idaes.core import LiquidPhase, VaporPhase, Component @@ -384,15 +385,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 10 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -529,6 +524,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestBTX_countercurrent(object): @@ -606,19 +608,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 10 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent( - btx.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -764,6 +756,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- def build_model(): @@ -846,19 +845,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 12 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.area, pyunits.m**2) - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent( - iapws.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -997,6 +986,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -1066,19 +1062,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 12 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.area, pyunits.m**2) - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent( - iapws.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1217,6 +1203,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- class TestSaponification_cocurrent(object): @@ -1243,8 +1236,8 @@ def sapon(self): m.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.cold_side_inlet.flow_vol[0].fix(1e-3) m.fs.unit.cold_side_inlet.temperature[0].fix(300) @@ -1252,8 +1245,8 @@ def sapon(self): m.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) return m @@ -1294,19 +1287,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 16 @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent(sapon.fs.unit.area, pyunits.m**2) - assert_units_equivalent(sapon.fs.unit.length, pyunits.m) - assert_units_equivalent( - sapon.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1409,25 +1392,37 @@ def test_solution(self, sapon): sapon.fs.unit.cold_side_outlet.flow_vol[0] ) - assert 55388.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"] + ) - assert 55388.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"] + ) assert pytest.approx(318.873, rel=1e-5) == value( sapon.fs.unit.hot_side_outlet.temperature[0] @@ -1466,6 +1461,13 @@ def test_conservation(self, sapon): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- class TestSaponification_countercurrent(object): @@ -1492,8 +1494,8 @@ def sapon(self): m.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.cold_side_inlet.flow_vol[0].fix(1e-3) m.fs.unit.cold_side_inlet.temperature[0].fix(300) @@ -1501,8 +1503,8 @@ def sapon(self): m.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) return m @@ -1543,19 +1545,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 16 @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent(sapon.fs.unit.area, pyunits.m**2) - assert_units_equivalent(sapon.fs.unit.length, pyunits.m) - assert_units_equivalent( - sapon.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1658,25 +1650,37 @@ def test_solution(self, sapon): sapon.fs.unit.cold_side_outlet.flow_vol[0] ) - assert 55388.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"] + ) - assert 55388.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"] + ) assert pytest.approx(318.869, rel=1e-5) == value( sapon.fs.unit.hot_side_outlet.temperature[0] @@ -1715,6 +1719,13 @@ def test_conservation(self, sapon): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @@ -1908,20 +1919,12 @@ def test_build(self, btx): assert number_unused_variables(btx) == 36 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent( - btx.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings( + ignore_evaluation_errors=True, ) - assert_units_consistent(btx) - - @pytest.mark.component - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 - @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btx): @@ -2061,6 +2064,13 @@ def test_conservation(self, btx): ) assert abs((hot_side - cold_side) / hot_side) <= 3e-4 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.integration + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.component def test_initialization_error(self, btx): btx.fs.unit.hot_side_outlet.flow_mol[0].fix(20) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py index a6c42b91b3..bb76ac7d16 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py @@ -31,9 +31,6 @@ from pyomo.common.config import ConfigBlock from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent -from pyomo.core.base.units_container import InconsistentUnitsError - -from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver from idaes.core.util.exceptions import DynamicError, ConfigurationError, IdaesError @@ -61,6 +58,7 @@ from idaes.core.initialization import ( InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # Get default solver for testing solver = get_solver() @@ -275,7 +273,7 @@ def unconstrained_model(self, dynamic_flowsheet_model): return m - @pytest.fixture() + @pytest.fixture def model(self, unconstrained_model): m = unconstrained_model m.discretizer = TransformationFactory("dae.finite_difference") @@ -297,12 +295,15 @@ def model(self, unconstrained_model): return m - @pytest.mark.unit - @pytest.mark.xfail(raises=InconsistentUnitsError) - def test_units(self, model): - # Note: using the discretizer makes the units of measure on the time - # derivative term inconsistent... - assert_units_consistent(model) + @pytest.mark.component + def test_structural_issues(self, model): + dt = DiagnosticsToolbox(model) + dt.display_potential_evaluation_errors() + # TODO: Evaluation errors due ot temperature differentials + # TODO: Skip unit consistency due to Pyomo DAE issue + dt.assert_no_structural_warnings( + ignore_evaluation_errors=True, ignore_unit_consistency=True + ) @pytest.mark.unit def test_units_unconstrained(self, unconstrained_model): @@ -346,10 +347,6 @@ def test_new_vars(self, model): assert isinstance(model.fs.unit.thermal_fouling_hot_side, Param) assert isinstance(model.fs.unit.thermal_fouling_cold_side, Param) - @pytest.mark.unit - def test_dof(self, model): - assert degrees_of_freedom(model) == 0 - @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -366,6 +363,11 @@ def test_solve(self, model): # Check for optimal solution assert check_optimal_termination(results) + # Combine with solve test due to how fixtures are set up + dt = DiagnosticsToolbox(model) + dt.display_constraints_with_large_residuals() + dt.assert_no_numerical_warnings() + @pytest.mark.unit def test_dynamic_heat_in_static_flowsheet(self): m = ConcreteModel() @@ -500,11 +502,11 @@ def test_get_performance_contents(self, model): "HX Area": model.fs.unit.area, "Heat Duty": model.fs.unit.heat_duty[0], "HX Coefficient": model.fs.unit.overall_heat_transfer_coefficient[0], + "Delta T In": model.fs.unit.delta_temperature_in[0], + "Delta T Out": model.fs.unit.delta_temperature_out[0], }, "exprs": { "Delta T Driving": model.fs.unit.delta_temperature[0], - "Delta T In": model.fs.unit.delta_temperature_in[0], - "Delta T Out": model.fs.unit.delta_temperature_out[0], }, } diff --git a/idaes/models/unit_models/tests/test_heater.py b/idaes/models/unit_models/tests/test_heater.py index 7d06e61c39..dbee5e970b 100644 --- a/idaes/models/unit_models/tests/test_heater.py +++ b/idaes/models/unit_models/tests/test_heater.py @@ -23,7 +23,6 @@ value, units as pyunits, ) -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent from idaes.core import ( FlowsheetBlock, @@ -43,7 +42,6 @@ from idaes.models.properties.modular_properties.examples.BT_PR import configuration from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -540,25 +538,10 @@ def test_build(self, btg): # Unused vars are density parameters assert number_unused_variables(btg) == 10 - # TODO: Modular properties results in many potential evaluation errors - # TODO: Comment out until fixed - # @pytest.mark.component - # def test_structural_issues(self, btg): - # dt = DiagnosticsToolbox(btg) - # dt.assert_no_structural_warnings() - - # TODO: Remove once diagnostics issues fixed - @pytest.mark.integration - def test_units(self, btg): - assert_units_equivalent(btg.fs.unit.control_volume.heat, pyunits.J / pyunits.s) - assert_units_equivalent(btg.fs.unit.heat_duty[0], pyunits.J / pyunits.s) - assert_units_equivalent(btg.fs.unit.deltaP[0], pyunits.Pa) - assert_units_consistent(btg) - - # TODO: Remove once diagnostics issues fixed - @pytest.mark.unit - def test_dof(self, btg): - assert degrees_of_freedom(btg) == 0 + @pytest.mark.component + def test_structural_issues(self, btg): + dt = DiagnosticsToolbox(btg) + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/models/unit_models/tests/test_statejunction.py b/idaes/models/unit_models/tests/test_statejunction.py index f96087eb74..43e5b920fc 100644 --- a/idaes/models/unit_models/tests/test_statejunction.py +++ b/idaes/models/unit_models/tests/test_statejunction.py @@ -18,7 +18,6 @@ import pytest from pyomo.environ import check_optimal_termination, ConcreteModel, value -from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock from idaes.models.unit_models.statejunction import ( @@ -35,7 +34,6 @@ ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables, @@ -46,6 +44,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -88,8 +87,8 @@ def sapon(self): m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.inlet.temperature.fix(303.15) m.fs.unit.inlet.pressure.fix(101325.0) @@ -120,12 +119,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 8 @pytest.mark.component - def test_units(self, sapon): - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -133,7 +129,7 @@ def test_dof(self, sapon): def test_initialize(self, sapon): initialization_tester(sapon) - # No solve, as problem has no constraints + # No solve or numerical tests, as problem has no constraints @pytest.mark.ui @pytest.mark.unit @@ -186,12 +182,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 2 @pytest.mark.component - def test_units(self, btx): - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -216,6 +209,13 @@ def test_solution(self, btx): assert pytest.approx(365, abs=1e-2) == value(btx.fs.unit.outlet.temperature[0]) assert pytest.approx(101325, abs=1e2) == value(btx.fs.unit.outlet.pressure[0]) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btx): @@ -264,12 +264,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 3 @pytest.mark.component - def test_units(self, iapws): - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -277,7 +274,7 @@ def test_dof(self, iapws): def test_initialize(self, iapws): initialization_tester(iapws) - # No solve, as problem has no constraints + # No solve or numerical tests, as problem has no constraints @pytest.mark.ui @pytest.mark.unit diff --git a/idaes/models/unit_models/tests/test_translator.py b/idaes/models/unit_models/tests/test_translator.py index 1508108e13..d906036ff4 100644 --- a/idaes/models/unit_models/tests/test_translator.py +++ b/idaes/models/unit_models/tests/test_translator.py @@ -40,7 +40,6 @@ from idaes.core.solvers import get_solver from idaes.core.initialization import ( BlockTriangularizationInitializer, - InitializationStatus, ) diff --git a/idaes/models/unit_models/tests/test_valve.py b/idaes/models/unit_models/tests/test_valve.py index f5d9c9ea42..cfa4cf2343 100644 --- a/idaes/models/unit_models/tests/test_valve.py +++ b/idaes/models/unit_models/tests/test_valve.py @@ -34,6 +34,7 @@ from idaes.models.properties import iapws95 import idaes.core.util.scaling as iscale from idaes.models.properties.general_helmholtz import helmholtz_available +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- # Get default solver for testing @@ -102,13 +103,9 @@ def test_build(self, valve_model): assert number_unused_variables(valve_model) == 0 @pytest.mark.component - def test_units(self, valve_model): - assert_units_consistent(valve_model) - assert_units_equivalent(valve_model.fs.valve.flow_var[0], units.mol / units.s) - - @pytest.mark.unit - def test_dof(self, valve_model): - assert degrees_of_freedom(valve_model) == 0 + def test_structural_issues(self, valve_model): + dt = DiagnosticsToolbox(valve_model) + dt.assert_no_structural_warnings() @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -134,6 +131,13 @@ def test_solution(self, valve_model): valve_model.fs.valve.outlet.flow_mol[0] ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, valve_model): + dt = DiagnosticsToolbox(valve_model) + dt.assert_no_numerical_warnings() + class TestLinearValve(GenericValve): type = ValveFunctionType.linear From 218934713d90fb0f3e8de2cc124faeedc3e7cd1e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 14 Mar 2024 11:36:59 -0400 Subject: [PATCH 06/21] Last unit model diagnostics tests --- idaes/models/unit_models/tests/test_hx_ntu.py | 25 ++- .../tests/test_shell_and_tube_1D.py | 161 ++++++------------ 2 files changed, 64 insertions(+), 122 deletions(-) diff --git a/idaes/models/unit_models/tests/test_hx_ntu.py b/idaes/models/unit_models/tests/test_hx_ntu.py index e382246652..86af622fa1 100644 --- a/idaes/models/unit_models/tests/test_hx_ntu.py +++ b/idaes/models/unit_models/tests/test_hx_ntu.py @@ -49,6 +49,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -281,20 +282,11 @@ def test_build(self, model): assert model.fs.unit.default_initializer is HXNTUInitializer @pytest.mark.component - def test_units(self, model): - assert_units_consistent(model) - - assert_units_equivalent(model.fs.unit.area, pyunits.m**2) - assert_units_equivalent( - model.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, + def test_structural_issues(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_structural_warnings( + ignore_evaluation_errors=True, ) - assert_units_equivalent(model.fs.unit.effectiveness[0], pyunits.dimensionless) - assert_units_equivalent(model.fs.unit.NTU[0], pyunits.dimensionless) - - @pytest.mark.unit - def test_dof(self, model): - assert degrees_of_freedom(model) == 0 @pytest.mark.ui @pytest.mark.unit @@ -505,6 +497,13 @@ def test_conservation(self, model): <= 1e-6 ) + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_numerical_warnings() + class TestInitializers(object): @pytest.fixture(scope="class") diff --git a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py index 65a44fa7a0..11c14c6dd1 100644 --- a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py +++ b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py @@ -62,6 +62,7 @@ from idaes.core.util.testing import PhysicalParameterTestBlock, initialization_tester from idaes.core.util import scaling as iscale from idaes.core.solvers import get_solver +from idaes.core.util import DiagnosticsToolbox # Imports to assemble BT-PR with different units from idaes.core import LiquidPhase, VaporPhase, Component @@ -414,28 +415,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 8 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent(btx.fs.unit.shell_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_inner_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_outer_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.number_of_tubes, pyunits.dimensionless) - - assert_units_equivalent( - btx.fs.unit.hot_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent( - btx.fs.unit.cold_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(btx.fs.unit.temperature_wall, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -575,6 +557,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestBTX_countercurrent(object): @@ -671,28 +660,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 8 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent(btx.fs.unit.shell_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_inner_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_outer_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.number_of_tubes, pyunits.dimensionless) - - assert_units_equivalent( - btx.fs.unit.hot_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent( - btx.fs.unit.cold_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(btx.fs.unit.temperature_wall, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -832,6 +802,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -916,28 +893,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 10 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent(iapws.fs.unit.shell_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.tube_inner_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.tube_outer_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.number_of_tubes, pyunits.dimensionless) - - assert_units_equivalent( - iapws.fs.unit.hot_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent( - iapws.fs.unit.cold_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(iapws.fs.unit.temperature_wall, pyunits.K) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1079,6 +1037,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -1163,28 +1128,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 10 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent(iapws.fs.unit.shell_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.tube_inner_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.tube_outer_diameter, pyunits.m) - assert_units_equivalent(iapws.fs.unit.number_of_tubes, pyunits.dimensionless) - - assert_units_equivalent( - iapws.fs.unit.hot_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent( - iapws.fs.unit.cold_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent(iapws.fs.unit.temperature_wall, pyunits.K) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1326,6 +1272,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @@ -1532,28 +1485,11 @@ def test_build(self, btx): assert number_unused_variables(btx) == 34 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent(btx.fs.unit.shell_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_inner_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.tube_outer_diameter, pyunits.m) - assert_units_equivalent(btx.fs.unit.number_of_tubes, pyunits.dimensionless) - - assert_units_equivalent( - btx.fs.unit.hot_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - assert_units_equivalent( - btx.fs.unit.cold_side_heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings( + ignore_evaluation_errors=True, ) - assert_units_equivalent(btx.fs.unit.temperature_wall, pyunits.K) - - assert_units_consistent(btx) - - @pytest.mark.component - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 @pytest.mark.ui @pytest.mark.unit @@ -1697,6 +1633,13 @@ def test_conservation(self, btx): ) assert abs((hot_side - cold_side) / hot_side) <= 3e-4 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.integration + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.component def test_initialization_error(self, btx): btx.fs.unit.hot_side_outlet.flow_mol[0].fix(20) From 8a9a7ee98cb88ed8028f9d9de041343a0a2735a8 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 14 Mar 2024 12:32:31 -0400 Subject: [PATCH 07/21] Fixing typo --- idaes/models/unit_models/tests/test_heat_exchanger_lc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py index bb76ac7d16..1b6997119e 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py @@ -299,7 +299,7 @@ def model(self, unconstrained_model): def test_structural_issues(self, model): dt = DiagnosticsToolbox(model) dt.display_potential_evaluation_errors() - # TODO: Evaluation errors due ot temperature differentials + # TODO: Evaluation errors due to temperature differentials # TODO: Skip unit consistency due to Pyomo DAE issue dt.assert_no_structural_warnings( ignore_evaluation_errors=True, ignore_unit_consistency=True From 1b915945e15a95d5e02ea10887a0b19782572553 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 15 Mar 2024 11:38:10 -0400 Subject: [PATCH 08/21] Improving fix for ASL issue --- idaes/core/util/model_diagnostics.py | 54 +++++++++++-------- idaes/core/util/scaling.py | 25 +++++++++ .../core/util/tests/test_model_diagnostics.py | 41 ++++++++++++++ 3 files changed, 98 insertions(+), 22 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index b2fb42700f..04f7fb3ba8 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -18,7 +18,6 @@ __author__ = "Alexander Dowling, Douglas Allan, Andrew Lee, Robby Parker, Ben Knueven" from operator import itemgetter -import os import sys from inspect import signature from math import log, isclose, inf, isfinite @@ -77,7 +76,6 @@ from pyomo.common.deprecation import deprecation_warning from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager -from pyomo.common.fileutils import find_library from idaes.core.util.model_statistics import ( activated_blocks_set, @@ -441,23 +439,27 @@ def __init__(self, model: _BlockData, **kwargs): self._model = model self.config = CONFIG(kwargs) - # There appears to be a bug in the ASL which causes terminal failures - # if you try to create multiple ASL structs with different external - # functions in the same process. This causes pytest to crash during testing. - # To avoid this, register all known external functions before we call - # PyNumero. - ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] - library_set = set() - libraries = [] - - for f in ext_funcs: - library = find_library(f) - if library not in library_set: - library_set.add(library) - libraries.append(library) - - lib_str = "\n".join(libraries) - os.environ["AMPLFUNC"] = lib_str + # # There appears to be a bug in the ASL which causes terminal failures + # # if you try to create multiple ASL structs with different external + # # functions in the same process. This causes pytest to crash during testing. + # # To avoid this, register all known external functions before we call + # # PyNumero. + # ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] + # library_set = set() + # libraries = [] + # + # for f in ext_funcs: + # library = find_library(f) + # if library not in library_set: + # library_set.add(library) + # libraries.append(library) + # + # if "AMPLFUNC" in os.environ: + # env_str = "\n".join([os.environ["AMPLFUNC"], *libraries]) + # else: + # env_str = "\n".join(libraries) + # + # os.environ["AMPLFUNC"] = env_str @property def model(self): @@ -1430,9 +1432,17 @@ def report_numerical_issues(self, stream=None): cautions = self._collect_numerical_cautions(jac=jac, nlp=nlp) stats = [] - stats.append( - f"Jacobian Condition Number: {jacobian_cond(jac=jac, scaled=False):.3E}" - ) + try: + stats.append( + f"Jacobian Condition Number: {jacobian_cond(jac=jac, scaled=False):.3E}" + ) + except RuntimeError as err: + if "Factor is exactly singular" in str(err): + _log.info(err) + stats.append(f"Jacobian Condition Number: Undefined (Exactly Singular)") + else: + raise + _write_report_section( stream=stream, lines_list=stats, title="Model Statistics", header="=" ) diff --git a/idaes/core/util/scaling.py b/idaes/core/util/scaling.py index c0ec998ac7..519801163a 100644 --- a/idaes/core/util/scaling.py +++ b/idaes/core/util/scaling.py @@ -28,6 +28,7 @@ __author__ = "John Eslick, Tim Bartholomew, Robert Parker, Andrew Lee" import math +import os import sys import scipy.sparse.linalg as spla @@ -49,12 +50,36 @@ from pyomo.core import expr as EXPR from pyomo.common.numeric_types import native_types from pyomo.core.base.units_container import _PyomoUnit +from pyomo.common.fileutils import find_library import idaes.logger as idaeslog _log = idaeslog.getLogger(__name__) +# There appears to be a bug in the ASL which causes terminal failures +# if you try to create multiple ASL structs with different external +# functions in the same process. This causes pytest to crash during testing. +# To avoid this, register all known external functions before we call +# PyNumero. +ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] +library_set = set() +libraries = [] + +for f in ext_funcs: + library = find_library(f) + if library not in library_set: + library_set.add(library) + libraries.append(library) + +if "AMPLFUNC" in os.environ: + env_str = "\n".join([os.environ["AMPLFUNC"], *libraries]) +else: + env_str = "\n".join(libraries) + +os.environ["AMPLFUNC"] = env_str + + def __none_left_mult(x, y): """PRIVATE FUNCTION, If x is None return None, else return x * y""" if x is not None: diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index 16cc4de6fb..c7d60acde8 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -33,6 +33,7 @@ acos, sqrt, Objective, + PositiveIntegers, Set, SolverFactory, Suffix, @@ -1342,6 +1343,46 @@ def test_report_numerical_issues_ok(self): prepare_degeneracy_hunter() prepare_svd_toolbox() +==================================================================================== +""" + + assert stream.getvalue() == expected + + @pytest.mark.component + def test_report_numerical_issues_exactly_singular(self): + m = ConcreteModel() + m.x = Var([1, 2], initialize=1.0) + m.eq = Constraint(PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == 1.5 + m.eq[2] = m.x[2] * m.x[1] == 1.5 + m.obj = Objective(expr=m.x[1] ** 2 + 2 * m.x[2] ** 2) + + dt = DiagnosticsToolbox(m) + dt.report_numerical_issues() + + stream = StringIO() + dt.report_numerical_issues(stream) + + expected = """==================================================================================== +Model Statistics + + Jacobian Condition Number: Undefined (Exactly Singular) + +------------------------------------------------------------------------------------ +1 WARNINGS + + WARNING: 2 Constraints with large residuals (>1.0E-05) + +------------------------------------------------------------------------------------ +0 Cautions + + No cautions found! + +------------------------------------------------------------------------------------ +Suggested next steps: + + display_constraints_with_large_residuals() + ==================================================================================== """ From 7f4a9765c6f277ca171d34d51ec910b98a96153f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 15 Mar 2024 13:47:39 -0400 Subject: [PATCH 09/21] Better implementation of fix --- idaes/core/util/scaling.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/idaes/core/util/scaling.py b/idaes/core/util/scaling.py index 519801163a..7d38e80f94 100644 --- a/idaes/core/util/scaling.py +++ b/idaes/core/util/scaling.py @@ -62,22 +62,21 @@ # functions in the same process. This causes pytest to crash during testing. # To avoid this, register all known external functions before we call # PyNumero. -ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] -library_set = set() -libraries = [] - -for f in ext_funcs: - library = find_library(f) - if library not in library_set: - library_set.add(library) - libraries.append(library) - -if "AMPLFUNC" in os.environ: - env_str = "\n".join([os.environ["AMPLFUNC"], *libraries]) -else: - env_str = "\n".join(libraries) - -os.environ["AMPLFUNC"] = env_str +def _ensure_external_functions_libs_in_env( + ext_funcs: list[str], var_name: str = "AMPLFUNC", sep: str = "\n" +): + libraries_str = os.environ.get(var_name, "") + libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] + for func_name in ext_funcs: + lib: Optional[str] = find_library(func_name) + if lib is not None and lib not in libraries: + libraries.append(lib) + os.environ[var_name] = sep.join(libraries) + + +_ensure_external_functions_libs_in_env( + ["cubic_roots", "general_helmholtz_external", "functions"] +) def __none_left_mult(x, y): From 448081a416f99ee17b1ed129142e193fcddc9c34 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 15 Mar 2024 14:24:56 -0400 Subject: [PATCH 10/21] Fixing pylint and Python 3.8 failures --- idaes/core/util/model_diagnostics.py | 2 +- idaes/core/util/scaling.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 04f7fb3ba8..43dd50cca8 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -1439,7 +1439,7 @@ def report_numerical_issues(self, stream=None): except RuntimeError as err: if "Factor is exactly singular" in str(err): _log.info(err) - stats.append(f"Jacobian Condition Number: Undefined (Exactly Singular)") + stats.append("Jacobian Condition Number: Undefined (Exactly Singular)") else: raise diff --git a/idaes/core/util/scaling.py b/idaes/core/util/scaling.py index 7d38e80f94..fcfe34f0fe 100644 --- a/idaes/core/util/scaling.py +++ b/idaes/core/util/scaling.py @@ -30,6 +30,7 @@ import math import os import sys +from typing import Optional, List import scipy.sparse.linalg as spla import scipy.linalg as la @@ -63,7 +64,7 @@ # To avoid this, register all known external functions before we call # PyNumero. def _ensure_external_functions_libs_in_env( - ext_funcs: list[str], var_name: str = "AMPLFUNC", sep: str = "\n" + ext_funcs: List[str], var_name: str = "AMPLFUNC", sep: str = "\n" ): libraries_str = os.environ.get(var_name, "") libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] From 2d8afda1279b415957328ea0ab88789e8a7723e7 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 15 Mar 2024 15:09:17 -0400 Subject: [PATCH 11/21] Removing old implementation of workaround --- idaes/core/util/model_diagnostics.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 43dd50cca8..faebe27e5b 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -439,28 +439,6 @@ def __init__(self, model: _BlockData, **kwargs): self._model = model self.config = CONFIG(kwargs) - # # There appears to be a bug in the ASL which causes terminal failures - # # if you try to create multiple ASL structs with different external - # # functions in the same process. This causes pytest to crash during testing. - # # To avoid this, register all known external functions before we call - # # PyNumero. - # ext_funcs = ["cubic_roots", "general_helmholtz_external", "functions"] - # library_set = set() - # libraries = [] - # - # for f in ext_funcs: - # library = find_library(f) - # if library not in library_set: - # library_set.add(library) - # libraries.append(library) - # - # if "AMPLFUNC" in os.environ: - # env_str = "\n".join([os.environ["AMPLFUNC"], *libraries]) - # else: - # env_str = "\n".join(libraries) - # - # os.environ["AMPLFUNC"] = env_str - @property def model(self): """ From a4a84ddf9474b407a5f4423b217ba84319c55967 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 15 Mar 2024 15:19:42 -0400 Subject: [PATCH 12/21] Fixing noisy test --- idaes/core/util/tests/test_model_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index c7d60acde8..d919a9dade 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -2282,7 +2282,7 @@ def test_run_ipopt_with_stats(self): assert iters == 1 assert iters_in_restoration == 0 assert iters_w_regularization == 0 - assert time < 0.01 + assert isinstance(time, float) @pytest.mark.component @pytest.mark.solver From 4cf5fb262965b3974b1e9bd6f78d7754f223425d Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 18 Mar 2024 10:28:14 -0400 Subject: [PATCH 13/21] Moving registration of external functions --- idaes/__init__.py | 24 ++++++++++++++++++++++++ idaes/core/util/scaling.py | 25 ------------------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/idaes/__init__.py b/idaes/__init__.py index 5dc49dbe87..d513749be4 100644 --- a/idaes/__init__.py +++ b/idaes/__init__.py @@ -22,6 +22,7 @@ import os import copy import logging +from typing import Optional, List from . import config from .ver import __version__ # noqa @@ -83,6 +84,29 @@ def _handle_optional_compat_activation( _log.debug("'idaes' logger debug test") +# TODO: Remove once AMPL bug is fixed +# TODO: https://github.com/ampl/asl/issues/13 +# There appears to be a bug in the ASL which causes terminal failures +# if you try to create multiple ASL structs with different external +# functions in the same process. This causes pytest to crash during testing. +# To avoid this, register all known external functions at initialization. +def _ensure_external_functions_libs_in_env( + ext_funcs: List[str], var_name: str = "AMPLFUNC", sep: str = "\n" +): + libraries_str = os.environ.get(var_name, "") + libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] + for func_name in ext_funcs: + lib: Optional[str] = os.path.join(bin_directory, func_name) + if lib is not None and lib not in libraries: + libraries.append(lib) + os.environ[var_name] = sep.join(libraries) + + +_ensure_external_functions_libs_in_env( + ["cubic_roots.so", "general_helmholtz_external.so", "functions.so"] +) + + def _create_data_dir(): """Create the IDAES directory to store data files in.""" config.create_dir(data_directory) diff --git a/idaes/core/util/scaling.py b/idaes/core/util/scaling.py index fcfe34f0fe..c0ec998ac7 100644 --- a/idaes/core/util/scaling.py +++ b/idaes/core/util/scaling.py @@ -28,9 +28,7 @@ __author__ = "John Eslick, Tim Bartholomew, Robert Parker, Andrew Lee" import math -import os import sys -from typing import Optional, List import scipy.sparse.linalg as spla import scipy.linalg as la @@ -51,35 +49,12 @@ from pyomo.core import expr as EXPR from pyomo.common.numeric_types import native_types from pyomo.core.base.units_container import _PyomoUnit -from pyomo.common.fileutils import find_library import idaes.logger as idaeslog _log = idaeslog.getLogger(__name__) -# There appears to be a bug in the ASL which causes terminal failures -# if you try to create multiple ASL structs with different external -# functions in the same process. This causes pytest to crash during testing. -# To avoid this, register all known external functions before we call -# PyNumero. -def _ensure_external_functions_libs_in_env( - ext_funcs: List[str], var_name: str = "AMPLFUNC", sep: str = "\n" -): - libraries_str = os.environ.get(var_name, "") - libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] - for func_name in ext_funcs: - lib: Optional[str] = find_library(func_name) - if lib is not None and lib not in libraries: - libraries.append(lib) - os.environ[var_name] = sep.join(libraries) - - -_ensure_external_functions_libs_in_env( - ["cubic_roots", "general_helmholtz_external", "functions"] -) - - def __none_left_mult(x, y): """PRIVATE FUNCTION, If x is None return None, else return x * y""" if x is not None: From 55258cff00700e14b693d39e69c9556f0d8376dd Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 18 Mar 2024 11:13:57 -0400 Subject: [PATCH 14/21] Reverting to version that works on Windows --- idaes/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/idaes/__init__.py b/idaes/__init__.py index d513749be4..268828d0e8 100644 --- a/idaes/__init__.py +++ b/idaes/__init__.py @@ -24,6 +24,8 @@ import logging from typing import Optional, List +from pyomo.common.fileutils import find_library + from . import config from .ver import __version__ # noqa @@ -96,14 +98,14 @@ def _ensure_external_functions_libs_in_env( libraries_str = os.environ.get(var_name, "") libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] for func_name in ext_funcs: - lib: Optional[str] = os.path.join(bin_directory, func_name) + lib: Optional[str] = find_library(func_name) if lib is not None and lib not in libraries: libraries.append(lib) os.environ[var_name] = sep.join(libraries) _ensure_external_functions_libs_in_env( - ["cubic_roots.so", "general_helmholtz_external.so", "functions.so"] + ["cubic_roots", "general_helmholtz_external", "functions"] ) From cd47ae2913cc9b572bbc6f246703c66169b96ac8 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 18 Mar 2024 12:15:22 -0400 Subject: [PATCH 15/21] Trying another way to get binary files --- idaes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/__init__.py b/idaes/__init__.py index 268828d0e8..790ce7c466 100644 --- a/idaes/__init__.py +++ b/idaes/__init__.py @@ -98,7 +98,7 @@ def _ensure_external_functions_libs_in_env( libraries_str = os.environ.get(var_name, "") libraries = [lib for lib in libraries_str.split(sep) if lib.strip()] for func_name in ext_funcs: - lib: Optional[str] = find_library(func_name) + lib: Optional[str] = find_library(os.path.join(bin_directory, func_name)) if lib is not None and lib not in libraries: libraries.append(lib) os.environ[var_name] = sep.join(libraries) From dc7f8ccb55d89f36b543a5749616be7b22e0bd77 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 19 Mar 2024 14:00:25 -0600 Subject: [PATCH 16/21] add parallel variable/constraint checks to report_numerical_issues; change default parallel tolerance to 1e-8 --- idaes/core/util/model_diagnostics.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index faebe27e5b..2992fc61a4 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -286,7 +286,7 @@ def svd_sparse(jacobian, number_singular_values): CONFIG.declare( "parallel_component_tolerance", ConfigValue( - default=1e-4, + default=1e-8, domain=float, description="Tolerance for identifying near-parallel Jacobian rows/columns", ), @@ -1179,6 +1179,23 @@ def _collect_numerical_warnings(self, jac=None, nlp=None): self.display_constraints_with_extreme_jacobians.__name__ + "()" ) + # Parallel variables and constraints + partol = self.config.parallel_component_tolerance + par_cons = check_parallel_jacobian(self._model, tolerance=partol, direction="row") + par_vars = check_parallel_jacobian(self._model, tolerance=partol, direction="column") + if par_cons: + warnings.append( + f"WARNING: {len(par_cons)} pairs of constraints are parallel" + f" (to tolerance {partol})" + ) + next_steps.append(self.display_near_parallel_constraints.__name__ + "()") + if par_vars: + warnings.append( + f"WARNING: {len(par_vars)} pairs of variables are parallel" + f" (to tolerance {partol})" + ) + next_steps.append(self.display_near_parallel_variables.__name__ + "()") + return warnings, next_steps def _collect_numerical_cautions(self, jac=None, nlp=None): @@ -1441,7 +1458,6 @@ def report_numerical_issues(self, stream=None): lines_list=next_steps, title="Suggested next steps:", line_if_empty=f"If you still have issues converging your model consider:\n" - f"{TAB*2}display_near_parallel_constraints()\n{TAB*2}display_near_parallel_variables()" f"\n{TAB*2}prepare_degeneracy_hunter()\n{TAB*2}prepare_svd_toolbox()", footer="=", ) From 6c5a6c4a05f763d5fd89ad72a3bd666332146344 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 15:10:50 -0600 Subject: [PATCH 17/21] update tests --- idaes/core/util/model_diagnostics.py | 34 +++++++++++++++---- .../core/util/tests/test_model_diagnostics.py | 17 ++++++---- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 2992fc61a4..0f8c6888bc 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -286,7 +286,7 @@ def svd_sparse(jacobian, number_singular_values): CONFIG.declare( "parallel_component_tolerance", ConfigValue( - default=1e-8, + default=1e-4, domain=float, description="Tolerance for identifying near-parallel Jacobian rows/columns", ), @@ -1181,17 +1181,23 @@ def _collect_numerical_warnings(self, jac=None, nlp=None): # Parallel variables and constraints partol = self.config.parallel_component_tolerance - par_cons = check_parallel_jacobian(self._model, tolerance=partol, direction="row") - par_vars = check_parallel_jacobian(self._model, tolerance=partol, direction="column") + par_cons = check_parallel_jacobian( + self._model, tolerance=partol, direction="row", jac=jac, nlp=nlp + ) + par_vars = check_parallel_jacobian( + self._model, tolerance=partol, direction="column", jac=jac, nlp=nlp + ) if par_cons: + p = "pair" if len(par_cons) == 1 else "pairs" warnings.append( - f"WARNING: {len(par_cons)} pairs of constraints are parallel" + f"WARNING: {len(par_cons)} {p} of constraints are parallel" f" (to tolerance {partol})" ) next_steps.append(self.display_near_parallel_constraints.__name__ + "()") if par_vars: + p = "pair" if len(par_vars) == 1 else "pairs" warnings.append( - f"WARNING: {len(par_vars)} pairs of variables are parallel" + f"WARNING: {len(par_vars)} {p} of variables are parallel" f" (to tolerance {partol})" ) next_steps.append(self.display_near_parallel_variables.__name__ + "()") @@ -3594,7 +3600,13 @@ def ipopt_solve_halt_on_error(model, options=None): ) -def check_parallel_jacobian(model, tolerance: float = 1e-4, direction: str = "row"): +def check_parallel_jacobian( + model, + tolerance: float = 1e-4, + direction: str = "row", + jac=None, + nlp=None, +): """ Check for near-parallel rows or columns in the Jacobian. @@ -3602,6 +3614,11 @@ def check_parallel_jacobian(model, tolerance: float = 1e-4, direction: str = "ro as this means that the associated constraints or variables are (near) duplicates of each other. + For efficiency, the ``jac`` and ``nlp`` arguments may be provided if they are + already available. If these are provided, the provided model is not used. If + either ``jac`` or ``nlp`` is not provided, a Jacobian and ``PyomoNLP`` are + computed using the model. + This method is based on work published in: Klotz, E., Identification, Assessment, and Correction of Ill-Conditioning and @@ -3612,6 +3629,8 @@ def check_parallel_jacobian(model, tolerance: float = 1e-4, direction: str = "ro model: model to be analysed tolerance: tolerance to use to determine if constraints/variables are parallel direction: 'row' (default, constraints) or 'column' (variables) + jac: model Jacobian as a ``scipy.sparse.coo_matrix``, optional + nlp: ``PyomoNLP`` of model, optional Returns: list of 2-tuples containing parallel Pyomo components @@ -3626,7 +3645,8 @@ def check_parallel_jacobian(model, tolerance: float = 1e-4, direction: str = "ro "Must be 'row' or 'column'." ) - jac, nlp = get_jacobian(model, scaled=False) + if jac is None or nlp is None: + jac, nlp = get_jacobian(model, scaled=False) # Get vectors that we will check, and the Pyomo components # they correspond to. diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index d919a9dade..00e4d4d7d2 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -1121,7 +1121,7 @@ def test_collect_numerical_warnings_jacobian(self): warnings, next_steps = dt._collect_numerical_warnings() - assert len(warnings) == 3 + assert len(warnings) == 4 assert ( "WARNING: 2 Variables with extreme Jacobian values (<1.0E-08 or >1.0E+08)" in warnings @@ -1132,7 +1132,7 @@ def test_collect_numerical_warnings_jacobian(self): ) assert "WARNING: 1 Constraint with large residuals (>1.0E-05)" in warnings - assert len(next_steps) == 3 + assert len(next_steps) == 4 assert "display_variables_with_extreme_jacobians()" in next_steps assert "display_constraints_with_extreme_jacobians()" in next_steps assert "display_constraints_with_large_residuals()" in next_steps @@ -1338,8 +1338,7 @@ def test_report_numerical_issues_ok(self): Suggested next steps: If you still have issues converging your model consider: - display_near_parallel_constraints() - display_near_parallel_variables() + prepare_degeneracy_hunter() prepare_svd_toolbox() @@ -1369,9 +1368,11 @@ def test_report_numerical_issues_exactly_singular(self): Jacobian Condition Number: Undefined (Exactly Singular) ------------------------------------------------------------------------------------ -1 WARNINGS +3 WARNINGS WARNING: 2 Constraints with large residuals (>1.0E-05) + WARNING: 1 pair of constraints are parallel (to tolerance 0.0001) + WARNING: 1 pair of variables are parallel (to tolerance 0.0001) ------------------------------------------------------------------------------------ 0 Cautions @@ -1382,6 +1383,8 @@ def test_report_numerical_issues_exactly_singular(self): Suggested next steps: display_constraints_with_large_residuals() + display_near_parallel_constraints() + display_near_parallel_variables() ==================================================================================== """ @@ -1448,11 +1451,12 @@ def test_report_numerical_issues_jacobian(self): Jacobian Condition Number: 1.407E+18 ------------------------------------------------------------------------------------ -3 WARNINGS +4 WARNINGS WARNING: 1 Constraint with large residuals (>1.0E-05) WARNING: 2 Variables with extreme Jacobian values (<1.0E-08 or >1.0E+08) WARNING: 1 Constraint with extreme Jacobian values (<1.0E-08 or >1.0E+08) + WARNING: 1 pair of variables are parallel (to tolerance 0.0001) ------------------------------------------------------------------------------------ 4 Cautions @@ -1468,6 +1472,7 @@ def test_report_numerical_issues_jacobian(self): display_constraints_with_large_residuals() display_variables_with_extreme_jacobians() display_constraints_with_extreme_jacobians() + display_near_parallel_variables() ==================================================================================== """ From ad00bf96a2728d7ede15f1aaa5e788a023f643dd Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Apr 2024 07:32:04 -0600 Subject: [PATCH 18/21] tighten parallel_component_tolerance to 1e-8 --- idaes/core/util/model_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 31445b4cd4..e0e826bc05 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -286,7 +286,7 @@ def svd_sparse(jacobian, number_singular_values): CONFIG.declare( "parallel_component_tolerance", ConfigValue( - default=1e-4, + default=1e-8, domain=float, description="Tolerance for identifying near-parallel Jacobian rows/columns", ), From 69ba71f1ae75a0960c6d26045226ea8474140da1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Apr 2024 10:03:06 -0600 Subject: [PATCH 19/21] adjust model to make parallel variable test less sensitive --- idaes/core/util/tests/test_model_diagnostics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index 85fef7a318..89bec93afa 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -975,10 +975,10 @@ def test_display_near_parallel_variables(self): model.v3 = Var() model.v4 = Var() - model.c1 = Constraint(expr=model.v1 == model.v2 - 0.99999 * model.v4) - model.c2 = Constraint(expr=model.v1 + 1.00001 * model.v4 == 1e-8 * model.v3) + model.c1 = Constraint(expr=1e-8 * model.v1 == 1e-8 * model.v2 - 1e-8 * model.v4) + model.c2 = Constraint(expr=1e-8 * model.v1 + 1e-8 * model.v4 == model.v3) model.c3 = Constraint( - expr=1e8 * (model.v1 + model.v4) + 1e10 * model.v2 == 1e-6 * model.v3 + expr=1e3 * (model.v1 + model.v4) + 1e3 * model.v2 == model.v3 ) dt = DiagnosticsToolbox(model=model) From 89603432f24267ba64a787987758f02e7f3c58b9 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Apr 2024 10:38:31 -0600 Subject: [PATCH 20/21] update test to reflect new default tolerance of 1e-8 --- idaes/core/util/tests/test_model_diagnostics.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index 89bec93afa..796beb07fa 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -1371,8 +1371,8 @@ def test_report_numerical_issues_exactly_singular(self): 3 WARNINGS WARNING: 2 Constraints with large residuals (>1.0E-05) - WARNING: 1 pair of constraints are parallel (to tolerance 0.0001) - WARNING: 1 pair of variables are parallel (to tolerance 0.0001) + WARNING: 1 pair of constraints are parallel (to tolerance 1e-08) + WARNING: 1 pair of variables are parallel (to tolerance 1e-08) ------------------------------------------------------------------------------------ 0 Cautions @@ -1436,8 +1436,8 @@ def test_report_numerical_issues_jacobian(self): model.v2 = Var(initialize=0) model.v3 = Var(initialize=0) - model.c1 = Constraint(expr=model.v1 == model.v2) - model.c2 = Constraint(expr=model.v1 == 1e-8 * model.v3) + model.c1 = Constraint(expr=1e-2 * model.v1 == model.v2) + model.c2 = Constraint(expr=1e-2 * model.v1 == 1e-8 * model.v3) model.c3 = Constraint(expr=1e8 * model.v1 + 1e10 * model.v2 == 1e-6 * model.v3) dt = DiagnosticsToolbox(model=model) @@ -1448,7 +1448,7 @@ def test_report_numerical_issues_jacobian(self): expected = """==================================================================================== Model Statistics - Jacobian Condition Number: 1.407E+18 + Jacobian Condition Number: 1.118E+18 ------------------------------------------------------------------------------------ 4 WARNINGS @@ -1456,7 +1456,7 @@ def test_report_numerical_issues_jacobian(self): WARNING: 1 Constraint with large residuals (>1.0E-05) WARNING: 2 Variables with extreme Jacobian values (<1.0E-08 or >1.0E+08) WARNING: 1 Constraint with extreme Jacobian values (<1.0E-08 or >1.0E+08) - WARNING: 1 pair of variables are parallel (to tolerance 0.0001) + WARNING: 3 pairs of variables are parallel (to tolerance 1e-08) ------------------------------------------------------------------------------------ 4 Cautions From c34099b8c8ab4db8d6662af8ad508ecce9d82ee5 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Apr 2024 10:42:22 -0600 Subject: [PATCH 21/21] consistent format for displaying parallel tolerance --- idaes/core/util/model_diagnostics.py | 4 ++-- idaes/core/util/tests/test_model_diagnostics.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index e0e826bc05..6ff4ee4def 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -1191,14 +1191,14 @@ def _collect_numerical_warnings(self, jac=None, nlp=None): p = "pair" if len(par_cons) == 1 else "pairs" warnings.append( f"WARNING: {len(par_cons)} {p} of constraints are parallel" - f" (to tolerance {partol})" + f" (to tolerance {partol:.1E})" ) next_steps.append(self.display_near_parallel_constraints.__name__ + "()") if par_vars: p = "pair" if len(par_vars) == 1 else "pairs" warnings.append( f"WARNING: {len(par_vars)} {p} of variables are parallel" - f" (to tolerance {partol})" + f" (to tolerance {partol:.1E})" ) next_steps.append(self.display_near_parallel_variables.__name__ + "()") diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index 796beb07fa..39dd09d561 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -1371,8 +1371,8 @@ def test_report_numerical_issues_exactly_singular(self): 3 WARNINGS WARNING: 2 Constraints with large residuals (>1.0E-05) - WARNING: 1 pair of constraints are parallel (to tolerance 1e-08) - WARNING: 1 pair of variables are parallel (to tolerance 1e-08) + WARNING: 1 pair of constraints are parallel (to tolerance 1.0E-08) + WARNING: 1 pair of variables are parallel (to tolerance 1.0E-08) ------------------------------------------------------------------------------------ 0 Cautions @@ -1456,7 +1456,7 @@ def test_report_numerical_issues_jacobian(self): WARNING: 1 Constraint with large residuals (>1.0E-05) WARNING: 2 Variables with extreme Jacobian values (<1.0E-08 or >1.0E+08) WARNING: 1 Constraint with extreme Jacobian values (<1.0E-08 or >1.0E+08) - WARNING: 3 pairs of variables are parallel (to tolerance 1e-08) + WARNING: 3 pairs of variables are parallel (to tolerance 1.0E-08) ------------------------------------------------------------------------------------ 4 Cautions