From 9e362067ab67bc0554e171fc86eba83f18e72265 Mon Sep 17 00:00:00 2001 From: JavalVyas2000 Date: Tue, 26 Mar 2024 05:46:47 -0400 Subject: [PATCH] applying black --- .../custom_unit_models/Aq_property.py | 6 +- .../custom_unit_models/LiqLiqExtractor.py | 13 +- .../custom_unit_models/Org_property.py | 9 +- .../custom_unit_models/liqliq_extractor.ipynb | 394 ++-- .../liqliq_extractor_doc.ipynb | 2022 ++++++++++++----- .../liqliq_extractor_test.ipynb | 2022 ++++++++++++----- .../liqliq_extractor_usr.ipynb | 2022 ++++++++++++----- .../custom_unit_models/liquid_extraction.py | 2 - 8 files changed, 4426 insertions(+), 2064 deletions(-) diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Aq_property.py b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Aq_property.py index f490ca6e..581d904a 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Aq_property.py +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Aq_property.py @@ -119,6 +119,7 @@ class _StateBlock(StateBlock): This Class contains methods which should be applied to Property Blocks as a whole, rather than individual elements of indexed Property Blocks. """ + def initialize( self, state_args=None, @@ -206,6 +207,7 @@ def release_state(self, flags, outlvl=idaeslog.NOTSET): revert_state_vars(self, flags) init_log.info("State Released.") + @declare_process_block_class("AqPhaseStateBlock", block_class=_StateBlock) class AqPhaseStateBlockData(StateBlockData): """ @@ -253,9 +255,9 @@ def _make_state_vars(self): def material_flow_expression(self, j): if j == "H2O": - return self.flow_vol*self.params.dens_mass + return self.flow_vol * self.params.dens_mass else: - return self.conc_mass_comp[j]*self.flow_vol + return self.conc_mass_comp[j] * self.flow_vol self.material_flow_expression = Expression( self.component_list, diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/LiqLiqExtractor.py b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/LiqLiqExtractor.py index 902a052f..46dd79c8 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/LiqLiqExtractor.py +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/LiqLiqExtractor.py @@ -34,15 +34,9 @@ m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour) m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K) m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm) -m.fs.lex.aqueous_inlet.conc_mass_comp[0, "NaCl"].fix( - 0.15 * pyo.units.g / pyo.units.L -) -m.fs.lex.aqueous_inlet.conc_mass_comp[0, "KNO3"].fix( - 0.2 * pyo.units.g / pyo.units.L -) -m.fs.lex.aqueous_inlet.conc_mass_comp[0, "CaSO4"].fix( - 0.1 * pyo.units.g / pyo.units.L -) +m.fs.lex.aqueous_inlet.conc_mass_comp[0, "NaCl"].fix(0.15 * pyo.units.g / pyo.units.L) +m.fs.lex.aqueous_inlet.conc_mass_comp[0, "KNO3"].fix(0.2 * pyo.units.g / pyo.units.L) +m.fs.lex.aqueous_inlet.conc_mass_comp[0, "CaSO4"].fix(0.1 * pyo.units.g / pyo.units.L) print(degrees_of_freedom(m)) @@ -66,4 +60,3 @@ m.fs.lex.aqueous_outlet.flow_vol.display() m.fs.lex.organic_outlet.flow_vol.display() pyo.assert_optimal_termination(results) - diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Org_property.py b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Org_property.py index 7b988c98..b7b71883 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Org_property.py +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/Org_property.py @@ -107,7 +107,7 @@ def build(self): self.solutes, initialize={"NaCl": 2.15, "KNO3": 3, "CaSO4": 1.5}, within=PositiveReals, - mutable=True + mutable=True, ) @classmethod @@ -216,10 +216,11 @@ def release_state(self, flags, outlvl=idaeslog.NOTSET): revert_state_vars(self, flags) init_log.info("State Released.") + @declare_process_block_class("OrgPhaseStateBlock", block_class=_StateBlock) class LiqPhaseStateBlockData(StateBlockData): """ - An example property package for Organic phase for liquid liquid extraction + An example property package for Organic phase for liquid liquid extraction """ def build(self): @@ -261,9 +262,9 @@ def _make_state_vars(self): def material_flow_expression(self, j): if j == "solvent": - return self.flow_vol*self.params.dens_mass + return self.flow_vol * self.params.dens_mass else: - return self.flow_vol*self.conc_mass_comp[j] + return self.flow_vol * self.conc_mass_comp[j] self.material_flow_expression = Expression( self.component_list, diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor.ipynb index 8f70e1c8..ba483acc 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor.ipynb @@ -39,14 +39,11 @@ "\n", "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", "- Steady-state only\n", - "- Organic phase property package has a single phase named Oeg\n", + "- Organic phase property package has a single phase named Org\n", "- Aquoeus phase property package has a single phase named Aq\n", "- Organic and Aqueous phase properties need not have the same component list. \n", "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). \n", - "\n" + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " ] }, { @@ -194,7 +191,7 @@ " self.solutes,\n", " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", " within=PositiveReals,\n", - " mutable=True\n", + " mutable=True,\n", " )\n", "\n", " @classmethod\n", @@ -242,6 +239,7 @@ " This Class contains methods which should be applied to Property Blocks as a\n", " whole, rather than individual elements of indexed Property Blocks.\n", " \"\"\"\n", + "\n", " def initialize(\n", " self,\n", " state_args=None,\n", @@ -367,9 +365,9 @@ "outputs": [], "source": [ "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_StateBlock)\n", - "class LiqPhaseStateBlockData(StateBlockData):\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for Organic phase for liquid liquid extraction\n", + " An example property package for Organic phase for liquid liquid extraction\n", " \"\"\"\n", "\n", " def build(self):\n", @@ -411,9 +409,9 @@ "\n", " def material_flow_expression(self, j):\n", " if j == \"solvent\":\n", - " return self.flow_vol*self.params.dens_mass\n", + " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.flow_vol*self.conc_mass_comp[j]\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -598,6 +596,7 @@ " This Class contains methods which should be applied to Property Blocks as a\n", " whole, rather than individual elements of indexed Property Blocks.\n", " \"\"\"\n", + "\n", " def fix_initialization_states(self):\n", " fix_state_vars(self)\n", "\n", @@ -649,9 +648,9 @@ "\n", " def material_flow_expression(self, j):\n", " if j == \"H2O\":\n", - " return self.flow_vol*self.params.dens_mass\n", + " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.conc_mass_comp[j]*self.flow_vol\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -932,187 +931,181 @@ "outputs": [], "source": [ "def build(self):\n", - " \"\"\"\n", - " Begin building model (pre-DAE transformation).\n", - " Args:\n", - " None\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " # Call UnitModel.build to setup dynamics\n", - " super().build()\n", - "\n", - " # Check phase lists match assumptions\n", - " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", - " f\"phase property package have a single phase named 'Aq'\"\n", - " )\n", - " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"phase property package have a single phase named 'Org'\"\n", - " )\n", - "\n", - " # Check for at least one common component in component lists\n", - " if not any(\n", - " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.organic_property_package.component_list\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"and aqueous phase property packages have at least one \"\n", - " f\"common component.\"\n", - " )\n", - "\n", - " self.organic_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.organic_property_package,\n", - " property_package_args=self.config.organic_property_package_args,\n", + " \"\"\"\n", + " Begin building model (pre-DAE transformation).\n", + " Args:\n", + " None\n", + " Returns:\n", + " None\n", + " \"\"\"\n", + " # Call UnitModel.build to setup dynamics\n", + " super().build()\n", + "\n", + " # Check phase lists match assumptions\n", + " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", + " f\"phase property package have a single phase named 'Aq'\"\n", + " )\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", " )\n", "\n", - " self.organic_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " # Check for at least one common component in component lists\n", + " if not any(\n", + " j in self.config.aqueous_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"and aqueous phase property packages have at least one \"\n", + " f\"common component.\"\n", " )\n", "\n", - " # Separate organic and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", + " self.organic_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", + " )\n", "\n", - " self.organic_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - " # ---------------------------------------------------------------------\n", + " self.organic_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", "\n", - " self.aqueous_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.aqueous_property_package,\n", - " property_package_args=self.config.aqueous_property_package_args,\n", - " )\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", "\n", - " self.aqueous_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", + " self.organic_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + " # ---------------------------------------------------------------------\n", "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", + " self.aqueous_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.aqueous_property_package,\n", + " property_package_args=self.config.aqueous_property_package_args,\n", + " )\n", "\n", - " self.aqueous_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " # has_rate_reactions=False,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", + " self.aqueous_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", "\n", - " self.aqueous_phase.add_geometry()\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Check flow basis is compatable\n", - " t_init = self.flowsheet().time.first()\n", - " if (\n", - " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} aqueous and organic property packages must use the \"\n", - " f\"same material flow basis.\"\n", - " )\n", + " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", "\n", - " self.organic_phase.add_geometry()\n", + " self.aqueous_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " # has_rate_reactions=False,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", "\n", - " # Add Ports\n", - " self.add_inlet_port(\n", - " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", - " )\n", - " self.add_inlet_port(\n", - " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"aqueous_outlet\",\n", - " block=self.aqueous_phase,\n", - " doc=\"Aqueous outlet\",\n", - " )\n", + " self.aqueous_phase.add_geometry()\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Add unit level constraints\n", - " # First, need the union and intersection of component lists\n", - " all_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " | self.organic_phase.properties_out.component_list\n", - " )\n", - " common_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " & self.organic_phase.properties_out.component_list\n", + " # ---------------------------------------------------------------------\n", + " # Check flow basis is compatable\n", + " t_init = self.flowsheet().time.first()\n", + " if (\n", + " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", + " f\"same material flow basis.\"\n", " )\n", "\n", - " # Get units for unit conversion\n", - " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", - " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + " self.organic_phase.add_geometry()\n", "\n", - " if flow_basis == MaterialFlowBasis.mass:\n", - " fb = \"flow_mass\"\n", - " elif flow_basis == MaterialFlowBasis.molar:\n", - " fb = \"flow_mole\"\n", - " else:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", - " f\"basis for MaterialFlowBasis.\"\n", - " )\n", + " # Add Ports\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", + " self.add_inlet_port(\n", + " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"aqueous_outlet\",\n", + " block=self.aqueous_phase,\n", + " doc=\"Aqueous outlet\",\n", + " )\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Add unit level constraints\n", + " # First, need the union and intersection of component lists\n", + " all_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", + " )\n", + " common_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", + " )\n", "\n", - " # Material balances\n", - " def rule_material_aq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return self.aqueous_phase.mass_transfer_term[\n", - " t, \"Aq\", j\n", - " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", - " )\n", - " elif j in self.organic_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(\n", - " fb\n", - " )\n", - " elif j in self.aqueous_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set aqueous flowrate to an arbitary small value\n", - " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(\n", - " fb\n", - " )\n", - "\n", - " self.material_aq_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Aq\",\n", + " # Get units for unit conversion\n", + " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", + " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + "\n", + " if flow_basis == MaterialFlowBasis.mass:\n", + " fb = \"flow_mass\"\n", + " elif flow_basis == MaterialFlowBasis.molar:\n", + " fb = \"flow_mole\"\n", + " else:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", + " f\"basis for MaterialFlowBasis.\"\n", " )\n", "\n", - " def rule_material_liq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return (\n", - " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", - " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", - " )\n", - " else:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(\n", - " fb\n", - " )\n", - "\n", - " self.material_org_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.organic_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances Org\",\n", - " )" + " # Material balances\n", + " def rule_material_aq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return self.aqueous_phase.mass_transfer_term[\n", + " t, \"Aq\", j\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", + " )\n", + " elif j in self.organic_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", + " elif j in self.aqueous_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set aqueous flowrate to an arbitary small value\n", + " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_aq_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.aqueous_phase.properties_out.component_list,\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", + " )\n", + "\n", + " def rule_material_liq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return (\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", + " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", + " )\n", + " else:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_org_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", + " )" ] }, { @@ -1214,15 +1207,9 @@ "m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", "m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", "m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", - "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 0.15 * pyo.units.g / pyo.units.L\n", - ")\n", - "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 0.2 * pyo.units.g / pyo.units.L\n", - ")\n", - "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 0.1 * pyo.units.g / pyo.units.L\n", - ")\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * pyo.units.g / pyo.units.L)\n", "\n", "initializer = BlockTriangularizationInitializer()\n", "initializer.initialize(m.fs.lex)\n", @@ -1843,7 +1830,7 @@ } ], "source": [ - "solver = pyo.SolverFactory('ipopt')\n", + "solver = pyo.SolverFactory(\"ipopt\")\n", "solver.solve(m, tee=True)" ] }, @@ -2008,7 +1995,7 @@ } ], "source": [ - "m.fs.lex.aqueous_phase.material_balances[0.0,'NaCl'].pprint()" + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" ] }, { @@ -2045,11 +2032,11 @@ } ], "source": [ - "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp['NaCl'].pprint() \n", + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp['NaCl'].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.mass_transfer_term[0.0,'Aq','NaCl'].pprint()" + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" ] }, { @@ -2080,7 +2067,7 @@ } ], "source": [ - "m.fs.lex.material_aq_balance[0.0,'NaCl'].pprint()" + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" ] }, { @@ -2098,9 +2085,15 @@ "metadata": {}, "outputs": [], "source": [ - "m.fs.org_properties.diffusion_factor['NaCl']=m.fs.org_properties.diffusion_factor['NaCl']/100\n", - "m.fs.org_properties.diffusion_factor['KNO3']=m.fs.org_properties.diffusion_factor['KNO3']/100\n", - "m.fs.org_properties.diffusion_factor['CaSO4']=m.fs.org_properties.diffusion_factor['CaSO4']/100\n", + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", "\n", "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" @@ -2396,7 +2389,7 @@ " assert value(model.params.dens_mass) == 997\n", "\n", " assert isinstance(model.params.temperature_ref, Param)\n", - " assert value(model.params.temperature_ref) == 298.15\n" + " assert value(model.params.temperature_ref) == 298.15" ] }, { @@ -2538,7 +2531,6 @@ " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", - "\n", " # Check unit config arguments\n", " assert len(m.fs.unit.config) == 9\n", "\n", @@ -2681,9 +2673,15 @@ " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", - " m.fs.org_properties.diffusion_factor['NaCl']=m.fs.org_properties.diffusion_factor['NaCl']/100\n", - " m.fs.org_properties.diffusion_factor['KNO3']=m.fs.org_properties.diffusion_factor['KNO3']/100\n", - " m.fs.org_properties.diffusion_factor['CaSO4']=m.fs.org_properties.diffusion_factor['CaSO4']/100\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", "\n", " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", @@ -2768,7 +2766,7 @@ " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", - " \n", + "\n", " @pytest.mark.component\n", " def test_structural_issues(self, model):\n", " dt = DiagnosticsToolbox(model)\n", diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_doc.ipynb index 85f5dd94..deea14b4 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_doc.ipynb @@ -37,21 +37,18 @@ "\n", "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", "- Steady-state only\n", - "- Liquid phase property package has a single phase named Liq\n", + "- Organic phase property package has a single phase named Org\n", "- Aquoeus phase property package has a single phase named Aq\n", - "- Liquid and Aqueous phase properties need not have the same component list. \n", + "- Organic and Aqueous phase properties need not have the same component list. \n", "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the liquid phase (Liq). \n", - "\n" + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Creating Liquid Property Package\n", + "# 1. Creating Organic Property Package\n", "\n", "Creating a property package is a 4 step process\n", "- Import necessary libraries \n", @@ -76,7 +73,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -110,14 +115,13 @@ "\n", "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", "\n", - "The Physical Parameter Block then refers to the `state block` in this case `LiqPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the liquid phase, we will assign the Phase as LiquidPhase and the variable will be named Liq as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + "The Physical Parameter Block then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the liquid phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). This method in turn needs to call two predefined methods (inherited from underlying base classes):\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", "\n", - "- `obj.add_properties()` is used to set the metadata regarding the supported properties, and here we define flow volume, pressure, temperature, and mass fraction.\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default." + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " ] }, { @@ -126,13 +130,13 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhase\")\n", + "@declare_process_block_class(\"OrgPhase\")\n", "class PhysicalParameterData(PhysicalParameterBlock):\n", " \"\"\"\n", " Property Parameter Block Class\n", "\n", " Contains parameters and indexing sets associated with properties for\n", - " liquid Phase\n", + " organic Phase\n", "\n", " \"\"\"\n", "\n", @@ -140,21 +144,22 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", - " self._state_block_class = LiqPhaseStateBlock\n", + " self._state_block_class = OrgPhaseStateBlock\n", "\n", " # List of valid phases in property package\n", - " self.Liq = LiquidPhase()\n", + " self.Org = LiquidPhase()\n", "\n", " # Component list - a list of component identifiers\n", " self.NaCl = Solute()\n", " self.KNO3 = Solute()\n", " self.CaSO4 = Solute()\n", - " self.C2H4Br2 = (\n", + " self.solvent = (\n", " Solvent()\n", " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", "\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", @@ -163,15 +168,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of solvent\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=2170,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference Temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -179,29 +181,20 @@ " doc=\"Reference temperature\",\n", " units=units.K,\n", " )\n", - "\n", - " # Distribution Factor\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.diffusion_factor = Param(\n", - " salts_d.keys(), initialize=salts_d, within=PositiveReals\n", + " self.solutes,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", " )\n", "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_vol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " \"conc_mass_comp\": {\"method\": None},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -216,16 +209,15 @@ "\n", "After the `Physical Parameter Block` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc. \n", "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization. \n", - "\n", - "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively. \n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization.\n", "\n", - "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete. \n", + "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively.\n", "\n", + "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete.\n", "\n", - "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged. \n", + "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged.\n", "\n", - "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `LiqPhaseStateBlockData` class. " + "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `OrgPhaseStateBlockData` class." ] }, { @@ -251,7 +243,6 @@ " ):\n", " \"\"\"\n", " Initialization routine for property package.\n", - "\n", " Keyword Arguments:\n", " state_args : Dictionary with initial guesses for the state vars\n", " chosen. Note that if this method is triggered\n", @@ -282,7 +273,6 @@ " False - state variables are unfixed after\n", " initialization by calling the\n", " release_state method\n", - "\n", " Returns:\n", " If hold_states is True, returns a dict containing flags for\n", " which states were fixed during initialization.\n", @@ -314,7 +304,6 @@ " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", " \"\"\"\n", " Method to release state variables fixed during initialization.\n", - "\n", " Keyword Arguments:\n", " flags : dict containing information of which state variables\n", " were fixed during initialization, and should now be\n", @@ -335,7 +324,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The class `LiqPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `LiqPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `LiqPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", "\n", "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", "\n", @@ -344,7 +333,6 @@ "- `pressure` - state pressure\n", "- `temperature` - state temperature\n", "\n", - "Additionally, a state parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", "\n", @@ -366,33 +354,32 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhaseStateBlock\", block_class=_StateBlock)\n", - "class LiqPhaseStateBlockData(StateBlockData):\n", + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_StateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for liquid phase for liquid liquid extraction\n", + " An example property package for Organic phase for liquid liquid extraction\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(LiqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.flow_vol = Var(\n", " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", " self.conc_mass_comp = Var(\n", - " salts_d.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", " initialize=1,\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", @@ -401,6 +388,7 @@ " units=units.atm,\n", " doc=\"State pressure [atm]\",\n", " )\n", + "\n", " self.temperature = Var(\n", " domain=NonNegativeReals,\n", " initialize=300,\n", @@ -408,18 +396,12 @@ " units=units.K,\n", " doc=\"State temperature [K]\",\n", " )\n", - " self.diffusion_factor = Param(\n", - " salts_d.keys(),\n", - " initialize=salts_d,\n", - " doc=\"Diffusion Factor of salts\",\n", - " within=PositiveReals,\n", - " )\n", "\n", " def material_flow_expression(self, j):\n", " if j == \"solvent\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.conc_mass_comp[j]\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -439,9 +421,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -475,7 +454,7 @@ "source": [ "# 2. Creating Aqueous Property Package\n", "\n", - "The structure of Aqueous Property Package mirrors that of the Liquid Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." ] }, { @@ -484,8 +463,18 @@ "metadata": {}, "outputs": [], "source": [ - "# Changes the divide behavior to not do integer division\n", - "from __future__ import division\n", + "#################################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "#################################################################################\n", "\n", "# Import Python libraries\n", "import logging\n", @@ -494,7 +483,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -533,7 +530,7 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", " self._state_block_class = AqPhaseStateBlock\n", "\n", @@ -546,7 +543,8 @@ " self.CaSO4 = Solute()\n", " self.H2O = Solvent()\n", "\n", - " # Heat capacity of Water\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", + " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", " initialize=4182,\n", @@ -554,15 +552,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of water\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=997,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -573,19 +568,11 @@ "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_mol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -598,108 +585,21 @@ " whole, rather than individual elements of indexed Property Blocks.\n", " \"\"\"\n", "\n", - " def initialize(\n", - " self,\n", - " state_args=None,\n", - " state_vars_fixed=False,\n", - " hold_state=False,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - " ):\n", - " \"\"\"\n", - " Initialization routine for property package.\n", - "\n", - " Keyword Arguments:\n", - " state_args : Dictionary with initial guesses for the state vars\n", - " chosen. Note that if this method is triggered\n", - " through the control volume, and if initial guesses\n", - " were not provided at the unit model level, the\n", - " control volume passes the inlet values as initial\n", - " guess.The keys for the state_args dictionary are:\n", - " flow_mol_comp : value at which to initialize component flows (default=None)\n", - " pressure : value at which to initialize pressure (default=None)\n", - " temperature : value at which to initialize temperature (default=None)\n", - " outlvl : sets output level of initialization routine\n", - " state_vars_fixed: Flag to denote if state vars have already been fixed.\n", - " True - states have already been fixed and\n", - " initialization does not need to worry\n", - " about fixing and unfixing variables.\n", - " False - states have not been fixed. The state\n", - " block will deal with fixing/unfixing.\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default solver)\n", - " hold_state : flag indicating whether the initialization routine\n", - " should unfix any state variables fixed during\n", - " initialization (default=False).\n", - " True - states variables are not unfixed, and\n", - " a dict of returned containing flags for\n", - " which states were fixed during initialization.\n", - " False - state variables are unfixed after\n", - " initialization by calling the\n", - " release_state method\n", - "\n", - " Returns:\n", - " If hold_states is True, returns a dict containing flags for\n", - " which states were fixed during initialization.\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if state_vars_fixed is False:\n", - " # Fix state variables if not already fixed\n", - " flags = fix_state_vars(self, state_args)\n", - "\n", - " else:\n", - " # Check when the state vars are fixed already result in dof 0\n", - " for k in self.keys():\n", - " if degrees_of_freedom(self[k]) != 0:\n", - " raise Exception(\n", - " \"State vars fixed but degrees of freedom \"\n", - " \"for state block is not zero during \"\n", - " \"initialization.\"\n", - " )\n", - "\n", - " if state_vars_fixed is False:\n", - " if hold_state is True:\n", - " return flags\n", - " else:\n", - " self.release_state(flags)\n", - "\n", - " init_log.info(\"Initialization Complete.\")\n", - "\n", - " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", - " \"\"\"\n", - " Method to release state variables fixed during initialization.\n", - "\n", - " Keyword Arguments:\n", - " flags : dict containing information of which state variables\n", - " were fixed during initialization, and should now be\n", - " unfixed. This dict is returned by initialize if\n", - " hold_state=True.\n", - " outlvl : sets output level of logging\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if flags is None:\n", - " return\n", - " # Unfix state variables\n", - " revert_state_vars(self, flags)\n", - " init_log.info(\"State Released.\")\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", "\n", "\n", "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_StateBlock)\n", "class AqPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for aqueous phase for liquid liquid extraction\n", + " An example property package for ideal gas properties with Gibbs energy\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(AqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", @@ -707,17 +607,17 @@ " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", "\n", - " salts_conc = {\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1}\n", " self.conc_mass_comp = Var(\n", - " salts_conc.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", - " initialize=1,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", + "\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", " initialize=1,\n", @@ -738,7 +638,7 @@ " if j == \"H2O\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -758,9 +658,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -810,13 +707,9 @@ "# Import Pyomo libraries\n", "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", "from pyomo.environ import (\n", - " Reference,\n", - " Var,\n", " value,\n", " Constraint,\n", - " units as pyunits,\n", " check_optimal_termination,\n", - " Suffix,\n", ")\n", "\n", "# Import IDAES cores\n", @@ -836,7 +729,6 @@ ")\n", "\n", "import idaes.logger as idaeslog\n", - "from idaes.core.util import scaling as iscale\n", "from idaes.core.solvers import get_solver\n", "from idaes.core.util.model_statistics import degrees_of_freedom\n", "from idaes.core.util.exceptions import ConfigurationError, InitializationError" @@ -848,28 +740,21 @@ "source": [ "## 3.2 Creating the unit model\n", "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the config arguments for the control volume. The config arguments includes the following properties:\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", "\n", - "1. `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "2. `energy_balance_type` - Indicates what type of energy balance should be constructed\n", - "3. `momentum_balance_type` - Indicates what type of momentum balance should be constructed\n", - "4. `has_heat_transfer` - Indicates whether terms for heat transfer should be constructed\n", - "5. `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", "constructed\n", - "6. `has_equilibrium_reactions` - Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed\n", - "7. `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", "constructed\n", - "8. `has_heat_of_reaction` - Indicates whether terms for heat of reaction terms should be\n", - "constructed\n", - "9. `Liquid Property` - Property parameter object used to define property calculations\n", - "for the liquid phase\n", - "10. `Liquid Property Arguments` - Arguments to use for constructing liquid phase properties\n", - "11. `Aqueous Property` - Property parameter object used to define property calculations\n", + "- `Organic Property` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", + "- `Aqueous Property` - Property parameter object used to define property calculations\n", "for the aqueous phase\n", - "12. `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "13. `Reaction Package` - Reaction parameter object used to define reaction calculations\n", - "14. `Reaction Package Arguments` - Arguments to use for constructing reaction packages\n" + "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." ] }, { @@ -893,62 +778,15 @@ " domain=In(MaterialBalanceType),\n", " description=\"Material balance construction flag\",\n", " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - "**default** - MaterialBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**MaterialBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**MaterialBalanceType.none** - exclude material balances,\n", - "**MaterialBalanceType.componentPhase** - use phase component balances,\n", - "**MaterialBalanceType.componentTotal** - use total component balances,\n", - "**MaterialBalanceType.elementTotal** - use total element balances,\n", - "**MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"energy_balance_type\",\n", - " ConfigValue(\n", - " default=EnergyBalanceType.useDefault,\n", - " domain=In(EnergyBalanceType),\n", - " description=\"Energy balance construction flag\",\n", - " doc=\"\"\"Indicates what type of energy balance should be constructed,\n", - "**default** - EnergyBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**EnergyBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**EnergyBalanceType.none** - exclude energy balances,\n", - "**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,\n", - "**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,\n", - "**EnergyBalanceType.energyTotal** - single energy balance for material,\n", - "**EnergyBalanceType.energyPhase** - energy balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"momentum_balance_type\",\n", - " ConfigValue(\n", - " default=MomentumBalanceType.pressureTotal,\n", - " domain=In(MomentumBalanceType),\n", - " description=\"Momentum balance construction flag\",\n", - " doc=\"\"\"Indicates what type of momentum balance should be constructed,\n", - "**default** - MomentumBalanceType.pressureTotal.\n", - "**Valid values:** {\n", - "**MomentumBalanceType.none** - exclude momentum balances,\n", - "**MomentumBalanceType.pressureTotal** - single pressure balance for material,\n", - "**MomentumBalanceType.pressurePhase** - pressure balances for each phase,\n", - "**MomentumBalanceType.momentumTotal** - single momentum balance for material,\n", - "**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_heat_transfer\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat transfer term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat transfer should be constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat transfer terms,\n", - "**False** - exclude heat transfer terms.}\"\"\",\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -958,25 +796,11 @@ " domain=Bool,\n", " description=\"Pressure change term construction flag\",\n", " doc=\"\"\"Indicates whether terms for pressure change should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include pressure change terms,\n", - "**False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_equilibrium_reactions\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Equilibrium reaction construction flag\",\n", - " doc=\"\"\"Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed,\n", - "**default** - True.\n", - "**Valid values:** {\n", - "**True** - include equilibrium reaction terms,\n", - "**False** - exclude equilibrium reaction terms.}\"\"\",\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -986,51 +810,37 @@ " domain=Bool,\n", " description=\"Phase equilibrium construction flag\",\n", " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - "constructed,\n", - "**default** = False.\n", - "**Valid values:** {\n", - "**True** - include phase equilibrium terms\n", - "**False** - exclude phase equilibrium terms.}\"\"\",\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"has_heat_of_reaction\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat of reaction term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat of reaction terms should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat of reaction terms,\n", - "**False** - exclude heat of reaction terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"liquid_property_package\",\n", + " \"organic_property_package\",\n", " ConfigValue(\n", " default=useDefault,\n", " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for liquid phase\",\n", + " description=\"Property package to use for organic phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the liquid phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"liquid_property_package_args\",\n", + " \"organic_property_package_args\",\n", " ConfigBlock(\n", " implicit=True,\n", - " description=\"Arguments to use for constructing liquid phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to liquid phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1040,11 +850,11 @@ " domain=is_physical_parameter_block,\n", " description=\"Property package to use for aqueous phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the aqueous phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1053,35 +863,10 @@ " implicit=True,\n", " description=\"Arguments to use for constructing aqueous phase properties\",\n", " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package\",\n", - " ConfigValue(\n", - " default=None,\n", - " domain=is_reaction_parameter_block,\n", - " description=\"Reaction package to use for control volume\",\n", - " doc=\"\"\"Reaction parameter object used to define reaction calculations,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "**None** - no reaction package,\n", - "**ReactionParameterBlock** - a ReactionParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing reaction packages\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to a reaction block(s)\n", - "and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see reaction package for documentation.}\"\"\",\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )" ] @@ -1098,25 +883,25 @@ "\n", "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Liq' for the liquid phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the liquid phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, the hold-up in the block, and the property package, along with property package arguments. \n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the liquid property package\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_rate_reactions`, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", "\n", "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", "\n", "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is reponsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", "\n", - "Similarly, `add_energy_balance` and `add_momentum_balance` functions are added to the control volume to create respective equations. This concludes the creation of liquid phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", "\n", "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", "\n", "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the liquid phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{liq} = - mass\\_transfer\\_term_{aq} $\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", "\n", "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." ] @@ -1136,9 +921,7 @@ " None\n", " \"\"\"\n", " # Call UnitModel.build to setup dynamics\n", - " super(LiqExtractionData, self).build()\n", - "\n", - " self.scaling_factor = Suffix(direction=Suffix.EXPORT)\n", + " super().build()\n", "\n", " # Check phase lists match assumptions\n", " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", @@ -1146,61 +929,46 @@ " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", " f\"phase property package have a single phase named 'Aq'\"\n", " )\n", - " if self.config.liquid_property_package.phase_list != [\"Liq\"]:\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", - " f\"phase property package have a single phase named 'Liq'\"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", " )\n", "\n", " # Check for at least one common component in component lists\n", " if not any(\n", " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.liquid_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", " f\"and aqueous phase property packages have at least one \"\n", " f\"common component.\"\n", " )\n", "\n", - " self.liquid_phase = ControlVolume0DBlock(\n", + " self.organic_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", - " property_package=self.config.liquid_property_package,\n", - " property_package_args=self.config.liquid_property_package_args,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", " )\n", "\n", - " self.liquid_phase.add_state_blocks(\n", + " self.organic_phase.add_state_blocks(\n", " has_phase_equilibrium=self.config.has_phase_equilibrium\n", " )\n", "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", " # be handled at the unit model level, thus has_phase_equilibrium is\n", " # False, but has_mass_transfer is True.\n", "\n", - " self.liquid_phase.add_material_balances(\n", + " self.organic_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", - " has_equilibrium_reactions=self.config.has_equilibrium_reactions,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", - "\n", - " self.liquid_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.liquid_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", - "\n", " # ---------------------------------------------------------------------\n", + "\n", " self.aqueous_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", " property_package=self.config.aqueous_property_package,\n", " property_package_args=self.config.aqueous_property_package_args,\n", " )\n", @@ -1215,46 +983,41 @@ "\n", " self.aqueous_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", + " # has_rate_reactions=False,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", "\n", - " self.aqueous_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.aqueous_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", + " self.aqueous_phase.add_geometry()\n", "\n", " # ---------------------------------------------------------------------\n", " # Check flow basis is compatable\n", " t_init = self.flowsheet().time.first()\n", " if (\n", " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.liquid_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} aqueous and liquid property packages must use the \"\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", " f\"same material flow basis.\"\n", " )\n", "\n", + " self.organic_phase.add_geometry()\n", + "\n", " # Add Ports\n", - " self.add_inlet_port(name=\"liquid_inlet\", block=self.liquid_phase, doc=\"Liquid feed\")\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", " self.add_inlet_port(\n", " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", " )\n", " self.add_outlet_port(\n", - " name=\"liquid_outlet\", block=self.liquid_phase, doc=\"Liquid Outlet\"\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", " )\n", " self.add_outlet_port(\n", " name=\"aqueous_outlet\",\n", " block=self.aqueous_phase,\n", - " doc=\"Aqueous Outlet\",\n", + " doc=\"Aqueous outlet\",\n", " )\n", "\n", " # ---------------------------------------------------------------------\n", @@ -1262,16 +1025,16 @@ " # First, need the union and intersection of component lists\n", " all_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " | self.liquid_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", " )\n", " common_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " & self.liquid_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", " )\n", "\n", " # Get units for unit conversion\n", " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.liquid_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", "\n", " if flow_basis == MaterialFlowBasis.mass:\n", @@ -1285,18 +1048,17 @@ " )\n", "\n", " # Material balances\n", - " def rule_material_liq_balance(self, t, j):\n", + " def rule_material_aq_balance(self, t, j):\n", " if j in common_comps:\n", " return self.aqueous_phase.mass_transfer_term[\n", " t, \"Aq\", j\n", - " ] == -self.liquid_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_mass_comp(j)\n", - " / self.aqueous_phase.properties_in[t].get_flow_rate()\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", " )\n", - " elif j in self.liquid_phase.properties_out.component_list:\n", + " elif j in self.organic_phase.properties_out.component_list:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * lunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", " elif j in self.aqueous_phase.properties_out.component_list:\n", " # No mass transfer term\n", " # Set aqueous flowrate to an arbitary small value\n", @@ -1305,27 +1067,26 @@ " self.material_aq_balance = Constraint(\n", " self.flowsheet().time,\n", " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances for aq\",\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", " )\n", "\n", - " def rule_material_aq_balance(self, t, j):\n", - " print(t)\n", + " def rule_material_liq_balance(self, t, j):\n", " if j in common_comps:\n", " return (\n", - " self.liquid_phase.mass_transfer_term[t, \"Liq\", j]\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", " )\n", " else:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * aunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", "\n", - " self.material_liq_balance = Constraint(\n", + " self.material_org_balance = Constraint(\n", " self.flowsheet().time,\n", - " self.liquid_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Liq\",\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", " )" ] }, @@ -1335,152 +1096,1175 @@ "source": [ "### Initialization Routine\n", "\n", - "After writing the unit model it is crucial to develop the initialization routine, as non-linear models may encounter local minima or infeasibility if not initialized properly. Thus, we introduce the function `initialize_build`, serving as the initialization routine for this unit model.\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", "\n", - "The initialize function accepts `liquid_state_args` and `aqueous_state_args` as inputs, along with the output level for the logger, solver, and solver arguments. The initialization routine unfolds in four steps:\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", "\n", - "1. Initialize the Liquid Phase: This involves initializing the state variables and constraints associated with the liquid phase.\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", "\n", - "2. Initialize the Aqueous Phase: Similarly, the state variables and constraints for the aqueous phase are initialized.\n", + "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", "\n", - "3. Solve the Entire Model: The entire model is solved. If the first attempt does not yield an optimal solution, a second attempt is made, and the results are logged.\n", "\n", - "4. Release the Inlet State Variables: The inlet state variables are released.\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_doc.md). The next sections will deal with the diagonistics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "So, now we have all the components ready and to be used in the flowsheet, once the flowsheet is ready, we need to pass that through DiagnosticsToolbox. This will help us understand the structural and numerical problems if at all with the model. \n", "\n", - "After step 4 releases the inlet state variables, and a final check is performed to verify if the results are optimal and an error is raised if the results are not optimal. This four-step process in the initialization routine aims to enhance the likelihood of obtaining a robust and feasible solution for the unit model. This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_doc.md). The next section will deal with the testing of the property package and unit model. " + "For this we start with the flowsheet with just the liquid liquid extractor model in it. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n" + ] + } + ], "source": [ - "def initialize_build(\n", - " self,\n", - " liquid_state_args=None,\n", - " aqueous_state_args=None,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - "):\n", - " \"\"\"\n", - " Initialization routine for Liquid Liquid Extractor unit model.\n", - "\n", - " Keyword Arguments:\n", - " liquid_state_args : a dict of arguments to be passed to the\n", - " liquid property packages to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " aqueous_state_args : a dict of arguments to be passed to the\n", - " aqueous property package to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " outlvl : sets output level of initialization routine\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default IDAES solver)\n", + "import pyomo.environ as pyo\n", + "import idaes.core\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "from pyomo.network import Arc\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.initialization import InitializationStatus\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from Org_property import OrgPhase\n", + "from Aq_property import AqPhase\n", + "from liquid_extraction import LiqExtraction\n", "\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " if optarg is None:\n", - " optarg = {}\n", - "\n", - " # Check DOF\n", - " if degrees_of_freedom(self) != 0:\n", - " raise InitializationError(\n", - " f\"{self.name} degrees of freedom were not 0 at the beginning \"\n", - " f\"of initialization. DoF = {degrees_of_freedom(self)}\"\n", - " )\n", + "m = pyo.ConcreteModel()\n", + "m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + "m.fs.org_properties = OrgPhase()\n", + "m.fs.aq_properties = AqPhase()\n", "\n", - " # Set solver options\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"unit\")\n", - " solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag=\"unit\")\n", + "m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + ")\n", + "m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "\n", + "m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * pyo.units.g / pyo.units.L)\n", + "\n", + "initializer = BlockTriangularizationInitializer()\n", + "initializer.initialize(m.fs.lex)\n", + "assert initializer.summary[m.fs.lex][\"status\"] == InitializationStatus.Ok" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once the flowsheet is ready, we can import the DiagnosticsToolbox from IDAES and run the Python help function on it see the decomentation. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class DiagnosticsToolbox in module idaes.core.util.model_diagnostics:\n", + "\n", + "class DiagnosticsToolbox(builtins.object)\n", + " | DiagnosticsToolbox(model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | \n", + " | The IDAES Model DiagnosticsToolbox.\n", + " | \n", + " | To get started:\n", + " | \n", + " | 1. Create an instance of your model (this does not need to be initialized yet).\n", + " | 2. Fix variables until you have 0 degrees of freedom. Many of these tools presume\n", + " | a square model, and a square model should always be the foundation of any more\n", + " | advanced model.\n", + " | 3. Create an instance of the DiagnosticsToolbox and provide the model to debug as\n", + " | the model argument.\n", + " | 4. Call the ``report_structural_issues()`` method.\n", + " | \n", + " | Model diagnostics is an iterative process and you will likely need to run these\n", + " | tools multiple times to resolve all issues. After making a change to your model,\n", + " | you should always start from the beginning again to ensure the change did not\n", + " | introduce any new issues; i.e., always start from the report_structural_issues()\n", + " | method.\n", + " | \n", + " | Note that structural checks do not require the model to be initialized, thus users\n", + " | should start with these. Numerical checks require at least a partial solution to the\n", + " | model and should only be run once all structural issues have been resolved.\n", + " | \n", + " | Report methods will print a summary containing three parts:\n", + " | \n", + " | 1. Warnings - these are critical issues that should be resolved before continuing.\n", + " | For each warning, a method will be suggested in the Next Steps section to get\n", + " | additional information.\n", + " | 2. Cautions - these are things that could be correct but could also be the source of\n", + " | solver issues. Not all cautions need to be addressed, but users should investigate\n", + " | each one to ensure that the behavior is correct and that they will not be the source\n", + " | of difficulties later. Methods exist to provide more information on all cautions,\n", + " | but these will not appear in the Next Steps section.\n", + " | 3. Next Steps - these are recommended methods to call from the DiagnosticsToolbox to\n", + " | get further information on warnings. If no warnings are found, this will suggest\n", + " | the next report method to call.\n", + " | \n", + " | Args:\n", + " | \n", + " | model: model to be diagnosed. The DiagnosticsToolbox does not support indexed Blocks.\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | variable_bounds_absolute_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_relative_tolerance: float, default=0.0001\n", + " | Relative tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_violation_tolerance: float, default=0\n", + " | Absolute tolerance for considering a variable to violate its bounds.\n", + " | Some solvers relax bounds on variables thus allowing a small violation\n", + " | to be considered acceptable.\n", + " | \n", + " | constraint_residual_tolerance: float, default=1e-05\n", + " | Absolute tolerance to use when checking constraint residuals.\n", + " | \n", + " | variable_large_value_tolerance: float, default=10000.0\n", + " | Absolute tolerance for considering a value to be large.\n", + " | \n", + " | variable_small_value_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a value to be small.\n", + " | \n", + " | variable_zero_value_tolerance: float, default=1e-08\n", + " | Absolute tolerance for considering a value to be near to zero.\n", + " | \n", + " | jacobian_large_value_caution: float, default=10000.0\n", + " | Tolerance for raising a caution for large Jacobian values.\n", + " | \n", + " | jacobian_large_value_warning: float, default=100000000.0\n", + " | Tolerance for raising a warning for large Jacobian values.\n", + " | \n", + " | jacobian_small_value_caution: float, default=0.0001\n", + " | Tolerance for raising a caution for small Jacobian values.\n", + " | \n", + " | jacobian_small_value_warning: float, default=1e-08\n", + " | Tolerance for raising a warning for small Jacobian values.\n", + " | \n", + " | warn_for_evaluation_error_at_bounds: bool, default=True\n", + " | If False, warnings will not be generated for things like log(x) with x\n", + " | >= 0\n", + " | \n", + " | parallel_component_tolerance: float, default=0.0001\n", + " | Tolerance for identifying near-parallel Jacobian rows/columns\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | Initialize self. See help(type(self)) for accurate signature.\n", + " | \n", + " | assert_no_numerical_warnings(self)\n", + " | Checks for numerical warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by numerical analysis.\n", + " | \n", + " | assert_no_structural_warnings(self)\n", + " | Checks for structural warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by structural analysis.\n", + " | \n", + " | display_components_with_inconsistent_units(self, stream=None)\n", + " | Prints a list of all Constraints, Expressions and Objectives in the\n", + " | model with inconsistent units of measurement.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_extreme_jacobians(self, stream=None)\n", + " | Prints the constraints associated with rows in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled constraints.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_large_residuals(self, stream=None)\n", + " | Prints a list of Constraints with residuals greater than a specified tolerance.\n", + " | Tolerance can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_external_variables(self, stream=None)\n", + " | Prints a list of variables that appear within activated Constraints in the\n", + " | model but are not contained within the model themselves.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_extreme_jacobian_entries(self, stream=None)\n", + " | Prints variables and constraints associated with entries in the Jacobian with extreme\n", + " | values. This can be indicative of poor scaling, especially for isolated terms (e.g.\n", + " | variables which appear only in one term of a single constraint).\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_constraints(self, stream=None)\n", + " | Display near-parallel (duplicate) constraints in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_variables(self, stream=None)\n", + " | Display near-parallel (duplicate) variables in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_overconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the over-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the over-defined part of a model and thus\n", + " | where constraints must be removed or variables unfixed.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_potential_evaluation_errors(self, stream=None)\n", + " | Prints constraints that may be prone to evaluation errors\n", + " | (e.g., log of a negative number) based on variable bounds.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_underconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the under-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the under-defined part of a model and thus\n", + " | where additional information (fixed variables or constraints) are required.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_unused_variables(self, stream=None)\n", + " | Prints a list of variables that do not appear in any activated Constraints.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_at_or_outside_bounds(self, stream=None)\n", + " | Prints a list of variables with values that fall at or outside the bounds\n", + " | on the variable.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_fixed_to_zero(self, stream=None)\n", + " | Prints a list of variables that are fixed to an absolute value of 0.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_near_bounds(self, stream=None)\n", + " | Prints a list of variables with values close to their bounds. Tolerance can\n", + " | be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_jacobians(self, stream=None)\n", + " | Prints the variables associated with columns in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled variables.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_values(self, stream=None)\n", + " | Prints a list of variables with extreme values.\n", + " | \n", + " | Tolerances can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_none_value(self, stream=None)\n", + " | Prints a list of variables with a value of None.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_value_near_zero(self, stream=None)\n", + " | Prints a list of variables with a value close to zero. The tolerance\n", + " | for determining what is close to zero can be set in the class configuration\n", + " | options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | get_dulmage_mendelsohn_partition(self)\n", + " | Performs a Dulmage-Mendelsohn partitioning on the model and returns\n", + " | the over- and under-constrained sub-problems.\n", + " | \n", + " | Returns:\n", + " | list-of-lists variables in each independent block of the under-constrained set\n", + " | list-of-lists constraints in each independent block of the under-constrained set\n", + " | list-of-lists variables in each independent block of the over-constrained set\n", + " | list-of-lists constraints in each independent block of the over-constrained set\n", + " | \n", + " | prepare_degeneracy_hunter(self, **kwargs)\n", + " | Create an instance of the DegeneracyHunter and store as self.degeneracy_hunter.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | report_irreducible_degenerate_sets.\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of DegeneracyHunter\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | solver: str, default='scip'\n", + " | MILP solver to use for finding irreducible degenerate sets.\n", + " | \n", + " | solver_options: optional\n", + " | Options to pass to MILP solver.\n", + " | \n", + " | M: float, default=100000.0\n", + " | Maximum value for nu in MILP models.\n", + " | \n", + " | m_small: float, default=1e-05\n", + " | Smallest value for nu to be considered non-zero in MILP models.\n", + " | \n", + " | trivial_constraint_tolerance: float, default=1e-06\n", + " | Tolerance for identifying non-zero rows in Jacobian.\n", + " | \n", + " | prepare_svd_toolbox(self, **kwargs)\n", + " | Create an instance of the SVDToolbox and store as self.svd_toolbox.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | display_underdetermined_variables_and_constraints().\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of SVDToolbox\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | number_of_smallest_singular_values: PositiveInt, optional\n", + " | Number of smallest singular values to compute\n", + " | \n", + " | svd_callback: svd_callback_validator, default=\n", + " | Callback to SVD method of choice (default = svd_dense). Callbacks\n", + " | should take the Jacobian and number of singular values to compute as\n", + " | options, plus any method specific arguments, and should return the u,\n", + " | s and v matrices as numpy arrays.\n", + " | \n", + " | svd_callback_arguments: dict, optional\n", + " | Optional arguments to pass to SVD callback (default = None)\n", + " | \n", + " | singular_value_tolerance: float, default=1e-06\n", + " | Tolerance for defining a small singular value\n", + " | \n", + " | size_cutoff_in_singular_vector: float, default=0.1\n", + " | Size below which to ignore constraints and variables in the singular\n", + " | vector\n", + " | \n", + " | report_numerical_issues(self, stream=None)\n", + " | Generates a summary report of any numerical issues identified in the model provided\n", + " | and suggest next steps for debugging model.\n", + " | \n", + " | Numerical checks should only be performed once all structural issues have been resolved,\n", + " | and require that at least a partial solution to the model is available.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | report_structural_issues(self, stream=None)\n", + " | Generates a summary report of any structural issues identified in the model provided\n", + " | and suggests next steps for debugging the model.\n", + " | \n", + " | This should be the first method called when debugging a model and after any change\n", + " | is made to the model. These checks can be run before trying to initialize and solve\n", + " | the model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Readonly properties defined here:\n", + " | \n", + " | model\n", + " | Model currently being diagnosed.\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + "\n" + ] + } + ], + "source": [ + "from idaes.core.util import DiagnosticsToolbox\n", "\n", - " solverobj = get_solver(solver, optarg)\n", + "help(DiagnosticsToolbox)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Initialize liquid phase control volume block\n", - " flags = self.liquid_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=liquid_state_args,\n", - " hold_state=True,\n", - " )\n", + "Here's a breakdown of the steps to start with:\n", "\n", - " init_log.info_high(\"Initialization Step 1 Complete.\")\n", - " # ---------------------------------------------------------------------\n", - " # Initialize aqueous phase state block\n", - " if aqueous_state_args is None:\n", - " t_init = self.flowsheet().time.first()\n", - " aqueous_state_args = {}\n", - " aq_state_vars = self.aqueous_phase[t_init].define_state_vars()\n", - "\n", - " liq_state = self.liquid_phase.properties_out[t_init]\n", - "\n", - " # Check for unindexed state variables\n", - " for sv in aq_state_vars:\n", - " if \"flow\" in sv:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", - " elif \"conc\" in sv:\n", - " # Flow is indexed by component\n", - " aqueous_state_args[sv] = {}\n", - " for j in aq_state_vars[sv]:\n", - " if j in liq_state.component_list:\n", - " aqueous_state_args[sv][j] = 1e3 * value(\n", - " getattr(liq_state, sv)[j]\n", - " )\n", - " else:\n", - " aqueous_state_args[sv][j] = 0.5\n", - "\n", - " elif \"pressure\" in sv:\n", - " aqueous_state_args[sv] = 1 * value(getattr(liq_state, sv))\n", + "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", "\n", - " else:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", "\n", - " self.aqueous_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=aqueous_state_args,\n", - " hold_state=False,\n", - " )\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", "\n", - " init_log.info_high(\"Initialization Step 2 Complete.\")\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # # Solve unit model\n", - " with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " if not check_optimal_termination(results):\n", - " init_log.warning(\n", - " f\"Trouble solving unit model {self.name}, trying one more time\"\n", - " )\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " init_log.info_high(\"Initialization Step 3 {}.\".format(idaeslog.condition(results)))\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393950243481484e-07 5.1393950243481484e-07\n", + "Constraint violation....: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06857442855834961}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 8 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 8 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Release Inlet state\n", - " self.liquid_phase.release_state(flags, outlvl)\n", - " self.aqueous_phase.release_state(flags, outlvl)\n", - " if not check_optimal_termination(results):\n", - " raise InitializationError(\n", - " f\"{self.name} failed to initialize successfully. Please check \"\n", - " f\"the output logs for more information.\"\n", - " )\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[NaCl] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[KNO3] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[CaSO4] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94834E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284418857944 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", "\n", - " init_log.info(\"Initialization Complete: {}\".format(idaeslog.condition(results)))" + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Testing\n", + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnigns we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 5.33e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0767662525177002}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. Just to be sure we would check if there are any more numerical issues by calling `report_numerical_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 WARNINGS\n", + "\n", + " WARNING: 3 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here there are some numerical issues with the variables at or outside the bounds which is a physical condition of being a pure stream and thus the salt concentration would be 0. Thus baring this there are no more issues, and thus we can say that the model has been debugged. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_doc.md).\n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", "\n", "There are typically 3 types of tests:\n", "\n", @@ -1495,7 +2279,7 @@ "\n", "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", "\n", - "## 4.1 Property package\n", + "## 5.1 Property package\n", "### Unit Tests\n", "\n", "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", @@ -1511,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -1520,7 +2304,7 @@ "from pyomo.util.check_units import assert_units_consistent\n", "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", "\n", - "from Liq_property import LiqPhase\n", + "from Org_property import OrgPhase\n", "from Aq_property import AqPhase\n", "from liquid_extraction import LiqExtraction\n", "from idaes.core.solvers import get_solver\n", @@ -1532,7 +2316,7 @@ " @pytest.fixture(scope=\"class\")\n", " def model(self):\n", " model = ConcreteModel()\n", - " model.params = LiqPhase()\n", + " model.params = AqPhase()\n", " return model\n", "\n", " @pytest.mark.unit\n", @@ -1541,8 +2325,6 @@ "\n", " @pytest.mark.unit\n", " def test_build(self, model):\n", - " assert model.params.state_block_class is AqPhaseStateBlock\n", - "\n", " assert len(model.params.phase_list) == 1\n", " for i in model.params.phase_list:\n", " assert i == \"Aq\"\n", @@ -1551,11 +2333,11 @@ " for i in model.params.component_list:\n", " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", "\n", - " assert isinstance(model.params.cp_mol, Param)\n", - " assert value(model.params.cp_mol) == 4182\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", "\n", - " assert isinstance(model.params.dens_mol, Param)\n", - " assert value(model.params.dens_mol) == 997\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", "\n", " assert isinstance(model.params.temperature_ref, Param)\n", " assert value(model.params.temperature_ref) == 298.15" @@ -1571,12 +2353,12 @@ "\n", "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the liquid property package to ensure consistency and reliability across both packages." + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -1601,10 +2383,6 @@ " assert isinstance(model.props[1].conc_mass_comp, Var)\n", " assert len(model.props[1].conc_mass_comp) == 3\n", "\n", - " for i in model.props[1].conc_mass_comp:\n", - " print(value(model.props[1].conc_mass_comp[i]))\n", - " assert value(model.props[1].conc_mass_comp[i]) == 1\n", - "\n", " @pytest.mark.unit\n", " def test_initialize(self, model):\n", " assert not model.props[1].flow_vol.fixed\n", @@ -1636,7 +2414,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -1658,7 +2436,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -1688,29 +2466,24 @@ "def test_config():\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 16\n", + " assert len(m.fs.unit.config) == 9\n", "\n", " # Check for config arguments\n", " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert m.fs.unit.config.energy_balance_type == EnergyBalanceType.useDefault\n", - " assert m.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal\n", - " assert not m.fs.unit.config.has_heat_transfer\n", " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_equilibrium_reactions\n", " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert not m.fs.unit.config.has_heat_of_reaction\n", - " assert m.fs.unit.config.liquid_property_package is m.fs.liq_properties\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", "\n", " # Check for unit initializer\n", @@ -1726,7 +2499,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -1735,29 +2508,29 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", "\n", " return m\n", "\n", @@ -1772,12 +2545,12 @@ " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_inlet\")\n", - " assert len(model.fs.unit.liquid_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", @@ -1786,18 +2559,18 @@ " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_outlet\")\n", - " assert len(model.fs.unit.liquid_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_liq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", "\n", " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 19" + " assert number_total_constraints(model) == 16" ] }, { @@ -1814,14 +2587,16 @@ "\n", "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered. \n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", "\n", "By performing these checks, we conclude the testing for the unit model. " ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 29, "metadata": {}, "outputs": [], "source": [ @@ -1830,21 +2605,31 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", "\n", " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", @@ -1865,7 +2650,7 @@ " assert check_optimal_termination(results)\n", "\n", " # Checking for outlet flows\n", - " assert value(model.fs.unit.liquid_outlet.flow_vol[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", " 80.0, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", @@ -1874,13 +2659,13 @@ "\n", " # Checking for outlet mass_comp\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", " ) == pytest.approx(0.000187499, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", " ) == pytest.approx(0.000749999, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", " ) == pytest.approx(0.000403124, rel=1e-5)\n", " assert value(\n", " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", @@ -1893,7 +2678,7 @@ " ) == pytest.approx(0.146775, rel=1e-5)\n", "\n", " # Checking for outlet temperature\n", - " assert value(model.fs.unit.liquid_outlet.temperature[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", " 300, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", @@ -1901,7 +2686,7 @@ " )\n", "\n", " # Checking for outlet pressure\n", - " assert value(model.fs.unit.liquid_outlet.pressure[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", " 1, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", @@ -1909,19 +2694,24 @@ " )\n", "\n", " # Fixed state variables\n", - " assert model.fs.unit.liquid_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.liquid_inlet.temperature[0].fixed\n", - " assert model.fs.unit.liquid_inlet.pressure[0].fixed\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", "\n", " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed" + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" ] } ], diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_test.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_test.ipynb index cad1e4db..facc45ed 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_test.ipynb @@ -37,21 +37,18 @@ "\n", "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", "- Steady-state only\n", - "- Liquid phase property package has a single phase named Liq\n", + "- Organic phase property package has a single phase named Org\n", "- Aquoeus phase property package has a single phase named Aq\n", - "- Liquid and Aqueous phase properties need not have the same component list. \n", + "- Organic and Aqueous phase properties need not have the same component list. \n", "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the liquid phase (Liq). \n", - "\n" + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Creating Liquid Property Package\n", + "# 1. Creating Organic Property Package\n", "\n", "Creating a property package is a 4 step process\n", "- Import necessary libraries \n", @@ -76,7 +73,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -110,14 +115,13 @@ "\n", "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", "\n", - "The Physical Parameter Block then refers to the `state block` in this case `LiqPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the liquid phase, we will assign the Phase as LiquidPhase and the variable will be named Liq as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + "The Physical Parameter Block then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the liquid phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). This method in turn needs to call two predefined methods (inherited from underlying base classes):\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", "\n", - "- `obj.add_properties()` is used to set the metadata regarding the supported properties, and here we define flow volume, pressure, temperature, and mass fraction.\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default." + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " ] }, { @@ -126,13 +130,13 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhase\")\n", + "@declare_process_block_class(\"OrgPhase\")\n", "class PhysicalParameterData(PhysicalParameterBlock):\n", " \"\"\"\n", " Property Parameter Block Class\n", "\n", " Contains parameters and indexing sets associated with properties for\n", - " liquid Phase\n", + " organic Phase\n", "\n", " \"\"\"\n", "\n", @@ -140,21 +144,22 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", - " self._state_block_class = LiqPhaseStateBlock\n", + " self._state_block_class = OrgPhaseStateBlock\n", "\n", " # List of valid phases in property package\n", - " self.Liq = LiquidPhase()\n", + " self.Org = LiquidPhase()\n", "\n", " # Component list - a list of component identifiers\n", " self.NaCl = Solute()\n", " self.KNO3 = Solute()\n", " self.CaSO4 = Solute()\n", - " self.C2H4Br2 = (\n", + " self.solvent = (\n", " Solvent()\n", " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", "\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", @@ -163,15 +168,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of solvent\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=2170,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference Temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -179,29 +181,20 @@ " doc=\"Reference temperature\",\n", " units=units.K,\n", " )\n", - "\n", - " # Distribution Factor\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.diffusion_factor = Param(\n", - " salts_d.keys(), initialize=salts_d, within=PositiveReals\n", + " self.solutes,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", " )\n", "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_vol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " \"conc_mass_comp\": {\"method\": None},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -216,16 +209,15 @@ "\n", "After the `Physical Parameter Block` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc. \n", "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization. \n", - "\n", - "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively. \n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization.\n", "\n", - "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete. \n", + "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively.\n", "\n", + "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete.\n", "\n", - "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged. \n", + "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged.\n", "\n", - "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `LiqPhaseStateBlockData` class. " + "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `OrgPhaseStateBlockData` class." ] }, { @@ -251,7 +243,6 @@ " ):\n", " \"\"\"\n", " Initialization routine for property package.\n", - "\n", " Keyword Arguments:\n", " state_args : Dictionary with initial guesses for the state vars\n", " chosen. Note that if this method is triggered\n", @@ -282,7 +273,6 @@ " False - state variables are unfixed after\n", " initialization by calling the\n", " release_state method\n", - "\n", " Returns:\n", " If hold_states is True, returns a dict containing flags for\n", " which states were fixed during initialization.\n", @@ -314,7 +304,6 @@ " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", " \"\"\"\n", " Method to release state variables fixed during initialization.\n", - "\n", " Keyword Arguments:\n", " flags : dict containing information of which state variables\n", " were fixed during initialization, and should now be\n", @@ -335,7 +324,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The class `LiqPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `LiqPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `LiqPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", "\n", "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", "\n", @@ -344,7 +333,6 @@ "- `pressure` - state pressure\n", "- `temperature` - state temperature\n", "\n", - "Additionally, a state parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", "\n", @@ -366,33 +354,32 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhaseStateBlock\", block_class=_StateBlock)\n", - "class LiqPhaseStateBlockData(StateBlockData):\n", + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_StateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for liquid phase for liquid liquid extraction\n", + " An example property package for Organic phase for liquid liquid extraction\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(LiqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.flow_vol = Var(\n", " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", " self.conc_mass_comp = Var(\n", - " salts_d.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", " initialize=1,\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", @@ -401,6 +388,7 @@ " units=units.atm,\n", " doc=\"State pressure [atm]\",\n", " )\n", + "\n", " self.temperature = Var(\n", " domain=NonNegativeReals,\n", " initialize=300,\n", @@ -408,18 +396,12 @@ " units=units.K,\n", " doc=\"State temperature [K]\",\n", " )\n", - " self.diffusion_factor = Param(\n", - " salts_d.keys(),\n", - " initialize=salts_d,\n", - " doc=\"Diffusion Factor of salts\",\n", - " within=PositiveReals,\n", - " )\n", "\n", " def material_flow_expression(self, j):\n", " if j == \"solvent\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.conc_mass_comp[j]\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -439,9 +421,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -475,7 +454,7 @@ "source": [ "# 2. Creating Aqueous Property Package\n", "\n", - "The structure of Aqueous Property Package mirrors that of the Liquid Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." ] }, { @@ -484,8 +463,18 @@ "metadata": {}, "outputs": [], "source": [ - "# Changes the divide behavior to not do integer division\n", - "from __future__ import division\n", + "#################################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "#################################################################################\n", "\n", "# Import Python libraries\n", "import logging\n", @@ -494,7 +483,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -533,7 +530,7 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", " self._state_block_class = AqPhaseStateBlock\n", "\n", @@ -546,7 +543,8 @@ " self.CaSO4 = Solute()\n", " self.H2O = Solvent()\n", "\n", - " # Heat capacity of Water\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", + " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", " initialize=4182,\n", @@ -554,15 +552,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of water\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=997,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -573,19 +568,11 @@ "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_mol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -598,108 +585,21 @@ " whole, rather than individual elements of indexed Property Blocks.\n", " \"\"\"\n", "\n", - " def initialize(\n", - " self,\n", - " state_args=None,\n", - " state_vars_fixed=False,\n", - " hold_state=False,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - " ):\n", - " \"\"\"\n", - " Initialization routine for property package.\n", - "\n", - " Keyword Arguments:\n", - " state_args : Dictionary with initial guesses for the state vars\n", - " chosen. Note that if this method is triggered\n", - " through the control volume, and if initial guesses\n", - " were not provided at the unit model level, the\n", - " control volume passes the inlet values as initial\n", - " guess.The keys for the state_args dictionary are:\n", - " flow_mol_comp : value at which to initialize component flows (default=None)\n", - " pressure : value at which to initialize pressure (default=None)\n", - " temperature : value at which to initialize temperature (default=None)\n", - " outlvl : sets output level of initialization routine\n", - " state_vars_fixed: Flag to denote if state vars have already been fixed.\n", - " True - states have already been fixed and\n", - " initialization does not need to worry\n", - " about fixing and unfixing variables.\n", - " False - states have not been fixed. The state\n", - " block will deal with fixing/unfixing.\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default solver)\n", - " hold_state : flag indicating whether the initialization routine\n", - " should unfix any state variables fixed during\n", - " initialization (default=False).\n", - " True - states variables are not unfixed, and\n", - " a dict of returned containing flags for\n", - " which states were fixed during initialization.\n", - " False - state variables are unfixed after\n", - " initialization by calling the\n", - " release_state method\n", - "\n", - " Returns:\n", - " If hold_states is True, returns a dict containing flags for\n", - " which states were fixed during initialization.\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if state_vars_fixed is False:\n", - " # Fix state variables if not already fixed\n", - " flags = fix_state_vars(self, state_args)\n", - "\n", - " else:\n", - " # Check when the state vars are fixed already result in dof 0\n", - " for k in self.keys():\n", - " if degrees_of_freedom(self[k]) != 0:\n", - " raise Exception(\n", - " \"State vars fixed but degrees of freedom \"\n", - " \"for state block is not zero during \"\n", - " \"initialization.\"\n", - " )\n", - "\n", - " if state_vars_fixed is False:\n", - " if hold_state is True:\n", - " return flags\n", - " else:\n", - " self.release_state(flags)\n", - "\n", - " init_log.info(\"Initialization Complete.\")\n", - "\n", - " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", - " \"\"\"\n", - " Method to release state variables fixed during initialization.\n", - "\n", - " Keyword Arguments:\n", - " flags : dict containing information of which state variables\n", - " were fixed during initialization, and should now be\n", - " unfixed. This dict is returned by initialize if\n", - " hold_state=True.\n", - " outlvl : sets output level of logging\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if flags is None:\n", - " return\n", - " # Unfix state variables\n", - " revert_state_vars(self, flags)\n", - " init_log.info(\"State Released.\")\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", "\n", "\n", "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_StateBlock)\n", "class AqPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for aqueous phase for liquid liquid extraction\n", + " An example property package for ideal gas properties with Gibbs energy\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(AqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", @@ -707,17 +607,17 @@ " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", "\n", - " salts_conc = {\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1}\n", " self.conc_mass_comp = Var(\n", - " salts_conc.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", - " initialize=1,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", + "\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", " initialize=1,\n", @@ -738,7 +638,7 @@ " if j == \"H2O\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -758,9 +658,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -810,13 +707,9 @@ "# Import Pyomo libraries\n", "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", "from pyomo.environ import (\n", - " Reference,\n", - " Var,\n", " value,\n", " Constraint,\n", - " units as pyunits,\n", " check_optimal_termination,\n", - " Suffix,\n", ")\n", "\n", "# Import IDAES cores\n", @@ -836,7 +729,6 @@ ")\n", "\n", "import idaes.logger as idaeslog\n", - "from idaes.core.util import scaling as iscale\n", "from idaes.core.solvers import get_solver\n", "from idaes.core.util.model_statistics import degrees_of_freedom\n", "from idaes.core.util.exceptions import ConfigurationError, InitializationError" @@ -848,28 +740,21 @@ "source": [ "## 3.2 Creating the unit model\n", "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the config arguments for the control volume. The config arguments includes the following properties:\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", "\n", - "1. `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "2. `energy_balance_type` - Indicates what type of energy balance should be constructed\n", - "3. `momentum_balance_type` - Indicates what type of momentum balance should be constructed\n", - "4. `has_heat_transfer` - Indicates whether terms for heat transfer should be constructed\n", - "5. `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", "constructed\n", - "6. `has_equilibrium_reactions` - Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed\n", - "7. `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", "constructed\n", - "8. `has_heat_of_reaction` - Indicates whether terms for heat of reaction terms should be\n", - "constructed\n", - "9. `Liquid Property` - Property parameter object used to define property calculations\n", - "for the liquid phase\n", - "10. `Liquid Property Arguments` - Arguments to use for constructing liquid phase properties\n", - "11. `Aqueous Property` - Property parameter object used to define property calculations\n", + "- `Organic Property` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", + "- `Aqueous Property` - Property parameter object used to define property calculations\n", "for the aqueous phase\n", - "12. `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "13. `Reaction Package` - Reaction parameter object used to define reaction calculations\n", - "14. `Reaction Package Arguments` - Arguments to use for constructing reaction packages\n" + "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." ] }, { @@ -893,62 +778,15 @@ " domain=In(MaterialBalanceType),\n", " description=\"Material balance construction flag\",\n", " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - "**default** - MaterialBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**MaterialBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**MaterialBalanceType.none** - exclude material balances,\n", - "**MaterialBalanceType.componentPhase** - use phase component balances,\n", - "**MaterialBalanceType.componentTotal** - use total component balances,\n", - "**MaterialBalanceType.elementTotal** - use total element balances,\n", - "**MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"energy_balance_type\",\n", - " ConfigValue(\n", - " default=EnergyBalanceType.useDefault,\n", - " domain=In(EnergyBalanceType),\n", - " description=\"Energy balance construction flag\",\n", - " doc=\"\"\"Indicates what type of energy balance should be constructed,\n", - "**default** - EnergyBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**EnergyBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**EnergyBalanceType.none** - exclude energy balances,\n", - "**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,\n", - "**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,\n", - "**EnergyBalanceType.energyTotal** - single energy balance for material,\n", - "**EnergyBalanceType.energyPhase** - energy balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"momentum_balance_type\",\n", - " ConfigValue(\n", - " default=MomentumBalanceType.pressureTotal,\n", - " domain=In(MomentumBalanceType),\n", - " description=\"Momentum balance construction flag\",\n", - " doc=\"\"\"Indicates what type of momentum balance should be constructed,\n", - "**default** - MomentumBalanceType.pressureTotal.\n", - "**Valid values:** {\n", - "**MomentumBalanceType.none** - exclude momentum balances,\n", - "**MomentumBalanceType.pressureTotal** - single pressure balance for material,\n", - "**MomentumBalanceType.pressurePhase** - pressure balances for each phase,\n", - "**MomentumBalanceType.momentumTotal** - single momentum balance for material,\n", - "**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_heat_transfer\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat transfer term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat transfer should be constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat transfer terms,\n", - "**False** - exclude heat transfer terms.}\"\"\",\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -958,25 +796,11 @@ " domain=Bool,\n", " description=\"Pressure change term construction flag\",\n", " doc=\"\"\"Indicates whether terms for pressure change should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include pressure change terms,\n", - "**False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_equilibrium_reactions\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Equilibrium reaction construction flag\",\n", - " doc=\"\"\"Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed,\n", - "**default** - True.\n", - "**Valid values:** {\n", - "**True** - include equilibrium reaction terms,\n", - "**False** - exclude equilibrium reaction terms.}\"\"\",\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -986,51 +810,37 @@ " domain=Bool,\n", " description=\"Phase equilibrium construction flag\",\n", " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - "constructed,\n", - "**default** = False.\n", - "**Valid values:** {\n", - "**True** - include phase equilibrium terms\n", - "**False** - exclude phase equilibrium terms.}\"\"\",\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"has_heat_of_reaction\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat of reaction term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat of reaction terms should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat of reaction terms,\n", - "**False** - exclude heat of reaction terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"liquid_property_package\",\n", + " \"organic_property_package\",\n", " ConfigValue(\n", " default=useDefault,\n", " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for liquid phase\",\n", + " description=\"Property package to use for organic phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the liquid phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"liquid_property_package_args\",\n", + " \"organic_property_package_args\",\n", " ConfigBlock(\n", " implicit=True,\n", - " description=\"Arguments to use for constructing liquid phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to liquid phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1040,11 +850,11 @@ " domain=is_physical_parameter_block,\n", " description=\"Property package to use for aqueous phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the aqueous phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1053,35 +863,10 @@ " implicit=True,\n", " description=\"Arguments to use for constructing aqueous phase properties\",\n", " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package\",\n", - " ConfigValue(\n", - " default=None,\n", - " domain=is_reaction_parameter_block,\n", - " description=\"Reaction package to use for control volume\",\n", - " doc=\"\"\"Reaction parameter object used to define reaction calculations,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "**None** - no reaction package,\n", - "**ReactionParameterBlock** - a ReactionParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing reaction packages\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to a reaction block(s)\n", - "and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see reaction package for documentation.}\"\"\",\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )" ] @@ -1098,25 +883,25 @@ "\n", "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Liq' for the liquid phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the liquid phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, the hold-up in the block, and the property package, along with property package arguments. \n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the liquid property package\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_rate_reactions`, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", "\n", "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", "\n", "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is reponsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", "\n", - "Similarly, `add_energy_balance` and `add_momentum_balance` functions are added to the control volume to create respective equations. This concludes the creation of liquid phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", "\n", "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", "\n", "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the liquid phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{liq} = - mass\\_transfer\\_term_{aq} $\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", "\n", "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." ] @@ -1136,9 +921,7 @@ " None\n", " \"\"\"\n", " # Call UnitModel.build to setup dynamics\n", - " super(LiqExtractionData, self).build()\n", - "\n", - " self.scaling_factor = Suffix(direction=Suffix.EXPORT)\n", + " super().build()\n", "\n", " # Check phase lists match assumptions\n", " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", @@ -1146,61 +929,46 @@ " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", " f\"phase property package have a single phase named 'Aq'\"\n", " )\n", - " if self.config.liquid_property_package.phase_list != [\"Liq\"]:\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", - " f\"phase property package have a single phase named 'Liq'\"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", " )\n", "\n", " # Check for at least one common component in component lists\n", " if not any(\n", " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.liquid_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", " f\"and aqueous phase property packages have at least one \"\n", " f\"common component.\"\n", " )\n", "\n", - " self.liquid_phase = ControlVolume0DBlock(\n", + " self.organic_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", - " property_package=self.config.liquid_property_package,\n", - " property_package_args=self.config.liquid_property_package_args,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", " )\n", "\n", - " self.liquid_phase.add_state_blocks(\n", + " self.organic_phase.add_state_blocks(\n", " has_phase_equilibrium=self.config.has_phase_equilibrium\n", " )\n", "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", " # be handled at the unit model level, thus has_phase_equilibrium is\n", " # False, but has_mass_transfer is True.\n", "\n", - " self.liquid_phase.add_material_balances(\n", + " self.organic_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", - " has_equilibrium_reactions=self.config.has_equilibrium_reactions,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", - "\n", - " self.liquid_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.liquid_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", - "\n", " # ---------------------------------------------------------------------\n", + "\n", " self.aqueous_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", " property_package=self.config.aqueous_property_package,\n", " property_package_args=self.config.aqueous_property_package_args,\n", " )\n", @@ -1215,46 +983,41 @@ "\n", " self.aqueous_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", + " # has_rate_reactions=False,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", "\n", - " self.aqueous_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.aqueous_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", + " self.aqueous_phase.add_geometry()\n", "\n", " # ---------------------------------------------------------------------\n", " # Check flow basis is compatable\n", " t_init = self.flowsheet().time.first()\n", " if (\n", " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.liquid_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} aqueous and liquid property packages must use the \"\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", " f\"same material flow basis.\"\n", " )\n", "\n", + " self.organic_phase.add_geometry()\n", + "\n", " # Add Ports\n", - " self.add_inlet_port(name=\"liquid_inlet\", block=self.liquid_phase, doc=\"Liquid feed\")\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", " self.add_inlet_port(\n", " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", " )\n", " self.add_outlet_port(\n", - " name=\"liquid_outlet\", block=self.liquid_phase, doc=\"Liquid Outlet\"\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", " )\n", " self.add_outlet_port(\n", " name=\"aqueous_outlet\",\n", " block=self.aqueous_phase,\n", - " doc=\"Aqueous Outlet\",\n", + " doc=\"Aqueous outlet\",\n", " )\n", "\n", " # ---------------------------------------------------------------------\n", @@ -1262,16 +1025,16 @@ " # First, need the union and intersection of component lists\n", " all_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " | self.liquid_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", " )\n", " common_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " & self.liquid_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", " )\n", "\n", " # Get units for unit conversion\n", " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.liquid_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", "\n", " if flow_basis == MaterialFlowBasis.mass:\n", @@ -1285,18 +1048,17 @@ " )\n", "\n", " # Material balances\n", - " def rule_material_liq_balance(self, t, j):\n", + " def rule_material_aq_balance(self, t, j):\n", " if j in common_comps:\n", " return self.aqueous_phase.mass_transfer_term[\n", " t, \"Aq\", j\n", - " ] == -self.liquid_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_mass_comp(j)\n", - " / self.aqueous_phase.properties_in[t].get_flow_rate()\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", " )\n", - " elif j in self.liquid_phase.properties_out.component_list:\n", + " elif j in self.organic_phase.properties_out.component_list:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * lunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", " elif j in self.aqueous_phase.properties_out.component_list:\n", " # No mass transfer term\n", " # Set aqueous flowrate to an arbitary small value\n", @@ -1305,27 +1067,26 @@ " self.material_aq_balance = Constraint(\n", " self.flowsheet().time,\n", " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances for aq\",\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", " )\n", "\n", - " def rule_material_aq_balance(self, t, j):\n", - " print(t)\n", + " def rule_material_liq_balance(self, t, j):\n", " if j in common_comps:\n", " return (\n", - " self.liquid_phase.mass_transfer_term[t, \"Liq\", j]\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", " )\n", " else:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * aunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", "\n", - " self.material_liq_balance = Constraint(\n", + " self.material_org_balance = Constraint(\n", " self.flowsheet().time,\n", - " self.liquid_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Liq\",\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", " )" ] }, @@ -1335,152 +1096,1175 @@ "source": [ "### Initialization Routine\n", "\n", - "After writing the unit model it is crucial to develop the initialization routine, as non-linear models may encounter local minima or infeasibility if not initialized properly. Thus, we introduce the function `initialize_build`, serving as the initialization routine for this unit model.\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", "\n", - "The initialize function accepts `liquid_state_args` and `aqueous_state_args` as inputs, along with the output level for the logger, solver, and solver arguments. The initialization routine unfolds in four steps:\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", "\n", - "1. Initialize the Liquid Phase: This involves initializing the state variables and constraints associated with the liquid phase.\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", "\n", - "2. Initialize the Aqueous Phase: Similarly, the state variables and constraints for the aqueous phase are initialized.\n", + "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", "\n", - "3. Solve the Entire Model: The entire model is solved. If the first attempt does not yield an optimal solution, a second attempt is made, and the results are logged.\n", "\n", - "4. Release the Inlet State Variables: The inlet state variables are released.\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_test.ipynb). The next sections will deal with the diagonistics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "So, now we have all the components ready and to be used in the flowsheet, once the flowsheet is ready, we need to pass that through DiagnosticsToolbox. This will help us understand the structural and numerical problems if at all with the model. \n", "\n", - "After step 4 releases the inlet state variables, and a final check is performed to verify if the results are optimal and an error is raised if the results are not optimal. This four-step process in the initialization routine aims to enhance the likelihood of obtaining a robust and feasible solution for the unit model. This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_test.ipynb). The next section will deal with the testing of the property package and unit model. " + "For this we start with the flowsheet with just the liquid liquid extractor model in it. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n" + ] + } + ], "source": [ - "def initialize_build(\n", - " self,\n", - " liquid_state_args=None,\n", - " aqueous_state_args=None,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - "):\n", - " \"\"\"\n", - " Initialization routine for Liquid Liquid Extractor unit model.\n", - "\n", - " Keyword Arguments:\n", - " liquid_state_args : a dict of arguments to be passed to the\n", - " liquid property packages to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " aqueous_state_args : a dict of arguments to be passed to the\n", - " aqueous property package to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " outlvl : sets output level of initialization routine\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default IDAES solver)\n", + "import pyomo.environ as pyo\n", + "import idaes.core\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "from pyomo.network import Arc\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.initialization import InitializationStatus\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from Org_property import OrgPhase\n", + "from Aq_property import AqPhase\n", + "from liquid_extraction import LiqExtraction\n", "\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " if optarg is None:\n", - " optarg = {}\n", - "\n", - " # Check DOF\n", - " if degrees_of_freedom(self) != 0:\n", - " raise InitializationError(\n", - " f\"{self.name} degrees of freedom were not 0 at the beginning \"\n", - " f\"of initialization. DoF = {degrees_of_freedom(self)}\"\n", - " )\n", + "m = pyo.ConcreteModel()\n", + "m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + "m.fs.org_properties = OrgPhase()\n", + "m.fs.aq_properties = AqPhase()\n", "\n", - " # Set solver options\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"unit\")\n", - " solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag=\"unit\")\n", + "m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + ")\n", + "m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "\n", + "m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * pyo.units.g / pyo.units.L)\n", + "\n", + "initializer = BlockTriangularizationInitializer()\n", + "initializer.initialize(m.fs.lex)\n", + "assert initializer.summary[m.fs.lex][\"status\"] == InitializationStatus.Ok" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once the flowsheet is ready, we can import the DiagnosticsToolbox from IDAES and run the Python help function on it see the decomentation. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class DiagnosticsToolbox in module idaes.core.util.model_diagnostics:\n", + "\n", + "class DiagnosticsToolbox(builtins.object)\n", + " | DiagnosticsToolbox(model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | \n", + " | The IDAES Model DiagnosticsToolbox.\n", + " | \n", + " | To get started:\n", + " | \n", + " | 1. Create an instance of your model (this does not need to be initialized yet).\n", + " | 2. Fix variables until you have 0 degrees of freedom. Many of these tools presume\n", + " | a square model, and a square model should always be the foundation of any more\n", + " | advanced model.\n", + " | 3. Create an instance of the DiagnosticsToolbox and provide the model to debug as\n", + " | the model argument.\n", + " | 4. Call the ``report_structural_issues()`` method.\n", + " | \n", + " | Model diagnostics is an iterative process and you will likely need to run these\n", + " | tools multiple times to resolve all issues. After making a change to your model,\n", + " | you should always start from the beginning again to ensure the change did not\n", + " | introduce any new issues; i.e., always start from the report_structural_issues()\n", + " | method.\n", + " | \n", + " | Note that structural checks do not require the model to be initialized, thus users\n", + " | should start with these. Numerical checks require at least a partial solution to the\n", + " | model and should only be run once all structural issues have been resolved.\n", + " | \n", + " | Report methods will print a summary containing three parts:\n", + " | \n", + " | 1. Warnings - these are critical issues that should be resolved before continuing.\n", + " | For each warning, a method will be suggested in the Next Steps section to get\n", + " | additional information.\n", + " | 2. Cautions - these are things that could be correct but could also be the source of\n", + " | solver issues. Not all cautions need to be addressed, but users should investigate\n", + " | each one to ensure that the behavior is correct and that they will not be the source\n", + " | of difficulties later. Methods exist to provide more information on all cautions,\n", + " | but these will not appear in the Next Steps section.\n", + " | 3. Next Steps - these are recommended methods to call from the DiagnosticsToolbox to\n", + " | get further information on warnings. If no warnings are found, this will suggest\n", + " | the next report method to call.\n", + " | \n", + " | Args:\n", + " | \n", + " | model: model to be diagnosed. The DiagnosticsToolbox does not support indexed Blocks.\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | variable_bounds_absolute_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_relative_tolerance: float, default=0.0001\n", + " | Relative tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_violation_tolerance: float, default=0\n", + " | Absolute tolerance for considering a variable to violate its bounds.\n", + " | Some solvers relax bounds on variables thus allowing a small violation\n", + " | to be considered acceptable.\n", + " | \n", + " | constraint_residual_tolerance: float, default=1e-05\n", + " | Absolute tolerance to use when checking constraint residuals.\n", + " | \n", + " | variable_large_value_tolerance: float, default=10000.0\n", + " | Absolute tolerance for considering a value to be large.\n", + " | \n", + " | variable_small_value_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a value to be small.\n", + " | \n", + " | variable_zero_value_tolerance: float, default=1e-08\n", + " | Absolute tolerance for considering a value to be near to zero.\n", + " | \n", + " | jacobian_large_value_caution: float, default=10000.0\n", + " | Tolerance for raising a caution for large Jacobian values.\n", + " | \n", + " | jacobian_large_value_warning: float, default=100000000.0\n", + " | Tolerance for raising a warning for large Jacobian values.\n", + " | \n", + " | jacobian_small_value_caution: float, default=0.0001\n", + " | Tolerance for raising a caution for small Jacobian values.\n", + " | \n", + " | jacobian_small_value_warning: float, default=1e-08\n", + " | Tolerance for raising a warning for small Jacobian values.\n", + " | \n", + " | warn_for_evaluation_error_at_bounds: bool, default=True\n", + " | If False, warnings will not be generated for things like log(x) with x\n", + " | >= 0\n", + " | \n", + " | parallel_component_tolerance: float, default=0.0001\n", + " | Tolerance for identifying near-parallel Jacobian rows/columns\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | Initialize self. See help(type(self)) for accurate signature.\n", + " | \n", + " | assert_no_numerical_warnings(self)\n", + " | Checks for numerical warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by numerical analysis.\n", + " | \n", + " | assert_no_structural_warnings(self)\n", + " | Checks for structural warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by structural analysis.\n", + " | \n", + " | display_components_with_inconsistent_units(self, stream=None)\n", + " | Prints a list of all Constraints, Expressions and Objectives in the\n", + " | model with inconsistent units of measurement.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_extreme_jacobians(self, stream=None)\n", + " | Prints the constraints associated with rows in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled constraints.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_large_residuals(self, stream=None)\n", + " | Prints a list of Constraints with residuals greater than a specified tolerance.\n", + " | Tolerance can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_external_variables(self, stream=None)\n", + " | Prints a list of variables that appear within activated Constraints in the\n", + " | model but are not contained within the model themselves.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_extreme_jacobian_entries(self, stream=None)\n", + " | Prints variables and constraints associated with entries in the Jacobian with extreme\n", + " | values. This can be indicative of poor scaling, especially for isolated terms (e.g.\n", + " | variables which appear only in one term of a single constraint).\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_constraints(self, stream=None)\n", + " | Display near-parallel (duplicate) constraints in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_variables(self, stream=None)\n", + " | Display near-parallel (duplicate) variables in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_overconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the over-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the over-defined part of a model and thus\n", + " | where constraints must be removed or variables unfixed.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_potential_evaluation_errors(self, stream=None)\n", + " | Prints constraints that may be prone to evaluation errors\n", + " | (e.g., log of a negative number) based on variable bounds.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_underconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the under-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the under-defined part of a model and thus\n", + " | where additional information (fixed variables or constraints) are required.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_unused_variables(self, stream=None)\n", + " | Prints a list of variables that do not appear in any activated Constraints.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_at_or_outside_bounds(self, stream=None)\n", + " | Prints a list of variables with values that fall at or outside the bounds\n", + " | on the variable.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_fixed_to_zero(self, stream=None)\n", + " | Prints a list of variables that are fixed to an absolute value of 0.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_near_bounds(self, stream=None)\n", + " | Prints a list of variables with values close to their bounds. Tolerance can\n", + " | be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_jacobians(self, stream=None)\n", + " | Prints the variables associated with columns in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled variables.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_values(self, stream=None)\n", + " | Prints a list of variables with extreme values.\n", + " | \n", + " | Tolerances can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_none_value(self, stream=None)\n", + " | Prints a list of variables with a value of None.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_value_near_zero(self, stream=None)\n", + " | Prints a list of variables with a value close to zero. The tolerance\n", + " | for determining what is close to zero can be set in the class configuration\n", + " | options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | get_dulmage_mendelsohn_partition(self)\n", + " | Performs a Dulmage-Mendelsohn partitioning on the model and returns\n", + " | the over- and under-constrained sub-problems.\n", + " | \n", + " | Returns:\n", + " | list-of-lists variables in each independent block of the under-constrained set\n", + " | list-of-lists constraints in each independent block of the under-constrained set\n", + " | list-of-lists variables in each independent block of the over-constrained set\n", + " | list-of-lists constraints in each independent block of the over-constrained set\n", + " | \n", + " | prepare_degeneracy_hunter(self, **kwargs)\n", + " | Create an instance of the DegeneracyHunter and store as self.degeneracy_hunter.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | report_irreducible_degenerate_sets.\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of DegeneracyHunter\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | solver: str, default='scip'\n", + " | MILP solver to use for finding irreducible degenerate sets.\n", + " | \n", + " | solver_options: optional\n", + " | Options to pass to MILP solver.\n", + " | \n", + " | M: float, default=100000.0\n", + " | Maximum value for nu in MILP models.\n", + " | \n", + " | m_small: float, default=1e-05\n", + " | Smallest value for nu to be considered non-zero in MILP models.\n", + " | \n", + " | trivial_constraint_tolerance: float, default=1e-06\n", + " | Tolerance for identifying non-zero rows in Jacobian.\n", + " | \n", + " | prepare_svd_toolbox(self, **kwargs)\n", + " | Create an instance of the SVDToolbox and store as self.svd_toolbox.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | display_underdetermined_variables_and_constraints().\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of SVDToolbox\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | number_of_smallest_singular_values: PositiveInt, optional\n", + " | Number of smallest singular values to compute\n", + " | \n", + " | svd_callback: svd_callback_validator, default=\n", + " | Callback to SVD method of choice (default = svd_dense). Callbacks\n", + " | should take the Jacobian and number of singular values to compute as\n", + " | options, plus any method specific arguments, and should return the u,\n", + " | s and v matrices as numpy arrays.\n", + " | \n", + " | svd_callback_arguments: dict, optional\n", + " | Optional arguments to pass to SVD callback (default = None)\n", + " | \n", + " | singular_value_tolerance: float, default=1e-06\n", + " | Tolerance for defining a small singular value\n", + " | \n", + " | size_cutoff_in_singular_vector: float, default=0.1\n", + " | Size below which to ignore constraints and variables in the singular\n", + " | vector\n", + " | \n", + " | report_numerical_issues(self, stream=None)\n", + " | Generates a summary report of any numerical issues identified in the model provided\n", + " | and suggest next steps for debugging model.\n", + " | \n", + " | Numerical checks should only be performed once all structural issues have been resolved,\n", + " | and require that at least a partial solution to the model is available.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | report_structural_issues(self, stream=None)\n", + " | Generates a summary report of any structural issues identified in the model provided\n", + " | and suggests next steps for debugging the model.\n", + " | \n", + " | This should be the first method called when debugging a model and after any change\n", + " | is made to the model. These checks can be run before trying to initialize and solve\n", + " | the model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Readonly properties defined here:\n", + " | \n", + " | model\n", + " | Model currently being diagnosed.\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + "\n" + ] + } + ], + "source": [ + "from idaes.core.util import DiagnosticsToolbox\n", "\n", - " solverobj = get_solver(solver, optarg)\n", + "help(DiagnosticsToolbox)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Initialize liquid phase control volume block\n", - " flags = self.liquid_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=liquid_state_args,\n", - " hold_state=True,\n", - " )\n", + "Here's a breakdown of the steps to start with:\n", "\n", - " init_log.info_high(\"Initialization Step 1 Complete.\")\n", - " # ---------------------------------------------------------------------\n", - " # Initialize aqueous phase state block\n", - " if aqueous_state_args is None:\n", - " t_init = self.flowsheet().time.first()\n", - " aqueous_state_args = {}\n", - " aq_state_vars = self.aqueous_phase[t_init].define_state_vars()\n", - "\n", - " liq_state = self.liquid_phase.properties_out[t_init]\n", - "\n", - " # Check for unindexed state variables\n", - " for sv in aq_state_vars:\n", - " if \"flow\" in sv:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", - " elif \"conc\" in sv:\n", - " # Flow is indexed by component\n", - " aqueous_state_args[sv] = {}\n", - " for j in aq_state_vars[sv]:\n", - " if j in liq_state.component_list:\n", - " aqueous_state_args[sv][j] = 1e3 * value(\n", - " getattr(liq_state, sv)[j]\n", - " )\n", - " else:\n", - " aqueous_state_args[sv][j] = 0.5\n", - "\n", - " elif \"pressure\" in sv:\n", - " aqueous_state_args[sv] = 1 * value(getattr(liq_state, sv))\n", + "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", "\n", - " else:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", "\n", - " self.aqueous_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=aqueous_state_args,\n", - " hold_state=False,\n", - " )\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", "\n", - " init_log.info_high(\"Initialization Step 2 Complete.\")\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # # Solve unit model\n", - " with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " if not check_optimal_termination(results):\n", - " init_log.warning(\n", - " f\"Trouble solving unit model {self.name}, trying one more time\"\n", - " )\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " init_log.info_high(\"Initialization Step 3 {}.\".format(idaeslog.condition(results)))\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393950243481484e-07 5.1393950243481484e-07\n", + "Constraint violation....: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06857442855834961}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 8 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 8 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Release Inlet state\n", - " self.liquid_phase.release_state(flags, outlvl)\n", - " self.aqueous_phase.release_state(flags, outlvl)\n", - " if not check_optimal_termination(results):\n", - " raise InitializationError(\n", - " f\"{self.name} failed to initialize successfully. Please check \"\n", - " f\"the output logs for more information.\"\n", - " )\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[NaCl] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[KNO3] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[CaSO4] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94834E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284418857944 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", "\n", - " init_log.info(\"Initialization Complete: {}\".format(idaeslog.condition(results)))" + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Testing\n", + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnigns we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 5.33e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0767662525177002}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. Just to be sure we would check if there are any more numerical issues by calling `report_numerical_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 WARNINGS\n", + "\n", + " WARNING: 3 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here there are some numerical issues with the variables at or outside the bounds which is a physical condition of being a pure stream and thus the salt concentration would be 0. Thus baring this there are no more issues, and thus we can say that the model has been debugged. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_test.ipynb).\n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", "\n", "There are typically 3 types of tests:\n", "\n", @@ -1495,7 +2279,7 @@ "\n", "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", "\n", - "## 4.1 Property package\n", + "## 5.1 Property package\n", "### Unit Tests\n", "\n", "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", @@ -1511,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -1520,7 +2304,7 @@ "from pyomo.util.check_units import assert_units_consistent\n", "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", "\n", - "from Liq_property import LiqPhase\n", + "from Org_property import OrgPhase\n", "from Aq_property import AqPhase\n", "from liquid_extraction import LiqExtraction\n", "from idaes.core.solvers import get_solver\n", @@ -1532,7 +2316,7 @@ " @pytest.fixture(scope=\"class\")\n", " def model(self):\n", " model = ConcreteModel()\n", - " model.params = LiqPhase()\n", + " model.params = AqPhase()\n", " return model\n", "\n", " @pytest.mark.unit\n", @@ -1541,8 +2325,6 @@ "\n", " @pytest.mark.unit\n", " def test_build(self, model):\n", - " assert model.params.state_block_class is AqPhaseStateBlock\n", - "\n", " assert len(model.params.phase_list) == 1\n", " for i in model.params.phase_list:\n", " assert i == \"Aq\"\n", @@ -1551,11 +2333,11 @@ " for i in model.params.component_list:\n", " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", "\n", - " assert isinstance(model.params.cp_mol, Param)\n", - " assert value(model.params.cp_mol) == 4182\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", "\n", - " assert isinstance(model.params.dens_mol, Param)\n", - " assert value(model.params.dens_mol) == 997\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", "\n", " assert isinstance(model.params.temperature_ref, Param)\n", " assert value(model.params.temperature_ref) == 298.15" @@ -1571,12 +2353,12 @@ "\n", "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the liquid property package to ensure consistency and reliability across both packages." + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -1601,10 +2383,6 @@ " assert isinstance(model.props[1].conc_mass_comp, Var)\n", " assert len(model.props[1].conc_mass_comp) == 3\n", "\n", - " for i in model.props[1].conc_mass_comp:\n", - " print(value(model.props[1].conc_mass_comp[i]))\n", - " assert value(model.props[1].conc_mass_comp[i]) == 1\n", - "\n", " @pytest.mark.unit\n", " def test_initialize(self, model):\n", " assert not model.props[1].flow_vol.fixed\n", @@ -1636,7 +2414,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -1658,7 +2436,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -1688,29 +2466,24 @@ "def test_config():\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 16\n", + " assert len(m.fs.unit.config) == 9\n", "\n", " # Check for config arguments\n", " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert m.fs.unit.config.energy_balance_type == EnergyBalanceType.useDefault\n", - " assert m.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal\n", - " assert not m.fs.unit.config.has_heat_transfer\n", " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_equilibrium_reactions\n", " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert not m.fs.unit.config.has_heat_of_reaction\n", - " assert m.fs.unit.config.liquid_property_package is m.fs.liq_properties\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", "\n", " # Check for unit initializer\n", @@ -1726,7 +2499,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -1735,29 +2508,29 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", "\n", " return m\n", "\n", @@ -1772,12 +2545,12 @@ " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_inlet\")\n", - " assert len(model.fs.unit.liquid_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", @@ -1786,18 +2559,18 @@ " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_outlet\")\n", - " assert len(model.fs.unit.liquid_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_liq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", "\n", " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 19" + " assert number_total_constraints(model) == 16" ] }, { @@ -1814,14 +2587,16 @@ "\n", "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered. \n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", "\n", "By performing these checks, we conclude the testing for the unit model. " ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 29, "metadata": {}, "outputs": [], "source": [ @@ -1830,21 +2605,31 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", "\n", " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", @@ -1865,7 +2650,7 @@ " assert check_optimal_termination(results)\n", "\n", " # Checking for outlet flows\n", - " assert value(model.fs.unit.liquid_outlet.flow_vol[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", " 80.0, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", @@ -1874,13 +2659,13 @@ "\n", " # Checking for outlet mass_comp\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", " ) == pytest.approx(0.000187499, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", " ) == pytest.approx(0.000749999, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", " ) == pytest.approx(0.000403124, rel=1e-5)\n", " assert value(\n", " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", @@ -1893,7 +2678,7 @@ " ) == pytest.approx(0.146775, rel=1e-5)\n", "\n", " # Checking for outlet temperature\n", - " assert value(model.fs.unit.liquid_outlet.temperature[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", " 300, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", @@ -1901,7 +2686,7 @@ " )\n", "\n", " # Checking for outlet pressure\n", - " assert value(model.fs.unit.liquid_outlet.pressure[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", " 1, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", @@ -1909,19 +2694,24 @@ " )\n", "\n", " # Fixed state variables\n", - " assert model.fs.unit.liquid_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.liquid_inlet.temperature[0].fixed\n", - " assert model.fs.unit.liquid_inlet.pressure[0].fixed\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", "\n", " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed" + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" ] } ], diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_usr.ipynb index 25331b24..b794f923 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liqliq_extractor_usr.ipynb @@ -37,21 +37,18 @@ "\n", "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", "- Steady-state only\n", - "- Liquid phase property package has a single phase named Liq\n", + "- Organic phase property package has a single phase named Org\n", "- Aquoeus phase property package has a single phase named Aq\n", - "- Liquid and Aqueous phase properties need not have the same component list. \n", + "- Organic and Aqueous phase properties need not have the same component list. \n", "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the liquid phase (Liq). \n", - "\n" + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 1. Creating Liquid Property Package\n", + "# 1. Creating Organic Property Package\n", "\n", "Creating a property package is a 4 step process\n", "- Import necessary libraries \n", @@ -76,7 +73,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -110,14 +115,13 @@ "\n", "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", "\n", - "The Physical Parameter Block then refers to the `state block` in this case `LiqPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the liquid phase, we will assign the Phase as LiquidPhase and the variable will be named Liq as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + "The Physical Parameter Block then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the liquid phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). This method in turn needs to call two predefined methods (inherited from underlying base classes):\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", "\n", - "- `obj.add_properties()` is used to set the metadata regarding the supported properties, and here we define flow volume, pressure, temperature, and mass fraction.\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default." + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " ] }, { @@ -126,13 +130,13 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhase\")\n", + "@declare_process_block_class(\"OrgPhase\")\n", "class PhysicalParameterData(PhysicalParameterBlock):\n", " \"\"\"\n", " Property Parameter Block Class\n", "\n", " Contains parameters and indexing sets associated with properties for\n", - " liquid Phase\n", + " organic Phase\n", "\n", " \"\"\"\n", "\n", @@ -140,21 +144,22 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", - " self._state_block_class = LiqPhaseStateBlock\n", + " self._state_block_class = OrgPhaseStateBlock\n", "\n", " # List of valid phases in property package\n", - " self.Liq = LiquidPhase()\n", + " self.Org = LiquidPhase()\n", "\n", " # Component list - a list of component identifiers\n", " self.NaCl = Solute()\n", " self.KNO3 = Solute()\n", " self.CaSO4 = Solute()\n", - " self.C2H4Br2 = (\n", + " self.solvent = (\n", " Solvent()\n", " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", "\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", @@ -163,15 +168,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of solvent\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=2170,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference Temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -179,29 +181,20 @@ " doc=\"Reference temperature\",\n", " units=units.K,\n", " )\n", - "\n", - " # Distribution Factor\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.diffusion_factor = Param(\n", - " salts_d.keys(), initialize=salts_d, within=PositiveReals\n", + " self.solutes,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", " )\n", "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_vol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " \"conc_mass_comp\": {\"method\": None},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -216,16 +209,15 @@ "\n", "After the `Physical Parameter Block` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc. \n", "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization. \n", - "\n", - "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively. \n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed `StateBlock` all at once (rather than element by element), such as initialization.\n", "\n", - "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete. \n", + "The class `_StateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the initialization routine and release_state function. Initialization is of extreme importance when it comes to non-linear programs like the one, we have on our hands. The initialization function takes initial guesses for the state variables chosen as an argument `state_args`. This has the following inputs for this unit model, initial guesses for the following state variables, flow_mol_comp, temperature, pressure. This also takes a boolean argument for if the state variable is fixed or not in `state_vars_fixed`. If `state_vars_fixed` is `True` then the initialization routine will not deal with fixing and unfixing variables. `hold_state` is a boolean argument that indicates if the state variables should be unfixed during initialization. If this argument is `False` then no state variables are fixed, else the state variables are fixed, and it returns a dictionary with flags for the state variables indicating which are fixed during the initialization. `outlvl` is an output level of logging. The other 2 arguments are `solver` and `optarg` which are the solver and solver options respectively.\n", "\n", + "When we look further into the initialization routine, we see that we write a logger. This helps debug the property package. The next step is to check for the state variables if they are fixed or not. If the state variables are not fixed then we fix them using `fix_state_vars` function and if they are fixed we check the degrees of freedom (dof) for the initialization routine. We need to have dof be 0 for the initialization routine and if the dof is not 0, we raise exception. The next step in the initialization routine will be to take into account the `hold_state` argument. This argument will only make sense if the `fix_state_vars` argument is `False`. We check for the same and return the flags for the states if `hold_state` is `True`, else the state is released using `release_state` function. This concludes the initialization routine, and we log that the initialization routine is complete.\n", "\n", - "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged. \n", + "`release_states` function is to used to free the state variable from the current state. It takes flags as the argument which determine if the state is to held or can be released from initialize function. This function uses `revert_state_vars` function which reverts the fixed state of the state variables within an IDAES StateBlock based on a set of flags of the previous state. After releasing the state variables, completion of state release is logged.\n", "\n", - "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `LiqPhaseStateBlockData` class. " + "The above two functions comprise of the `_StateBlock`, next we shall see the construction of the `OrgPhaseStateBlockData` class." ] }, { @@ -251,7 +243,6 @@ " ):\n", " \"\"\"\n", " Initialization routine for property package.\n", - "\n", " Keyword Arguments:\n", " state_args : Dictionary with initial guesses for the state vars\n", " chosen. Note that if this method is triggered\n", @@ -282,7 +273,6 @@ " False - state variables are unfixed after\n", " initialization by calling the\n", " release_state method\n", - "\n", " Returns:\n", " If hold_states is True, returns a dict containing flags for\n", " which states were fixed during initialization.\n", @@ -314,7 +304,6 @@ " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", " \"\"\"\n", " Method to release state variables fixed during initialization.\n", - "\n", " Keyword Arguments:\n", " flags : dict containing information of which state variables\n", " were fixed during initialization, and should now be\n", @@ -335,7 +324,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The class `LiqPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `LiqPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `LiqPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_StateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_StateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", "\n", "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", "\n", @@ -344,7 +333,6 @@ "- `pressure` - state pressure\n", "- `temperature` - state temperature\n", "\n", - "Additionally, a state parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", "\n", "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", "\n", @@ -366,33 +354,32 @@ "metadata": {}, "outputs": [], "source": [ - "@declare_process_block_class(\"LiqPhaseStateBlock\", block_class=_StateBlock)\n", - "class LiqPhaseStateBlockData(StateBlockData):\n", + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_StateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for liquid phase for liquid liquid extraction\n", + " An example property package for Organic phase for liquid liquid extraction\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(LiqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", - " salts_d = {\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5}\n", " self.flow_vol = Var(\n", " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", " self.conc_mass_comp = Var(\n", - " salts_d.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", " initialize=1,\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", @@ -401,6 +388,7 @@ " units=units.atm,\n", " doc=\"State pressure [atm]\",\n", " )\n", + "\n", " self.temperature = Var(\n", " domain=NonNegativeReals,\n", " initialize=300,\n", @@ -408,18 +396,12 @@ " units=units.K,\n", " doc=\"State temperature [K]\",\n", " )\n", - " self.diffusion_factor = Param(\n", - " salts_d.keys(),\n", - " initialize=salts_d,\n", - " doc=\"Diffusion Factor of salts\",\n", - " within=PositiveReals,\n", - " )\n", "\n", " def material_flow_expression(self, j):\n", " if j == \"solvent\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.conc_mass_comp[j]\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -439,9 +421,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -475,7 +454,7 @@ "source": [ "# 2. Creating Aqueous Property Package\n", "\n", - "The structure of Aqueous Property Package mirrors that of the Liquid Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." ] }, { @@ -484,8 +463,18 @@ "metadata": {}, "outputs": [], "source": [ - "# Changes the divide behavior to not do integer division\n", - "from __future__ import division\n", + "#################################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "#################################################################################\n", "\n", "# Import Python libraries\n", "import logging\n", @@ -494,7 +483,15 @@ "from idaes.core.util.initialization import fix_state_vars, revert_state_vars\n", "\n", "# Import Pyomo libraries\n", - "from pyomo.environ import Param, Var, NonNegativeReals, units, Expression, PositiveReals\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", "\n", "# Import IDAES cores\n", "from idaes.core import (\n", @@ -533,7 +530,7 @@ " \"\"\"\n", " Callable method for Block construction.\n", " \"\"\"\n", - " super(PhysicalParameterData, self).build()\n", + " super().build()\n", "\n", " self._state_block_class = AqPhaseStateBlock\n", "\n", @@ -546,7 +543,8 @@ " self.CaSO4 = Solute()\n", " self.H2O = Solvent()\n", "\n", - " # Heat capacity of Water\n", + " self.solutes = Set(initialize=[\"NaCl\", \"KNO3\", \"CaSO4\"])\n", + " # Heat capacity of solvent\n", " self.cp_mass = Param(\n", " mutable=True,\n", " initialize=4182,\n", @@ -554,15 +552,12 @@ " units=units.J / units.kg / units.K,\n", " )\n", "\n", - " # Density of water\n", " self.dens_mass = Param(\n", " mutable=True,\n", " initialize=997,\n", " doc=\"Density of ethylene dibromide\",\n", " units=units.kg / units.m**3,\n", " )\n", - "\n", - " # Reference temperature\n", " self.temperature_ref = Param(\n", " within=PositiveReals,\n", " mutable=True,\n", @@ -573,19 +568,11 @@ "\n", " @classmethod\n", " def define_metadata(cls, obj):\n", - " obj.add_properties(\n", - " {\n", - " \"flow_mol\": {\"method\": None, \"units\": \"kmol/s\"},\n", - " \"pressure\": {\"method\": None, \"units\": \"MPa\"},\n", - " \"temperature\": {\"method\": None, \"units\": \"K\"},\n", - " }\n", - " )\n", - "\n", " obj.add_default_units(\n", " {\n", - " \"time\": units.s,\n", + " \"time\": units.hour,\n", " \"length\": units.m,\n", - " \"mass\": units.kg,\n", + " \"mass\": units.g,\n", " \"amount\": units.mol,\n", " \"temperature\": units.K,\n", " }\n", @@ -598,108 +585,21 @@ " whole, rather than individual elements of indexed Property Blocks.\n", " \"\"\"\n", "\n", - " def initialize(\n", - " self,\n", - " state_args=None,\n", - " state_vars_fixed=False,\n", - " hold_state=False,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - " ):\n", - " \"\"\"\n", - " Initialization routine for property package.\n", - "\n", - " Keyword Arguments:\n", - " state_args : Dictionary with initial guesses for the state vars\n", - " chosen. Note that if this method is triggered\n", - " through the control volume, and if initial guesses\n", - " were not provided at the unit model level, the\n", - " control volume passes the inlet values as initial\n", - " guess.The keys for the state_args dictionary are:\n", - " flow_mol_comp : value at which to initialize component flows (default=None)\n", - " pressure : value at which to initialize pressure (default=None)\n", - " temperature : value at which to initialize temperature (default=None)\n", - " outlvl : sets output level of initialization routine\n", - " state_vars_fixed: Flag to denote if state vars have already been fixed.\n", - " True - states have already been fixed and\n", - " initialization does not need to worry\n", - " about fixing and unfixing variables.\n", - " False - states have not been fixed. The state\n", - " block will deal with fixing/unfixing.\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default solver)\n", - " hold_state : flag indicating whether the initialization routine\n", - " should unfix any state variables fixed during\n", - " initialization (default=False).\n", - " True - states variables are not unfixed, and\n", - " a dict of returned containing flags for\n", - " which states were fixed during initialization.\n", - " False - state variables are unfixed after\n", - " initialization by calling the\n", - " release_state method\n", - "\n", - " Returns:\n", - " If hold_states is True, returns a dict containing flags for\n", - " which states were fixed during initialization.\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if state_vars_fixed is False:\n", - " # Fix state variables if not already fixed\n", - " flags = fix_state_vars(self, state_args)\n", - "\n", - " else:\n", - " # Check when the state vars are fixed already result in dof 0\n", - " for k in self.keys():\n", - " if degrees_of_freedom(self[k]) != 0:\n", - " raise Exception(\n", - " \"State vars fixed but degrees of freedom \"\n", - " \"for state block is not zero during \"\n", - " \"initialization.\"\n", - " )\n", - "\n", - " if state_vars_fixed is False:\n", - " if hold_state is True:\n", - " return flags\n", - " else:\n", - " self.release_state(flags)\n", - "\n", - " init_log.info(\"Initialization Complete.\")\n", - "\n", - " def release_state(self, flags, outlvl=idaeslog.NOTSET):\n", - " \"\"\"\n", - " Method to release state variables fixed during initialization.\n", - "\n", - " Keyword Arguments:\n", - " flags : dict containing information of which state variables\n", - " were fixed during initialization, and should now be\n", - " unfixed. This dict is returned by initialize if\n", - " hold_state=True.\n", - " outlvl : sets output level of logging\n", - " \"\"\"\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"properties\")\n", - "\n", - " if flags is None:\n", - " return\n", - " # Unfix state variables\n", - " revert_state_vars(self, flags)\n", - " init_log.info(\"State Released.\")\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", "\n", "\n", "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_StateBlock)\n", "class AqPhaseStateBlockData(StateBlockData):\n", " \"\"\"\n", - " An example property package for aqueous phase for liquid liquid extraction\n", + " An example property package for ideal gas properties with Gibbs energy\n", " \"\"\"\n", "\n", " def build(self):\n", " \"\"\"\n", " Callable method for Block construction\n", " \"\"\"\n", - " super(AqPhaseStateBlockData, self).build()\n", + " super().build()\n", " self._make_state_vars()\n", "\n", " def _make_state_vars(self):\n", @@ -707,17 +607,17 @@ " initialize=1,\n", " domain=NonNegativeReals,\n", " doc=\"Total volumetric flowrate\",\n", - " units=units.ml / units.min,\n", + " units=units.L / units.hour,\n", " )\n", "\n", - " salts_conc = {\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1}\n", " self.conc_mass_comp = Var(\n", - " salts_conc.keys(),\n", + " self.params.solutes,\n", " domain=NonNegativeReals,\n", - " initialize=1,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", " doc=\"Component mass concentrations\",\n", - " units=units.g / units.kg,\n", + " units=units.g / units.L,\n", " )\n", + "\n", " self.pressure = Var(\n", " domain=NonNegativeReals,\n", " initialize=1,\n", @@ -738,7 +638,7 @@ " if j == \"H2O\":\n", " return self.flow_vol * self.params.dens_mass\n", " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", "\n", " self.material_flow_expression = Expression(\n", " self.component_list,\n", @@ -758,9 +658,6 @@ " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", " )\n", "\n", - " def get_mass_comp(self, j):\n", - " return self.conc_mass_comp[j]\n", - "\n", " def get_flow_rate(self):\n", " return self.flow_vol\n", "\n", @@ -810,13 +707,9 @@ "# Import Pyomo libraries\n", "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", "from pyomo.environ import (\n", - " Reference,\n", - " Var,\n", " value,\n", " Constraint,\n", - " units as pyunits,\n", " check_optimal_termination,\n", - " Suffix,\n", ")\n", "\n", "# Import IDAES cores\n", @@ -836,7 +729,6 @@ ")\n", "\n", "import idaes.logger as idaeslog\n", - "from idaes.core.util import scaling as iscale\n", "from idaes.core.solvers import get_solver\n", "from idaes.core.util.model_statistics import degrees_of_freedom\n", "from idaes.core.util.exceptions import ConfigurationError, InitializationError" @@ -848,28 +740,21 @@ "source": [ "## 3.2 Creating the unit model\n", "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the config arguments for the control volume. The config arguments includes the following properties:\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherts the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", "\n", - "1. `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "2. `energy_balance_type` - Indicates what type of energy balance should be constructed\n", - "3. `momentum_balance_type` - Indicates what type of momentum balance should be constructed\n", - "4. `has_heat_transfer` - Indicates whether terms for heat transfer should be constructed\n", - "5. `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", "constructed\n", - "6. `has_equilibrium_reactions` - Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed\n", - "7. `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", "constructed\n", - "8. `has_heat_of_reaction` - Indicates whether terms for heat of reaction terms should be\n", - "constructed\n", - "9. `Liquid Property` - Property parameter object used to define property calculations\n", - "for the liquid phase\n", - "10. `Liquid Property Arguments` - Arguments to use for constructing liquid phase properties\n", - "11. `Aqueous Property` - Property parameter object used to define property calculations\n", + "- `Organic Property` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", + "- `Aqueous Property` - Property parameter object used to define property calculations\n", "for the aqueous phase\n", - "12. `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "13. `Reaction Package` - Reaction parameter object used to define reaction calculations\n", - "14. `Reaction Package Arguments` - Arguments to use for constructing reaction packages\n" + "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." ] }, { @@ -893,62 +778,15 @@ " domain=In(MaterialBalanceType),\n", " description=\"Material balance construction flag\",\n", " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - "**default** - MaterialBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**MaterialBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**MaterialBalanceType.none** - exclude material balances,\n", - "**MaterialBalanceType.componentPhase** - use phase component balances,\n", - "**MaterialBalanceType.componentTotal** - use total component balances,\n", - "**MaterialBalanceType.elementTotal** - use total element balances,\n", - "**MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"energy_balance_type\",\n", - " ConfigValue(\n", - " default=EnergyBalanceType.useDefault,\n", - " domain=In(EnergyBalanceType),\n", - " description=\"Energy balance construction flag\",\n", - " doc=\"\"\"Indicates what type of energy balance should be constructed,\n", - "**default** - EnergyBalanceType.useDefault.\n", - "**Valid values:** {\n", - "**EnergyBalanceType.useDefault - refer to property package for default\n", - "balance type\n", - "**EnergyBalanceType.none** - exclude energy balances,\n", - "**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,\n", - "**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,\n", - "**EnergyBalanceType.energyTotal** - single energy balance for material,\n", - "**EnergyBalanceType.energyPhase** - energy balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"momentum_balance_type\",\n", - " ConfigValue(\n", - " default=MomentumBalanceType.pressureTotal,\n", - " domain=In(MomentumBalanceType),\n", - " description=\"Momentum balance construction flag\",\n", - " doc=\"\"\"Indicates what type of momentum balance should be constructed,\n", - "**default** - MomentumBalanceType.pressureTotal.\n", - "**Valid values:** {\n", - "**MomentumBalanceType.none** - exclude momentum balances,\n", - "**MomentumBalanceType.pressureTotal** - single pressure balance for material,\n", - "**MomentumBalanceType.pressurePhase** - pressure balances for each phase,\n", - "**MomentumBalanceType.momentumTotal** - single momentum balance for material,\n", - "**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_heat_transfer\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat transfer term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat transfer should be constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat transfer terms,\n", - "**False** - exclude heat transfer terms.}\"\"\",\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -958,25 +796,11 @@ " domain=Bool,\n", " description=\"Pressure change term construction flag\",\n", " doc=\"\"\"Indicates whether terms for pressure change should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include pressure change terms,\n", - "**False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_equilibrium_reactions\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Equilibrium reaction construction flag\",\n", - " doc=\"\"\"Indicates whether terms for equilibrium controlled reactions\n", - "should be constructed,\n", - "**default** - True.\n", - "**Valid values:** {\n", - "**True** - include equilibrium reaction terms,\n", - "**False** - exclude equilibrium reaction terms.}\"\"\",\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -986,51 +810,37 @@ " domain=Bool,\n", " description=\"Phase equilibrium construction flag\",\n", " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - "constructed,\n", - "**default** = False.\n", - "**Valid values:** {\n", - "**True** - include phase equilibrium terms\n", - "**False** - exclude phase equilibrium terms.}\"\"\",\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"has_heat_of_reaction\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Heat of reaction term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for heat of reaction terms should be\n", - "constructed,\n", - "**default** - False.\n", - "**Valid values:** {\n", - "**True** - include heat of reaction terms,\n", - "**False** - exclude heat of reaction terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"liquid_property_package\",\n", + " \"organic_property_package\",\n", " ConfigValue(\n", " default=useDefault,\n", " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for liquid phase\",\n", + " description=\"Property package to use for organic phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the liquid phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", - " \"liquid_property_package_args\",\n", + " \"organic_property_package_args\",\n", " ConfigBlock(\n", " implicit=True,\n", - " description=\"Arguments to use for constructing liquid phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to liquid phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1040,11 +850,11 @@ " domain=is_physical_parameter_block,\n", " description=\"Property package to use for aqueous phase\",\n", " doc=\"\"\"Property parameter object used to define property calculations\n", - "for the aqueous phase,\n", - "**default** - useDefault.\n", - "**Valid values:** {\n", - "**useDefault** - use default package from parent model or flowsheet,\n", - "**PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", " ),\n", " )\n", " CONFIG.declare(\n", @@ -1053,35 +863,10 @@ " implicit=True,\n", " description=\"Arguments to use for constructing aqueous phase properties\",\n", " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - "property block(s) and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package\",\n", - " ConfigValue(\n", - " default=None,\n", - " domain=is_reaction_parameter_block,\n", - " description=\"Reaction package to use for control volume\",\n", - " doc=\"\"\"Reaction parameter object used to define reaction calculations,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "**None** - no reaction package,\n", - "**ReactionParameterBlock** - a ReactionParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"reaction_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing reaction packages\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to a reaction block(s)\n", - "and used when constructing these,\n", - "**default** - None.\n", - "**Valid values:** {\n", - "see reaction package for documentation.}\"\"\",\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", " ),\n", " )" ] @@ -1098,25 +883,25 @@ "\n", "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Liq' for the liquid phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the liquid phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, the hold-up in the block, and the property package, along with property package arguments. \n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the liquid property package\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_rate_reactions`, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", "\n", "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", "\n", "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is reponsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", "\n", - "Similarly, `add_energy_balance` and `add_momentum_balance` functions are added to the control volume to create respective equations. This concludes the creation of liquid phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", "\n", "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", "\n", "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the liquid phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{liq} = - mass\\_transfer\\_term_{aq} $\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", "\n", "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." ] @@ -1136,9 +921,7 @@ " None\n", " \"\"\"\n", " # Call UnitModel.build to setup dynamics\n", - " super(LiqExtractionData, self).build()\n", - "\n", - " self.scaling_factor = Suffix(direction=Suffix.EXPORT)\n", + " super().build()\n", "\n", " # Check phase lists match assumptions\n", " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", @@ -1146,61 +929,46 @@ " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", " f\"phase property package have a single phase named 'Aq'\"\n", " )\n", - " if self.config.liquid_property_package.phase_list != [\"Liq\"]:\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", - " f\"phase property package have a single phase named 'Liq'\"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", " )\n", "\n", " # Check for at least one common component in component lists\n", " if not any(\n", " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.liquid_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the liquid \"\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", " f\"and aqueous phase property packages have at least one \"\n", " f\"common component.\"\n", " )\n", "\n", - " self.liquid_phase = ControlVolume0DBlock(\n", + " self.organic_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", - " property_package=self.config.liquid_property_package,\n", - " property_package_args=self.config.liquid_property_package_args,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", " )\n", "\n", - " self.liquid_phase.add_state_blocks(\n", + " self.organic_phase.add_state_blocks(\n", " has_phase_equilibrium=self.config.has_phase_equilibrium\n", " )\n", "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", " # be handled at the unit model level, thus has_phase_equilibrium is\n", " # False, but has_mass_transfer is True.\n", "\n", - " self.liquid_phase.add_material_balances(\n", + " self.organic_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", - " has_equilibrium_reactions=self.config.has_equilibrium_reactions,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", - "\n", - " self.liquid_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.liquid_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", - "\n", " # ---------------------------------------------------------------------\n", + "\n", " self.aqueous_phase = ControlVolume0DBlock(\n", " dynamic=self.config.dynamic,\n", - " has_holdup=self.config.has_holdup,\n", " property_package=self.config.aqueous_property_package,\n", " property_package_args=self.config.aqueous_property_package_args,\n", " )\n", @@ -1215,46 +983,41 @@ "\n", " self.aqueous_phase.add_material_balances(\n", " balance_type=self.config.material_balance_type,\n", - " has_rate_reactions=False,\n", + " # has_rate_reactions=False,\n", " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", " has_mass_transfer=True,\n", " )\n", "\n", - " self.aqueous_phase.add_energy_balances(\n", - " balance_type=self.config.energy_balance_type,\n", - " has_heat_transfer=False,\n", - " has_enthalpy_transfer=False,\n", - " )\n", - "\n", - " self.aqueous_phase.add_momentum_balances(\n", - " balance_type=self.config.momentum_balance_type,\n", - " has_pressure_change=self.config.has_pressure_change,\n", - " )\n", + " self.aqueous_phase.add_geometry()\n", "\n", " # ---------------------------------------------------------------------\n", " # Check flow basis is compatable\n", " t_init = self.flowsheet().time.first()\n", " if (\n", " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.liquid_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", " ):\n", " raise ConfigurationError(\n", - " f\"{self.name} aqueous and liquid property packages must use the \"\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", " f\"same material flow basis.\"\n", " )\n", "\n", + " self.organic_phase.add_geometry()\n", + "\n", " # Add Ports\n", - " self.add_inlet_port(name=\"liquid_inlet\", block=self.liquid_phase, doc=\"Liquid feed\")\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", " self.add_inlet_port(\n", " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", " )\n", " self.add_outlet_port(\n", - " name=\"liquid_outlet\", block=self.liquid_phase, doc=\"Liquid Outlet\"\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", " )\n", " self.add_outlet_port(\n", " name=\"aqueous_outlet\",\n", " block=self.aqueous_phase,\n", - " doc=\"Aqueous Outlet\",\n", + " doc=\"Aqueous outlet\",\n", " )\n", "\n", " # ---------------------------------------------------------------------\n", @@ -1262,16 +1025,16 @@ " # First, need the union and intersection of component lists\n", " all_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " | self.liquid_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", " )\n", " common_comps = (\n", " self.aqueous_phase.properties_out.component_list\n", - " & self.liquid_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", " )\n", "\n", " # Get units for unit conversion\n", " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.liquid_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", "\n", " if flow_basis == MaterialFlowBasis.mass:\n", @@ -1285,18 +1048,17 @@ " )\n", "\n", " # Material balances\n", - " def rule_material_liq_balance(self, t, j):\n", + " def rule_material_aq_balance(self, t, j):\n", " if j in common_comps:\n", " return self.aqueous_phase.mass_transfer_term[\n", " t, \"Aq\", j\n", - " ] == -self.liquid_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_mass_comp(j)\n", - " / self.aqueous_phase.properties_in[t].get_flow_rate()\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", " )\n", - " elif j in self.liquid_phase.properties_out.component_list:\n", + " elif j in self.organic_phase.properties_out.component_list:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * lunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", " elif j in self.aqueous_phase.properties_out.component_list:\n", " # No mass transfer term\n", " # Set aqueous flowrate to an arbitary small value\n", @@ -1305,27 +1067,26 @@ " self.material_aq_balance = Constraint(\n", " self.flowsheet().time,\n", " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances for aq\",\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", " )\n", "\n", - " def rule_material_aq_balance(self, t, j):\n", - " print(t)\n", + " def rule_material_liq_balance(self, t, j):\n", " if j in common_comps:\n", " return (\n", - " self.liquid_phase.mass_transfer_term[t, \"Liq\", j]\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", " )\n", " else:\n", " # No mass transfer term\n", - " # Set Liquid flowrate to an arbitary small value\n", - " return self.liquid_phase.mass_transfer_term[t, \"Liq\", j] == 0 * aunits(fb)\n", + " # Set organic flowrate to an arbitary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", "\n", - " self.material_liq_balance = Constraint(\n", + " self.material_org_balance = Constraint(\n", " self.flowsheet().time,\n", - " self.liquid_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Liq\",\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", " )" ] }, @@ -1335,152 +1096,1175 @@ "source": [ "### Initialization Routine\n", "\n", - "After writing the unit model it is crucial to develop the initialization routine, as non-linear models may encounter local minima or infeasibility if not initialized properly. Thus, we introduce the function `initialize_build`, serving as the initialization routine for this unit model.\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", "\n", - "The initialize function accepts `liquid_state_args` and `aqueous_state_args` as inputs, along with the output level for the logger, solver, and solver arguments. The initialization routine unfolds in four steps:\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", "\n", - "1. Initialize the Liquid Phase: This involves initializing the state variables and constraints associated with the liquid phase.\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", "\n", - "2. Initialize the Aqueous Phase: Similarly, the state variables and constraints for the aqueous phase are initialized.\n", + "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", "\n", - "3. Solve the Entire Model: The entire model is solved. If the first attempt does not yield an optimal solution, a second attempt is made, and the results are logged.\n", "\n", - "4. Release the Inlet State Variables: The inlet state variables are released.\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_usr.ipynb). The next sections will deal with the diagonistics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "So, now we have all the components ready and to be used in the flowsheet, once the flowsheet is ready, we need to pass that through DiagnosticsToolbox. This will help us understand the structural and numerical problems if at all with the model. \n", "\n", - "After step 4 releases the inlet state variables, and a final check is performed to verify if the results are optimal and an error is raised if the results are not optimal. This four-step process in the initialization routine aims to enhance the likelihood of obtaining a robust and feasible solution for the unit model. This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_usr.ipynb). The next section will deal with the testing of the property package and unit model. " + "For this we start with the flowsheet with just the liquid liquid extractor model in it. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n" + ] + } + ], "source": [ - "def initialize_build(\n", - " self,\n", - " liquid_state_args=None,\n", - " aqueous_state_args=None,\n", - " outlvl=idaeslog.NOTSET,\n", - " solver=None,\n", - " optarg=None,\n", - "):\n", - " \"\"\"\n", - " Initialization routine for Liquid Liquid Extractor unit model.\n", - "\n", - " Keyword Arguments:\n", - " liquid_state_args : a dict of arguments to be passed to the\n", - " liquid property packages to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " aqueous_state_args : a dict of arguments to be passed to the\n", - " aqueous property package to provide an initial state for\n", - " initialization (see documentation of the specific property\n", - " package) (default = none).\n", - " outlvl : sets output level of initialization routine\n", - " optarg : solver options dictionary object (default=None, use\n", - " default solver options)\n", - " solver : str indicating which solver to use during\n", - " initialization (default = None, use default IDAES solver)\n", + "import pyomo.environ as pyo\n", + "import idaes.core\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "from pyomo.network import Arc\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.initialization import InitializationStatus\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from Org_property import OrgPhase\n", + "from Aq_property import AqPhase\n", + "from liquid_extraction import LiqExtraction\n", "\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " if optarg is None:\n", - " optarg = {}\n", - "\n", - " # Check DOF\n", - " if degrees_of_freedom(self) != 0:\n", - " raise InitializationError(\n", - " f\"{self.name} degrees of freedom were not 0 at the beginning \"\n", - " f\"of initialization. DoF = {degrees_of_freedom(self)}\"\n", - " )\n", + "m = pyo.ConcreteModel()\n", + "m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + "m.fs.org_properties = OrgPhase()\n", + "m.fs.aq_properties = AqPhase()\n", "\n", - " # Set solver options\n", - " init_log = idaeslog.getInitLogger(self.name, outlvl, tag=\"unit\")\n", - " solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag=\"unit\")\n", + "m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + ")\n", + "m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * pyo.units.g / pyo.units.L)\n", + "\n", + "m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + "m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + "m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * pyo.units.g / pyo.units.L)\n", + "m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * pyo.units.g / pyo.units.L)\n", + "\n", + "initializer = BlockTriangularizationInitializer()\n", + "initializer.initialize(m.fs.lex)\n", + "assert initializer.summary[m.fs.lex][\"status\"] == InitializationStatus.Ok" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once the flowsheet is ready, we can import the DiagnosticsToolbox from IDAES and run the Python help function on it see the decomentation. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class DiagnosticsToolbox in module idaes.core.util.model_diagnostics:\n", + "\n", + "class DiagnosticsToolbox(builtins.object)\n", + " | DiagnosticsToolbox(model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | \n", + " | The IDAES Model DiagnosticsToolbox.\n", + " | \n", + " | To get started:\n", + " | \n", + " | 1. Create an instance of your model (this does not need to be initialized yet).\n", + " | 2. Fix variables until you have 0 degrees of freedom. Many of these tools presume\n", + " | a square model, and a square model should always be the foundation of any more\n", + " | advanced model.\n", + " | 3. Create an instance of the DiagnosticsToolbox and provide the model to debug as\n", + " | the model argument.\n", + " | 4. Call the ``report_structural_issues()`` method.\n", + " | \n", + " | Model diagnostics is an iterative process and you will likely need to run these\n", + " | tools multiple times to resolve all issues. After making a change to your model,\n", + " | you should always start from the beginning again to ensure the change did not\n", + " | introduce any new issues; i.e., always start from the report_structural_issues()\n", + " | method.\n", + " | \n", + " | Note that structural checks do not require the model to be initialized, thus users\n", + " | should start with these. Numerical checks require at least a partial solution to the\n", + " | model and should only be run once all structural issues have been resolved.\n", + " | \n", + " | Report methods will print a summary containing three parts:\n", + " | \n", + " | 1. Warnings - these are critical issues that should be resolved before continuing.\n", + " | For each warning, a method will be suggested in the Next Steps section to get\n", + " | additional information.\n", + " | 2. Cautions - these are things that could be correct but could also be the source of\n", + " | solver issues. Not all cautions need to be addressed, but users should investigate\n", + " | each one to ensure that the behavior is correct and that they will not be the source\n", + " | of difficulties later. Methods exist to provide more information on all cautions,\n", + " | but these will not appear in the Next Steps section.\n", + " | 3. Next Steps - these are recommended methods to call from the DiagnosticsToolbox to\n", + " | get further information on warnings. If no warnings are found, this will suggest\n", + " | the next report method to call.\n", + " | \n", + " | Args:\n", + " | \n", + " | model: model to be diagnosed. The DiagnosticsToolbox does not support indexed Blocks.\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | variable_bounds_absolute_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_relative_tolerance: float, default=0.0001\n", + " | Relative tolerance for considering a variable to be close to its\n", + " | bounds.\n", + " | \n", + " | variable_bounds_violation_tolerance: float, default=0\n", + " | Absolute tolerance for considering a variable to violate its bounds.\n", + " | Some solvers relax bounds on variables thus allowing a small violation\n", + " | to be considered acceptable.\n", + " | \n", + " | constraint_residual_tolerance: float, default=1e-05\n", + " | Absolute tolerance to use when checking constraint residuals.\n", + " | \n", + " | variable_large_value_tolerance: float, default=10000.0\n", + " | Absolute tolerance for considering a value to be large.\n", + " | \n", + " | variable_small_value_tolerance: float, default=0.0001\n", + " | Absolute tolerance for considering a value to be small.\n", + " | \n", + " | variable_zero_value_tolerance: float, default=1e-08\n", + " | Absolute tolerance for considering a value to be near to zero.\n", + " | \n", + " | jacobian_large_value_caution: float, default=10000.0\n", + " | Tolerance for raising a caution for large Jacobian values.\n", + " | \n", + " | jacobian_large_value_warning: float, default=100000000.0\n", + " | Tolerance for raising a warning for large Jacobian values.\n", + " | \n", + " | jacobian_small_value_caution: float, default=0.0001\n", + " | Tolerance for raising a caution for small Jacobian values.\n", + " | \n", + " | jacobian_small_value_warning: float, default=1e-08\n", + " | Tolerance for raising a warning for small Jacobian values.\n", + " | \n", + " | warn_for_evaluation_error_at_bounds: bool, default=True\n", + " | If False, warnings will not be generated for things like log(x) with x\n", + " | >= 0\n", + " | \n", + " | parallel_component_tolerance: float, default=0.0001\n", + " | Tolerance for identifying near-parallel Jacobian rows/columns\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, model: pyomo.core.base.block._BlockData, **kwargs)\n", + " | Initialize self. See help(type(self)) for accurate signature.\n", + " | \n", + " | assert_no_numerical_warnings(self)\n", + " | Checks for numerical warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by numerical analysis.\n", + " | \n", + " | assert_no_structural_warnings(self)\n", + " | Checks for structural warnings in the model and raises an AssertionError\n", + " | if any are found.\n", + " | \n", + " | Raises:\n", + " | AssertionError if any warnings are identified by structural analysis.\n", + " | \n", + " | display_components_with_inconsistent_units(self, stream=None)\n", + " | Prints a list of all Constraints, Expressions and Objectives in the\n", + " | model with inconsistent units of measurement.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_extreme_jacobians(self, stream=None)\n", + " | Prints the constraints associated with rows in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled constraints.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_constraints_with_large_residuals(self, stream=None)\n", + " | Prints a list of Constraints with residuals greater than a specified tolerance.\n", + " | Tolerance can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_external_variables(self, stream=None)\n", + " | Prints a list of variables that appear within activated Constraints in the\n", + " | model but are not contained within the model themselves.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_extreme_jacobian_entries(self, stream=None)\n", + " | Prints variables and constraints associated with entries in the Jacobian with extreme\n", + " | values. This can be indicative of poor scaling, especially for isolated terms (e.g.\n", + " | variables which appear only in one term of a single constraint).\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_constraints(self, stream=None)\n", + " | Display near-parallel (duplicate) constraints in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_near_parallel_variables(self, stream=None)\n", + " | Display near-parallel (duplicate) variables in model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_overconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the over-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the over-defined part of a model and thus\n", + " | where constraints must be removed or variables unfixed.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_potential_evaluation_errors(self, stream=None)\n", + " | Prints constraints that may be prone to evaluation errors\n", + " | (e.g., log of a negative number) based on variable bounds.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_underconstrained_set(self, stream=None)\n", + " | Prints the variables and constraints in the under-constrained sub-problem\n", + " | from a Dulmage-Mendelsohn partitioning.\n", + " | \n", + " | This can be used to identify the under-defined part of a model and thus\n", + " | where additional information (fixed variables or constraints) are required.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_unused_variables(self, stream=None)\n", + " | Prints a list of variables that do not appear in any activated Constraints.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_at_or_outside_bounds(self, stream=None)\n", + " | Prints a list of variables with values that fall at or outside the bounds\n", + " | on the variable.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_fixed_to_zero(self, stream=None)\n", + " | Prints a list of variables that are fixed to an absolute value of 0.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_near_bounds(self, stream=None)\n", + " | Prints a list of variables with values close to their bounds. Tolerance can\n", + " | be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_jacobians(self, stream=None)\n", + " | Prints the variables associated with columns in the Jacobian with extreme\n", + " | L2 norms. This often indicates poorly scaled variables.\n", + " | \n", + " | Tolerances can be set via the DiagnosticsToolbox config.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the output to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_extreme_values(self, stream=None)\n", + " | Prints a list of variables with extreme values.\n", + " | \n", + " | Tolerances can be set in the class configuration options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_none_value(self, stream=None)\n", + " | Prints a list of variables with a value of None.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | display_variables_with_value_near_zero(self, stream=None)\n", + " | Prints a list of variables with a value close to zero. The tolerance\n", + " | for determining what is close to zero can be set in the class configuration\n", + " | options.\n", + " | \n", + " | Args:\n", + " | stream: an I/O object to write the list to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | get_dulmage_mendelsohn_partition(self)\n", + " | Performs a Dulmage-Mendelsohn partitioning on the model and returns\n", + " | the over- and under-constrained sub-problems.\n", + " | \n", + " | Returns:\n", + " | list-of-lists variables in each independent block of the under-constrained set\n", + " | list-of-lists constraints in each independent block of the under-constrained set\n", + " | list-of-lists variables in each independent block of the over-constrained set\n", + " | list-of-lists constraints in each independent block of the over-constrained set\n", + " | \n", + " | prepare_degeneracy_hunter(self, **kwargs)\n", + " | Create an instance of the DegeneracyHunter and store as self.degeneracy_hunter.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | report_irreducible_degenerate_sets.\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of DegeneracyHunter\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | solver: str, default='scip'\n", + " | MILP solver to use for finding irreducible degenerate sets.\n", + " | \n", + " | solver_options: optional\n", + " | Options to pass to MILP solver.\n", + " | \n", + " | M: float, default=100000.0\n", + " | Maximum value for nu in MILP models.\n", + " | \n", + " | m_small: float, default=1e-05\n", + " | Smallest value for nu to be considered non-zero in MILP models.\n", + " | \n", + " | trivial_constraint_tolerance: float, default=1e-06\n", + " | Tolerance for identifying non-zero rows in Jacobian.\n", + " | \n", + " | prepare_svd_toolbox(self, **kwargs)\n", + " | Create an instance of the SVDToolbox and store as self.svd_toolbox.\n", + " | \n", + " | After creating an instance of the toolbox, call\n", + " | display_underdetermined_variables_and_constraints().\n", + " | \n", + " | Returns:\n", + " | \n", + " | Instance of SVDToolbox\n", + " | \n", + " | Keyword Arguments\n", + " | -----------------\n", + " | number_of_smallest_singular_values: PositiveInt, optional\n", + " | Number of smallest singular values to compute\n", + " | \n", + " | svd_callback: svd_callback_validator, default=\n", + " | Callback to SVD method of choice (default = svd_dense). Callbacks\n", + " | should take the Jacobian and number of singular values to compute as\n", + " | options, plus any method specific arguments, and should return the u,\n", + " | s and v matrices as numpy arrays.\n", + " | \n", + " | svd_callback_arguments: dict, optional\n", + " | Optional arguments to pass to SVD callback (default = None)\n", + " | \n", + " | singular_value_tolerance: float, default=1e-06\n", + " | Tolerance for defining a small singular value\n", + " | \n", + " | size_cutoff_in_singular_vector: float, default=0.1\n", + " | Size below which to ignore constraints and variables in the singular\n", + " | vector\n", + " | \n", + " | report_numerical_issues(self, stream=None)\n", + " | Generates a summary report of any numerical issues identified in the model provided\n", + " | and suggest next steps for debugging model.\n", + " | \n", + " | Numerical checks should only be performed once all structural issues have been resolved,\n", + " | and require that at least a partial solution to the model is available.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | report_structural_issues(self, stream=None)\n", + " | Generates a summary report of any structural issues identified in the model provided\n", + " | and suggests next steps for debugging the model.\n", + " | \n", + " | This should be the first method called when debugging a model and after any change\n", + " | is made to the model. These checks can be run before trying to initialize and solve\n", + " | the model.\n", + " | \n", + " | Args:\n", + " | stream: I/O object to write report to (default = stdout)\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Readonly properties defined here:\n", + " | \n", + " | model\n", + " | Model currently being diagnosed.\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + "\n" + ] + } + ], + "source": [ + "from idaes.core.util import DiagnosticsToolbox\n", "\n", - " solverobj = get_solver(solver, optarg)\n", + "help(DiagnosticsToolbox)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Initialize liquid phase control volume block\n", - " flags = self.liquid_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=liquid_state_args,\n", - " hold_state=True,\n", - " )\n", + "Here's a breakdown of the steps to start with:\n", "\n", - " init_log.info_high(\"Initialization Step 1 Complete.\")\n", - " # ---------------------------------------------------------------------\n", - " # Initialize aqueous phase state block\n", - " if aqueous_state_args is None:\n", - " t_init = self.flowsheet().time.first()\n", - " aqueous_state_args = {}\n", - " aq_state_vars = self.aqueous_phase[t_init].define_state_vars()\n", - "\n", - " liq_state = self.liquid_phase.properties_out[t_init]\n", - "\n", - " # Check for unindexed state variables\n", - " for sv in aq_state_vars:\n", - " if \"flow\" in sv:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", - " elif \"conc\" in sv:\n", - " # Flow is indexed by component\n", - " aqueous_state_args[sv] = {}\n", - " for j in aq_state_vars[sv]:\n", - " if j in liq_state.component_list:\n", - " aqueous_state_args[sv][j] = 1e3 * value(\n", - " getattr(liq_state, sv)[j]\n", - " )\n", - " else:\n", - " aqueous_state_args[sv][j] = 0.5\n", - "\n", - " elif \"pressure\" in sv:\n", - " aqueous_state_args[sv] = 1 * value(getattr(liq_state, sv))\n", + "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", "\n", - " else:\n", - " aqueous_state_args[sv] = value(getattr(liq_state, sv))\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", "\n", - " self.aqueous_phase.initialize(\n", - " outlvl=outlvl,\n", - " optarg=optarg,\n", - " solver=solver,\n", - " state_args=aqueous_state_args,\n", - " hold_state=False,\n", - " )\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", "\n", - " init_log.info_high(\"Initialization Step 2 Complete.\")\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # # Solve unit model\n", - " with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " if not check_optimal_termination(results):\n", - " init_log.warning(\n", - " f\"Trouble solving unit model {self.name}, trying one more time\"\n", - " )\n", - " results = solverobj.solve(self, tee=slc.tee)\n", - " init_log.info_high(\"Initialization Step 3 {}.\".format(idaeslog.condition(results)))\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393950243481484e-07 5.1393950243481484e-07\n", + "Constraint violation....: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105164720274452e+01 3.9105164720274452e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06857442855834961}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 8 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 8 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", "\n", - " # ---------------------------------------------------------------------\n", - " # Release Inlet state\n", - " self.liquid_phase.release_state(flags, outlvl)\n", - " self.aqueous_phase.release_state(flags, outlvl)\n", - " if not check_optimal_termination(results):\n", - " raise InitializationError(\n", - " f\"{self.name} failed to initialize successfully. Please check \"\n", - " f\"the output logs for more information.\"\n", - " )\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[NaCl] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[KNO3] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].conc_mass_comp[CaSO4] (fixed): value=0.0 bounds=(0, None)\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94834E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284418857944 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", "\n", - " init_log.info(\"Initialization Complete: {}\".format(idaeslog.condition(results)))" + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Testing\n", + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 variables fixed to 0\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnigns we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: \n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 5.33e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.3290705182007514e-15 5.3290705182007514e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0767662525177002}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. Just to be sure we would check if there are any more numerical issues by calling `report_numerical_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 WARNINGS\n", + "\n", + " WARNING: 3 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 Cautions\n", + "\n", + " Caution: 3 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here there are some numerical issues with the variables at or outside the bounds which is a physical condition of being a pure stream and thus the salt concentration would be 0. Thus baring this there are no more issues, and thus we can say that the model has been debugged. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_usr.ipynb).\n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", "\n", "There are typically 3 types of tests:\n", "\n", @@ -1495,7 +2279,7 @@ "\n", "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", "\n", - "## 4.1 Property package\n", + "## 5.1 Property package\n", "### Unit Tests\n", "\n", "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", @@ -1511,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -1520,7 +2304,7 @@ "from pyomo.util.check_units import assert_units_consistent\n", "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", "\n", - "from Liq_property import LiqPhase\n", + "from Org_property import OrgPhase\n", "from Aq_property import AqPhase\n", "from liquid_extraction import LiqExtraction\n", "from idaes.core.solvers import get_solver\n", @@ -1532,7 +2316,7 @@ " @pytest.fixture(scope=\"class\")\n", " def model(self):\n", " model = ConcreteModel()\n", - " model.params = LiqPhase()\n", + " model.params = AqPhase()\n", " return model\n", "\n", " @pytest.mark.unit\n", @@ -1541,8 +2325,6 @@ "\n", " @pytest.mark.unit\n", " def test_build(self, model):\n", - " assert model.params.state_block_class is AqPhaseStateBlock\n", - "\n", " assert len(model.params.phase_list) == 1\n", " for i in model.params.phase_list:\n", " assert i == \"Aq\"\n", @@ -1551,11 +2333,11 @@ " for i in model.params.component_list:\n", " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", "\n", - " assert isinstance(model.params.cp_mol, Param)\n", - " assert value(model.params.cp_mol) == 4182\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", "\n", - " assert isinstance(model.params.dens_mol, Param)\n", - " assert value(model.params.dens_mol) == 997\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", "\n", " assert isinstance(model.params.temperature_ref, Param)\n", " assert value(model.params.temperature_ref) == 298.15" @@ -1571,12 +2353,12 @@ "\n", "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the liquid property package to ensure consistency and reliability across both packages." + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -1601,10 +2383,6 @@ " assert isinstance(model.props[1].conc_mass_comp, Var)\n", " assert len(model.props[1].conc_mass_comp) == 3\n", "\n", - " for i in model.props[1].conc_mass_comp:\n", - " print(value(model.props[1].conc_mass_comp[i]))\n", - " assert value(model.props[1].conc_mass_comp[i]) == 1\n", - "\n", " @pytest.mark.unit\n", " def test_initialize(self, model):\n", " assert not model.props[1].flow_vol.fixed\n", @@ -1636,7 +2414,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -1658,7 +2436,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -1688,29 +2466,24 @@ "def test_config():\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 16\n", + " assert len(m.fs.unit.config) == 9\n", "\n", " # Check for config arguments\n", " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert m.fs.unit.config.energy_balance_type == EnergyBalanceType.useDefault\n", - " assert m.fs.unit.config.momentum_balance_type == MomentumBalanceType.pressureTotal\n", - " assert not m.fs.unit.config.has_heat_transfer\n", " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_equilibrium_reactions\n", " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert not m.fs.unit.config.has_heat_of_reaction\n", - " assert m.fs.unit.config.liquid_property_package is m.fs.liq_properties\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", "\n", " # Check for unit initializer\n", @@ -1726,7 +2499,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -1735,29 +2508,29 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", "\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", "\n", " return m\n", "\n", @@ -1772,12 +2545,12 @@ " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_inlet\")\n", - " assert len(model.fs.unit.liquid_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_inlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", @@ -1786,18 +2559,18 @@ " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", "\n", - " assert hasattr(model.fs.unit, \"liquid_outlet\")\n", - " assert len(model.fs.unit.liquid_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.liquid_outlet, \"pressure\")\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", "\n", " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_liq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", "\n", " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 19" + " assert number_total_constraints(model) == 16" ] }, { @@ -1814,14 +2587,16 @@ "\n", "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered. \n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", "\n", "By performing these checks, we conclude the testing for the unit model. " ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 29, "metadata": {}, "outputs": [], "source": [ @@ -1830,21 +2605,31 @@ " def model(self):\n", " m = ConcreteModel()\n", " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.liq_properties = LiqPhase()\n", + " m.fs.org_properties = OrgPhase()\n", " m.fs.aq_properties = AqPhase()\n", "\n", " m.fs.unit = LiqExtraction(\n", " dynamic=False,\n", " has_pressure_change=False,\n", - " liquid_property_package=m.fs.liq_properties,\n", + " organic_property_package=m.fs.org_properties,\n", " aqueous_property_package=m.fs.aq_properties,\n", " )\n", - " m.fs.unit.liquid_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.liquid_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.liquid_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", "\n", " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", @@ -1865,7 +2650,7 @@ " assert check_optimal_termination(results)\n", "\n", " # Checking for outlet flows\n", - " assert value(model.fs.unit.liquid_outlet.flow_vol[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", " 80.0, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", @@ -1874,13 +2659,13 @@ "\n", " # Checking for outlet mass_comp\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", " ) == pytest.approx(0.000187499, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", " ) == pytest.approx(0.000749999, rel=1e-5)\n", " assert value(\n", - " model.fs.unit.liquid_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", " ) == pytest.approx(0.000403124, rel=1e-5)\n", " assert value(\n", " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", @@ -1893,7 +2678,7 @@ " ) == pytest.approx(0.146775, rel=1e-5)\n", "\n", " # Checking for outlet temperature\n", - " assert value(model.fs.unit.liquid_outlet.temperature[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", " 300, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", @@ -1901,7 +2686,7 @@ " )\n", "\n", " # Checking for outlet pressure\n", - " assert value(model.fs.unit.liquid_outlet.pressure[0]) == pytest.approx(\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", " 1, rel=1e-5\n", " )\n", " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", @@ -1909,19 +2694,24 @@ " )\n", "\n", " # Fixed state variables\n", - " assert model.fs.unit.liquid_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.liquid_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.liquid_inlet.temperature[0].fixed\n", - " assert model.fs.unit.liquid_inlet.pressure[0].fixed\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", "\n", " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed" + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" ] } ], diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liquid_extraction.py b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liquid_extraction.py index de0c1db4..b6be1bb4 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liquid_extraction.py +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/liquid_extraction.py @@ -345,5 +345,3 @@ def rule_material_liq_balance(self, t, j): rule=rule_material_liq_balance, doc="Unit level material balances Org", ) - - \ No newline at end of file